mirror of
https://github.com/apache/superset.git
synced 2026-05-05 07:54:17 +00:00
Compare commits
87 Commits
ce/issue-3
...
react18-em
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2358fa11 | ||
|
|
60d70e519c | ||
|
|
88bedebd09 | ||
|
|
41a22d7918 | ||
|
|
28239c18d4 | ||
|
|
6205afbaa0 | ||
|
|
dc1c0f6ba1 | ||
|
|
ad73395c89 | ||
|
|
867e173427 | ||
|
|
c90c8612ad | ||
|
|
b14cca15f6 | ||
|
|
9d4384e49e | ||
|
|
d8dd2d99b3 | ||
|
|
dbe26d81ce | ||
|
|
98eaaaa6d6 | ||
|
|
cb74438865 | ||
|
|
e77fb5e3fc | ||
|
|
1ac113fd44 | ||
|
|
6bfdee98cd | ||
|
|
de45f3a928 | ||
|
|
2ec53c0694 | ||
|
|
d23b0cad92 | ||
|
|
e585406fff | ||
|
|
957b298ae1 | ||
|
|
f29d82b3b1 | ||
|
|
3f550f166f | ||
|
|
86eb6176d1 | ||
|
|
4244ae87bf | ||
|
|
512ba43e76 | ||
|
|
f57ba7645d | ||
|
|
12f69760f9 | ||
|
|
4fcb3144ff | ||
|
|
3f68104007 | ||
|
|
9faeda5723 | ||
|
|
c15b208fda | ||
|
|
6ad503201b | ||
|
|
56e9331dad | ||
|
|
a135e29035 | ||
|
|
bc875aa3e3 | ||
|
|
7842a9b05d | ||
|
|
1061b0612c | ||
|
|
bfacc3b5ac | ||
|
|
9001e7dcf2 | ||
|
|
a4532844f4 | ||
|
|
43a2cd3660 | ||
|
|
c895c4ffa9 | ||
|
|
ce3f19d373 | ||
|
|
2c26914c2e | ||
|
|
f7c955f81a | ||
|
|
9c3c8dcc0b | ||
|
|
df396aa6e9 | ||
|
|
e4fe08ab9e | ||
|
|
ae4c765d7d | ||
|
|
49c249c7a9 | ||
|
|
c2b9272f4c | ||
|
|
81a08f0a0e | ||
|
|
e3e834bbf7 | ||
|
|
ebb43404c8 | ||
|
|
4c4f3341de | ||
|
|
979f60a6d4 | ||
|
|
6ce3885f2e | ||
|
|
8d17c34068 | ||
|
|
b4f595953e | ||
|
|
2b623fd09a | ||
|
|
fe074c0d76 | ||
|
|
549aff7cf9 | ||
|
|
c7c9a17d6b | ||
|
|
54f1e32763 | ||
|
|
2a884e8456 | ||
|
|
7b02c21bff | ||
|
|
1dd28c6fcd | ||
|
|
eba08ae52a | ||
|
|
171414f165 | ||
|
|
dbd7984ce9 | ||
|
|
4b42f82f13 | ||
|
|
ea3a1955b7 | ||
|
|
d0abb66fdf | ||
|
|
ef50b688ee | ||
|
|
3aa99c577e | ||
|
|
09c7e1fc08 | ||
|
|
6947881ba7 | ||
|
|
7bee2afa8e | ||
|
|
c4a8b34b11 | ||
|
|
3395620b6e | ||
|
|
3f28f5d012 | ||
|
|
cf587caca7 | ||
|
|
523ecb65a4 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -36,6 +36,10 @@
|
||||
**/*.geojson @villebro @rusackas
|
||||
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
|
||||
|
||||
# Notify translation maintainers of changes to translations
|
||||
|
||||
/superset/translations/ @sfirke
|
||||
|
||||
# Notify PMC members of changes to extension-related files
|
||||
|
||||
/docs/developer_portal/extensions/ @michael-s-molina @villebro @rusackas
|
||||
|
||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -59,15 +59,13 @@ updates:
|
||||
versioning-strategy: increase
|
||||
|
||||
|
||||
# NOTE: `uv` support is in beta, more details here:
|
||||
# https://github.com/dependabot/dependabot-core/pull/10040#issuecomment-2696978430
|
||||
- package-ecosystem: "uv"
|
||||
directory: "requirements/"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- uv
|
||||
- pip
|
||||
- dependabot
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
|
||||
10
.github/labeler.yml
vendored
10
.github/labeler.yml
vendored
@@ -77,6 +77,11 @@
|
||||
- any-glob-to-any-file:
|
||||
- 'superset/translations/zh/**'
|
||||
|
||||
"i18n:czech":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'superset/translations/cs/**'
|
||||
|
||||
"i18n:traditional-chinese":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -122,6 +127,11 @@
|
||||
- any-glob-to-any-file:
|
||||
- 'superset/translations/sk/**'
|
||||
|
||||
"i18n:latvian":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'superset/translations/lv/**'
|
||||
|
||||
"i18n:ukrainian":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
14
.github/workflows/bashlib.sh
vendored
14
.github/workflows/bashlib.sh
vendored
@@ -127,6 +127,20 @@ playwright_testdata() {
|
||||
superset load_test_users
|
||||
superset load_examples
|
||||
superset init
|
||||
# Enable DML on the examples database so Playwright tests can create/drop
|
||||
# temporary tables via SQL Lab without depending on external data sources.
|
||||
superset shell <<'PYEOF'
|
||||
import sys
|
||||
from superset.extensions import db
|
||||
from superset.models.core import Database
|
||||
examples_db = db.session.query(Database).filter_by(database_name='examples').first()
|
||||
if not examples_db:
|
||||
sys.exit('ERROR: examples database not found. load_examples may have failed.')
|
||||
|
||||
examples_db.allow_dml = True
|
||||
db.session.commit()
|
||||
print('Enabled allow_dml on examples database')
|
||||
PYEOF
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
|
||||
4
.github/workflows/ephemeral-env.yml
vendored
4
.github/workflows/ephemeral-env.yml
vendored
@@ -265,7 +265,7 @@ jobs:
|
||||
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@6853cfae8c3a7d978fbf68b5a55453395541dfbb # v1
|
||||
with:
|
||||
task-definition: .github/workflows/ecs-task-definition.json
|
||||
container-name: superset-ci
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
id: deploy-task
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@a310a830f5c14e583e35d84e4e1ec7dd177c3c9c # v2
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service
|
||||
|
||||
4
.github/workflows/superset-docs-deploy.yml
vendored
4
.github/workflows/superset-docs-deploy.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
2
.github/workflows/superset-docs-verify.yml
vendored
2
.github/workflows/superset-docs-verify.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -58,6 +58,10 @@ categories:
|
||||
url: https://www.ontruck.com/
|
||||
|
||||
Financial Services:
|
||||
- name: Aadhar Housing Finance Limited
|
||||
url: https://www.aadharhousing.com
|
||||
contributors: ["@thakerhardiks"]
|
||||
|
||||
- name: Aktia Bank plc
|
||||
url: https://www.aktia.com
|
||||
|
||||
|
||||
@@ -138,14 +138,33 @@ THUMBNAIL_CACHE_CONFIG = init_thumbnail_cache
|
||||
```
|
||||
|
||||
Using the above example cache keys for dashboards will be `superset_thumb__dashboard__{ID}`. You can
|
||||
override the base URL for selenium using:
|
||||
override the base URL for Selenium using:
|
||||
|
||||
```
|
||||
WEBDRIVER_BASEURL = "https://superset.company.com"
|
||||
```
|
||||
|
||||
Additional selenium web drive configuration can be set using `WEBDRIVER_CONFIGURATION`. You can
|
||||
implement a custom function to authenticate selenium. The default function uses the `flask-login`
|
||||
To control which user account is used for rendering thumbnails and warming up caches, configure
|
||||
`THUMBNAIL_EXECUTORS` and `CACHE_WARMUP_EXECUTORS`. Each accepts a list of executor types (which
|
||||
resolve to an owner, creator, modifier, or the currently-logged-in user) and/or a `FixedExecutor`
|
||||
pinned to a specific username. By default, thumbnails render as the current user
|
||||
(`ExecutorType.CURRENT_USER`) and cache warmup runs as the chart/dashboard owner
|
||||
(`ExecutorType.OWNER`).
|
||||
|
||||
To force both to run as a dedicated service account (`admin` in this example):
|
||||
|
||||
```python
|
||||
from superset.tasks.types import ExecutorType, FixedExecutor
|
||||
|
||||
THUMBNAIL_EXECUTORS = [FixedExecutor("admin")]
|
||||
CACHE_WARMUP_EXECUTORS = [FixedExecutor("admin")]
|
||||
```
|
||||
|
||||
Use a dedicated read-only service account here rather than a personal admin account, so that
|
||||
thumbnail rendering and cache warmup tasks don't fail if a specific user's credentials change.
|
||||
|
||||
Additional Selenium WebDriver configuration can be set using `WEBDRIVER_CONFIGURATION`. You can
|
||||
implement a custom function to authenticate Selenium. The default function uses the `flask-login`
|
||||
session cookie. Here's an example of a custom function signature:
|
||||
|
||||
```python
|
||||
@@ -159,6 +178,20 @@ Then on configuration:
|
||||
WEBDRIVER_AUTH_FUNC = auth_driver
|
||||
```
|
||||
|
||||
## ETag Support for Thumbnails
|
||||
|
||||
Thumbnail and screenshot endpoints return `ETag` response headers based on the cached content digest. Clients can use conditional requests to avoid downloading unchanged images:
|
||||
|
||||
```
|
||||
GET /api/v1/chart/42/thumbnail/
|
||||
If-None-Match: "abc123..."
|
||||
|
||||
→ 304 Not Modified (if unchanged)
|
||||
→ 200 OK (with new image if changed)
|
||||
```
|
||||
|
||||
This is particularly useful for embedded dashboards and external integrations that periodically poll for updated screenshots — unchanged thumbnails return immediately with no payload.
|
||||
|
||||
## Distributed Coordination Backend
|
||||
|
||||
Superset supports an optional distributed coordination (`DISTRIBUTED_COORDINATION_CONFIG`) for
|
||||
|
||||
@@ -372,6 +372,26 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
|
||||
]
|
||||
```
|
||||
|
||||
### PKCE Support
|
||||
|
||||
For public OAuth2 clients that cannot securely store a client secret, enable Proof Key for Code Exchange (PKCE) by adding `code_challenge_method` to the `remote_app` configuration:
|
||||
|
||||
```python
|
||||
OAUTH_PROVIDERS = [
|
||||
{
|
||||
'name': 'myProvider',
|
||||
'remote_app': {
|
||||
'client_id': 'myClientId',
|
||||
'client_secret': 'mySecret', # may be empty for pure public clients
|
||||
'code_challenge_method': 'S256', # enables PKCE
|
||||
'server_metadata_url': 'https://myAuthorizationServer/.well-known/openid-configuration'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
PKCE (`S256`) is recommended for all OAuth2 flows, even when a client secret is present, as it protects against authorization code interception attacks.
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
FAB supports authenticating user credentials against an LDAP server.
|
||||
|
||||
@@ -30,6 +30,10 @@ Superset's ZIP-based import/export also covers **dashboards**, **charts**, and *
|
||||
| └── ... (more databases)
|
||||
```
|
||||
|
||||
:::note
|
||||
When you export a database connection, the `masked_encrypted_extra` field (used for sensitive connection parameters such as service account JSON, OAuth tokens, and other encrypted credentials) is included in the export. When importing on another instance, these values are decrypted and re-encrypted using the destination instance's `SECRET_KEY`. Ensure the receiving instance has a valid `SECRET_KEY` configured before importing.
|
||||
:::
|
||||
|
||||
## Exporting Datasources to YAML
|
||||
|
||||
You can print your current datasources to stdout by running:
|
||||
|
||||
@@ -501,7 +501,7 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_SERVICE_URL` | `None` | Public base URL for MCP-generated links (set this when behind a reverse proxy) |
|
||||
| `MCP_DEBUG` | `False` | Enable debug logging |
|
||||
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
|
||||
| `MCP_PARSE_REQUEST_ENABLED` | `True` | Pre-parse MCP tool inputs from JSON strings into objects. Set to `False` for clients (Claude Desktop, LangChain) that do not double-serialize arguments — this produces cleaner tool schemas for those clients |
|
||||
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -517,6 +517,7 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_REQUIRED_SCOPES` | `[]` | Required JWT scopes |
|
||||
| `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. |
|
||||
|
||||
### Response Size Guard
|
||||
|
||||
@@ -600,6 +601,43 @@ MCP_STORE_CONFIG = {
|
||||
| `event_store_max_events` | `100` | Maximum events retained per session |
|
||||
| `event_store_ttl` | `3600` | Event TTL in seconds |
|
||||
|
||||
### Tool Search
|
||||
|
||||
By default the MCP server exposes a lightweight tool-search interface instead of advertising every tool at once. This reduces the initial context sent to the LLM by ~70%, which lowers cost and latency. The AI client discovers tools on demand by calling `search_tools` and then invokes them via `call_tool`.
|
||||
|
||||
```python
|
||||
MCP_TOOL_SEARCH_CONFIG = {
|
||||
"enabled": True,
|
||||
"strategy": "bm25", # "bm25" (natural language) or "regex"
|
||||
"max_results": 5,
|
||||
"always_visible": [ # Tools always listed (pinned)
|
||||
"health_check",
|
||||
"get_instance_info",
|
||||
],
|
||||
"search_tool_name": "search_tools",
|
||||
"call_tool_name": "call_tool",
|
||||
"include_schemas": False, # False=summary mode (name + parameters_hint)
|
||||
"compact_schemas": True, # Strip $defs (only applies when include_schemas=True)
|
||||
"max_description_length": 300,
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `enabled` | `True` | Enable tool search. When `False`, all tools are listed upfront |
|
||||
| `strategy` | `"bm25"` | Search ranking algorithm. `"bm25"` supports natural language; `"regex"` supports pattern matching |
|
||||
| `max_results` | `5` | Maximum tools returned per search query |
|
||||
| `always_visible` | See above | Tools that always appear in `list_tools`, regardless of search |
|
||||
| `include_schemas` | `False` | When `False` (default, "summary mode"), search results omit `inputSchema` entirely and include a lightweight `parameters_hint` listing top-level parameter names. Set to `True` to include the full `inputSchema` in search results. Full schemas are always used when a tool is actually invoked via `call_tool`. |
|
||||
| `compact_schemas` | `True` | Strip `$defs` / `$ref` and replace with `{"type": "object"}` in search results to reduce token cost. Only takes effect when `include_schemas=True` — ignored in summary mode. |
|
||||
| `max_description_length` | `300` | Truncate tool descriptions in search results (0 = no truncation). Applies in both summary and full-schema modes. |
|
||||
|
||||
:::tip
|
||||
Set `enabled: False` to revert to the traditional "show all tools at once" behavior, which some clients or workflows may prefer.
|
||||
:::
|
||||
|
||||
Tool search reduces the initial token cost from ~15–20K tokens (full catalog) down to ~4–5K tokens (pinned tools + search interface) — roughly 85% savings at the start of each conversation.
|
||||
|
||||
### Session & CSRF
|
||||
|
||||
These values are flat-merged into the Flask app config used by the MCP server process:
|
||||
@@ -621,6 +659,102 @@ MCP_CSRF_CONFIG = {
|
||||
|
||||
---
|
||||
|
||||
## Access Control
|
||||
|
||||
### RBAC Enforcement
|
||||
|
||||
The MCP server respects Superset's full role-based access control (RBAC). Every authenticated user can only access the data and operations their Superset roles permit — the same rules that apply in the Superset UI apply through MCP.
|
||||
|
||||
Each tool declares one or more required FAB permissions. The table below maps tool groups to their permission requirements:
|
||||
|
||||
| Tool group | Required FAB permission |
|
||||
|------------|------------------------|
|
||||
| `list_charts`, `get_chart_info`, `get_chart_data`, `get_chart_preview`, `generate_chart`, `update_chart` | `can_read` on `Chart` (read), `can_write` on `Chart` (mutate) |
|
||||
| `list_dashboards`, `get_dashboard_info`, `generate_dashboard`, `add_chart_to_existing_dashboard` | `can_read` on `Dashboard` (read), `can_write` on `Dashboard` (mutate) |
|
||||
| `list_datasets`, `get_dataset_info`, `create_virtual_dataset` | `can_read` on `Dataset` (read), `can_write` on `Dataset` (mutate) |
|
||||
| `list_databases`, `get_database_info` | `can_read` on `Database` |
|
||||
| `execute_sql` | `can_execute_sql_query` on `SQLLab` |
|
||||
| `open_sql_lab_with_context` | `can_read` on `SQLLab` |
|
||||
| `save_sql_query` | `can_write` on `SavedQuery` |
|
||||
| `health_check` | None (public) |
|
||||
|
||||
To disable RBAC checking globally (for trusted-network deployments or testing), set:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_RBAC_ENABLED = False
|
||||
```
|
||||
|
||||
:::warning
|
||||
Disabling RBAC removes all permission checks from MCP tool calls. Only do this on isolated, internal deployments where all MCP users are trusted admins.
|
||||
:::
|
||||
|
||||
### Audit Log
|
||||
|
||||
All MCP tool calls are recorded in Superset's action log. You can view them at **Settings → Action Log** (admin only). Each log entry records:
|
||||
|
||||
- The tool name (e.g., `mcp.generate_chart.db_write`)
|
||||
- The authenticated user
|
||||
- A timestamp
|
||||
|
||||
This makes MCP activity fully auditable alongside regular Superset activity. The action log uses the same event logger as the rest of Superset, so existing log ingestion pipelines (e.g., sending logs to Elasticsearch or a SIEM) capture MCP events automatically.
|
||||
|
||||
### Middleware Pipeline
|
||||
|
||||
Every MCP request passes through a middleware stack before reaching the tool function. The default stack (assembled in `build_middleware_list()` in `server.py`) is:
|
||||
|
||||
| Middleware | Purpose | Default |
|
||||
|------------|---------|---------|
|
||||
| `StructuredContentStripperMiddleware` | Strips `structuredContent` from responses for Claude.ai bridge compatibility | Enabled |
|
||||
| `LoggingMiddleware` | Logs each tool call with user, parameters, and duration | Enabled |
|
||||
| `GlobalErrorHandlerMiddleware` | Catches unhandled exceptions and sanitizes sensitive data before it reaches the client | Enabled |
|
||||
| `ResponseSizeGuardMiddleware` | Estimates token count, warns at 80% of limit, blocks at limit | Enabled (configurable via `MCP_RESPONSE_SIZE_CONFIG`) |
|
||||
| `ResponseCachingMiddleware` | Caches read-heavy tool responses (in-memory or Redis) | Disabled (enable via `MCP_CACHE_CONFIG`) |
|
||||
|
||||
Additional middleware classes (`RateLimitMiddleware`, `FieldPermissionsMiddleware`, `PrivateToolMiddleware`) are implemented in `superset/mcp_service/middleware.py` but are not added to the default pipeline. They are available for operators who want to layer them in via a custom startup path.
|
||||
|
||||
### Error Sanitization
|
||||
|
||||
The `GlobalErrorHandlerMiddleware` automatically redacts sensitive information from all error messages before they reach the LLM client. The following are replaced with generic messages:
|
||||
|
||||
- **Database connection strings** — replaced with a generic connection error message
|
||||
- **API keys and tokens** — redacted from error traces
|
||||
- **File system paths** — stripped to prevent information disclosure
|
||||
- **IP addresses** — removed from error context
|
||||
|
||||
This ensures that a misconfigured database connection or an unexpected exception never leaks credentials or internal topology to the LLM or its users. All regex patterns used for redaction are bounded to prevent ReDoS attacks.
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
Each MCP server process maintains its own SQLAlchemy connection pool to the database. For multi-worker deployments, total open connections = **workers × pool size**.
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
SQLALCHEMY_POOL_SIZE = 5
|
||||
SQLALCHEMY_MAX_OVERFLOW = 10
|
||||
SQLALCHEMY_POOL_TIMEOUT = 30
|
||||
SQLALCHEMY_POOL_RECYCLE = 3600 # Recycle connections after 1 hour
|
||||
```
|
||||
|
||||
For a 3-pod Kubernetes deployment with the defaults above, expect up to 3 × (5 + 10) = 45 connections. Size your database's `max_connections` accordingly.
|
||||
|
||||
### Response Caching
|
||||
|
||||
Enable response caching for read-heavy workloads (dashboards/datasets that don't change frequently). With the in-memory backend (default when `MCP_STORE_CONFIG` is disabled), caching is per-process. Use Redis-backed caching for consistent cache hits across multiple pods:
|
||||
|
||||
```python
|
||||
MCP_CACHE_CONFIG = {"enabled": True, "call_tool_ttl": 3600}
|
||||
MCP_STORE_CONFIG = {"enabled": True, "CACHE_REDIS_URL": "redis://redis:6379/0"}
|
||||
```
|
||||
|
||||
Mutating tools (`generate_chart`, `update_chart`, `execute_sql`, `generate_dashboard`) are always excluded from caching regardless of this setting.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server won't start
|
||||
|
||||
@@ -84,6 +84,35 @@ THEME_DARK = {
|
||||
# - OS preference detection is automatically enabled
|
||||
```
|
||||
|
||||
### App Branding
|
||||
|
||||
The application name shown in the browser title bar and navigation can be
|
||||
set via the `brandAppName` theme token:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"brandAppName": "Acme Analytics",
|
||||
# ... other tokens
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or in the theme CRUD UI JSON editor:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"brandAppName": "Acme Analytics"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The existing `APP_NAME` Python config key continues to work for backward compatibility.
|
||||
`brandAppName` takes precedence when both are set, and allows different themes to carry different brand names.
|
||||
Email and alert/report notification subjects are driven by backend settings such as
|
||||
`EMAIL_REPORTS_SUBJECT_PREFIX` and `APP_NAME`, not by this theme token.
|
||||
|
||||
### Migration from Configuration to UI
|
||||
|
||||
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
|
||||
@@ -312,11 +341,25 @@ Available chart types for `echartsOptionsOverridesByChartType`:
|
||||
- `echarts_heatmap` - Heatmaps
|
||||
- `echarts_mixed_timeseries` - Mixed time series
|
||||
|
||||
### Array Property Overrides
|
||||
|
||||
Array properties (such as color palettes) are fully supported in overrides. Arrays are **replaced entirely** rather than merged, so specify the complete array:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": { ... },
|
||||
"echartsOptionsOverrides": {
|
||||
# Replace the default color palette for all ECharts visualizations
|
||||
"color": ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Start with global overrides** for consistent styling across all charts
|
||||
2. **Use chart-specific overrides** for unique requirements per visualization type
|
||||
3. **Test thoroughly** as overrides use deep merge - nested objects are combined, but arrays are completely replaced
|
||||
3. **Test thoroughly** as overrides use deep merge for objects, but arrays are completely replaced — always specify the full array value
|
||||
4. **Document your overrides** to help team members understand custom styling
|
||||
5. **Consider performance** - complex overrides may impact chart rendering speed
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@ only see the objects that they have access to.
|
||||
The **sql_lab** role grants access to SQL Lab. Note that while **Admin** users have access
|
||||
to all databases by default, both **Alpha** and **Gamma** users need to be given access on a per database basis.
|
||||
|
||||
Beyond the base `sql_lab` role, two additional SQL Lab permissions must be explicitly granted for users who need these capabilities:
|
||||
|
||||
| Permission | Feature |
|
||||
|------------|---------|
|
||||
| `can_estimate_query_cost` on `SQLLab` | Estimate query cost before running |
|
||||
| `can_format_sql` on `SQLLab` | Format SQL using the database's dialect |
|
||||
|
||||
Grant these in **Security → List Roles** by adding the permissions to the relevant role.
|
||||
|
||||
### Public
|
||||
|
||||
The **Public** role is the most restrictive built-in role, designed specifically for anonymous/unauthenticated
|
||||
@@ -182,6 +191,8 @@ However, it is crucial to understand the following:
|
||||
|
||||
By combining Superset's configurable safeguards with strong database-level security practices, you can achieve a more robust and layered security posture.
|
||||
|
||||
**Dataset Sample Access**: The `get_samples()` endpoint now enforces datasource-level access control. Users can only fetch sample rows from datasets they have been explicitly granted access to — the same permission check applied when running chart queries. This closes a prior gap where unauthenticated or under-privileged access could retrieve sample data.
|
||||
|
||||
### REST API for user & role management
|
||||
|
||||
Flask-AppBuilder supports a REST API for user CRUD,
|
||||
|
||||
@@ -485,7 +485,7 @@ Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in or
|
||||
|
||||
First, be sure you are using the following versions of Node.js and npm:
|
||||
|
||||
- `Node.js`: Version 20
|
||||
- `Node.js`: Version 22 (LTS)
|
||||
- `npm`: Version 10
|
||||
|
||||
We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage your node environment:
|
||||
|
||||
@@ -256,6 +256,34 @@ For example, when running the local development build, the following will disabl
|
||||
Top Nav and remove the Filter Bar:
|
||||
`http://localhost:8088/superset/dashboard/my-dashboard/?standalone=1&show_filters=0`
|
||||
|
||||
### Table Chart Features
|
||||
|
||||
The **Table** chart type has several advanced capabilities worth knowing:
|
||||
|
||||
#### Conditional Formatting
|
||||
|
||||
Conditional formatting rules highlight cells based on their values. Rules can be applied to:
|
||||
- **Numeric columns** — color cells above/below a threshold, or use a gradient across a range
|
||||
- **String columns** — highlight cells matching specific text values or patterns
|
||||
- **Boolean columns** — color cells that are `true` or `false`, or `null`/`not null`
|
||||
|
||||
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.
|
||||
|
||||
#### HTML Rendering in Table Cells
|
||||
|
||||
Table chart cells can render raw HTML, enabling rich formatting such as hyperlinks, colored badges, and icons directly in the data. Enable this per-column in the chart's **Column Configuration** panel by toggling **Render HTML**.
|
||||
|
||||
:::caution
|
||||
Only enable HTML rendering for columns sourced from data you control. Rendering untrusted HTML can expose users to cross-site scripting (XSS) risks.
|
||||
:::
|
||||
|
||||
#### Column Header Tooltips
|
||||
|
||||
Column headers display a tooltip with the column's **Description** from the dataset editor when the user hovers over them. Keep dataset column descriptions up to date to improve chart discoverability.
|
||||
|
||||
#### Display Controls
|
||||
|
||||
In dashboard view mode (without entering Edit mode), charts with configurable display options expose a **Display Controls** panel accessible from the chart's context menu. This surfaces controls such as Time Grain, Time Column, and layer visibility for applicable chart types — making it easy to adjust a chart's view without going to Explore.
|
||||
### AG Grid Interactive Table
|
||||
|
||||
The **AG Grid Interactive Table** chart type is Superset's fully-featured data grid, suitable for large paginated datasets where the standard Table chart is not enough.
|
||||
@@ -314,6 +342,26 @@ ECharts option overrides bypass Superset's validation layer. Invalid option keys
|
||||
|
||||
When the **Search Box** is visible in a Table chart, the **Download** action exports only the rows currently visible after the search filter is applied — not the full underlying dataset. This matches the visual output and is intentional. To export the full dataset regardless of search state, use the **Download as CSV** option from the chart's three-dot menu in the dashboard or from the Explore chart toolbar before applying a search filter.
|
||||
|
||||
### Sharing a Specific Tab
|
||||
|
||||
When a dashboard has tabs, each tab gets its own shareable URL. Navigate to the tab you want to share and copy the URL from your browser's address bar — the tab anchor is encoded in the URL so that anyone opening the link lands directly on that tab.
|
||||
|
||||
### Auto-Refresh
|
||||
|
||||
Dashboards can be configured to refresh automatically at a fixed interval without user interaction. Open a dashboard, click the **⋮** (more options) menu in the top-right, and select **Set auto-refresh interval**. Choose an interval (e.g., every 10 seconds, 1 minute, or 10 minutes). The setting is per-session and resets when you close the tab.
|
||||
|
||||
:::note
|
||||
Auto-refresh triggers a full data reload for all charts on the dashboard. For dashboards with expensive queries, choose longer intervals to avoid overloading your database.
|
||||
:::
|
||||
|
||||
### Last Queried Timestamp
|
||||
|
||||
Charts can display a "Last queried at" timestamp showing when the chart data was last fetched. This is useful on auto-refreshing dashboards to confirm data freshness. Enable it in **Dashboard Properties → Styling → Show last queried time**.
|
||||
|
||||
### Saving a Chart to a Specific Tab
|
||||
|
||||
When saving or adding a chart to a dashboard from Explore, you can select which tab it should land on using the tab tree-select dropdown in the "Add to dashboard" modal.
|
||||
|
||||
:::resources
|
||||
- [Dashboard Customization](https://docs.preset.io/docs/dashboard-customization) - Advanced dashboard styling and layout options
|
||||
- [Blog: BI Dashboard Best Practices](https://preset.io/blog/bi-dashboard-best-practices/)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Embedding Superset
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
{/*
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
@@ -17,10 +22,6 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
*/}
|
||||
|
||||
---
|
||||
title: Embedding Superset
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Embedding Superset
|
||||
|
||||
|
||||
143
docs/docs/using-superset/handlebars-chart.mdx
Normal file
143
docs/docs/using-superset/handlebars-chart.mdx
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: Handlebars Chart
|
||||
hide_title: true
|
||||
sidebar_position: 10
|
||||
version: 1
|
||||
---
|
||||
|
||||
## Handlebars Chart
|
||||
|
||||
The Handlebars chart lets you render query results using a custom [Handlebars](https://handlebarsjs.com/) template. This gives you full control over how your data is displayed — from simple tables to rich HTML layouts.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
In the chart editor, write a Handlebars template in the **Template** field. Your query results are available as `data`, an array of row objects.
|
||||
|
||||
```handlebars
|
||||
{{#each data}}
|
||||
<p>{{this.name}}: {{this.value}}</p>
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
### Built-in Helpers
|
||||
|
||||
Superset registers several custom helpers on top of the standard Handlebars built-ins.
|
||||
|
||||
#### `dateFormat`
|
||||
|
||||
Formats a date value using [Day.js](https://day.js.org/) format strings.
|
||||
|
||||
```handlebars
|
||||
{{dateFormat my_date format="MMMM YYYY"}}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `format` | `YYYY-MM-DD` | A Day.js-compatible format string |
|
||||
|
||||
---
|
||||
|
||||
#### `stringify`
|
||||
|
||||
Converts an object to a JSON string, or any other value to its string representation.
|
||||
|
||||
```handlebars
|
||||
{{stringify myObj}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `formatNumber`
|
||||
|
||||
Formats a number using locale-aware formatting.
|
||||
|
||||
```handlebars
|
||||
{{formatNumber myNumber "en-US"}}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `locale` | `en-US` | A BCP 47 language tag |
|
||||
|
||||
---
|
||||
|
||||
#### `parseJson`
|
||||
|
||||
Parses a JSON string into an object that can be used in your template.
|
||||
|
||||
```handlebars
|
||||
{{parseJson myJsonString}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `groupBy`
|
||||
|
||||
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by).
|
||||
|
||||
```handlebars
|
||||
{{#groupBy data "department"}}
|
||||
<h3>{{value}}</h3>
|
||||
{{#each items}}
|
||||
<p>{{this.name}}</p>
|
||||
{{/each}}
|
||||
{{/groupBy}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Helpers from just-handlebars-helpers
|
||||
|
||||
Superset also registers all helpers from the [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) library. These include a wide range of comparison, math, string, and conditional helpers. Commonly used ones include:
|
||||
|
||||
#### Comparison
|
||||
|
||||
| Helper | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `eq` | Strict equality | `{{#if (eq status "active")}}` |
|
||||
| `eqw` | Weak equality | `{{#if (eqw count "5")}}` |
|
||||
| `neq` | Strict inequality | `{{#if (neq role "admin")}}` |
|
||||
| `lt` | Less than | `{{#if (lt score 50)}}` |
|
||||
| `lte` | Less than or equal | `{{#if (lte score 100)}}` |
|
||||
| `gt` | Greater than | `{{#if (gt price 0)}}` |
|
||||
| `gte` | Greater than or equal | `{{#if (gte age 18)}}` |
|
||||
|
||||
#### Logical
|
||||
|
||||
| Helper | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `and` | Logical AND | `{{#if (and isActive isVerified)}}` |
|
||||
| `or` | Logical OR | `{{#if (or isAdmin isMod)}}` |
|
||||
| `not` | Logical NOT | `{{#if (not isDisabled)}}` |
|
||||
| `ifx` | Inline conditional | `{{ifx isActive "Yes" "No"}}` |
|
||||
| `coalesce` | Returns first non-falsy value | `{{coalesce nickname name "Anonymous"}}` |
|
||||
|
||||
#### String
|
||||
|
||||
| Helper | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `capitalize` | Capitalizes first letter | `{{capitalize name}}` |
|
||||
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
|
||||
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
|
||||
| `truncate` | Truncates a string | `{{truncate description 100}}` |
|
||||
| `contains` | Checks if string contains substring | `{{#if (contains tag "urgent")}}` |
|
||||
|
||||
#### Math
|
||||
|
||||
| Helper | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `add` | Addition | `{{add a b}}` |
|
||||
| `subtract` | Subtraction | `{{subtract total discount}}` |
|
||||
| `multiply` | Multiplication | `{{multiply price quantity}}` |
|
||||
| `divide` | Division | `{{divide total count}}` |
|
||||
| `ceil` | Ceiling | `{{ceil value}}` |
|
||||
| `floor` | Floor | `{{floor value}}` |
|
||||
| `round` | Round | `{{round value}}` |
|
||||
|
||||
For the full list of available helpers, see the [just-handlebars-helpers documentation](https://github.com/leapfrogtechnology/just-handlebars-helpers).
|
||||
|
||||
### Tips
|
||||
|
||||
- Use raw blocks to escape Handlebars syntax if you need to display double curly braces literally.
|
||||
- Comparison helpers like `eq` must be wrapped in a subexpression when used with `#if`: `{{#if (eq myVal "foo")}}`.
|
||||
- HTML output is sanitized by default based on your Superset configuration (`HTML_SANITIZATION`).
|
||||
@@ -33,6 +33,29 @@ SQL templating must be enabled by your administrator via the `ENABLE_TEMPLATE_PR
|
||||
For advanced configuration options, see the [SQL Templating Configuration Guide](/admin-docs/configuration/sql-templating).
|
||||
:::
|
||||
|
||||
## Using Jinja in Calculated Columns
|
||||
|
||||
Jinja template macros are available in calculated column expressions in the dataset editor — not just in SQL Lab queries and virtual datasets. This allows column expressions to reference the current user or dynamic context.
|
||||
|
||||
**Example: User-scoped calculated column**
|
||||
|
||||
```sql
|
||||
CASE WHEN sales_rep = '{{ current_username() }}' THEN amount ELSE 0 END
|
||||
```
|
||||
|
||||
**Example: Conditional display based on role**
|
||||
|
||||
Because `current_user_roles()` returns a Python list, test role membership with a Jinja
|
||||
conditional at template time rather than matching against the list's string representation:
|
||||
|
||||
```sql
|
||||
{% if 'Finance' in current_user_roles() %}revenue{% else %}NULL{% endif %} AS finance_revenue
|
||||
```
|
||||
|
||||
:::note
|
||||
The `ENABLE_TEMPLATE_PROCESSING` feature flag must be enabled by your administrator for Jinja in calculated columns to work.
|
||||
:::
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Jinja templates use double curly braces `{{ }}` for expressions and `{% %}` for logic blocks.
|
||||
@@ -243,6 +266,7 @@ Using `remove_filter=True` applies the filter in the inner query for better perf
|
||||
- Use `|tojson` to serialize arrays as JSON strings
|
||||
- Test queries with explicit parameter values before relying on filter context
|
||||
- For complex templating needs, ask your administrator about custom Jinja macros
|
||||
- **Format SQL is Jinja-aware**: The "Format SQL" button in SQL Lab correctly preserves `{{ }}` and `{% %}` template syntax and applies your selected database's SQL dialect when formatting.
|
||||
|
||||
:::resources
|
||||
- [Admin Guide: SQL Templating Configuration](/admin-docs/configuration/sql-templating)
|
||||
|
||||
@@ -55,9 +55,10 @@ Ask your AI assistant to browse what's available in your Superset instance:
|
||||
|
||||
Describe the visualization you want and AI creates it for you:
|
||||
|
||||
- **Preview-first workflow** -- by default AI generates an Explore link so you can review the chart before it is saved. Say "save it" to commit permanently
|
||||
- **Create charts from natural language** -- describe what you want to see and AI picks the right chart type, metrics, and dimensions
|
||||
- **Preview before saving** -- `generate_chart` defaults to `save_chart=False`, showing the chart in Explore before it's committed. Ask AI to save once you're satisfied.
|
||||
- **Modify existing charts** -- `update_chart` also supports preview mode so you can review changes before saving
|
||||
- **Modify existing charts** -- `update_chart` also supports preview mode so you can review changes before saving (update filters, change chart types, add metrics)
|
||||
- **Get Explore links** -- open any chart in Superset's Explore view for further refinement
|
||||
|
||||
**Example prompts:**
|
||||
@@ -65,6 +66,16 @@ Describe the visualization you want and AI creates it for you:
|
||||
> "Update chart 42 to use a line chart instead"
|
||||
> "Give me a link to explore this chart further"
|
||||
|
||||
:::tip Preview-first workflow
|
||||
Charts are **not saved by default**. The workflow is intentionally iterative:
|
||||
|
||||
1. **Explore** — AI generates an Explore link so you can see the chart before it exists in Superset
|
||||
2. **Iterate** — ask the AI to adjust the chart; changes are previewed without touching the database
|
||||
3. **Save** — when you're happy, say "save it" and the chart is permanently stored
|
||||
|
||||
To skip the preview and save immediately, include "and save it" in your prompt.
|
||||
:::
|
||||
|
||||
### Create Dashboards
|
||||
|
||||
Build dashboards from a collection of charts:
|
||||
@@ -76,16 +87,40 @@ Build dashboards from a collection of charts:
|
||||
> "Create a dashboard called 'Q4 Sales Overview' with charts 10, 15, and 22"
|
||||
> "Add the revenue trend chart to the executive dashboard"
|
||||
|
||||
### Browse Databases
|
||||
|
||||
Discover what database connections are configured in your Superset instance:
|
||||
|
||||
- **List databases** -- see all database connections you have access to
|
||||
- **Get database details** -- name, backend type (PostgreSQL, Snowflake, etc.), and connection status
|
||||
|
||||
**Example prompts:**
|
||||
> "What databases are connected to Superset?"
|
||||
> "Show me details about the data warehouse connection"
|
||||
|
||||
### Create Virtual Datasets
|
||||
|
||||
Build ad-hoc SQL datasets that can be used as the basis for charts:
|
||||
|
||||
- **Create virtual datasets** -- write a SQL query and save it as a reusable dataset
|
||||
- **Use immediately in charts** -- the returned dataset ID can be passed directly to chart creation
|
||||
|
||||
**Example prompts:**
|
||||
> "Create a dataset from: SELECT region, SUM(revenue) as total_revenue FROM orders GROUP BY region"
|
||||
> "Make a virtual dataset called 'monthly_signups' from the users table filtered to last 12 months"
|
||||
|
||||
### Run SQL Queries
|
||||
|
||||
Execute SQL directly through your AI assistant:
|
||||
|
||||
- **Run queries** -- execute SQL with full Superset RBAC enforcement (you can only query data your roles allow)
|
||||
- **Open SQL Lab** -- get a link to SQL Lab pre-populated with a query, ready to run and explore
|
||||
- **Save queries** -- save a SQL query to SQL Lab's Saved Queries for later reuse
|
||||
|
||||
**Example prompts:**
|
||||
> "Run this query: SELECT region, SUM(revenue) FROM sales GROUP BY region"
|
||||
> "Open SQL Lab with a query to show the top 10 customers by order count"
|
||||
> "Save this query as 'Weekly Revenue Report'"
|
||||
|
||||
### Analyze Chart Data
|
||||
|
||||
|
||||
@@ -885,10 +885,8 @@ const config: Config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/user-docs/',
|
||||
type: 'custom-getStartedSplit',
|
||||
position: 'right',
|
||||
className: 'default-button-theme get-started-button',
|
||||
label: 'Get Started',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/apache/superset',
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
"version:remove:components": "node scripts/manage-versions.mjs remove components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.0",
|
||||
"@docusaurus/core": "^3.10.0",
|
||||
"@docusaurus/faster": "^3.10.0",
|
||||
"@docusaurus/plugin-client-redirects": "^3.10.0",
|
||||
"@docusaurus/preset-classic": "3.10.0",
|
||||
"@docusaurus/theme-live-codeblock": "^3.10.0",
|
||||
"@docusaurus/theme-mermaid": "^3.10.0",
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@docusaurus/core": "^3.10.1",
|
||||
"@docusaurus/faster": "^3.10.1",
|
||||
"@docusaurus/plugin-client-redirects": "^3.10.1",
|
||||
"@docusaurus/preset-classic": "3.10.1",
|
||||
"@docusaurus/theme-live-codeblock": "^3.10.1",
|
||||
"@docusaurus/theme-mermaid": "^3.10.1",
|
||||
"@emotion/core": "^11.0.0",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
@@ -67,9 +67,9 @@
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.30",
|
||||
"@swc/core": "^1.15.32",
|
||||
"antd": "^6.3.7",
|
||||
"baseline-browser-mapping": "^2.10.23",
|
||||
"baseline-browser-mapping": "^2.10.24",
|
||||
"caniuse-lite": "^1.0.30001791",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.1",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.1",
|
||||
@@ -86,14 +86,14 @@
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.18",
|
||||
"swagger-ui-react": "^5.32.4",
|
||||
"swagger-ui-react": "^5.32.5",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.10.0",
|
||||
"@docusaurus/tsconfig": "^3.10.0",
|
||||
"@docusaurus/module-type-aliases": "^3.10.1",
|
||||
"@docusaurus/tsconfig": "^3.10.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
@@ -106,7 +106,7 @@
|
||||
"globals": "^17.5.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"typescript-eslint": "^8.59.1",
|
||||
"webpack": "^5.106.2"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -124,8 +124,7 @@
|
||||
"resolutions": {
|
||||
"react-redux": "^9.2.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"webpackbar": "^7.0.0"
|
||||
"baseline-browser-mapping": "^2.9.19"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
155
docs/src/components/GetStartedSplitButton.tsx
Normal file
155
docs/src/components/GetStartedSplitButton.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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 { DownOutlined } from '@ant-design/icons';
|
||||
import Link from '@docusaurus/Link';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
import { mq } from '../utils.js';
|
||||
|
||||
const getStartedMenuItems: MenuProps['items'] = [
|
||||
{ key: 'users', label: <Link to="/user-docs/">Users</Link> },
|
||||
{ key: 'admins', label: <Link to="/admin-docs/">Admins</Link> },
|
||||
{ key: 'developers', label: <Link to="/developer-docs/">Developers</Link> },
|
||||
];
|
||||
|
||||
const Root = styled.div<{ $variant: 'hero' | 'navbar' }>`
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-weight: bold;
|
||||
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `
|
||||
width: 208px;
|
||||
margin: 15px auto 0;
|
||||
font-size: 20px;
|
||||
${mq[1]} {
|
||||
font-size: 19px;
|
||||
width: 214px;
|
||||
}
|
||||
`
|
||||
: `
|
||||
width: 176px;
|
||||
margin-right: 20px;
|
||||
font-size: 18px;
|
||||
`}
|
||||
|
||||
.split-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
min-width: 0;
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `padding: 10px 10px;`
|
||||
: `padding: 7px 8px;`}
|
||||
}
|
||||
|
||||
.split-main:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.split-divider {
|
||||
width: 1px;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `margin: 8px 0;`
|
||||
: `margin: 6px 0;`}
|
||||
}
|
||||
|
||||
.split-dropdown-trigger {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `
|
||||
width: 44px;
|
||||
font-size: 11px;
|
||||
${mq[1]} {
|
||||
width: 46px;
|
||||
}
|
||||
`
|
||||
: `
|
||||
width: 38px;
|
||||
font-size: 10px;
|
||||
`}
|
||||
}
|
||||
|
||||
.split-dropdown-trigger:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
`;
|
||||
|
||||
export type GetStartedSplitButtonProps = {
|
||||
variant: 'hero' | 'navbar';
|
||||
/** Classes for the outer control (include default-button-theme get-started-split) */
|
||||
rootClassName: string;
|
||||
};
|
||||
|
||||
export default function GetStartedSplitButton({
|
||||
variant,
|
||||
rootClassName,
|
||||
}: GetStartedSplitButtonProps) {
|
||||
const menuClassName = `get-started-split-dropdown-menu get-started-split-dropdown-menu--${variant}`;
|
||||
|
||||
return (
|
||||
<Root $variant={variant} className={rootClassName}>
|
||||
<Link to="/user-docs/" className="split-main">
|
||||
Get Started
|
||||
</Link>
|
||||
<span className="split-divider" aria-hidden />
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: getStartedMenuItems,
|
||||
className: menuClassName,
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="split-dropdown-trigger"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Choose documentation: Users, Admins, or Developers"
|
||||
>
|
||||
<DownOutlined />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import databaseData from '../data/databases.json';
|
||||
import BlurredSection from '../components/BlurredSection';
|
||||
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
|
||||
import type { DatabaseData } from '../components/databases/types';
|
||||
import GetStartedSplitButton from '../components/GetStartedSplitButton';
|
||||
import '../styles/main.css';
|
||||
|
||||
// Build database list from databases.json (databases with logos)
|
||||
@@ -191,20 +192,6 @@ const StyledTitleContainer = styled('div')`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Link)`
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
width: 170px;
|
||||
padding: 10px 0;
|
||||
margin: 15px auto 0;
|
||||
${mq[1]} {
|
||||
font-size: 19px;
|
||||
width: 175px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledScreenshotContainer = styled('div')`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -717,9 +704,10 @@ export default function Home(): JSX.Element {
|
||||
</span>
|
||||
</div>
|
||||
<img src="/img/community/line.png" alt="line" />
|
||||
<StyledButton className="default-button-theme" href="/user-docs/intro">
|
||||
Get Started
|
||||
</StyledButton>
|
||||
<GetStartedSplitButton
|
||||
variant="hero"
|
||||
rootClassName="default-button-theme get-started-split"
|
||||
/>
|
||||
</div>
|
||||
<StyledScreenshotContainer>
|
||||
<img
|
||||
|
||||
@@ -105,6 +105,45 @@ a > span > svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Homepage split "Get started": gradient button + chevron column */
|
||||
.default-button-theme.get-started-split {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.get-started-split-dropdown-menu.ant-dropdown-menu {
|
||||
background: linear-gradient(180deg, #20a7c9 0%, #0c8fae 100%) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.get-started-split-dropdown-menu--hero.ant-dropdown-menu {
|
||||
min-width: 208px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.get-started-split-dropdown-menu--hero.ant-dropdown-menu {
|
||||
min-width: 214px;
|
||||
}
|
||||
}
|
||||
|
||||
.get-started-split-dropdown-menu--navbar.ant-dropdown-menu {
|
||||
min-width: 176px;
|
||||
}
|
||||
|
||||
.get-started-split-dropdown-menu .ant-dropdown-menu-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.get-started-split-dropdown-menu .ant-dropdown-menu-item:hover,
|
||||
.get-started-split-dropdown-menu .ant-dropdown-menu-item-active {
|
||||
background: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.get-started-split-dropdown-menu .ant-dropdown-menu-item a {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
|
||||
.navbar {
|
||||
@@ -117,11 +156,14 @@ a > span > svg {
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
width: 142px;
|
||||
padding: 7px 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.navbar .get-started-button.get-started-split {
|
||||
width: 176px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar .github-button {
|
||||
background-image: url('/img/github.png');
|
||||
background-size: contain;
|
||||
|
||||
41
docs/src/theme/NavbarItem/ComponentTypes.tsx
Normal file
41
docs/src/theme/NavbarItem/ComponentTypes.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 DocNavbarItem from '@theme-original/NavbarItem/DocNavbarItem';
|
||||
import DocSidebarNavbarItem from '@theme-original/NavbarItem/DocSidebarNavbarItem';
|
||||
import DocsVersionDropdownNavbarItem from '@theme-original/NavbarItem/DocsVersionDropdownNavbarItem';
|
||||
import DocsVersionNavbarItem from '@theme-original/NavbarItem/DocsVersionNavbarItem';
|
||||
import DropdownNavbarItem from '@theme-original/NavbarItem/DropdownNavbarItem';
|
||||
import DefaultNavbarItem from '@theme-original/NavbarItem/DefaultNavbarItem';
|
||||
import HtmlNavbarItem from '@theme-original/NavbarItem/HtmlNavbarItem';
|
||||
import LocaleDropdownNavbarItem from '@theme-original/NavbarItem/LocaleDropdownNavbarItem';
|
||||
import SearchNavbarItem from '@theme-original/NavbarItem/SearchNavbarItem';
|
||||
import GetStartedSplitNavbarItem from './GetStartedSplitNavbarItem';
|
||||
|
||||
export default {
|
||||
default: DefaultNavbarItem,
|
||||
localeDropdown: LocaleDropdownNavbarItem,
|
||||
search: SearchNavbarItem,
|
||||
dropdown: DropdownNavbarItem,
|
||||
html: HtmlNavbarItem,
|
||||
doc: DocNavbarItem,
|
||||
docSidebar: DocSidebarNavbarItem,
|
||||
docsVersion: DocsVersionNavbarItem,
|
||||
docsVersionDropdown: DocsVersionDropdownNavbarItem,
|
||||
'custom-getStartedSplit': GetStartedSplitNavbarItem,
|
||||
};
|
||||
29
docs/src/theme/NavbarItem/GetStartedSplitNavbarItem.tsx
Normal file
29
docs/src/theme/NavbarItem/GetStartedSplitNavbarItem.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 GetStartedSplitButton from '../../components/GetStartedSplitButton';
|
||||
import '../../styles/main.css';
|
||||
|
||||
export default function GetStartedSplitNavbarItem() {
|
||||
return (
|
||||
<GetStartedSplitButton
|
||||
variant="navbar"
|
||||
rootClassName="default-button-theme get-started-split get-started-button"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -147,7 +147,9 @@ export default function Root({ children }) {
|
||||
const button = event.target.closest('.get-started-button, .default-button-theme');
|
||||
if (button) {
|
||||
const buttonText = button.textContent?.trim() || 'Unknown';
|
||||
const href = button.getAttribute('href') || '';
|
||||
const clickedLink = event.target.closest?.('a');
|
||||
const href =
|
||||
clickedLink?.getAttribute('href') || button.getAttribute('href') || '';
|
||||
trackEvent('CTA', 'Click', `${buttonText} - ${href}`);
|
||||
}
|
||||
};
|
||||
|
||||
1374
docs/yarn.lock
1374
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -70,13 +70,13 @@ dependencies = [
|
||||
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
|
||||
"marshmallow>=3.0, <4",
|
||||
"marshmallow-union>=0.1",
|
||||
"msgpack>=1.0.0, <1.1",
|
||||
"msgpack>=1.0.0, <1.2",
|
||||
"nh3>=0.2.11, <0.3",
|
||||
"numpy>1.23.5, <2.3",
|
||||
"packaging",
|
||||
# --------------------------
|
||||
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
||||
"pandas[excel]>=2.1.4, <2.2",
|
||||
"pandas[excel]>=2.1.4, <2.4",
|
||||
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
||||
# --------------------------
|
||||
"parsedatetime",
|
||||
@@ -109,7 +109,7 @@ dependencies = [
|
||||
"watchdog>=6.0.0",
|
||||
"wtforms>=2.3.3, <4",
|
||||
"wtforms-json",
|
||||
"xlsxwriter>=3.0.7, <3.1",
|
||||
"xlsxwriter>=3.0.7, <3.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -145,7 +145,7 @@ solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.12, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = ["fastmcp>=3.1.0,<4.0"]
|
||||
fastmcp = ["fastmcp>=3.2.4,<4.0"]
|
||||
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=23.9.1"]
|
||||
@@ -175,7 +175,7 @@ oracle = ["cx-Oracle>8.0.0, <8.1"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <6.0.0"]
|
||||
playwright = ["playwright>=1.37.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.9"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.328.0"]
|
||||
prophet = ["prophet>=1.1.6, <2"]
|
||||
|
||||
@@ -339,7 +339,7 @@ python-dateutil==2.9.0.post0
|
||||
# holidays
|
||||
# pandas
|
||||
# shillelagh
|
||||
python-dotenv==1.1.0
|
||||
python-dotenv==1.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
pytz==2025.2
|
||||
# via
|
||||
|
||||
@@ -236,7 +236,7 @@ et-xmlfile==2.0.0
|
||||
# openpyxl
|
||||
exceptiongroup==1.3.0
|
||||
# via fastmcp
|
||||
fastmcp==3.1.0
|
||||
fastmcp==3.2.4
|
||||
# via apache-superset
|
||||
filelock==3.20.3
|
||||
# via
|
||||
@@ -379,6 +379,8 @@ greenlet==3.1.1
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
griffelib==2.0.2
|
||||
# via fastmcp
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -705,7 +707,7 @@ protobuf==4.25.8
|
||||
# proto-plus
|
||||
psutil==6.1.0
|
||||
# via apache-superset
|
||||
psycopg2-binary==2.9.9
|
||||
psycopg2-binary==2.9.12
|
||||
# via apache-superset
|
||||
py-key-value-aio==0.4.4
|
||||
# via fastmcp
|
||||
@@ -825,7 +827,7 @@ python-dateutil==2.9.0.post0
|
||||
# pyhive
|
||||
# shillelagh
|
||||
# trino
|
||||
python-dotenv==1.1.0
|
||||
python-dotenv==1.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -45,7 +45,17 @@ if [ ${#js_ts_files[@]} -gt 0 ]; then
|
||||
# Skip custom OXC build in pre-commit for speed
|
||||
export SKIP_CUSTOM_OXC=true
|
||||
# Use quiet mode in pre-commit to reduce noise (only show errors)
|
||||
npx oxlint --config oxlint.json --fix --quiet "${js_ts_files[@]}"
|
||||
# Capture output so we can treat "No files found" (all files ignored by
|
||||
# ignorePatterns) as success rather than a false-positive failure.
|
||||
output=$(npx oxlint --config oxlint.json --fix --quiet "${js_ts_files[@]}" 2>&1) || {
|
||||
if echo "$output" | grep -q "No files found"; then
|
||||
echo "No files to lint after applying ignore patterns"
|
||||
exit 0
|
||||
fi
|
||||
echo "$output" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -n "$output" ] && echo "$output"
|
||||
else
|
||||
echo "No JavaScript/TypeScript files to lint"
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-core"
|
||||
version = "0.1.0rc2"
|
||||
version = "0.1.0rc3"
|
||||
description = "Core Python package for building Apache Superset backend extensions and integrations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-extensions-cli"
|
||||
version = "0.1.0rc2"
|
||||
version = "0.1.0rc3"
|
||||
description = "Official command-line interface for building, bundling, and managing Apache Superset extensions"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
10717
superset-frontend/package-lock.json
generated
10717
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -183,7 +183,7 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"mapbox-gl": "^3.22.0",
|
||||
"mapbox-gl": "^3.23.0",
|
||||
"markdown-to-jsx": "^9.7.16",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^5.2.1",
|
||||
@@ -194,13 +194,13 @@
|
||||
"pretty-ms": "^9.3.0",
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-arborist": "^3.5.0",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-json-tree": "^0.20.0",
|
||||
@@ -211,7 +211,6 @@
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-split": "^2.0.9",
|
||||
"react-table": "^7.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
@@ -244,14 +243,13 @@
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-env": "^7.29.3",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/register": "^7.23.7",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@babel/runtime-corejs3": "^7.29.2",
|
||||
"@babel/types": "^7.28.6",
|
||||
"@cypress/react": "^8.0.2",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
@@ -270,13 +268,12 @@
|
||||
"@storybook/test": "^8.6.18",
|
||||
"@storybook/test-runner": "^0.17.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.30",
|
||||
"@swc/plugin-emotion": "^14.8.0",
|
||||
"@swc/core": "^1.15.32",
|
||||
"@swc/plugin-emotion": "^14.9.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -286,8 +283,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^17.0.83",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
@@ -299,14 +296,14 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"babel-jest": "^30.0.2",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"baseline-browser-mapping": "^2.10.21",
|
||||
"baseline-browser-mapping": "^2.10.24",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
@@ -323,7 +320,7 @@
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest-dom": "^5.5.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.3",
|
||||
@@ -346,7 +343,7 @@
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.61.0",
|
||||
"oxlint": "^1.62.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
@@ -361,7 +358,6 @@
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.5.0",
|
||||
"thread-loader": "^4.0.4",
|
||||
"ts-jest": "^29.4.9",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.21.0",
|
||||
@@ -374,7 +370,7 @@
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.3",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-sources": "^3.4.0",
|
||||
"webpack-sources": "^3.4.1",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"lodash-es": "^4.18.1",
|
||||
"yeoman-generator": "^8.1.2",
|
||||
"yeoman-generator": "^8.2.2",
|
||||
"yosay": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"jest": "^30.3.0",
|
||||
"yeoman-test": "^11.3.1"
|
||||
"yeoman-test": "^11.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.1.0-rc2",
|
||||
"version": "0.1.0-rc3",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
@@ -75,16 +75,15 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-env": "^7.29.3",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
@@ -98,8 +97,8 @@
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*",
|
||||
"lodash": "^4.18.1",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ReactElement } from 'react';
|
||||
import { render, RenderOptions } from '@testing-library/react';
|
||||
import { render, RenderOptions, RenderResult } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { themeObject } from './theme';
|
||||
|
||||
@@ -33,7 +33,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => (
|
||||
const customRender = (
|
||||
ui: ReactElement,
|
||||
options?: Omit<RenderOptions, 'wrapper'>,
|
||||
) => render(ui, { wrapper: Providers, ...options });
|
||||
): RenderResult => render(ui, { wrapper: Providers, ...options });
|
||||
|
||||
export {
|
||||
createEvent,
|
||||
|
||||
@@ -29,14 +29,20 @@ import '@fontsource/ibm-plex-mono/600.css';
|
||||
/* eslint-enable import/extensions */
|
||||
|
||||
import { css, useTheme, Global } from '@emotion/react';
|
||||
import { useThemeMode } from './utils/themeUtils';
|
||||
|
||||
export const GlobalStyles = () => {
|
||||
const theme = useTheme();
|
||||
const isDark = useThemeMode();
|
||||
return (
|
||||
<Global
|
||||
key={`global-${theme.colorLink}`}
|
||||
styles={css`
|
||||
// SPA
|
||||
html {
|
||||
color-scheme: ${isDark ? 'dark' : 'light'};
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { theme as antdTheme } from 'antd';
|
||||
import {
|
||||
|
||||
@@ -33,17 +33,16 @@
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
"memoize-one": "^5.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^17.0.2"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
@@ -91,10 +91,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
@@ -102,8 +101,8 @@
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^5.26.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*"
|
||||
},
|
||||
|
||||
@@ -17,19 +17,15 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
Tooltip,
|
||||
type TooltipPlacement,
|
||||
type IconType,
|
||||
} from '@superset-ui/core/components';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
|
||||
export interface ActionProps {
|
||||
label: string;
|
||||
tooltip?: string | ReactElement;
|
||||
placement?: TooltipPlacement;
|
||||
icon: IconType;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useJsonValidation } from './useJsonValidation';
|
||||
|
||||
describe('useJsonValidation', () => {
|
||||
|
||||
@@ -16,16 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
RefObject,
|
||||
forwardRef,
|
||||
ComponentType,
|
||||
ForwardRefExoticComponent,
|
||||
PropsWithoutRef,
|
||||
RefAttributes,
|
||||
} from 'react';
|
||||
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
|
||||
|
||||
import { Loading } from '../Loading';
|
||||
import type { PlaceholderProps } from './types';
|
||||
@@ -93,15 +84,16 @@ export function AsyncEsmComponent<
|
||||
return promise;
|
||||
}
|
||||
|
||||
type AsyncComponent = ForwardRefExoticComponent<
|
||||
PropsWithoutRef<FullProps> & RefAttributes<ComponentType<FullProps>>
|
||||
type AsyncComponent = React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<FullProps> & React.RefAttributes<unknown>
|
||||
> & {
|
||||
preload?: typeof waitForPromise;
|
||||
};
|
||||
|
||||
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
|
||||
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
|
||||
props: FullProps,
|
||||
ref: RefObject<ComponentType<FullProps>>,
|
||||
ref,
|
||||
) {
|
||||
const [loaded, setLoaded] = useState(component !== undefined);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
ButtonVariantType,
|
||||
ButtonColorType,
|
||||
} from 'antd/es/button';
|
||||
import { IconType } from '@superset-ui/core/components/Icons/types';
|
||||
import type { TooltipPlacement } from '../Tooltip/types';
|
||||
|
||||
export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
|
||||
@@ -49,5 +48,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
|
||||
buttonStyle?: ButtonStyle;
|
||||
cta?: boolean;
|
||||
showMarginRight?: boolean;
|
||||
icon?: IconType;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Component = (props: DropdownContainerProps) => {
|
||||
const [overflowingState, setOverflowingState] = useState<OverflowingState>();
|
||||
const containerRef = useRef<DropdownRef>(null);
|
||||
const onOverflowingStateChange = useCallback(
|
||||
value => {
|
||||
(value: OverflowingState) => {
|
||||
if (!isEqual(overflowingState, value)) {
|
||||
setItems(generateItems(value));
|
||||
setOverflowingState(value);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
|
||||
import { IconType } from '../Icons';
|
||||
|
||||
/**
|
||||
* Container item.
|
||||
@@ -70,7 +69,7 @@ export interface DropdownContainerProps {
|
||||
/**
|
||||
* Icon of the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerIcon?: IconType;
|
||||
dropdownTriggerIcon?: ReactNode;
|
||||
/**
|
||||
* Text of the dropdown trigger.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { fireEvent, render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { useState } from 'react';
|
||||
import { DynamicEditableTitle } from '.';
|
||||
|
||||
const Harness = ({ initialTitle = 'Original' }: { initialTitle?: string }) => {
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
return (
|
||||
<DynamicEditableTitle
|
||||
title={title}
|
||||
placeholder="placeholder"
|
||||
canEdit
|
||||
label="Title"
|
||||
onSave={setTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
test('rapid typing then backspacing keeps every keystroke', async () => {
|
||||
render(<Harness />);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
userEvent.click(input);
|
||||
await userEvent.type(input, 'abc', { delay: 1 });
|
||||
expect(input.value).toBe('Originalabc');
|
||||
await userEvent.type(input, '{backspace}{backspace}{backspace}', {
|
||||
delay: 1,
|
||||
});
|
||||
expect(input.value).toBe('Original');
|
||||
});
|
||||
|
||||
test('a change event that arrives before isEditing flips is not dropped', () => {
|
||||
// Reproduces the regression: the input is focused but `isEditing` is still
|
||||
// false because no click has been registered yet (e.g. focus arrived via
|
||||
// tab, autofocus, or programmatic focus). The pre-fix `handleChange`
|
||||
// bailed out with `!isEditing`, dropping the keystroke. Because the
|
||||
// input is controlled, antd's internal `useMergedState` then resyncs the
|
||||
// DOM value back to the (stale) `props.value`, so the user sees their
|
||||
// typed character disappear. This test fires a raw change event so it
|
||||
// doesn't go through userEvent's implicit click.
|
||||
const onSave = jest.fn();
|
||||
render(
|
||||
<DynamicEditableTitle
|
||||
title="Foo"
|
||||
placeholder="placeholder"
|
||||
canEdit
|
||||
label="Title"
|
||||
onSave={onSave}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: 'FooX' } });
|
||||
expect(input.value).toBe('FooX');
|
||||
});
|
||||
|
||||
test('prop changes mid-edit do not clobber unsaved typing', async () => {
|
||||
const { rerender } = render(<Harness initialTitle="Foo" />);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
userEvent.click(input);
|
||||
await userEvent.type(input, 'X', { delay: 1 });
|
||||
expect(input.value).toBe('FooX');
|
||||
rerender(<Harness initialTitle="Foo" />);
|
||||
expect(input.value).toBe('FooX');
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
@@ -30,6 +31,7 @@ import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { Input } from '../Input';
|
||||
import type { InputRef } from '../Input';
|
||||
import type { DynamicEditableTitleProps } from './types';
|
||||
|
||||
const titleStyles = (theme: SupersetTheme) => css`
|
||||
@@ -75,8 +77,10 @@ export const DynamicEditableTitle = memo(
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [currentTitle, setCurrentTitle] = useState(title || '');
|
||||
const [inputWidth, setInputWidth] = useState<number>(0);
|
||||
|
||||
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
|
||||
const sizerRef = useRef<HTMLSpanElement>(null);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const { width: containerWidth, ref: containerRef } = useResizeDetector({
|
||||
refreshMode: 'debounce',
|
||||
});
|
||||
@@ -85,27 +89,33 @@ export const DynamicEditableTitle = memo(
|
||||
setCurrentTitle(title);
|
||||
}, [title]);
|
||||
useEffect(() => {
|
||||
if (isEditing && sizerRef?.current) {
|
||||
if (isEditing) {
|
||||
// move cursor and scroll to the end
|
||||
if (sizerRef.current.setSelectionRange) {
|
||||
const { length } = sizerRef.current.value;
|
||||
sizerRef.current.setSelectionRange(length, length);
|
||||
sizerRef.current.scrollLeft = sizerRef.current.scrollWidth;
|
||||
const inputElement = inputRef.current?.input;
|
||||
if (inputElement) {
|
||||
const { length } = inputElement.value;
|
||||
inputElement.setSelectionRange(length, length);
|
||||
inputElement.scrollLeft = inputElement.scrollWidth;
|
||||
}
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
// a trick to make the input grow when user types text
|
||||
// we make additional span component, place it somewhere out of view and copy input
|
||||
// then we can measure the width of that span to resize the input element
|
||||
// we make an additional span component, place it somewhere out of view and
|
||||
// mirror the input value, then measure the span synchronously (pre-paint)
|
||||
// to resize the input element. Reading offsetWidth in a useLayoutEffect
|
||||
// forces a sync layout, so the input width updates in the same commit as
|
||||
// the value change — preventing a flicker frame where the input is shown
|
||||
// with new value but stale width.
|
||||
useLayoutEffect(() => {
|
||||
if (sizerRef?.current) {
|
||||
if (sizerRef.current) {
|
||||
sizerRef.current.textContent = currentTitle || placeholder;
|
||||
setInputWidth(sizerRef.current.offsetWidth);
|
||||
}
|
||||
}, [currentTitle, placeholder, sizerRef]);
|
||||
}, [currentTitle, placeholder]);
|
||||
|
||||
useEffect(() => {
|
||||
const inputElement = sizerRef.current?.input;
|
||||
const inputElement = inputRef.current?.input;
|
||||
|
||||
if (inputElement) {
|
||||
if (inputElement.scrollWidth > inputElement.clientWidth) {
|
||||
@@ -137,9 +147,17 @@ export const DynamicEditableTitle = memo(
|
||||
|
||||
const handleChange = useCallback(
|
||||
(ev: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!canEdit || !isEditing) {
|
||||
if (!canEdit) {
|
||||
return;
|
||||
}
|
||||
// Any change implies the user is editing. Ensure isEditing is true
|
||||
// even if the change event arrives before the click handler has
|
||||
// committed (e.g. focus via tab, autofocus, or batched click+type
|
||||
// events). Otherwise the keystroke would be dropped and the
|
||||
// controlled input would revert to the previous value.
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
setCurrentTitle(ev.target.value);
|
||||
},
|
||||
[canEdit, isEditing],
|
||||
@@ -168,6 +186,7 @@ export const DynamicEditableTitle = memo(
|
||||
}
|
||||
>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-test="editable-title-input"
|
||||
variant="borderless"
|
||||
aria-label={label ?? t('Title')}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type { ReactNode, SyntheticEvent } from 'react';
|
||||
import type { IconType } from '@superset-ui/core/components';
|
||||
|
||||
export type EmptyStateSize = 'small' | 'medium' | 'large';
|
||||
|
||||
@@ -26,7 +25,7 @@ export type EmptyStateProps = {
|
||||
description?: ReactNode;
|
||||
image?: ReactNode | string;
|
||||
buttonText?: ReactNode;
|
||||
buttonIcon?: IconType;
|
||||
buttonIcon?: ReactNode;
|
||||
buttonAction?: (event: SyntheticEvent) => void;
|
||||
/** Controls image size. Defaults to 'medium'. */
|
||||
size?: EmptyStateSize;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Form as AntdForm } from 'antd';
|
||||
import { FormProps } from './types';
|
||||
|
||||
function CustomForm(props: FormProps) {
|
||||
return <AntdForm {...props} />;
|
||||
return <AntdForm {...(props as any)} />;
|
||||
}
|
||||
|
||||
export const Form = Object.assign(CustomForm, {
|
||||
|
||||
@@ -41,7 +41,6 @@ test('renders with monospace prop', () => {
|
||||
|
||||
// test stories from the storybook!
|
||||
test('renders all the storybook gallery variants', () => {
|
||||
// @ts-expect-error: Suppress TypeScript error for LabelGallery usage
|
||||
const { container } = render(<LabelGallery />);
|
||||
const nonInteractiveLabelCount = 4;
|
||||
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { BackgroundPosition } from './ImageLoader';
|
||||
|
||||
export interface LinkProps {
|
||||
to: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export interface ListViewCardProps {
|
||||
|
||||
@@ -194,7 +194,7 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
|
||||
}
|
||||
|
||||
const onResize = useCallback(
|
||||
width => {
|
||||
(width: number | undefined) => {
|
||||
// Calculates the breakpoint width to collapse the bar.
|
||||
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
|
||||
const breakpoint =
|
||||
|
||||
@@ -54,7 +54,7 @@ export function FormModal({
|
||||
}, [onSave, resetForm]);
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
async values => {
|
||||
async (values: object) => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await formSubmitHandler(values);
|
||||
|
||||
@@ -104,6 +104,9 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
// Keep the close button clickable when modal body content uses
|
||||
// position: sticky with elevated z-index (e.g. DatabaseModal header).
|
||||
z-index: ${theme.zIndexPopupBase + 1};
|
||||
}
|
||||
|
||||
.ant-modal-close:hover {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import type { ModalFuncProps } from 'antd';
|
||||
import type { FormInstance, ModalFuncProps } from 'antd';
|
||||
import type { ResizableProps } from 're-resizable';
|
||||
import type { DraggableProps } from 'react-draggable';
|
||||
import { ButtonStyle } from '../Button/types';
|
||||
@@ -68,7 +68,8 @@ export interface StyledModalProps {
|
||||
|
||||
export type { ModalFuncProps };
|
||||
|
||||
export interface FormModalProps extends ModalProps {
|
||||
export interface FormModalProps extends Omit<ModalProps, 'children'> {
|
||||
children: ReactNode | ((form: FormInstance) => ReactNode);
|
||||
initialValues?: object;
|
||||
formSubmitHandler: (values: object) => Promise<void>;
|
||||
onSave: () => void;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
import { ReactNode, ReactElement, memo } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
@@ -118,62 +118,64 @@ export type PageHeaderWithActionsProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export const PageHeaderWithActions = ({
|
||||
editableTitleProps,
|
||||
showTitlePanelItems,
|
||||
certificatiedBadgeProps,
|
||||
showFaveStar,
|
||||
faveStarProps,
|
||||
titlePanelAdditionalItems,
|
||||
rightPanelAdditionalItems,
|
||||
additionalActionsMenu,
|
||||
menuDropdownProps,
|
||||
showMenuDropdown = true,
|
||||
tooltipProps,
|
||||
}: PageHeaderWithActionsProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div css={headerStyles} className="header-with-actions">
|
||||
<div className="title-panel">
|
||||
<DynamicEditableTitle {...editableTitleProps} />
|
||||
{showTitlePanelItems && (
|
||||
<div css={buttonsStyles}>
|
||||
{certificatiedBadgeProps?.certifiedBy && (
|
||||
<CertifiedBadge {...certificatiedBadgeProps} />
|
||||
)}
|
||||
{showFaveStar && <FaveStar {...faveStarProps} />}
|
||||
{titlePanelAdditionalItems}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="right-button-panel">
|
||||
{rightPanelAdditionalItems}
|
||||
<div css={additionalActionsContainerStyles}>
|
||||
{showMenuDropdown && (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
popupRender={() => additionalActionsMenu}
|
||||
{...menuDropdownProps}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
css={menuTriggerStyles}
|
||||
buttonStyle="tertiary"
|
||||
aria-label={t('Menu actions trigger')}
|
||||
tooltip={tooltipProps?.text}
|
||||
placement={tooltipProps?.placement}
|
||||
data-test="actions-trigger"
|
||||
>
|
||||
<Icons.EllipsisOutlined
|
||||
iconColor={theme.colorPrimary}
|
||||
iconSize="l"
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</Dropdown>
|
||||
export const PageHeaderWithActions = memo(
|
||||
({
|
||||
editableTitleProps,
|
||||
showTitlePanelItems,
|
||||
certificatiedBadgeProps,
|
||||
showFaveStar,
|
||||
faveStarProps,
|
||||
titlePanelAdditionalItems,
|
||||
rightPanelAdditionalItems,
|
||||
additionalActionsMenu,
|
||||
menuDropdownProps,
|
||||
showMenuDropdown = true,
|
||||
tooltipProps,
|
||||
}: PageHeaderWithActionsProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div css={headerStyles} className="header-with-actions">
|
||||
<div className="title-panel">
|
||||
<DynamicEditableTitle {...editableTitleProps} />
|
||||
{showTitlePanelItems && (
|
||||
<div css={buttonsStyles}>
|
||||
{certificatiedBadgeProps?.certifiedBy && (
|
||||
<CertifiedBadge {...certificatiedBadgeProps} />
|
||||
)}
|
||||
{showFaveStar && <FaveStar {...faveStarProps} />}
|
||||
{titlePanelAdditionalItems}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="right-button-panel">
|
||||
{rightPanelAdditionalItems}
|
||||
<div css={additionalActionsContainerStyles}>
|
||||
{showMenuDropdown && (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
popupRender={() => additionalActionsMenu}
|
||||
{...menuDropdownProps}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
css={menuTriggerStyles}
|
||||
buttonStyle="tertiary"
|
||||
aria-label={t('Menu actions trigger')}
|
||||
tooltip={tooltipProps?.text}
|
||||
placement={tooltipProps?.placement}
|
||||
data-test="actions-trigger"
|
||||
>
|
||||
<Icons.EllipsisOutlined
|
||||
iconColor={theme.colorPrimary}
|
||||
iconSize="l"
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, fireEvent } from '@superset-ui/core/spec';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { TableInstance, useTable } from 'react-table';
|
||||
import TableCollection from '.';
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export function mapColumns<T extends object>(
|
||||
return columns.map(column => {
|
||||
const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id);
|
||||
return {
|
||||
title: column.Header,
|
||||
title: column.Header as ReactNode,
|
||||
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
|
||||
hidden: column.hidden,
|
||||
key: column.id,
|
||||
@@ -121,7 +121,7 @@ export function mapColumns<T extends object>(
|
||||
column,
|
||||
});
|
||||
}
|
||||
return val;
|
||||
return val as ReactNode;
|
||||
},
|
||||
className: column.className,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
|
||||
import { TableView, TableViewProps } from '.';
|
||||
|
||||
// Mock window.scrollTo to prevent jsdom "Not implemented" errors
|
||||
beforeAll(() => {
|
||||
window.scrollTo = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const mockedProps: TableViewProps = {
|
||||
columns: [
|
||||
{
|
||||
@@ -125,27 +133,25 @@ test('should change page when pagination is clicked', async () => {
|
||||
expect(screen.getByText('Emily')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
await userEvent.click(screen.getByTitle('Next Page'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText('Kate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
|
||||
|
||||
const page1 = screen.getByRole('listitem', { name: '1' });
|
||||
await userEvent.click(page1);
|
||||
await userEvent.click(screen.getByTitle('Previous Page'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('27')).toBeInTheDocument();
|
||||
expect(screen.getByText('Emily')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('27')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should sort by age', async () => {
|
||||
@@ -240,8 +246,7 @@ test('should handle server-side pagination', async () => {
|
||||
render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
await userEvent.click(screen.getByTitle('Next Page'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onServerPagination).toHaveBeenCalledWith({
|
||||
@@ -301,9 +306,7 @@ test('should scroll to top when scrollTopOnPagination is true', async () => {
|
||||
};
|
||||
render(<TableView {...scrollProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
await userEvent.click(screen.getByTitle('Next Page'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
||||
@@ -324,9 +327,7 @@ test('should NOT scroll to top when scrollTopOnPagination is false', async () =>
|
||||
};
|
||||
render(<TableView {...scrollProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
await userEvent.click(screen.getByTitle('Next Page'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { memo, useEffect, useRef, useMemo, useCallback, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
|
||||
import { useFilters, useSortBy, useTable } from 'react-table';
|
||||
import { Empty } from '@superset-ui/core/components';
|
||||
import TableCollection from '@superset-ui/core/components/TableCollection';
|
||||
import { TableSize } from '@superset-ui/core/components/Table';
|
||||
@@ -117,43 +117,45 @@ const RawTableView = ({
|
||||
...props
|
||||
}: TableViewProps) => {
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const effectivePageSize = initialPageSize ?? DEFAULT_PAGE_SIZE;
|
||||
const [pageIndex, setPageIndex] = useState(initialPageIndex ?? 0);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageIndex: initialPageIndex ?? 0,
|
||||
pageSize: effectivePageSize,
|
||||
pageIndex: 0,
|
||||
sortBy: initialSortBy,
|
||||
}),
|
||||
[initialPageSize, initialPageIndex, initialSortBy],
|
||||
[effectivePageSize, initialSortBy],
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
rows,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
setSortBy,
|
||||
state: { pageIndex, sortBy },
|
||||
state: { sortBy },
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState,
|
||||
manualPagination: serverPagination,
|
||||
manualPagination: true,
|
||||
manualSortBy: serverPagination,
|
||||
pageCount: serverPagination
|
||||
? Math.ceil(totalCount / initialState.pageSize)
|
||||
: undefined,
|
||||
autoResetSortBy: false,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
...(withPagination ? [usePagination] : []),
|
||||
);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!withPagination || serverPagination) return rows;
|
||||
const start = pageIndex * effectivePageSize;
|
||||
return rows.slice(start, start + effectivePageSize);
|
||||
}, [withPagination, serverPagination, rows, pageIndex, effectivePageSize]);
|
||||
|
||||
const EmptyWrapperComponent = useMemo(() => {
|
||||
switch (emptyWrapperType) {
|
||||
case EmptyWrapperType.Small:
|
||||
@@ -164,11 +166,6 @@ const RawTableView = ({
|
||||
}
|
||||
}, [emptyWrapperType]);
|
||||
|
||||
const content = useMemo(
|
||||
() => (withPagination ? page : rows),
|
||||
[withPagination, page, rows],
|
||||
);
|
||||
|
||||
const isEmpty = useMemo(
|
||||
() => !loading && content.length === 0,
|
||||
[loading, content.length],
|
||||
@@ -192,10 +189,9 @@ const RawTableView = ({
|
||||
const handlePageChange = useCallback(
|
||||
(p: number) => {
|
||||
if (scrollTopOnPagination) handleScrollToTop();
|
||||
|
||||
gotoPage(p);
|
||||
setPageIndex(p);
|
||||
},
|
||||
[scrollTopOnPagination, handleScrollToTop, gotoPage],
|
||||
[scrollTopOnPagination, handleScrollToTop],
|
||||
);
|
||||
|
||||
const paginationProps = useMemo(() => {
|
||||
@@ -211,7 +207,7 @@ const RawTableView = ({
|
||||
if (serverPagination) {
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageSize: effectivePageSize,
|
||||
totalCount,
|
||||
onPageChange: handlePageChange,
|
||||
};
|
||||
@@ -219,7 +215,7 @@ const RawTableView = ({
|
||||
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageSize: effectivePageSize,
|
||||
totalCount: data.length,
|
||||
onPageChange: handlePageChange,
|
||||
};
|
||||
@@ -227,28 +223,28 @@ const RawTableView = ({
|
||||
withPagination,
|
||||
serverPagination,
|
||||
pageIndex,
|
||||
initialPageSize,
|
||||
effectivePageSize,
|
||||
totalCount,
|
||||
data.length,
|
||||
handlePageChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination && pageIndex !== initialState.pageIndex) {
|
||||
if (serverPagination && pageIndex !== (initialPageIndex ?? 0)) {
|
||||
onServerPagination({
|
||||
pageIndex,
|
||||
});
|
||||
}
|
||||
}, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
|
||||
}, [initialPageIndex, onServerPagination, pageIndex, serverPagination]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
|
||||
if (serverPagination && !isEqual(sortBy, initialSortBy)) {
|
||||
onServerPagination({
|
||||
pageIndex: 0,
|
||||
sortBy,
|
||||
});
|
||||
}
|
||||
}, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
|
||||
}, [initialSortBy, onServerPagination, serverPagination, sortBy]);
|
||||
|
||||
return (
|
||||
<TableViewStyles {...props} ref={tableRef}>
|
||||
|
||||
@@ -97,8 +97,8 @@ const StyledPlus = styled.span`
|
||||
|
||||
export default function TruncatedList<ListItemType>({
|
||||
items,
|
||||
renderVisibleItem = item => item,
|
||||
renderTooltipItem = item => item,
|
||||
renderVisibleItem = item => item as ReactNode,
|
||||
renderTooltipItem = item => item as ReactNode,
|
||||
getKey = item => item as unknown as Key,
|
||||
maxLinks = 20,
|
||||
}: TruncatedListProps<ListItemType>) {
|
||||
|
||||
@@ -51,6 +51,7 @@ const SupersetClient: SupersetClientInterface = {
|
||||
reAuthenticate: () => getInstance().reAuthenticate(),
|
||||
request: request => getInstance().request(request),
|
||||
getCSRFToken: () => getInstance().getCSRFToken(),
|
||||
getUrl: (...args) => getInstance().getUrl(...args),
|
||||
};
|
||||
|
||||
export default SupersetClient;
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface SupersetClientInterface extends Pick<
|
||||
| 'isAuthenticated'
|
||||
| 'reAuthenticate'
|
||||
| 'getGuestToken'
|
||||
| 'getUrl'
|
||||
> {
|
||||
configure: (config?: ClientConfig) => SupersetClientInterface;
|
||||
reset: () => void;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useChangeEffect } from './useChangeEffect';
|
||||
|
||||
test('call callback the first time with undefined and value', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useComponentDidMount } from './useComponentDidMount';
|
||||
|
||||
test('the effect should only be executed on the first render', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useComponentDidUpdate } from './useComponentDidUpdate';
|
||||
|
||||
test('the effect should not be executed on the first render', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useElementOnScreen } from './useElementOnScreen';
|
||||
|
||||
const observeMock = jest.fn();
|
||||
@@ -46,10 +46,9 @@ test('should return isSticky as true when intersectionRatio < 1', async () => {
|
||||
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
|
||||
);
|
||||
const callback = IntersectionObserverMock.mock.calls[0][0];
|
||||
const callBack = callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
|
||||
const observer = new IntersectionObserverMock(callBack, {});
|
||||
const newDiv = document.createElement('div');
|
||||
observer.observe(newDiv);
|
||||
act(() => {
|
||||
callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
|
||||
});
|
||||
expect(hook.result.current[1]).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -58,10 +57,9 @@ test('should return isSticky as false when intersectionRatio >= 1', async () =>
|
||||
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
|
||||
);
|
||||
const callback = IntersectionObserverMock.mock.calls[0][0];
|
||||
const callBack = callback([{ isIntersecting: true, intersectionRatio: 1 }]);
|
||||
const observer = new IntersectionObserverMock(callBack, {});
|
||||
const newDiv = document.createElement('div');
|
||||
observer.observe(newDiv);
|
||||
act(() => {
|
||||
callback([{ isIntersecting: true, intersectionRatio: 1 }]);
|
||||
});
|
||||
expect(hook.result.current[1]).toEqual(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { usePrevious } from './usePrevious';
|
||||
|
||||
test('get undefined on the first render when initialValue is not defined', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useCSSTextTruncation from './useCSSTextTruncation';
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RefObject } from 'react';
|
||||
import useChildElementTruncation from './useChildElementTruncation';
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PREVIEW_TIME = new Date(Date.UTC(2017, 1, 14, 11, 22, 33));
|
||||
// Use type augmentation to indicate that
|
||||
// an instance of TimeFormatter is also a function
|
||||
interface TimeFormatter {
|
||||
(value: Date | number | null | undefined): string;
|
||||
(value: Date | number | string | null | undefined): string;
|
||||
}
|
||||
|
||||
class TimeFormatter extends ExtensibleFunction {
|
||||
@@ -49,7 +49,9 @@ class TimeFormatter extends ExtensibleFunction {
|
||||
formatFunc: TimeFormatFunction;
|
||||
useLocalTime?: boolean;
|
||||
}) {
|
||||
super((value: Date | number | null | undefined) => this.format(value));
|
||||
super((value: Date | number | string | null | undefined) =>
|
||||
this.format(value),
|
||||
);
|
||||
|
||||
const {
|
||||
id = isRequired('config.id'),
|
||||
@@ -66,7 +68,7 @@ class TimeFormatter extends ExtensibleFunction {
|
||||
this.useLocalTime = useLocalTime;
|
||||
}
|
||||
|
||||
format(value: Date | number | null | undefined) {
|
||||
format(value: Date | number | string | null | undefined) {
|
||||
return stringifyTimeInput(value, time => this.formatFunc(time));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,18 @@
|
||||
*/
|
||||
|
||||
export default function stringifyTimeInput(
|
||||
value: Date | number | undefined | null,
|
||||
value: Date | number | string | undefined | null,
|
||||
fn: (time: Date) => string,
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
const isIntegerString = /^-?\d+$/.test(trimmed);
|
||||
return fn(new Date(isIntegerString ? Number(trimmed) : value));
|
||||
}
|
||||
|
||||
return fn(value instanceof Date ? value : new Date(value));
|
||||
}
|
||||
|
||||
@@ -249,7 +249,8 @@ export type Extensions = Partial<{
|
||||
'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>;
|
||||
'navbar.right': ComponentType;
|
||||
'report-modal.dropdown.item.icon': ComponentType;
|
||||
'root.context.provider': ComponentType;
|
||||
'root.context.provider': ComponentType<{ children?: ReactNode }>;
|
||||
|
||||
'welcome.message': ComponentType;
|
||||
'welcome.banner': ComponentType;
|
||||
'welcome.main.replacement': ComponentType;
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('SuperChart', () => {
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
|
||||
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
|
||||
expect(CustomFallbackComponent).toHaveBeenCalled();
|
||||
});
|
||||
test('call onErrorBoundary', async () => {
|
||||
expectedErrors = 1;
|
||||
|
||||
@@ -31,33 +31,42 @@ describe('SupersetClient', () => {
|
||||
|
||||
afterEach(() => SupersetClient.reset());
|
||||
|
||||
test('exposes reset, configure, init, get, post, postForm, isAuthenticated, and reAuthenticate methods', () => {
|
||||
test('exposes configure, init, get, post, postForm, delete, put, request, reset, getGuestToken, getCSRFToken, getUrl, isAuthenticated, and reAuthenticate methods', () => {
|
||||
expect(typeof SupersetClient.configure).toBe('function');
|
||||
expect(typeof SupersetClient.init).toBe('function');
|
||||
expect(typeof SupersetClient.get).toBe('function');
|
||||
expect(typeof SupersetClient.post).toBe('function');
|
||||
expect(typeof SupersetClient.postForm).toBe('function');
|
||||
expect(typeof SupersetClient.isAuthenticated).toBe('function');
|
||||
expect(typeof SupersetClient.reAuthenticate).toBe('function');
|
||||
expect(typeof SupersetClient.getGuestToken).toBe('function');
|
||||
expect(typeof SupersetClient.delete).toBe('function');
|
||||
expect(typeof SupersetClient.put).toBe('function');
|
||||
expect(typeof SupersetClient.request).toBe('function');
|
||||
expect(typeof SupersetClient.reset).toBe('function');
|
||||
expect(typeof SupersetClient.getGuestToken).toBe('function');
|
||||
expect(typeof SupersetClient.getCSRFToken).toBe('function');
|
||||
expect(typeof SupersetClient.getUrl).toBe('function');
|
||||
expect(typeof SupersetClient.isAuthenticated).toBe('function');
|
||||
expect(typeof SupersetClient.reAuthenticate).toBe('function');
|
||||
});
|
||||
|
||||
test('throws if you call init, get, post, postForm, isAuthenticated, or reAuthenticate before configure', () => {
|
||||
test('throws if you call init, get, post, postForm, delete, put, request, getGuestToken, getCSRFToken, getUrl, isAuthenticated, or reAuthenticate before configure', () => {
|
||||
expect(SupersetClient.init).toThrow();
|
||||
expect(SupersetClient.get).toThrow();
|
||||
expect(SupersetClient.post).toThrow();
|
||||
expect(SupersetClient.postForm).toThrow();
|
||||
expect(SupersetClient.delete).toThrow();
|
||||
expect(SupersetClient.put).toThrow();
|
||||
expect(SupersetClient.request).toThrow();
|
||||
expect(SupersetClient.getGuestToken).toThrow();
|
||||
expect(SupersetClient.getCSRFToken).toThrow();
|
||||
expect(SupersetClient.getUrl).toThrow();
|
||||
expect(SupersetClient.isAuthenticated).toThrow();
|
||||
expect(SupersetClient.reAuthenticate).toThrow();
|
||||
expect(SupersetClient.request).toThrow();
|
||||
expect(SupersetClient.configure).not.toThrow();
|
||||
});
|
||||
|
||||
// this also tests that the ^above doesn't throw if configure is called appropriately
|
||||
test('calls appropriate SupersetClient methods when configured', async () => {
|
||||
expect.assertions(16);
|
||||
expect.assertions(18);
|
||||
const mockGetUrl = '/mock/get/url';
|
||||
const mockPostUrl = '/mock/post/url';
|
||||
const mockRequestUrl = '/mock/request/url';
|
||||
@@ -88,6 +97,13 @@ describe('SupersetClient', () => {
|
||||
SupersetClientClass.prototype,
|
||||
'getGuestToken',
|
||||
);
|
||||
const getUrlSpy = jest.spyOn(SupersetClientClass.prototype, 'getUrl');
|
||||
|
||||
SupersetClient.configure({ appRoot: '/app' });
|
||||
expect(SupersetClient.getUrl({ endpoint: '/some/path' })).toContain(
|
||||
'/app/some/path',
|
||||
);
|
||||
expect(getUrlSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
SupersetClient.configure({});
|
||||
await SupersetClient.init();
|
||||
@@ -141,6 +157,7 @@ describe('SupersetClient', () => {
|
||||
postSpy.mockRestore();
|
||||
authenticatedSpy.mockRestore();
|
||||
csrfSpy.mockRestore();
|
||||
getUrlSpy.mockRestore();
|
||||
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
});
|
||||
|
||||
@@ -67,6 +67,15 @@ describe('TimeFormatter', () => {
|
||||
test('handles number, treating it as a timestamp', () => {
|
||||
expect(formatter.format(PREVIEW_TIME.getTime())).toEqual('2017');
|
||||
});
|
||||
test('handles numeric string, treating it as a timestamp', () => {
|
||||
// PivotData.processRecord coerces values with String(), turning numeric
|
||||
// timestamps into strings.
|
||||
const timestamp = PREVIEW_TIME.getTime().toString();
|
||||
expect(formatter.format(timestamp)).toEqual('2017');
|
||||
});
|
||||
test('handles ISO-8601 string without misinterpreting it as a number', () => {
|
||||
expect(formatter.format('2017-02-14T11:22:33.000Z')).toEqual('2017');
|
||||
});
|
||||
test('otherwise returns formatted value', () => {
|
||||
expect(formatter.format(PREVIEW_TIME)).toEqual('2017');
|
||||
});
|
||||
|
||||
@@ -39,6 +39,24 @@ export class ImportDatasetModal extends Modal {
|
||||
.setInputFiles(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file buffer to the import modal (no temp file needed)
|
||||
* @param buffer - File contents as a Buffer
|
||||
* @param fileName - Name to use for the uploaded file
|
||||
*/
|
||||
async uploadFileBuffer(
|
||||
buffer: Buffer,
|
||||
fileName: string = 'dataset_export.zip',
|
||||
): Promise<void> {
|
||||
await this.page
|
||||
.locator(ImportDatasetModal.SELECTORS.FILE_INPUT)
|
||||
.setInputFiles({
|
||||
name: fileName,
|
||||
mimeType: 'application/zip',
|
||||
buffer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the overwrite confirmation input (only needed if dataset exists)
|
||||
*/
|
||||
|
||||
Binary file not shown.
@@ -17,9 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { Response, APIResponse } from '@playwright/test';
|
||||
import type { Page, Response, APIResponse } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import * as unzipper from 'unzipper';
|
||||
import { apiGet } from './requests';
|
||||
|
||||
/**
|
||||
* Common interface for response types with status() method.
|
||||
@@ -61,6 +62,35 @@ export function expectStatusOneOf<T extends ResponseLike>(
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll an API endpoint until it returns 404, confirming a resource was deleted.
|
||||
* Shared across chart, dashboard, and dataset list tests.
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param endpoint - API endpoint path (e.g. 'api/v1/dataset/')
|
||||
* @param id - Resource ID to check
|
||||
* @param options - Optional timeout (default 10000ms) and label for error messages
|
||||
*/
|
||||
export async function expectDeleted(
|
||||
page: Page,
|
||||
endpoint: string,
|
||||
id: number,
|
||||
options?: { timeout?: number; label?: string },
|
||||
): Promise<void> {
|
||||
const timeout = options?.timeout ?? 10000;
|
||||
const label = options?.label ?? `Resource ${id}`;
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGet(page, `${endpoint}${id}`, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout, message: `${label} should return 404 after delete` },
|
||||
)
|
||||
.toBe(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the resource ID from a JSON response body.
|
||||
* Handles both `{ result: { id } }` and `{ id }` shapes.
|
||||
|
||||
@@ -203,6 +203,21 @@ export async function apiDeleteDataset(
|
||||
return apiDelete(page, `${ENDPOINTS.DATASET}${datasetId}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export datasets as a zip file via the API.
|
||||
* Uses Rison encoding for the query parameter (required by the endpoint).
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param datasetIds - Array of dataset IDs to export
|
||||
* @returns API response containing the export zip
|
||||
*/
|
||||
export async function apiExportDatasets(
|
||||
page: Page,
|
||||
datasetIds: number[],
|
||||
): Promise<APIResponse> {
|
||||
const query = rison.encode(datasetIds);
|
||||
return apiGet(page, `${ENDPOINTS.DATASET_EXPORT}?q=${query}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate a dataset via the API
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
|
||||
53
superset-frontend/playwright/helpers/api/sqllab.ts
Normal file
53
superset-frontend/playwright/helpers/api/sqllab.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 { Page, APIResponse } from '@playwright/test';
|
||||
import { apiPost, ApiRequestOptions } from './requests';
|
||||
|
||||
const ENDPOINTS = {
|
||||
SQLLAB_EXECUTE: 'api/v1/sqllab/execute/',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Execute a SQL query via SQL Lab API.
|
||||
* Requires `allow_dml=True` on the target database for DDL/DML statements.
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param databaseId - ID of the database to execute against
|
||||
* @param sql - SQL statement to execute
|
||||
* @param schema - Optional schema context for the query
|
||||
* @returns API response from SQL Lab execution
|
||||
*/
|
||||
export async function apiExecuteSql(
|
||||
page: Page,
|
||||
databaseId: number,
|
||||
sql: string,
|
||||
schema?: string,
|
||||
options?: ApiRequestOptions,
|
||||
): Promise<APIResponse> {
|
||||
return apiPost(
|
||||
page,
|
||||
ENDPOINTS.SQLLAB_EXECUTE,
|
||||
{
|
||||
database_id: databaseId,
|
||||
sql,
|
||||
schema: schema ?? null,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class CreateDatasetPage {
|
||||
*/
|
||||
private static readonly SELECTORS = {
|
||||
DATABASE: '[data-test="select-database"]',
|
||||
SCHEMA: '[data-test="Select schema or type to search schemas"]',
|
||||
SCHEMA: '[data-test="Select schema"]',
|
||||
TABLE: '[data-test="Select table or type to search tables"]',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { apiGetChart, ENDPOINTS } from '../../helpers/api/chart';
|
||||
import { createTestChart } from './chart-test-helpers';
|
||||
import { waitForGet, waitForPut } from '../../helpers/api/intercepts';
|
||||
import {
|
||||
expectDeleted,
|
||||
expectStatusOneOf,
|
||||
expectValidExportZip,
|
||||
} from '../../helpers/api/assertions';
|
||||
@@ -88,17 +89,9 @@ test('should delete a chart with confirmation', async ({
|
||||
await expect(chartListPage.getChartRow(chartName)).not.toBeVisible();
|
||||
|
||||
// Backend verification: API returns 404
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetChart(page, chartId, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 10000, message: `Chart ${chartId} should return 404` },
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.CHART, chartId, {
|
||||
label: `Chart ${chartId}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('should edit chart name via properties modal', async ({
|
||||
@@ -246,17 +239,9 @@ test('should bulk delete multiple charts', async ({
|
||||
|
||||
// Backend verification: Both return 404
|
||||
for (const chart of [chart1, chart2]) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetChart(page, chart.id, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 10000, message: `Chart ${chart.id} should return 404` },
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.CHART, chart.id, {
|
||||
label: `Chart ${chart.id}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from '../../components/modals';
|
||||
import { Toast } from '../../components/core';
|
||||
import {
|
||||
apiGetDashboard,
|
||||
apiDeleteDashboard,
|
||||
apiExportDashboards,
|
||||
getDashboardByName,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
import { createTestDashboard } from './dashboard-test-helpers';
|
||||
import { waitForGet, waitForPost } from '../../helpers/api/intercepts';
|
||||
import {
|
||||
expectDeleted,
|
||||
expectStatusOneOf,
|
||||
expectValidExportZip,
|
||||
} from '../../helpers/api/assertions';
|
||||
@@ -97,17 +97,9 @@ test('should delete a dashboard with confirmation', async ({
|
||||
).not.toBeVisible();
|
||||
|
||||
// Backend verification: API returns 404
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetDashboard(page, dashboardId, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 10000, message: `Dashboard ${dashboardId} should return 404` },
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboardId, {
|
||||
label: `Dashboard ${dashboardId}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('should export a dashboard as a zip file', async ({
|
||||
@@ -210,20 +202,9 @@ test('should bulk delete multiple dashboards', async ({
|
||||
|
||||
// Backend verification: Both return 404
|
||||
for (const dashboard of [dashboard1, dashboard2]) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetDashboard(page, dashboard.id, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
message: `Dashboard ${dashboard.id} should return 404`,
|
||||
},
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboard.id, {
|
||||
label: `Dashboard ${dashboard.id}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,20 +289,9 @@ test.describe('import dashboard', () => {
|
||||
await apiDeleteDashboard(page, dashboardId);
|
||||
|
||||
// Verify it's gone
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetDashboard(page, dashboardId, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
message: `Dashboard ${dashboardId} should return 404 after delete`,
|
||||
},
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.DASHBOARD, dashboardId, {
|
||||
label: `Dashboard ${dashboardId}`,
|
||||
});
|
||||
|
||||
// Refresh to confirm dashboard is no longer in the list
|
||||
await dashboardListPage.goto();
|
||||
|
||||
@@ -27,193 +27,190 @@ import { ChartCreationPage } from '../../pages/ChartCreationPage';
|
||||
import { ENDPOINTS } from '../../helpers/api/dataset';
|
||||
import { waitForPost } from '../../helpers/api/intercepts';
|
||||
import { expectStatusOneOf } from '../../helpers/api/assertions';
|
||||
import { apiPostDatabase } from '../../helpers/api/database';
|
||||
import { getDatabaseByName } from '../../helpers/api/database';
|
||||
import { apiExecuteSql } from '../../helpers/api/sqllab';
|
||||
|
||||
interface GsheetsSetupResult {
|
||||
sheetName: string;
|
||||
dbName: string;
|
||||
interface ExamplesSetupResult {
|
||||
tableName: string;
|
||||
dbId: number;
|
||||
createDatasetPage: CreateDatasetPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up gsheets database and navigates to create dataset page.
|
||||
* Skips test if gsheets connector unavailable (test.skip() throws, so no return).
|
||||
* @param testInfo - Test info for parallelIndex to avoid name collisions in parallel runs
|
||||
* @returns Setup result with names and page object
|
||||
* Creates a temporary table in the examples database via SQL Lab,
|
||||
* then navigates to the create dataset wizard with it pre-selected.
|
||||
*
|
||||
* Requires `allow_dml=True` on the examples database (configured in CI setup).
|
||||
*
|
||||
* @param page - Playwright page instance
|
||||
* @param testAssets - Test assets tracker for cleanup
|
||||
* @param testInfo - Test info for parallelIndex to avoid name collisions
|
||||
* @returns Setup result with table name, database ID, and page object
|
||||
*/
|
||||
async function setupGsheetsDataset(
|
||||
async function setupExamplesDataset(
|
||||
page: Page,
|
||||
testAssets: TestAssets,
|
||||
_testAssets: TestAssets,
|
||||
testInfo: TestInfo,
|
||||
): Promise<GsheetsSetupResult> {
|
||||
// Public Google Sheet for testing (published to web, no auth required).
|
||||
// This is a Netflix dataset that is publicly accessible via the Google Visualization API.
|
||||
// NOTE: This sheet is hosted on an external Google account and is not created by the test itself.
|
||||
// If this sheet is deleted, its ID changes, or its sharing settings are restricted,
|
||||
// these tests will start failing when they attempt to create a database pointing at it.
|
||||
// In that case, create or select a new publicly readable test sheet, update `sheetUrl`
|
||||
// to use its URL, and update this comment to describe who owns/maintains that sheet
|
||||
// and the expected access controls (e.g., "anyone with the link can view").
|
||||
const sheetUrl =
|
||||
'https://docs.google.com/spreadsheets/d/19XNqckHGKGGPh83JGFdFGP4Bw9gdXeujq5EoIGwttdM/edit#gid=347941303';
|
||||
// Include parallelIndex to avoid collisions when tests run in parallel
|
||||
): Promise<ExamplesSetupResult> {
|
||||
// Look up the examples database (always available in CI via load_examples)
|
||||
const examplesDb = await getDatabaseByName(page, 'examples');
|
||||
if (!examplesDb) {
|
||||
throw new Error(
|
||||
'Examples database not found. Ensure "superset load_examples" has run.',
|
||||
);
|
||||
}
|
||||
const dbId = examplesDb.id;
|
||||
|
||||
// Create a uniquely-named temporary table via SQL Lab
|
||||
const uniqueSuffix = `${Date.now()}_${testInfo.parallelIndex}`;
|
||||
const sheetName = `test_netflix_${uniqueSuffix}`;
|
||||
const dbName = `test_gsheets_db_${uniqueSuffix}`;
|
||||
const tableName = `test_pw_${uniqueSuffix}`;
|
||||
|
||||
// Create a Google Sheets database via API
|
||||
// The catalog must be in `extra` as JSON with engine_params.catalog format
|
||||
const catalogDict = { [sheetName]: sheetUrl };
|
||||
const createDbRes = await apiPostDatabase(page, {
|
||||
database_name: dbName,
|
||||
engine: 'gsheets',
|
||||
sqlalchemy_uri: 'gsheets://',
|
||||
configuration_method: 'dynamic_form',
|
||||
expose_in_sqllab: true,
|
||||
extra: JSON.stringify({
|
||||
engine_params: {
|
||||
catalog: catalogDict,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Check if gsheets connector is available
|
||||
if (!createDbRes.ok()) {
|
||||
const errorBody = await createDbRes.json();
|
||||
const errorText = JSON.stringify(errorBody);
|
||||
// Skip test if gsheets connector not installed
|
||||
if (
|
||||
errorText.includes('gsheets') ||
|
||||
errorText.includes('No such DB engine')
|
||||
) {
|
||||
await test.info().attach('skip-reason', {
|
||||
body: `Google Sheets connector unavailable: ${errorText}`,
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
test.skip(); // throws, no return needed
|
||||
}
|
||||
throw new Error(`Failed to create gsheets database: ${errorText}`);
|
||||
// CI examples DB is always PostgreSQL, so 'public' is the correct schema.
|
||||
const createTableRes = await apiExecuteSql(
|
||||
page,
|
||||
dbId,
|
||||
`CREATE TABLE ${tableName} AS SELECT 1 AS id, 'test' AS name`,
|
||||
'public',
|
||||
);
|
||||
if (!createTableRes.ok()) {
|
||||
const errorBody = await createTableRes.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
`Failed to create temp table "${tableName}": ${JSON.stringify(errorBody)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const createDbBody = await createDbRes.json();
|
||||
const dbId = createDbBody.result?.id ?? createDbBody.id;
|
||||
if (!dbId) {
|
||||
throw new Error('Database creation did not return an ID');
|
||||
}
|
||||
testAssets.trackDatabase(dbId);
|
||||
|
||||
// Navigate to create dataset page
|
||||
const createDatasetPage = new CreateDatasetPage(page);
|
||||
await createDatasetPage.goto();
|
||||
await createDatasetPage.waitForPageLoad();
|
||||
|
||||
// Select the Google Sheets database
|
||||
await createDatasetPage.selectDatabase(dbName);
|
||||
// Select the examples database, public schema, and temp table.
|
||||
// Schema is 'public' because the CI examples DB is always PostgreSQL.
|
||||
await createDatasetPage.selectDatabase('examples');
|
||||
await createDatasetPage.selectSchema('public');
|
||||
await createDatasetPage.selectTable(tableName);
|
||||
|
||||
// Try to select the sheet - if not found due to timeout, skip
|
||||
try {
|
||||
await createDatasetPage.selectTable(sheetName);
|
||||
} catch (error) {
|
||||
// Only skip on TimeoutError (sheet not loaded); re-throw everything else
|
||||
if (!(error instanceof Error) || error.name !== 'TimeoutError') {
|
||||
throw error;
|
||||
}
|
||||
await test.info().attach('skip-reason', {
|
||||
body: `Table "${sheetName}" not found in dropdown after timeout.`,
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
test.skip(); // throws, no return needed
|
||||
}
|
||||
|
||||
return { sheetName, dbName, createDatasetPage };
|
||||
return { tableName, dbId, createDatasetPage };
|
||||
}
|
||||
|
||||
test('should create a dataset via wizard', async ({ page, testAssets }) => {
|
||||
const { sheetName, createDatasetPage } = await setupGsheetsDataset(
|
||||
/**
|
||||
* Drop a temporary table created during test setup.
|
||||
* Uses failOnStatusCode: false so cleanup doesn't throw if the table was already removed.
|
||||
*/
|
||||
async function dropTempTable(
|
||||
page: Page,
|
||||
dbId: number,
|
||||
tableName: string,
|
||||
): Promise<void> {
|
||||
// Schema matches 'public' used in setupExamplesDataset (CI examples DB is PostgreSQL).
|
||||
await apiExecuteSql(
|
||||
page,
|
||||
testAssets,
|
||||
test.info(),
|
||||
dbId,
|
||||
`DROP TABLE IF EXISTS ${tableName}`,
|
||||
'public',
|
||||
{ failOnStatusCode: false },
|
||||
);
|
||||
}
|
||||
|
||||
// Set up response intercept to capture new dataset ID
|
||||
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
|
||||
pathMatch: true,
|
||||
// Both tests create a temp table and use the dataset wizard, so they must run serially.
|
||||
// Uses test.describe only because Playwright's serial mode API requires it -
|
||||
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
|
||||
test.describe('create dataset wizard', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test('should create a dataset via wizard', async ({ page, testAssets }) => {
|
||||
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(
|
||||
page,
|
||||
testAssets,
|
||||
test.info(),
|
||||
);
|
||||
|
||||
// Set up response intercept to capture new dataset ID
|
||||
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
|
||||
pathMatch: true,
|
||||
});
|
||||
|
||||
// Click "Create and explore dataset" button
|
||||
await createDatasetPage.clickCreateAndExploreDataset();
|
||||
|
||||
// Wait for dataset creation and capture ID for cleanup
|
||||
const createResponse = expectStatusOneOf(
|
||||
await createResponsePromise,
|
||||
[200, 201],
|
||||
);
|
||||
const createBody = await createResponse.json();
|
||||
const newDatasetId = createBody.result?.id ?? createBody.id;
|
||||
|
||||
if (newDatasetId) {
|
||||
testAssets.trackDataset(newDatasetId);
|
||||
}
|
||||
|
||||
// Verify we navigated to Chart Creation page with dataset pre-selected
|
||||
await page.waitForURL(/.*\/chart\/add.*/);
|
||||
const chartCreationPage = new ChartCreationPage(page);
|
||||
await chartCreationPage.waitForPageLoad();
|
||||
|
||||
// Verify the dataset is pre-selected
|
||||
await chartCreationPage.expectDatasetSelected(tableName);
|
||||
|
||||
// Select a visualization type and create chart
|
||||
await chartCreationPage.selectVizType('Table');
|
||||
|
||||
// Click "Create new chart" to go to Explore
|
||||
await chartCreationPage.clickCreateNewChart();
|
||||
|
||||
// Verify we navigated to Explore page
|
||||
await page.waitForURL(/.*\/explore\/.*/);
|
||||
const explorePage = new ExplorePage(page);
|
||||
await explorePage.waitForPageLoad();
|
||||
|
||||
// Verify the dataset name is shown in Explore
|
||||
const loadedDatasetName = await explorePage.getDatasetName();
|
||||
expect(loadedDatasetName).toContain(tableName);
|
||||
|
||||
// Clean up temp table (dataset cleanup handled by testAssets)
|
||||
await dropTempTable(page, dbId, tableName);
|
||||
});
|
||||
|
||||
// Click "Create and explore dataset" button
|
||||
await createDatasetPage.clickCreateAndExploreDataset();
|
||||
|
||||
// Wait for dataset creation and capture ID for cleanup
|
||||
const createResponse = expectStatusOneOf(
|
||||
await createResponsePromise,
|
||||
[200, 201],
|
||||
);
|
||||
const createBody = await createResponse.json();
|
||||
const newDatasetId = createBody.result?.id ?? createBody.id;
|
||||
|
||||
if (newDatasetId) {
|
||||
testAssets.trackDataset(newDatasetId);
|
||||
}
|
||||
|
||||
// Verify we navigated to Chart Creation page with dataset pre-selected
|
||||
await page.waitForURL(/.*\/chart\/add.*/);
|
||||
const chartCreationPage = new ChartCreationPage(page);
|
||||
await chartCreationPage.waitForPageLoad();
|
||||
|
||||
// Verify the dataset is pre-selected
|
||||
await chartCreationPage.expectDatasetSelected(sheetName);
|
||||
|
||||
// Select a visualization type and create chart
|
||||
await chartCreationPage.selectVizType('Table');
|
||||
|
||||
// Click "Create new chart" to go to Explore
|
||||
await chartCreationPage.clickCreateNewChart();
|
||||
|
||||
// Verify we navigated to Explore page
|
||||
await page.waitForURL(/.*\/explore\/.*/);
|
||||
const explorePage = new ExplorePage(page);
|
||||
await explorePage.waitForPageLoad();
|
||||
|
||||
// Verify the dataset name is shown in Explore
|
||||
const loadedDatasetName = await explorePage.getDatasetName();
|
||||
expect(loadedDatasetName).toContain(sheetName);
|
||||
});
|
||||
|
||||
test('should create a dataset without exploring', async ({
|
||||
page,
|
||||
testAssets,
|
||||
}) => {
|
||||
const { sheetName, createDatasetPage } = await setupGsheetsDataset(
|
||||
test('should create a dataset without exploring', async ({
|
||||
page,
|
||||
testAssets,
|
||||
test.info(),
|
||||
);
|
||||
}) => {
|
||||
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(
|
||||
page,
|
||||
testAssets,
|
||||
test.info(),
|
||||
);
|
||||
|
||||
// Set up response intercept to capture dataset ID
|
||||
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
|
||||
pathMatch: true,
|
||||
// Set up response intercept to capture dataset ID
|
||||
const createResponsePromise = waitForPost(page, ENDPOINTS.DATASET, {
|
||||
pathMatch: true,
|
||||
});
|
||||
|
||||
// Click "Create dataset" (not explore)
|
||||
await createDatasetPage.clickCreateDataset();
|
||||
|
||||
// Capture dataset ID from response for cleanup
|
||||
const createResponse = expectStatusOneOf(
|
||||
await createResponsePromise,
|
||||
[200, 201],
|
||||
);
|
||||
const createBody = await createResponse.json();
|
||||
const datasetId = createBody.result?.id ?? createBody.id;
|
||||
if (datasetId) {
|
||||
testAssets.trackDataset(datasetId);
|
||||
}
|
||||
|
||||
// Verify redirect to dataset list (not chart creation)
|
||||
// Note: "Create dataset" action does not show a toast
|
||||
await page.waitForURL(/.*tablemodelview\/list.*/);
|
||||
|
||||
// Wait for table load, verify row visible
|
||||
const datasetListPage = new DatasetListPage(page);
|
||||
await datasetListPage.waitForTableLoad();
|
||||
await expect(datasetListPage.getDatasetRow(tableName)).toBeVisible();
|
||||
|
||||
// Clean up temp table (dataset cleanup handled by testAssets)
|
||||
await dropTempTable(page, dbId, tableName);
|
||||
});
|
||||
|
||||
// Click "Create dataset" (not explore)
|
||||
await createDatasetPage.clickCreateDataset();
|
||||
|
||||
// Capture dataset ID from response for cleanup
|
||||
const createResponse = expectStatusOneOf(
|
||||
await createResponsePromise,
|
||||
[200, 201],
|
||||
);
|
||||
const createBody = await createResponse.json();
|
||||
const datasetId = createBody.result?.id ?? createBody.id;
|
||||
if (datasetId) {
|
||||
testAssets.trackDataset(datasetId);
|
||||
}
|
||||
|
||||
// Verify redirect to dataset list (not chart creation)
|
||||
// Note: "Create dataset" action does not show a toast
|
||||
await page.waitForURL(/.*tablemodelview\/list.*/);
|
||||
|
||||
// Wait for table load, verify row visible
|
||||
const datasetListPage = new DatasetListPage(page);
|
||||
await datasetListPage.waitForTableLoad();
|
||||
await expect(datasetListPage.getDatasetRow(sheetName)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import path from 'path';
|
||||
import { DatasetListPage } from '../../pages/DatasetListPage';
|
||||
import { ExplorePage } from '../../pages/ExplorePage';
|
||||
import {
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
import { Toast } from '../../components/core';
|
||||
import {
|
||||
apiDeleteDataset,
|
||||
apiExportDatasets,
|
||||
apiGetDataset,
|
||||
apiPostVirtualDataset,
|
||||
getDatasetByName,
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
waitForPut,
|
||||
} from '../../helpers/api/intercepts';
|
||||
import {
|
||||
expectDeleted,
|
||||
expectStatusOneOf,
|
||||
expectValidExportZip,
|
||||
} from '../../helpers/api/assertions';
|
||||
@@ -135,17 +136,9 @@ test('should delete a dataset with confirmation', async ({
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
|
||||
|
||||
// Verify via API that dataset no longer exists (404)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetDataset(page, datasetId, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 10000, message: `Dataset ${datasetId} should return 404` },
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.DATASET, datasetId, {
|
||||
label: `Dataset ${datasetId}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('should duplicate a dataset with new name', async ({
|
||||
@@ -420,34 +413,17 @@ test('should bulk delete multiple datasets', async ({
|
||||
await expect(datasetListPage.getDatasetRow(dataset2.name)).not.toBeVisible();
|
||||
|
||||
// Verify via API that datasets no longer exist (404)
|
||||
// Use polling with explicit timeout since deletes may be async
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetDataset(page, dataset1.id, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 10000, message: `Dataset ${dataset1.id} should return 404` },
|
||||
)
|
||||
.toBe(404);
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const response = await apiGetDataset(page, dataset2.id, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
return response.status();
|
||||
},
|
||||
{ timeout: 10000, message: `Dataset ${dataset2.id} should return 404` },
|
||||
)
|
||||
.toBe(404);
|
||||
await expectDeleted(page, ENDPOINTS.DATASET, dataset1.id, {
|
||||
label: `Dataset ${dataset1.id}`,
|
||||
});
|
||||
await expectDeleted(page, ENDPOINTS.DATASET, dataset2.id, {
|
||||
label: `Dataset ${dataset2.id}`,
|
||||
});
|
||||
});
|
||||
|
||||
// Import test uses a fixed dataset name from the zip fixture.
|
||||
// Import test uses export-then-reimport approach (no static fixture needed).
|
||||
// Uses test.describe only because Playwright's serial mode API requires it -
|
||||
// this prevents race conditions when parallel workers import the same fixture.
|
||||
// this prevents race conditions when parallel workers import the same dataset.
|
||||
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
|
||||
test.describe('import dataset', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
@@ -456,22 +432,33 @@ test.describe('import dataset', () => {
|
||||
datasetListPage,
|
||||
testAssets,
|
||||
}) => {
|
||||
// Dataset name from fixture (test_netflix_1768502050965)
|
||||
// Note: Fixture contains a Google Sheets dataset backed by shillelagh[gsheetsapi],
|
||||
// which is a base dependency — import failure fails the test hard (no skip).
|
||||
const importedDatasetName = 'test_netflix_1768502050965';
|
||||
const fixturePath = path.resolve(
|
||||
__dirname,
|
||||
'../../fixtures/dataset_export.zip',
|
||||
test.setTimeout(60_000);
|
||||
|
||||
// Create a dataset, export it via API, then delete it, then reimport via UI
|
||||
const { id: datasetId, name: datasetName } = await createTestDataset(
|
||||
page,
|
||||
testAssets,
|
||||
test.info(),
|
||||
{ prefix: 'test_import' },
|
||||
);
|
||||
|
||||
// Cleanup: Delete any existing dataset with the same name from previous runs
|
||||
const existingDataset = await getDatasetByName(page, importedDatasetName);
|
||||
if (existingDataset) {
|
||||
await apiDeleteDataset(page, existingDataset.id, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
}
|
||||
// Export the dataset via API to get a zip buffer
|
||||
const exportResponse = await apiExportDatasets(page, [datasetId]);
|
||||
expect(exportResponse.ok()).toBe(true);
|
||||
const exportBuffer = await exportResponse.body();
|
||||
|
||||
// Delete the dataset so reimport creates it fresh
|
||||
await apiDeleteDataset(page, datasetId);
|
||||
|
||||
// Verify it's gone
|
||||
await expectDeleted(page, ENDPOINTS.DATASET, datasetId, {
|
||||
label: `Dataset ${datasetId}`,
|
||||
});
|
||||
|
||||
// Refresh to confirm dataset is no longer in the list
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).not.toBeVisible();
|
||||
|
||||
// Click the import button
|
||||
await datasetListPage.clickImportButton();
|
||||
@@ -480,11 +467,10 @@ test.describe('import dataset', () => {
|
||||
const importModal = new ImportDatasetModal(page);
|
||||
await importModal.waitForReady();
|
||||
|
||||
// Upload the fixture zip file
|
||||
await importModal.uploadFile(fixturePath);
|
||||
// Upload the exported zip via buffer (no temp file needed)
|
||||
await importModal.uploadFileBuffer(exportBuffer);
|
||||
|
||||
// Set up response intercept to catch the import POST
|
||||
// Use pathMatch to avoid false matches if URL lacks trailing slash
|
||||
let importResponsePromise = waitForPost(page, ENDPOINTS.DATASET_IMPORT, {
|
||||
pathMatch: true,
|
||||
});
|
||||
@@ -496,35 +482,27 @@ test.describe('import dataset', () => {
|
||||
let importResponse = await importResponsePromise;
|
||||
|
||||
// Handle overwrite confirmation if dataset already exists
|
||||
// First response may be 409/422 indicating overwrite is required - this is expected
|
||||
// First response may be 409/422 indicating overwrite is required
|
||||
const overwriteInput = importModal.getOverwriteInput();
|
||||
await overwriteInput
|
||||
.waitFor({ state: 'visible', timeout: 3000 })
|
||||
.catch(error => {
|
||||
// Only ignore TimeoutError (input not visible); re-throw other errors
|
||||
if (!(error instanceof Error) || error.name !== 'TimeoutError') {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
if (await overwriteInput.isVisible()) {
|
||||
// Set up new intercept for the actual import after overwrite confirmation
|
||||
importResponsePromise = waitForPost(page, ENDPOINTS.DATASET_IMPORT, {
|
||||
pathMatch: true,
|
||||
});
|
||||
await importModal.fillOverwriteConfirmation();
|
||||
await importModal.clickImport();
|
||||
// Wait for the second (final) import response
|
||||
importResponse = await importResponsePromise;
|
||||
}
|
||||
|
||||
// Fail hard if dataset import fails.
|
||||
// The fixture contains a gsheets dataset; shillelagh[gsheetsapi] is a base
|
||||
// dependency (pyproject.toml), so the engine is always available in CI.
|
||||
if (!importResponse.ok()) {
|
||||
const errorBody = await importResponse.json().catch(() => ({}));
|
||||
throw new Error(`Import failed: ${JSON.stringify(errorBody)}`);
|
||||
}
|
||||
// Verify import succeeded
|
||||
expectStatusOneOf(importResponse, [200]);
|
||||
|
||||
// Modal should close on success
|
||||
await importModal.waitForHidden({ timeout: TIMEOUT.FILE_IMPORT });
|
||||
@@ -533,19 +511,19 @@ test.describe('import dataset', () => {
|
||||
const toast = new Toast(page);
|
||||
await expect(toast.getSuccess()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Refresh the page to see the imported dataset
|
||||
// Refresh to see the imported dataset
|
||||
await datasetListPage.goto();
|
||||
await datasetListPage.waitForTableLoad();
|
||||
|
||||
// Verify dataset appears in list
|
||||
await expect(
|
||||
datasetListPage.getDatasetRow(importedDatasetName),
|
||||
).toBeVisible();
|
||||
await expect(datasetListPage.getDatasetRow(datasetName)).toBeVisible();
|
||||
|
||||
// Get dataset ID for cleanup
|
||||
const importedDataset = await getDatasetByName(page, importedDatasetName);
|
||||
expect(importedDataset).not.toBeNull();
|
||||
testAssets.trackDataset(importedDataset!.id);
|
||||
// Track for cleanup: the dataset import API returns {"message": "OK"}
|
||||
// with no ID, so look up the reimported dataset by name.
|
||||
const reimported = await getDatasetByName(page, datasetName);
|
||||
if (reimported) {
|
||||
testAssets.trackDataset(reimported.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^17.0.2"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^17.0.2"
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -29,27 +29,27 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-46", "NAME_1": "Aïn Témouchent" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -1.361975709870848, 35.319898656042554 ], [ -1.267730272999927, 35.390326239000046 ], [ -1.183338995999918, 35.57648346600007 ], [ -1.106353318999936, 35.623236395000049 ], [ -1.006728888227599, 35.524912014078154 ], [ -1.06920569458714, 35.454451199263417 ], [ -1.059025438273352, 35.411533922552451 ], [ -1.017787644584132, 35.455872300766032 ], [ -0.954122279819387, 35.437320462369428 ], [ -0.715428840368702, 35.482485662882993 ], [ -0.629490933259945, 35.409621893735391 ], [ -0.668868374076226, 35.332003892765442 ], [ -0.880845098622387, 35.254308377429652 ], [ -0.890456915953905, 35.159895535450573 ], [ -1.000527715581597, 35.087393499710231 ], [ -1.271622280443921, 35.195655626006328 ], [ -1.337664761119868, 35.190823879818197 ], [ -1.320663215435047, 35.283789780973564 ], [ -1.361975709870848, 35.319898656042554 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-19", "NAME_1": "Sétif" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.838444044756841, 36.304967760054069 ], [ 4.789919874602504, 36.333854885294727 ], [ 4.801081983746542, 36.379743557119468 ], [ 4.922573275835987, 36.38940705039505 ], [ 4.986807082281018, 36.462425849173599 ], [ 4.949858433320003, 36.502991848395084 ], [ 5.045304803174133, 36.524179186020604 ], [ 5.076517368381815, 36.574305324830846 ], [ 5.187621697683198, 36.53448863444288 ], [ 5.165555860914253, 36.494155178318806 ], [ 5.187776727314088, 36.408423976785059 ], [ 5.255421176645996, 36.378865057976043 ], [ 5.303221877287797, 36.389303696708225 ], [ 5.327871534643577, 36.449300035268379 ], [ 5.457889439346161, 36.499917101393066 ], [ 5.475459424912515, 36.590635076947194 ], [ 5.542793816781227, 36.523972480445593 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.776164585427239, 36.439998277198697 ], [ 5.872386101637289, 36.401886909153518 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.94018558060003, 35.862126573082833 ], [ 5.822880079552021, 35.914733181491101 ], [ 5.745623813787972, 35.89021271444517 ], [ 5.711620720619749, 35.849672552746085 ], [ 5.761901889960257, 35.810915229554155 ], [ 5.736425409405115, 35.771046861423486 ], [ 5.575091586707345, 35.818279120385057 ], [ 5.476854688892786, 35.736087754746052 ], [ 5.423472935027917, 35.635654609072901 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.065768670387797, 35.762081000138039 ], [ 5.054916618706955, 35.820862941871269 ], [ 5.139976026672286, 35.856623033326287 ], [ 5.184056024266113, 36.115366929739992 ], [ 5.25356082467232, 36.197377428225707 ], [ 5.212426384669925, 36.196653957813908 ], [ 5.188086785676603, 36.245514023853104 ], [ 5.125558301574358, 36.232362372425541 ], [ 5.034762810755126, 36.310884710960636 ], [ 4.838444044756841, 36.304967760054069 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-34", "NAME_1": "Bordj Bou Arréridj" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.801081983746542, 36.379743557119468 ], [ 4.785217318724278, 36.346644802415653 ], [ 4.821235792597747, 36.306853950449408 ], [ 5.034762810755126, 36.310884710960636 ], [ 5.125558301574358, 36.232362372425541 ], [ 5.188086785676603, 36.245514023853104 ], [ 5.212426384669925, 36.196653957813908 ], [ 5.25356082467232, 36.197377428225707 ], [ 5.184056024266113, 36.115366929739992 ], [ 5.139976026672286, 35.856623033326287 ], [ 5.066543816743717, 35.840474148363228 ], [ 5.05445153161287, 35.777325548435272 ], [ 4.854928827403455, 35.873159491917022 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.356354608178606, 36.340443631568348 ], [ 4.423378939886163, 36.22616119977954 ], [ 4.511383905442869, 36.225747789528839 ], [ 4.630084669571829, 36.253343004476051 ], [ 4.635820754224426, 36.338454088385447 ], [ 4.748010288244188, 36.420748805912638 ], [ 4.801081983746542, 36.379743557119468 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-10", "NAME_1": "Bouira" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.356354608178606, 36.340443631568348 ], [ 4.093631626298532, 36.046068834009475 ], [ 4.035133905405416, 35.870808214427541 ], [ 3.954156934794696, 35.890755316804416 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.578934359632967, 35.999353338985372 ], [ 3.551752556735096, 36.298301500313983 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.456357862825087, 36.576837470372936 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.477441846763782, 36.688561917298614 ], [ 3.635726759082672, 36.694401352940076 ], [ 3.680736931763988, 36.666806138892184 ], [ 3.641927930829354, 36.600401923010338 ], [ 3.733653599434035, 36.607610785109614 ], [ 3.888786247687165, 36.488522447353034 ], [ 4.37092736200816, 36.47265778323009 ], [ 4.398729281631006, 36.373697415003676 ], [ 4.356354608178606, 36.340443631568348 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-10", "NAME_1": "Bouira" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.398729281631006, 36.373697415003676 ], [ 4.093631626298532, 36.046068834009475 ], [ 4.035133905405416, 35.870808214427541 ], [ 3.954156934794696, 35.890755316804416 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.578934359632967, 35.999353338985372 ], [ 3.551752556735096, 36.298301500313983 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.456357862825087, 36.576837470372936 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.477441846763782, 36.688561917298614 ], [ 3.635726759082672, 36.694401352940076 ], [ 3.680736931763988, 36.666806138892184 ], [ 3.641927930829354, 36.600401923010338 ], [ 3.733653599434035, 36.607610785109614 ], [ 3.888786247687165, 36.488522447353034 ], [ 4.37092736200816, 36.47265778323009 ], [ 4.398729281631006, 36.373697415003676 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-09", "NAME_1": "Blida" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.477441846763782, 36.688561917298614 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.467364943237556, 36.583167833328844 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.008064812940745, 36.356437486001141 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.824458448798339, 36.365170803289914 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.64338423019808, 36.397261868540397 ], [ 2.52251305483361, 36.363982244884653 ], [ 2.476262647802969, 36.420619614703412 ], [ 2.499723747652808, 36.470384020106394 ], [ 2.602456495770639, 36.467438463414339 ], [ 2.837377557128548, 36.637867336808142 ], [ 3.02992394413468, 36.662232774223128 ], [ 3.025118036368269, 36.71005931238733 ], [ 3.097413363835642, 36.649933784416589 ], [ 3.323652784748219, 36.712307237089306 ], [ 3.477441846763782, 36.688561917298614 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-38", "NAME_1": "Tissemsilt" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.712691685372988, 35.92341482193649 ], [ 1.853561639058398, 35.860498766005207 ], [ 1.964510938728836, 35.879825750757789 ], [ 2.008745965054231, 35.917911282179887 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.158194207296845, 35.698234768219947 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.249713169427196, 35.559612739236513 ], [ 1.744937777556402, 35.551938788244456 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.658069696259474, 35.547933865255629 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.346460809019106, 35.68981151019301 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.37720828623344, 35.788048408007626 ], [ 1.447953321888292, 35.914268093497697 ], [ 1.504900750069567, 35.935222887126542 ], [ 1.548360630039042, 36.003952542076149 ], [ 1.638484328189179, 35.934292711139676 ], [ 1.712691685372988, 35.92341482193649 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-38", "NAME_1": "Tissemsilt" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.638484328189179, 35.934292711139676 ], [ 1.853561639058398, 35.860498766005207 ], [ 1.964510938728836, 35.879825750757789 ], [ 2.008745965054231, 35.917911282179887 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.158194207296845, 35.698234768219947 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.249713169427196, 35.559612739236513 ], [ 1.744937777556402, 35.551938788244456 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.658069696259474, 35.547933865255629 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.346460809019106, 35.68981151019301 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.37720828623344, 35.788048408007626 ], [ 1.447953321888292, 35.914268093497697 ], [ 1.504900750069567, 35.935222887126542 ], [ 1.548360630039042, 36.003952542076149 ], [ 1.638484328189179, 35.934292711139676 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-44", "NAME_1": "Aïn Defla" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.853561639058398, 35.860498766005207 ], [ 1.712691685372988, 35.92341482193649 ], [ 1.717187533877564, 35.973205063962496 ], [ 1.588513218110506, 36.143298041471439 ], [ 1.628717482126092, 36.2114850940618 ], [ 1.594921095432198, 36.234377753130786 ], [ 1.603292676615695, 36.279077867449587 ], [ 1.553166537805453, 36.367677110410284 ], [ 1.647786086258861, 36.443150540365139 ], [ 1.716722445884159, 36.36666942005769 ], [ 1.793048536560718, 36.43198843032178 ], [ 2.476262647802969, 36.420619614703412 ], [ 2.52251305483361, 36.363982244884653 ], [ 2.652685988267706, 36.39209422556803 ], [ 2.647880079601975, 36.33251129815784 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.531659784171666, 36.189574286024481 ], [ 2.577445103208959, 36.211795152424315 ], [ 2.639043410425018, 36.181564439147508 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.008745965054231, 35.917911282179887 ], [ 1.964510938728836, 35.879825750757789 ], [ 1.853561639058398, 35.860498766005207 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-48", "NAME_1": "Relizane" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.411624789652308, 35.841559353081664 ], [ 1.361808709204581, 35.776524562758368 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.010977409879558, 35.575038153787773 ], [ 1.01795372888148, 35.544238999730055 ], [ 0.959456007988365, 35.54775299720302 ], [ 0.866076693884281, 35.436080227120783 ], [ 0.532298617986839, 35.5099775250427 ], [ 0.456851027353025, 35.558139959991138 ], [ 0.319081659091296, 35.557778224785238 ], [ 0.229371372091123, 35.695935167573907 ], [ 0.423984815746621, 35.817633165238362 ], [ 0.451735060324779, 35.951966051292231 ], [ 0.529043002932212, 35.968063259411849 ], [ 0.542840610405847, 36.048962713858145 ], [ 0.704071079416792, 36.186525377444184 ], [ 0.897392611879525, 36.197635809744781 ], [ 0.948138869213437, 36.032012844117446 ], [ 1.040949740737915, 36.025449937164865 ], [ 1.159340448303055, 35.94597158512056 ], [ 1.288893263213538, 35.965867011553314 ], [ 1.356227655082193, 35.868146878575544 ], [ 1.411624789652308, 35.841559353081664 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-32", "NAME_1": "El Bayadh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.340785759755022, 34.234138088701798 ], [ 0.729495884027813, 34.435469468940823 ], [ 0.973718702556084, 34.382087714176635 ], [ 1.031958041930068, 34.339351305518335 ], [ 1.023689812634757, 34.191246650412552 ], [ 1.231480747038859, 34.191892605559303 ], [ 1.409144320953658, 33.961803290389526 ], [ 1.793978713446847, 33.816411647979123 ], [ 2.019287956573919, 33.499040839463134 ], [ 2.031380242604087, 33.392535712353208 ], [ 1.975207960778732, 33.292180081045899 ], [ 2.054221225728952, 33.108909613687729 ], [ 2.174937370563214, 32.97499013878388 ], [ 2.291467726154622, 32.695989080731465 ], [ 2.297513869169734, 32.377300523500367 ], [ 2.344849480918811, 32.172791043471875 ], [ 2.315652297315637, 32.062177638786977 ], [ 2.215296665109008, 31.923297227385092 ], [ 2.24211673280098, 31.791186428510855 ], [ 2.199432000086745, 31.735246689782912 ], [ 2.085382114093306, 31.658584703221436 ], [ 0.842460565302815, 31.096939399333792 ], [ 0.399128451916511, 30.708694363054406 ], [ -0.154016078679092, 31.123785305447484 ], [ -0.400822719592838, 31.390668239947274 ], [ -0.003585985404925, 32.443963120901287 ], [ -0.012887742575288, 32.500006212416736 ], [ -0.083942836592655, 32.524397488253442 ], [ -0.10156449990177, 32.567108059389398 ], [ -0.006686570828606, 32.804767970965088 ], [ -0.027047085254765, 33.02062042908949 ], [ 0.063490024045336, 33.308096422012227 ], [ 0.050622592558568, 33.803104966920671 ], [ 0.086020948807743, 34.048309638279136 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.340785759755022, 34.234138088701798 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-20", "NAME_1": "Saïda" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.75760786291255, 34.438182480736884 ], [ 0.351482781804918, 34.238117174168224 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.086020948807743, 34.048309638279136 ], [ -0.044100307782969, 33.987253933422267 ], [ -0.081772427155784, 34.08153758419212 ], [ -0.220213588985928, 34.059445909001454 ], [ -0.346614141629345, 33.955214545015224 ], [ -0.356122606173358, 33.915036119421359 ], [ -0.499679735031748, 34.193158678330292 ], [ -0.060895147892779, 34.550087795715172 ], [ -0.056502651276332, 34.609050605501011 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.24687862794633, 35.098891506537768 ], [ -0.135567593069993, 35.12764944146852 ], [ -0.111693081170756, 35.160593167440709 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.014334683398943, 35.06385488549455 ], [ 0.112220899774684, 35.096100979476603 ], [ 0.189993931274898, 35.023495591848075 ], [ 0.625057813567253, 35.095920112323313 ], [ 0.754197218227034, 35.007837633300142 ], [ 0.783032668422891, 34.948151353102446 ], [ 0.669757927886053, 34.855495510309538 ], [ 0.673478630934085, 34.789375515267807 ], [ 0.856878288602104, 34.499858303218105 ], [ 0.75760786291255, 34.438182480736884 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-20", "NAME_1": "Saïda" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.856878288602104, 34.499858303218105 ], [ 0.351482781804918, 34.238117174168224 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.086020948807743, 34.048309638279136 ], [ -0.044100307782969, 33.987253933422267 ], [ -0.081772427155784, 34.08153758419212 ], [ -0.220213588985928, 34.059445909001454 ], [ -0.346614141629345, 33.955214545015224 ], [ -0.356122606173358, 33.915036119421359 ], [ -0.499679735031748, 34.193158678330292 ], [ -0.060895147892779, 34.550087795715172 ], [ -0.056502651276332, 34.609050605501011 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.24687862794633, 35.098891506537768 ], [ -0.135567593069993, 35.12764944146852 ], [ -0.111693081170756, 35.160593167440709 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.014334683398943, 35.06385488549455 ], [ 0.112220899774684, 35.096100979476603 ], [ 0.189993931274898, 35.023495591848075 ], [ 0.625057813567253, 35.095920112323313 ], [ 0.754197218227034, 35.007837633300142 ], [ 0.783032668422891, 34.948151353102446 ], [ 0.669757927886053, 34.855495510309538 ], [ 0.673478630934085, 34.789375515267807 ], [ 0.856878288602104, 34.499858303218105 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-22", "NAME_1": "Sidi Bel Abbès" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -0.16517818782313, 35.122921047168575 ], [ -0.250702683781867, 35.093568833934512 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.055004035407933, 34.60403799126027 ], [ -0.071902228305191, 34.529701442867292 ], [ -0.485727097927224, 34.229332180036067 ], [ -0.523140834881644, 34.290362047370593 ], [ -0.578176235145122, 34.312944648077007 ], [ -0.836196662046348, 34.215121161412469 ], [ -1.120417039015138, 34.271293443237766 ], [ -0.880018276322403, 34.467612210135371 ], [ -0.903789435434135, 34.547193914967181 ], [ -0.829582078250326, 34.578380641753199 ], [ -0.758475308288837, 34.711912543029428 ], [ -0.775631882705227, 34.737440701327273 ], [ -0.886116095281579, 34.74302175634898 ], [ -0.89056026874141, 34.843609930753701 ], [ -0.941358201120067, 34.842576401979329 ], [ -0.924149949860293, 34.881514594123189 ], [ -0.96611121306205, 34.922726549390745 ], [ -0.929834356770186, 35.047163398172245 ], [ -1.000527715581597, 35.087393499710231 ], [ -0.890456915953905, 35.159895535450573 ], [ -0.880845098622387, 35.254308377429652 ], [ -0.753669398723787, 35.318257962135249 ], [ -0.668868374076226, 35.332003892765442 ], [ -0.632281460321167, 35.298155829228108 ], [ -0.569184536337275, 35.405229397118944 ], [ -0.464436408413519, 35.429388128958976 ], [ -0.463661261158279, 35.358048814101494 ], [ -0.361807013083194, 35.358772284513293 ], [ -0.189207729353257, 35.299318549211648 ], [ -0.216957973931358, 35.190927231706382 ], [ -0.16517818782313, 35.122921047168575 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-29", "NAME_1": "Mascara" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.229371372091123, 35.695935167573907 ], [ 0.319081659091296, 35.557778224785238 ], [ 0.456851027353025, 35.558139959991138 ], [ 0.532298617986839, 35.5099775250427 ], [ 0.827887810573998, 35.443469957272669 ], [ 0.878168979914506, 35.374740302323062 ], [ 0.778536818119676, 35.279371445935453 ], [ 0.684330681715551, 35.299551093208379 ], [ 0.617771437102135, 35.276038316515098 ], [ 0.448789503632668, 35.059074815250483 ], [ 0.209837680864268, 35.02148021114283 ], [ 0.112220899774684, 35.096100979476603 ], [ -0.014334683398943, 35.06385488549455 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.105956997417479, 35.159740505819684 ], [ -0.16517818782313, 35.122921047168575 ], [ -0.216957973931358, 35.190927231706382 ], [ -0.189207729353257, 35.299318549211648 ], [ -0.361807013083194, 35.358772284513293 ], [ -0.463661261158279, 35.358048814101494 ], [ -0.464436408413519, 35.429388128958976 ], [ -0.545361701281479, 35.411973172124192 ], [ -0.302482468990775, 35.669399318923411 ], [ -0.220575324191884, 35.683558660703625 ], [ -0.114380256343793, 35.783759264178684 ], [ -0.043996954995464, 35.714461168448167 ], [ 0.104469435316162, 35.756732490012382 ], [ 0.152115106327074, 35.682628486515398 ], [ 0.229371372091123, 35.695935167573907 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-47", "NAME_1": "Ghardaïa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.085382114093306, 31.658584703221436 ], [ 2.239326205739815, 31.77353892678002 ], [ 2.215296665109008, 31.923297227385092 ], [ 2.34329918820697, 32.132664292922755 ], [ 2.297513869169734, 32.377300523500367 ], [ 2.291467726154622, 32.695989080731465 ], [ 3.100668979789589, 32.830683701991234 ], [ 3.321327344781139, 32.780790107177666 ], [ 3.427470736685166, 32.87551300751926 ], [ 3.738614535932072, 33.000466620238285 ], [ 3.884445427914159, 32.981863104998297 ], [ 4.176210564665837, 33.029637966319058 ], [ 4.978280469667936, 32.840760606416836 ], [ 4.738863559805452, 32.531890571192889 ], [ 4.359145135239771, 32.255990099463475 ], [ 4.110839877558305, 31.791393134085865 ], [ 3.802538283115268, 29.989849352465626 ], [ 3.721509636560484, 29.863345445236064 ], [ 3.479612257999293, 29.636847642804469 ], [ 3.397550082670193, 29.364874578798776 ], [ 3.34447838716784, 29.306557725958271 ], [ 2.9859989761718, 29.123545641018495 ], [ 2.079335971977514, 29.036367499560413 ], [ 1.943426954790141, 30.094804185964392 ], [ 2.071739536250561, 30.859692898008802 ], [ 2.085382114093306, 31.658584703221436 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-03", "NAME_1": "Laghouat" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.291467726154622, 32.695989080731465 ], [ 2.174937370563214, 32.97499013878388 ], [ 2.054221225728952, 33.108909613687729 ], [ 1.975207960778732, 33.292180081045899 ], [ 2.031380242604087, 33.392535712353208 ], [ 2.019287956573919, 33.499040839463134 ], [ 1.776925490019323, 33.831346137014577 ], [ 1.428109572298922, 33.951183782705414 ], [ 1.336228874962615, 34.068928534224426 ], [ 1.64158491361286, 34.256152249526679 ], [ 1.691090935698128, 34.345810858784091 ], [ 1.806226027309322, 34.449086209261111 ], [ 1.981099074162955, 34.506989650951596 ], [ 2.220567660868824, 34.674679674127617 ], [ 2.357716912405579, 34.692869778217641 ], [ 2.295808546826947, 34.439035143257229 ], [ 2.360972528359468, 34.352735500942572 ], [ 2.393735386279047, 34.225301419524897 ], [ 2.48339399643578, 34.113576971699842 ], [ 2.458279250187218, 34.003247788754436 ], [ 2.474867383822698, 33.92371775986669 ], [ 2.639198439156644, 33.911005357111549 ], [ 2.650205518669736, 34.09797068909603 ], [ 2.817637160326683, 34.138846747579294 ], [ 2.949412062416741, 34.263619493145086 ], [ 3.093537632056041, 34.240029202085964 ], [ 3.14924482588799, 34.074767970765208 ], [ 3.232133822617811, 33.947411403713318 ], [ 3.640997755741807, 33.564334011305561 ], [ 4.059060093248718, 33.252518419389503 ], [ 4.176210564665837, 33.029637966319058 ], [ 3.884445427914159, 32.981863104998297 ], [ 3.738614535932072, 33.000466620238285 ], [ 3.427470736685166, 32.87551300751926 ], [ 3.321327344781139, 32.780790107177666 ], [ 3.100668979789589, 32.830683701991234 ], [ 2.291467726154622, 32.695989080731465 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-26", "NAME_1": "Médéa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.249713169427196, 35.559612739236513 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.169511346071772, 35.666815497437256 ], [ 2.16269005670074, 35.708983466213965 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.643694288560539, 36.191537990785605 ], [ 2.526388786613211, 36.192778225134987 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.614083692908082, 36.309670315032974 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.845284051217959, 36.369227403122125 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.996282586172413, 36.353285223734076 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.552217644728501, 36.30827505105276 ], [ 3.56436160580347, 36.05097809546271 ], [ 3.633866408008316, 35.889540920876755 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.540900505953573, 35.803938910552233 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.411037631781255, 35.805928452835815 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.336726921809998, 35.747534084730205 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.081910434918598, 35.67187978852138 ], [ 2.960264113198264, 35.804739895329874 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.671031121089356, 35.44657054269635 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.285731642401345, 35.439077459756959 ], [ 2.249713169427196, 35.559612739236513 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-26", "NAME_1": "Médéa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.285731642401345, 35.439077459756959 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.169511346071772, 35.666815497437256 ], [ 2.16269005670074, 35.708983466213965 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.643694288560539, 36.191537990785605 ], [ 2.526388786613211, 36.192778225134987 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.614083692908082, 36.309670315032974 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.845284051217959, 36.369227403122125 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.996282586172413, 36.353285223734076 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.552217644728501, 36.30827505105276 ], [ 3.56436160580347, 36.05097809546271 ], [ 3.633866408008316, 35.889540920876755 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.540900505953573, 35.803938910552233 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.411037631781255, 35.805928452835815 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.336726921809998, 35.747534084730205 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.081910434918598, 35.67187978852138 ], [ 2.960264113198264, 35.804739895329874 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.671031121089356, 35.44657054269635 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.285731642401345, 35.439077459756959 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-14", "NAME_1": "Tiaret" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.346460809019106, 35.68981151019301 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.634608594610938, 35.5504660098984 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.744937777556402, 35.551938788244456 ], [ 2.249713169427196, 35.559612739236513 ], [ 2.286816847119781, 35.329265042547661 ], [ 2.360042352372659, 35.287975572015 ], [ 2.379317661181119, 35.205009060020075 ], [ 2.620284864654764, 35.031143704418412 ], [ 2.523443230820419, 34.999104316011369 ], [ 2.460294630892463, 34.870171616926541 ], [ 2.369964227167316, 34.776301378206028 ], [ 2.357716912405579, 34.692869778217641 ], [ 2.220567660868824, 34.674679674127617 ], [ 1.981099074162955, 34.506989650951596 ], [ 1.806226027309322, 34.449086209261111 ], [ 1.691090935698128, 34.345810858784091 ], [ 1.64158491361286, 34.256152249526679 ], [ 1.336228874962615, 34.068928534224426 ], [ 1.231480747038859, 34.191892605559303 ], [ 1.023689812634757, 34.191246650412552 ], [ 1.031958041930068, 34.339351305518335 ], [ 0.973718702556084, 34.382087714176635 ], [ 0.75760786291255, 34.438182480736884 ], [ 0.856878288602104, 34.499858303218105 ], [ 0.693529086997728, 34.736329658187174 ], [ 0.667432488818292, 34.84221466677343 ], [ 0.781637404442677, 34.972697659469361 ], [ 0.652549675726959, 35.089796454043096 ], [ 0.492249382702823, 35.086334133413516 ], [ 0.617771437102135, 35.276038316515098 ], [ 0.684330681715551, 35.299551093208379 ], [ 0.778536818119676, 35.279371445935453 ], [ 0.819981317383906, 35.30611400016096 ], [ 0.880494418982266, 35.386367499460505 ], [ 0.827887810573998, 35.443469957272669 ], [ 0.881114535707241, 35.443702501269399 ], [ 0.959456007988365, 35.54775299720302 ], [ 1.01795372888148, 35.544238999730055 ], [ 1.010977409879558, 35.575038153787773 ], [ 1.206210972058727, 35.645679837554439 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.346460809019106, 35.68981151019301 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-28", "NAME_1": "M'Sila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.05445153161287, 35.777325548435272 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.379702995796606, 35.632915757955857 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.022980583986737, 35.520674547092653 ], [ 4.963242628744297, 35.4887901883165 ], [ 4.876477899335555, 35.519150091903214 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.779016146977597, 35.172840481303126 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.041119012132697, 34.918644111136814 ], [ 4.932805209892535, 34.838907375774738 ], [ 4.394646844276451, 34.746587428866746 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.036374138855422, 34.441929023105899 ], [ 3.921084019411978, 34.672664293422372 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.67112511623111, 34.790279852832896 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.571803012798796, 35.005227973392209 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.657585890276607, 35.374146023120431 ], [ 3.695929803217837, 35.472047024150811 ], [ 3.436927524385737, 35.671983141308885 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.394294467615566, 35.793836167704967 ], [ 3.432276646250216, 35.796600857243732 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.536714714912137, 35.792234199049005 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.954156934794696, 35.890755316804416 ], [ 4.022266473019329, 35.866854967382835 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.854928827403455, 35.873159491917022 ], [ 5.05445153161287, 35.777325548435272 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-28", "NAME_1": "M'Sila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.854928827403455, 35.873159491917022 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.379702995796606, 35.632915757955857 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.022980583986737, 35.520674547092653 ], [ 4.963242628744297, 35.4887901883165 ], [ 4.876477899335555, 35.519150091903214 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.779016146977597, 35.172840481303126 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.041119012132697, 34.918644111136814 ], [ 4.932805209892535, 34.838907375774738 ], [ 4.394646844276451, 34.746587428866746 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.036374138855422, 34.441929023105899 ], [ 3.921084019411978, 34.672664293422372 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.67112511623111, 34.790279852832896 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.571803012798796, 35.005227973392209 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.657585890276607, 35.374146023120431 ], [ 3.695929803217837, 35.472047024150811 ], [ 3.436927524385737, 35.671983141308885 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.394294467615566, 35.793836167704967 ], [ 3.432276646250216, 35.796600857243732 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.536714714912137, 35.792234199049005 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.954156934794696, 35.890755316804416 ], [ 4.022266473019329, 35.866854967382835 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.854928827403455, 35.873159491917022 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-05", "NAME_1": "Batna" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.94018558060003, 35.862126573082833 ], [ 5.976669141567641, 35.915792547787817 ], [ 6.275513950108689, 35.866079820127595 ], [ 6.351064894429328, 35.884735012211024 ], [ 6.47694868313522, 35.840396633098123 ], [ 6.487025587560822, 35.781433824211604 ], [ 6.649496290921093, 35.796678372508836 ], [ 6.769127231036862, 35.683920395909524 ], [ 6.781684605060491, 35.562558295029248 ], [ 6.592703892370707, 35.436958727163528 ], [ 6.584022251026056, 35.34332103154037 ], [ 6.628360630138957, 35.316862698154978 ], [ 6.514930860870493, 35.119717108957389 ], [ 6.512140333809327, 35.052382717088676 ], [ 6.598595004855554, 34.900350654259285 ], [ 6.58102501838988, 34.769660955988286 ], [ 6.508729689123868, 34.775500393428388 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.072218866007859, 35.058583888835358 ], [ 5.993102248270134, 34.989363308369946 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.932279087409938, 35.143565782434905 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.830631544909863, 35.288440660008405 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.610748325374914, 35.126383367798155 ], [ 5.596640658639501, 35.091760158804277 ], [ 5.306012404348962, 35.021428534299389 ], [ 5.184831169722713, 35.031040350731587 ], [ 5.004325391903308, 35.08545563427009 ], [ 4.925053745434013, 35.142196356876354 ], [ 4.782426791663113, 35.166871853553175 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.880198602383587, 35.525402940493279 ], [ 4.963242628744297, 35.4887901883165 ], [ 5.022980583986737, 35.520674547092653 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.438200717589041, 35.641855780819583 ], [ 5.476854688892786, 35.736087754746052 ], [ 5.575091586707345, 35.818279120385057 ], [ 5.736425409405115, 35.771046861423486 ], [ 5.761901889960257, 35.810915229554155 ], [ 5.71379113095594, 35.860421250740103 ], [ 5.80009077327054, 35.913467108720056 ], [ 5.94018558060003, 35.862126573082833 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-40", "NAME_1": "Khenchela" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.599680209573989, 35.327818101724006 ], [ 6.592703892370707, 35.436958727163528 ], [ 6.781684605060491, 35.562558295029248 ], [ 6.769127231036862, 35.683920395909524 ], [ 6.82498945539902, 35.623562323942053 ], [ 7.00327314603885, 35.629815172532176 ], [ 7.051228875412278, 35.577182725702187 ], [ 7.141197543931469, 35.617464504982877 ], [ 7.267546420630822, 35.621236883974973 ], [ 7.30335818892928, 35.600695502395524 ], [ 7.315037062010788, 35.535247300922151 ], [ 7.385161980940666, 35.571756700311425 ], [ 7.462263217973145, 35.539123032701752 ], [ 7.544842157239771, 35.447810777045675 ], [ 7.413067254250393, 35.38662588097958 ], [ 7.411516960639233, 35.156097317137437 ], [ 7.222226189586991, 34.94629100202809 ], [ 7.26553103992552, 34.832344468822157 ], [ 7.264445835207141, 34.940632433539974 ], [ 7.30831912632658, 34.977193507973993 ], [ 7.365886672132206, 34.962465725412869 ], [ 7.402835321093164, 34.74604482740682 ], [ 7.274057650739962, 34.540734360802105 ], [ 7.135771519440084, 34.16057668756406 ], [ 6.960588413324615, 34.193933823786892 ], [ 6.714970329916753, 34.326613064341416 ], [ 6.696521844307654, 34.475828761687978 ], [ 6.748456659147507, 34.726097724130625 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.587897982805657, 34.778549302908004 ], [ 6.570483025970873, 34.839010728562243 ], [ 6.598595004855554, 34.900350654259285 ], [ 6.512140333809327, 35.052382717088676 ], [ 6.569707878715633, 35.253455714909308 ], [ 6.628360630138957, 35.316862698154978 ], [ 6.599680209573989, 35.327818101724006 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-24", "NAME_1": "Guelma" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 7.010559523403288, 36.155157783504876 ], [ 7.04564782128989, 36.275770575551576 ], [ 6.93795413577476, 36.432866930364526 ], [ 7.075723504036489, 36.502552598823343 ], [ 7.077118768016703, 36.545263169959298 ], [ 7.123989291772375, 36.585829169180784 ], [ 7.314727003648272, 36.651613268337655 ], [ 7.3672819352131, 36.721376451162257 ], [ 7.456837191683064, 36.693057765803246 ], [ 7.475595736553998, 36.638849188739073 ], [ 7.533783399983918, 36.615388088889176 ], [ 7.697029249700108, 36.657736924819176 ], [ 7.771546665246376, 36.581643378139347 ], [ 7.98786421136424, 36.484388333155039 ], [ 7.871437208560337, 36.432582709524411 ], [ 7.795266148413987, 36.440980130028947 ], [ 7.835005324436167, 36.334862576546641 ], [ 7.813869662754712, 36.304916083210628 ], [ 7.410431755920797, 36.181254380785049 ], [ 7.357515090049333, 36.129758816416199 ], [ 7.223776483198151, 36.144099026249023 ], [ 7.194889357058173, 36.080743719846794 ], [ 7.133601108204573, 36.053148504899582 ], [ 7.066421745966807, 36.047283229937079 ], [ 7.010559523403288, 36.155157783504876 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-25", "NAME_1": "Constantine" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.93795413577476, 36.432866930364526 ], [ 7.048283318720166, 36.238408515440597 ], [ 7.010559523403288, 36.155157783504876 ], [ 6.898525018115095, 36.10689199486967 ], [ 6.87992150197573, 36.146424465316784 ], [ 6.772692905353267, 36.192442328350751 ], [ 6.577149285710959, 36.10919159641503 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.599370151211474, 36.57399526646833 ], [ 6.652441846713828, 36.607895005949729 ], [ 6.777188754757219, 36.556270250371711 ], [ 6.920125766890635, 36.557148749515136 ], [ 6.904106073136745, 36.529036769731135 ], [ 6.845453321713421, 36.5314138865416 ], [ 6.818013136397099, 36.466818345789989 ], [ 6.93795413577476, 36.432866930364526 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-43", "NAME_1": "Mila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.810167676796823, 36.431135768700756 ], [ 5.766242709733262, 36.45260732716639 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.904322137256827, 36.545521552377693 ], [ 6.023229607860117, 36.605931301188605 ], [ 6.215310906872844, 36.582625230070278 ], [ 6.294582554241458, 36.624069729334508 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.525007765296095, 36.204663804690824 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.170300734191528, 35.951242580880375 ], [ 6.149216750252833, 35.900392970758958 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.872386101637289, 36.401886909153518 ], [ 5.810167676796823, 36.431135768700756 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-43", "NAME_1": "Mila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.872386101637289, 36.401886909153518 ], [ 5.766242709733262, 36.45260732716639 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.904322137256827, 36.545521552377693 ], [ 6.023229607860117, 36.605931301188605 ], [ 6.215310906872844, 36.582625230070278 ], [ 6.294582554241458, 36.624069729334508 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.525007765296095, 36.204663804690824 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.170300734191528, 35.951242580880375 ], [ 6.149216750252833, 35.900392970758958 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.872386101637289, 36.401886909153518 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-04", "NAME_1": "Oum el Bouaghi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.275513950108689, 35.866079820127595 ], [ 6.149216750252833, 35.900392970758958 ], [ 6.151387159689648, 35.935274563070607 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.477103712766109, 36.158620103235137 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.584797397381976, 36.108700669999905 ], [ 6.772692905353267, 36.192442328350751 ], [ 6.87992150197573, 36.146424465316784 ], [ 6.898525018115095, 36.10689199486967 ], [ 7.010559523403288, 36.155157783504876 ], [ 7.078979119990379, 36.042503159693069 ], [ 7.194889357058173, 36.080743719846794 ], [ 7.223776483198151, 36.144099026249023 ], [ 7.290800814905651, 36.143323879893103 ], [ 7.325889112792311, 36.127407538027455 ], [ 7.324958936805501, 36.080020250334258 ], [ 7.398339470789949, 35.967003893115134 ], [ 7.577243279953393, 35.833497830260626 ], [ 7.660132276683214, 35.834195462250761 ], [ 7.734598016285418, 35.934499416714687 ], [ 7.865856154437949, 35.858715929296693 ], [ 7.823378127298724, 35.739007473016443 ], [ 7.840741408189444, 35.615164903437517 ], [ 7.544842157239771, 35.447810777045675 ], [ 7.462263217973145, 35.539123032701752 ], [ 7.385161980940666, 35.571756700311425 ], [ 7.315037062010788, 35.535247300922151 ], [ 7.30335818892928, 35.600695502395524 ], [ 7.267546420630822, 35.621236883974973 ], [ 7.141197543931469, 35.617464504982877 ], [ 7.083009881400869, 35.576872667339728 ], [ 6.992576124888217, 35.631985581968991 ], [ 6.82498945539902, 35.623562323942053 ], [ 6.649496290921093, 35.796678372508836 ], [ 6.487025587560822, 35.781433824211604 ], [ 6.466820102765496, 35.846391100169114 ], [ 6.339437697291885, 35.88680206975971 ], [ 6.275513950108689, 35.866079820127595 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-07", "NAME_1": "Biskra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.83208784337927, 34.819244493338658 ], [ 4.932805209892535, 34.838907375774738 ], [ 5.028406610276818, 34.900195623729019 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.272474399174143, 35.018431300763893 ], [ 5.536747673766115, 35.0663611917156 ], [ 5.613538853335399, 35.1064362663206 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.830631544909863, 35.288440660008405 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.932279087409938, 35.143565782434905 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.993102248270134, 34.989363308369946 ], [ 6.072218866007859, 35.058583888835358 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.540407342324954, 34.765681871421236 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.749541863865886, 34.712222602291263 ], [ 6.695901726683303, 34.376093248004963 ], [ 6.558545769571595, 34.317027086330938 ], [ 6.211435174193923, 34.428415636472437 ], [ 5.998528272761575, 34.394955146562722 ], [ 5.477164748154564, 34.449835517195368 ], [ 5.311748488102239, 34.498256334562143 ], [ 5.29903608624636, 34.449060369940128 ], [ 5.217232293335655, 34.411956692247543 ], [ 5.185451287347007, 34.356662909565614 ], [ 5.359859246207236, 34.166209418529832 ], [ 5.37381188331176, 34.058386541805419 ], [ 5.334847852746179, 33.845996406109236 ], [ 5.121785922582262, 33.572679754966714 ], [ 4.406635775720474, 33.927645169389052 ], [ 4.335063917765581, 33.986039537494662 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.210627068984024, 34.174606839034368 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.273310580918576, 34.625845444711501 ], [ 4.434696078661091, 34.772658189523781 ], [ 4.83208784337927, 34.819244493338658 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-07", "NAME_1": "Biskra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.434696078661091, 34.772658189523781 ], [ 4.932805209892535, 34.838907375774738 ], [ 5.028406610276818, 34.900195623729019 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.272474399174143, 35.018431300763893 ], [ 5.536747673766115, 35.0663611917156 ], [ 5.613538853335399, 35.1064362663206 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.830631544909863, 35.288440660008405 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.932279087409938, 35.143565782434905 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.993102248270134, 34.989363308369946 ], [ 6.072218866007859, 35.058583888835358 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.540407342324954, 34.765681871421236 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.749541863865886, 34.712222602291263 ], [ 6.695901726683303, 34.376093248004963 ], [ 6.558545769571595, 34.317027086330938 ], [ 6.211435174193923, 34.428415636472437 ], [ 5.998528272761575, 34.394955146562722 ], [ 5.477164748154564, 34.449835517195368 ], [ 5.311748488102239, 34.498256334562143 ], [ 5.29903608624636, 34.449060369940128 ], [ 5.217232293335655, 34.411956692247543 ], [ 5.185451287347007, 34.356662909565614 ], [ 5.359859246207236, 34.166209418529832 ], [ 5.37381188331176, 34.058386541805419 ], [ 5.334847852746179, 33.845996406109236 ], [ 5.121785922582262, 33.572679754966714 ], [ 4.406635775720474, 33.927645169389052 ], [ 4.335063917765581, 33.986039537494662 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.210627068984024, 34.174606839034368 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.273310580918576, 34.625845444711501 ], [ 4.434696078661091, 34.772658189523781 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-17", "NAME_1": "Djelfa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.978280469667936, 32.840760606416836 ], [ 4.176210564665837, 33.029637966319058 ], [ 4.059060093248718, 33.252518419389503 ], [ 3.640997755741807, 33.564334011305561 ], [ 3.232133822617811, 33.947411403713318 ], [ 3.14924482588799, 34.074767970765208 ], [ 3.093537632056041, 34.240029202085964 ], [ 2.949412062416741, 34.263619493145086 ], [ 2.817637160326683, 34.138846747579294 ], [ 2.650205518669736, 34.09797068909603 ], [ 2.639198439156644, 33.911005357111549 ], [ 2.474867383822698, 33.92371775986669 ], [ 2.458279250187218, 34.003247788754436 ], [ 2.48339399643578, 34.113576971699842 ], [ 2.393735386279047, 34.225301419524897 ], [ 2.293483106859867, 34.474950263443873 ], [ 2.309451124669636, 34.563420315195344 ], [ 2.364383172145665, 34.639591376241015 ], [ 2.369964227167316, 34.776301378206028 ], [ 2.460294630892463, 34.870171616926541 ], [ 2.523443230820419, 34.999104316011369 ], [ 2.620284864654764, 35.031143704418412 ], [ 2.379317661181119, 35.205009060020075 ], [ 2.360042352372659, 35.287975572015 ], [ 2.286816847119781, 35.329265042547661 ], [ 2.285731642401345, 35.439077459756959 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.619819776661359, 35.445692044452244 ], [ 2.687154167630695, 35.453701891329217 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.960264113198264, 35.804739895329874 ], [ 3.081910434918598, 35.67187978852138 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.305979444595664, 35.754587918097911 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.449691603085, 35.659115708922798 ], [ 3.680426873401473, 35.495559800844092 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.572113071161311, 35.001688137497524 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.658826124625932, 34.805576077074249 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.921084019411978, 34.672664293422372 ], [ 4.036374138855422, 34.441929023105899 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.218068475080031, 34.166235256052232 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.362400750294398, 33.957384955351415 ], [ 5.121785922582262, 33.572679754966714 ], [ 5.008046094951283, 33.441886704807587 ], [ 4.95311404837463, 33.24148550145469 ], [ 4.978280469667936, 32.840760606416836 ] ] ] } }
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user