mirror of
https://github.com/apache/superset.git
synced 2026-05-07 08:54:23 +00:00
Compare commits
76 Commits
alexandrus
...
enxdev/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3057488cf | ||
|
|
9459bc7bf4 | ||
|
|
cb53745d43 | ||
|
|
9e91ae8cff | ||
|
|
5b5f23d127 | ||
|
|
8173cfe9e3 | ||
|
|
586de12a05 | ||
|
|
d6188374b4 | ||
|
|
2edae162f0 | ||
|
|
e80207218b | ||
|
|
76955017eb | ||
|
|
5325b87e73 | ||
|
|
e76318633e | ||
|
|
c2725e86f3 | ||
|
|
2f605724e7 | ||
|
|
ebb02d0ecf | ||
|
|
319b8a1124 | ||
|
|
2be971ce77 | ||
|
|
812f4ae080 | ||
|
|
af8d15fdfc | ||
|
|
673634f7af | ||
|
|
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 | ||
|
|
a3c5d0356d | ||
|
|
60ef1ed246 | ||
|
|
624b5ec260 | ||
|
|
c288f3ffd6 | ||
|
|
e21fb5659a | ||
|
|
f80c46005e | ||
|
|
b5e76ccf35 | ||
|
|
ad53a4c744 |
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
|
||||
|
||||
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
|
||||
|
||||
@@ -54,6 +54,7 @@ jobs:
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ The Deck.gl MapBox chart's **Opacity**, **Default longitude**, **Default latitud
|
||||
|
||||
**To restore fit-to-data behavior:** Open the chart in Explore, clear the **Default longitude**, **Default latitude**, and **Zoom** fields in the Viewport section, and re-save the chart.
|
||||
|
||||
### Combined datasource list endpoint
|
||||
|
||||
Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to serve datasets and semantic views in one response.
|
||||
|
||||
- The endpoint is available to users with at least one of `can_read` on `Dataset` or `SemanticView`.
|
||||
- Semantic views are included only when the `SEMANTIC_LAYERS` feature flag is enabled.
|
||||
- The endpoint enforces strict `order_column` validation and returns `400` for invalid sort columns.
|
||||
### ClickHouse minimum driver version bump
|
||||
|
||||
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
|
||||
|
||||
@@ -105,7 +105,13 @@ class CeleryConfig:
|
||||
|
||||
CELERY_CONFIG = CeleryConfig
|
||||
|
||||
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
|
||||
FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True,
|
||||
"DATASET_FOLDERS": True,
|
||||
"ENABLE_EXTENSIONS": True,
|
||||
"SEMANTIC_LAYERS": True,
|
||||
}
|
||||
EXTENSIONS_PATH = "/app/docker/extensions"
|
||||
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
|
||||
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
|
||||
# The base URL for the email report hyperlinks.
|
||||
|
||||
@@ -224,3 +224,52 @@ async def analysis_guide(ctx: Context) -> str:
|
||||
```
|
||||
|
||||
See [MCP Integration](./mcp) for implementation details.
|
||||
|
||||
### Semantic Layers
|
||||
|
||||
Extensions can register custom semantic layer implementations that allow Superset to connect to external data modeling frameworks. Each semantic layer defines how to authenticate, discover semantic views (tables/metrics/dimensions), and execute queries against the external system.
|
||||
|
||||
```python
|
||||
from superset_core.semantic_layers.decorators import semantic_layer
|
||||
from superset_core.semantic_layers.layer import SemanticLayer
|
||||
|
||||
from my_extension.config import MyConfig
|
||||
from my_extension.view import MySemanticView
|
||||
|
||||
|
||||
@semantic_layer(
|
||||
id="my_platform",
|
||||
name="My Data Platform",
|
||||
description="Connect to My Data Platform's semantic layer",
|
||||
)
|
||||
class MySemanticLayer(SemanticLayer[MyConfig, MySemanticView]):
|
||||
configuration_class = MyConfig
|
||||
|
||||
@classmethod
|
||||
def from_configuration(cls, configuration: dict) -> "MySemanticLayer":
|
||||
config = MyConfig.model_validate(configuration)
|
||||
return cls(config)
|
||||
|
||||
@classmethod
|
||||
def get_configuration_schema(cls, configuration=None) -> dict:
|
||||
return MyConfig.model_json_schema()
|
||||
|
||||
@classmethod
|
||||
def get_runtime_schema(cls, configuration=None, runtime_data=None) -> dict:
|
||||
return {"type": "object", "properties": {}}
|
||||
|
||||
def get_semantic_views(self, runtime_configuration: dict) -> set[MySemanticView]:
|
||||
# Return available views from the external platform
|
||||
...
|
||||
|
||||
def get_semantic_view(self, name: str, additional_configuration: dict) -> MySemanticView:
|
||||
# Return a specific view by name
|
||||
...
|
||||
```
|
||||
|
||||
**Note**: The `@semantic_layer` decorator automatically detects context and applies appropriate ID prefixing:
|
||||
|
||||
- **Extension context**: ID prefixed as `extensions.{publisher}.{name}.{id}`
|
||||
- **Host context**: Original ID used as-is
|
||||
|
||||
The decorator registers the class in the semantic layers registry, making it available in the UI for users to create connections. The `configuration_class` should be a Pydantic model that defines the fields needed to connect (credentials, project, database, etc.). Superset uses the model's JSON schema to render the configuration form dynamically.
|
||||
|
||||
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`).
|
||||
@@ -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,12 +67,12 @@
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.32",
|
||||
"@swc/core": "^1.15.33",
|
||||
"antd": "^6.3.7",
|
||||
"baseline-browser-mapping": "^2.10.24",
|
||||
"baseline-browser-mapping": "^2.10.27",
|
||||
"caniuse-lite": "^1.0.30001791",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.1",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.1",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
@@ -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",
|
||||
@@ -103,10 +103,10 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.5.0",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.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"
|
||||
}
|
||||
|
||||
6
docs/static/feature-flags.json
vendored
6
docs/static/feature-flags.json
vendored
@@ -81,6 +81,12 @@
|
||||
"lifecycle": "development",
|
||||
"description": "Expand nested types in Presto into extra columns/arrays. Experimental, doesn't work with all nested types."
|
||||
},
|
||||
{
|
||||
"name": "SEMANTIC_LAYERS",
|
||||
"default": false,
|
||||
"lifecycle": "development",
|
||||
"description": "Enable semantic layers and show semantic views alongside datasets"
|
||||
},
|
||||
{
|
||||
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
|
||||
"default": false,
|
||||
|
||||
1405
docs/yarn.lock
1405
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.15.4 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.15.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -844,6 +844,8 @@ postgresql:
|
||||
database: superset
|
||||
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: bitnamilegacy/postgresql
|
||||
tag: "14.17.0-debian-12-r3"
|
||||
|
||||
## PostgreSQL Primary parameters
|
||||
@@ -918,6 +920,11 @@ redis:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: bitnamilegacy/redis
|
||||
tag: 7.0.10-debian-11-r4
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +95,7 @@ dependencies = [
|
||||
"redis>=5.0.0, <6.0",
|
||||
"rison>=2.0.0, <3.0",
|
||||
"selenium>=4.14.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.3, <2.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=3.15.0",
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
@@ -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]
|
||||
@@ -149,7 +149,7 @@ fastmcp = ["fastmcp>=3.2.4,<4.0"]
|
||||
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=23.9.1"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.4.3, <2"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
|
||||
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
|
||||
hive = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
@@ -175,13 +175,13 @@ 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"]
|
||||
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
|
||||
risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.3, <2"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
|
||||
sqlite = ["syntaqlite>=0.1.0"]
|
||||
@@ -288,6 +288,7 @@ module = [
|
||||
"superset.tags.filters",
|
||||
"superset.commands.security.update",
|
||||
"superset.commands.security.create",
|
||||
"superset.semantic_layers.api",
|
||||
]
|
||||
warn_unused_ignores = false
|
||||
|
||||
|
||||
@@ -381,7 +381,7 @@ selenium==4.32.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
setuptools==80.9.0
|
||||
# via -r requirements/base.in
|
||||
shillelagh==1.4.3
|
||||
shillelagh==1.4.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
simplejson==3.20.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
|
||||
@@ -707,7 +707,7 @@ protobuf==4.25.8
|
||||
# proto-plus
|
||||
psutil==6.1.0
|
||||
# via apache-superset
|
||||
psycopg2-binary==2.9.9
|
||||
psycopg2-binary==2.9.12
|
||||
# via apache-superset
|
||||
py-key-value-aio==0.4.4
|
||||
# via fastmcp
|
||||
@@ -931,7 +931,7 @@ setuptools==80.9.0
|
||||
# pydata-google-auth
|
||||
# zope-event
|
||||
# zope-interface
|
||||
shillelagh==1.4.3
|
||||
shillelagh==1.4.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-core"
|
||||
version = "0.1.0rc2"
|
||||
version = "0.1.0rc3"
|
||||
description = "Core Python package for building Apache Superset backend extensions and integrations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -43,6 +43,8 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"flask-appbuilder>=5.0.2,<6",
|
||||
"isodate>=0.7.0",
|
||||
"pyarrow>=16.0.0",
|
||||
"pydantic>=2.8.0",
|
||||
"sqlalchemy>=1.4.0,<2.0",
|
||||
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
|
||||
|
||||
73
superset-core/src/superset_core/semantic_layers/config.py
Normal file
73
superset-core/src/superset_core/semantic_layers/config.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def build_configuration_schema(
|
||||
config_class: type[BaseModel],
|
||||
configuration: BaseModel | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a JSON schema from a Pydantic configuration class.
|
||||
|
||||
Handles generic boilerplate that any semantic layer with dynamic fields needs:
|
||||
|
||||
- Reorders properties to match model field order (Pydantic sorts alphabetically)
|
||||
- When ``configuration`` is None, sets ``enum: []`` on all ``x-dynamic`` properties
|
||||
so the frontend renders them as empty dropdowns
|
||||
|
||||
Semantic layer implementations call this instead of
|
||||
``model_json_schema()`` directly,
|
||||
then only need to add their own dynamic population logic.
|
||||
"""
|
||||
schema = config_class.model_json_schema()
|
||||
|
||||
# Pydantic sorts properties alphabetically; restore model field order
|
||||
field_order = [
|
||||
field.alias or name for name, field in config_class.model_fields.items()
|
||||
]
|
||||
schema["properties"] = {
|
||||
key: schema["properties"][key]
|
||||
for key in field_order
|
||||
if key in schema["properties"]
|
||||
}
|
||||
|
||||
if configuration is None:
|
||||
for prop_schema in schema["properties"].values():
|
||||
if prop_schema.get("x-dynamic"):
|
||||
prop_schema["enum"] = []
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def check_dependencies(
|
||||
prop_schema: dict[str, Any],
|
||||
configuration: BaseModel,
|
||||
) -> bool:
|
||||
"""
|
||||
Check whether a dynamic property's dependencies are satisfied.
|
||||
|
||||
Reads the ``x-dependsOn`` list from the property schema and returns ``True``
|
||||
when every referenced attribute on ``configuration`` is truthy.
|
||||
"""
|
||||
dependencies = prop_schema.get("x-dependsOn", [])
|
||||
return all(getattr(configuration, dep, None) for dep in dependencies)
|
||||
169
superset-core/src/superset_core/semantic_layers/daos.py
Normal file
169
superset-core/src/superset_core/semantic_layers/daos.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Semantic layer DAO interfaces for superset-core.
|
||||
|
||||
Provides abstract DAO classes for semantic layers and views that define the
|
||||
interface contract. Host implementations replace these with concrete classes
|
||||
backed by SQLAlchemy during initialization.
|
||||
|
||||
Usage:
|
||||
from superset_core.semantic_layers.daos import (
|
||||
AbstractSemanticLayerDAO,
|
||||
AbstractSemanticViewDAO,
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from superset_core.common.daos import BaseDAO
|
||||
from superset_core.semantic_layers.models import SemanticLayerModel, SemanticViewModel
|
||||
|
||||
|
||||
class AbstractSemanticLayerDAO(BaseDAO[SemanticLayerModel]):
|
||||
"""
|
||||
Abstract DAO interface for SemanticLayer.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete DAO providing actual database access.
|
||||
"""
|
||||
|
||||
model_cls: ClassVar[type[Any] | None] = None
|
||||
base_filter = None
|
||||
id_column_name = "uuid"
|
||||
uuid_column_name = "uuid"
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def validate_uniqueness(cls, name: str) -> bool:
|
||||
"""
|
||||
Validate that a semantic layer name is unique.
|
||||
|
||||
:param name: Semantic layer name to validate
|
||||
:return: True if the name is unique, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def validate_update_uniqueness(cls, layer_uuid: str, name: str) -> bool:
|
||||
"""
|
||||
Validate that a semantic layer name is unique for an update operation,
|
||||
excluding the layer being updated.
|
||||
|
||||
:param layer_uuid: UUID of the semantic layer being updated
|
||||
:param name: New name to validate
|
||||
:return: True if the name is unique, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def find_by_name(cls, name: str) -> SemanticLayerModel | None:
|
||||
"""
|
||||
Find a semantic layer by name.
|
||||
|
||||
:param name: Semantic layer name
|
||||
:return: SemanticLayerModel instance or None
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_semantic_views(cls, layer_uuid: str) -> list[SemanticViewModel]:
|
||||
"""
|
||||
Get all semantic views associated with a semantic layer.
|
||||
|
||||
:param layer_uuid: UUID of the semantic layer
|
||||
:return: List of SemanticViewModel instances
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class AbstractSemanticViewDAO(BaseDAO[SemanticViewModel]):
|
||||
"""
|
||||
Abstract DAO interface for SemanticView.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete DAO providing actual database access.
|
||||
"""
|
||||
|
||||
model_cls: ClassVar[type[Any] | None] = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
uuid_column_name = "uuid"
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def validate_uniqueness(
|
||||
cls,
|
||||
name: str,
|
||||
layer_uuid: str,
|
||||
configuration: dict[str, Any],
|
||||
) -> bool:
|
||||
"""
|
||||
Validate that a semantic view is unique within a semantic layer.
|
||||
|
||||
Uniqueness is determined by the combination of name, layer UUID, and
|
||||
configuration.
|
||||
|
||||
:param name: View name
|
||||
:param layer_uuid: UUID of the parent semantic layer
|
||||
:param configuration: Configuration dict to compare
|
||||
:return: True if unique, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def validate_update_uniqueness(
|
||||
cls,
|
||||
view_uuid: str,
|
||||
name: str,
|
||||
layer_uuid: str,
|
||||
configuration: dict[str, Any],
|
||||
) -> bool:
|
||||
"""
|
||||
Validate that a semantic view is unique within a semantic layer for an
|
||||
update operation, excluding the view being updated.
|
||||
|
||||
:param view_uuid: UUID of the view being updated
|
||||
:param name: New name to validate
|
||||
:param layer_uuid: UUID of the parent semantic layer
|
||||
:param configuration: Configuration dict to compare
|
||||
:return: True if unique, False otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def find_by_name(cls, name: str, layer_uuid: str) -> SemanticViewModel | None:
|
||||
"""
|
||||
Find a semantic view by name within a semantic layer.
|
||||
|
||||
:param name: View name
|
||||
:param layer_uuid: UUID of the parent semantic layer
|
||||
:return: SemanticViewModel instance or None
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = ["AbstractSemanticLayerDAO", "AbstractSemanticViewDAO"]
|
||||
102
superset-core/src/superset_core/semantic_layers/decorators.py
Normal file
102
superset-core/src/superset_core/semantic_layers/decorators.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Semantic layer registration decorator for Superset.
|
||||
|
||||
This module provides a decorator interface to register semantic layer
|
||||
implementations with the host application, enabling automatic discovery
|
||||
by the extensions framework.
|
||||
|
||||
Usage:
|
||||
from superset_core.semantic_layers.decorators import semantic_layer
|
||||
|
||||
@semantic_layer(
|
||||
id="snowflake",
|
||||
name="Snowflake Cortex",
|
||||
description="Snowflake semantic layer via Cortex Analyst",
|
||||
)
|
||||
class SnowflakeSemanticLayer(SemanticLayer[SnowflakeConfig, SnowflakeView]):
|
||||
...
|
||||
|
||||
# Or with minimal arguments:
|
||||
@semantic_layer(id="dbt", name="dbt Semantic Layer")
|
||||
class DbtSemanticLayer(SemanticLayer[DbtConfig, DbtView]):
|
||||
...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
# Type variable for decorated semantic layer classes
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def semantic_layer(
|
||||
id: str,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
) -> Callable[[T], T]:
|
||||
"""
|
||||
Decorator to register a semantic layer implementation.
|
||||
|
||||
Automatically detects extension context and applies appropriate
|
||||
namespacing to prevent ID conflicts between host and extension
|
||||
semantic layers.
|
||||
|
||||
Host implementations will replace this function during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
Args:
|
||||
id: Unique semantic layer type identifier (e.g., "snowflake",
|
||||
"dbt"). Used as the key in the semantic layers registry and
|
||||
stored in the ``type`` column of the ``SemanticLayer`` model.
|
||||
name: Human-readable display name (e.g., "Snowflake Cortex").
|
||||
Shown in the UI when listing available semantic layer types.
|
||||
description: Optional description for documentation and UI
|
||||
tooltips.
|
||||
|
||||
Returns:
|
||||
Decorated semantic layer class registered with the host
|
||||
application.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If called before host implementation is
|
||||
initialized.
|
||||
|
||||
Example:
|
||||
from superset_core.semantic_layers.decorators import semantic_layer
|
||||
from superset_core.semantic_layers.layer import SemanticLayer
|
||||
|
||||
@semantic_layer(
|
||||
id="snowflake",
|
||||
name="Snowflake Cortex",
|
||||
description="Connect to Snowflake Cortex Analyst",
|
||||
)
|
||||
class SnowflakeSemanticLayer(
|
||||
SemanticLayer[SnowflakeConfig, SnowflakeView]
|
||||
):
|
||||
...
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Semantic layer decorator not initialized. "
|
||||
"This decorator should be replaced during Superset startup."
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["semantic_layer"]
|
||||
129
superset-core/src/superset_core/semantic_layers/layer.py
Normal file
129
superset-core/src/superset_core/semantic_layers/layer.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
from superset_core.semantic_layers.view import SemanticView
|
||||
|
||||
ConfigT = TypeVar("ConfigT", bound=BaseModel)
|
||||
SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView")
|
||||
|
||||
|
||||
class SemanticLayer(ABC, Generic[ConfigT, SemanticViewT]):
|
||||
"""
|
||||
Abstract base class for semantic layers.
|
||||
"""
|
||||
|
||||
configuration_class: type[BaseModel]
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def from_configuration(
|
||||
cls,
|
||||
configuration: dict[str, Any],
|
||||
) -> SemanticLayer[ConfigT, SemanticViewT]:
|
||||
"""
|
||||
Create a semantic layer from its configuration.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Semantic layers must implement the from_configuration method"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_configuration_schema(
|
||||
cls,
|
||||
configuration: ConfigT | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get the JSON schema for the configuration needed to add the semantic layer.
|
||||
|
||||
A partial configuration `configuration` can be sent to improve the schema,
|
||||
allowing for progressive validation and better UX. For example, a semantic
|
||||
layer might require:
|
||||
|
||||
- auth information
|
||||
- a database
|
||||
|
||||
If the user provides the auth information, a client can send the partial
|
||||
configuration to this method, and the resulting JSON schema would include
|
||||
the list of databases the user has access to, allowing a dropdown to be
|
||||
populated.
|
||||
|
||||
The Snowflake semantic layer has an example implementation of this method, where
|
||||
database and schema names are populated based on the provided connection info.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Semantic layers must implement the get_configuration_schema method"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_runtime_schema(
|
||||
cls,
|
||||
configuration: ConfigT,
|
||||
runtime_data: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get the JSON schema for the runtime parameters needed to load semantic views.
|
||||
|
||||
This returns the schema needed to connect to a semantic view given the
|
||||
configuration for the semantic layer. For example, a semantic layer might
|
||||
be configured by:
|
||||
|
||||
- auth information
|
||||
- an optional database
|
||||
|
||||
If the user does not provide a database when creating the semantic layer, the
|
||||
runtime schema would require the database name to be provided before loading any
|
||||
semantic views. This allows users to create semantic layers that connect to a
|
||||
specific database (or project, account, etc.), or that allow users to select it
|
||||
at query time.
|
||||
|
||||
The Snowflake semantic layer has an example implementation of this method, where
|
||||
database and schema names are required if they were not provided in the initial
|
||||
configuration.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Semantic layers must implement the get_runtime_schema method"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_semantic_views(
|
||||
self,
|
||||
runtime_configuration: dict[str, Any],
|
||||
) -> set[SemanticViewT]:
|
||||
"""
|
||||
Get the semantic views available in the semantic layer.
|
||||
|
||||
The runtime configuration can provide information like a given project or
|
||||
schema, used to restrict the semantic views returned.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_semantic_view(
|
||||
self,
|
||||
name: str,
|
||||
additional_configuration: dict[str, Any],
|
||||
) -> SemanticViewT:
|
||||
"""
|
||||
Get a specific semantic view by its name and additional configuration.
|
||||
"""
|
||||
85
superset-core/src/superset_core/semantic_layers/models.py
Normal file
85
superset-core/src/superset_core/semantic_layers/models.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Semantic layer model interfaces for superset-core.
|
||||
|
||||
Provides abstract model classes for semantic layers and views that will be
|
||||
replaced by the host implementation's concrete SQLAlchemy models during
|
||||
initialization.
|
||||
|
||||
Usage:
|
||||
from superset_core.semantic_layers.models import (
|
||||
SemanticLayerModel,
|
||||
SemanticViewModel,
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from superset_core.common.models import CoreModel
|
||||
|
||||
|
||||
class SemanticLayerModel(CoreModel):
|
||||
"""
|
||||
Abstract interface for the SemanticLayer database model.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete SQLAlchemy model providing actual persistence.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected column attributes
|
||||
uuid: UUID
|
||||
name: str
|
||||
description: str | None
|
||||
type: str
|
||||
configuration: str
|
||||
configuration_version: int
|
||||
cache_timeout: int | None
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
|
||||
|
||||
class SemanticViewModel(CoreModel):
|
||||
"""
|
||||
Abstract interface for the SemanticView database model.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete SQLAlchemy model providing actual persistence.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected column attributes
|
||||
id: int
|
||||
uuid: UUID
|
||||
name: str
|
||||
description: str | None
|
||||
configuration: str
|
||||
configuration_version: int
|
||||
cache_timeout: int | None
|
||||
semantic_layer_uuid: UUID
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
|
||||
|
||||
__all__ = ["SemanticLayerModel", "SemanticViewModel"]
|
||||
209
superset-core/src/superset_core/semantic_layers/types.py
Normal file
209
superset-core/src/superset_core/semantic_layers/types.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
import isodate
|
||||
import pyarrow as pa
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Grain:
|
||||
"""
|
||||
Represents a time grain (e.g., day, month, year).
|
||||
|
||||
Attributes:
|
||||
name: Human-readable name of the grain (e.g., "Second")
|
||||
representation: ISO 8601 duration (e.g., "PT1S", "P1D", "P1M")
|
||||
"""
|
||||
|
||||
name: str
|
||||
representation: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
isodate.parse_duration(self.representation)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Grain):
|
||||
return self.representation == other.representation
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.representation)
|
||||
|
||||
|
||||
class Grains:
|
||||
"""Pre-defined common grains and factory for custom ones."""
|
||||
|
||||
SECOND = Grain("Second", "PT1S")
|
||||
MINUTE = Grain("Minute", "PT1M")
|
||||
HOUR = Grain("Hour", "PT1H")
|
||||
DAY = Grain("Day", "P1D")
|
||||
WEEK = Grain("Week", "P1W")
|
||||
MONTH = Grain("Month", "P1M")
|
||||
QUARTER = Grain("Quarter", "P3M")
|
||||
YEAR = Grain("Year", "P1Y")
|
||||
|
||||
_REGISTRY: dict[str, Grain] = {
|
||||
"PT1S": SECOND,
|
||||
"PT1M": MINUTE,
|
||||
"PT1H": HOUR,
|
||||
"P1D": DAY,
|
||||
"P1W": WEEK,
|
||||
"P1M": MONTH,
|
||||
"P3M": QUARTER,
|
||||
"P1Y": YEAR,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, representation: str, name: str | None = None) -> Grain:
|
||||
"""Return a pre-defined grain or create a custom one."""
|
||||
if grain := cls._REGISTRY.get(representation):
|
||||
return grain
|
||||
return Grain(name or representation, representation)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dimension:
|
||||
id: str
|
||||
name: str
|
||||
type: pa.DataType
|
||||
|
||||
definition: str | None = None
|
||||
description: str | None = None
|
||||
grain: Grain | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metric:
|
||||
id: str
|
||||
name: str
|
||||
type: pa.DataType
|
||||
|
||||
definition: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdhocExpression:
|
||||
id: str
|
||||
definition: str
|
||||
|
||||
|
||||
class Operator(str, enum.Enum):
|
||||
EQUALS = "="
|
||||
NOT_EQUALS = "!="
|
||||
GREATER_THAN = ">"
|
||||
LESS_THAN = "<"
|
||||
GREATER_THAN_OR_EQUAL = ">="
|
||||
LESS_THAN_OR_EQUAL = "<="
|
||||
IN = "IN"
|
||||
NOT_IN = "NOT IN"
|
||||
LIKE = "LIKE"
|
||||
NOT_LIKE = "NOT LIKE"
|
||||
IS_NULL = "IS NULL"
|
||||
IS_NOT_NULL = "IS NOT NULL"
|
||||
ADHOC = "ADHOC"
|
||||
|
||||
|
||||
FilterValues = str | int | float | bool | datetime | date | time | timedelta | None
|
||||
|
||||
|
||||
class PredicateType(enum.Enum):
|
||||
WHERE = "WHERE"
|
||||
HAVING = "HAVING"
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Filter:
|
||||
type: PredicateType
|
||||
column: Dimension | Metric | None
|
||||
operator: Operator
|
||||
value: FilterValues | frozenset[FilterValues]
|
||||
|
||||
|
||||
class OrderDirection(enum.Enum):
|
||||
ASC = "ASC"
|
||||
DESC = "DESC"
|
||||
|
||||
|
||||
OrderTuple = tuple[Metric | Dimension | AdhocExpression, OrderDirection]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroupLimit:
|
||||
"""
|
||||
Limit query to top/bottom N combinations of specified dimensions.
|
||||
|
||||
The `filters` parameter allows specifying separate filter constraints for the
|
||||
group limit subquery. This is useful when you want to determine the top N groups
|
||||
using different criteria (e.g., a different time range) than the main query.
|
||||
|
||||
For example, you might want to find the top 10 products by sales over the last
|
||||
30 days, but then show daily sales for those products over the last 7 days.
|
||||
"""
|
||||
|
||||
dimensions: list[Dimension]
|
||||
top: int
|
||||
metric: Metric | None
|
||||
direction: OrderDirection = OrderDirection.DESC
|
||||
group_others: bool = False
|
||||
filters: set[Filter] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticRequest:
|
||||
"""
|
||||
Represents a request made to obtain semantic results.
|
||||
|
||||
This could be a SQL query, an HTTP request, etc.
|
||||
"""
|
||||
|
||||
type: str
|
||||
definition: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticResult:
|
||||
"""
|
||||
Represents the results of a semantic query.
|
||||
|
||||
This includes any requests (SQL queries, HTTP requests) that were performed in order
|
||||
to obtain the results, in order to help troubleshooting.
|
||||
"""
|
||||
|
||||
requests: list[SemanticRequest]
|
||||
results: pa.Table
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticQuery:
|
||||
"""
|
||||
Represents a semantic query.
|
||||
"""
|
||||
|
||||
metrics: list[Metric]
|
||||
dimensions: list[Dimension]
|
||||
filters: set[Filter] | None = None
|
||||
order: list[OrderTuple] | None = None
|
||||
limit: int | None = None
|
||||
offset: int | None = None
|
||||
group_limit: GroupLimit | None = None
|
||||
113
superset-core/src/superset_core/semantic_layers/view.py
Normal file
113
superset-core/src/superset_core/semantic_layers/view.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from superset_core.semantic_layers.types import (
|
||||
Dimension,
|
||||
Filter,
|
||||
Metric,
|
||||
SemanticQuery,
|
||||
SemanticResult,
|
||||
)
|
||||
|
||||
|
||||
# TODO (betodealmeida): move to the extension JSON
|
||||
class SemanticViewFeature(enum.Enum):
|
||||
"""
|
||||
Custom features supported by semantic layers.
|
||||
"""
|
||||
|
||||
ADHOC_EXPRESSIONS_IN_ORDERBY = "ADHOC_EXPRESSIONS_IN_ORDERBY"
|
||||
GROUP_LIMIT = "GROUP_LIMIT"
|
||||
GROUP_OTHERS = "GROUP_OTHERS"
|
||||
|
||||
|
||||
class SemanticView(ABC):
|
||||
"""
|
||||
Abstract base class for semantic views.
|
||||
"""
|
||||
|
||||
features: frozenset[SemanticViewFeature]
|
||||
|
||||
# Implementations must expose a display name for the view.
|
||||
# Declared here as a type annotation (not abstract) so that existing
|
||||
# implementations are not required to add a formal @abstractmethod.
|
||||
name: str
|
||||
|
||||
@abstractmethod
|
||||
def uid(self) -> str:
|
||||
"""
|
||||
Returns a unique identifier for the semantic view.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_dimensions(self) -> set[Dimension]:
|
||||
"""
|
||||
Get the dimensions defined in the semantic view.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_metrics(self) -> set[Metric]:
|
||||
"""
|
||||
Get the metrics defined in the semantic view.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_values(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
filters: set[Filter] | None = None,
|
||||
) -> SemanticResult:
|
||||
"""
|
||||
Return distinct values for a dimension.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_table(self, query: SemanticQuery) -> SemanticResult:
|
||||
"""
|
||||
Execute a semantic query and return the results.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_row_count(self, query: SemanticQuery) -> SemanticResult:
|
||||
"""
|
||||
Execute a query and return the number of rows the result would have.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_compatible_metrics(
|
||||
self,
|
||||
selected_metrics: set[Metric],
|
||||
selected_dimensions: set[Dimension],
|
||||
) -> set[Metric]:
|
||||
"""
|
||||
Return metrics compatible with the selected dimensions.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_compatible_dimensions(
|
||||
self,
|
||||
selected_metrics: set[Metric],
|
||||
selected_dimensions: set[Dimension],
|
||||
) -> set[Dimension]:
|
||||
"""
|
||||
Return dimensions compatible with the selected metrics.
|
||||
"""
|
||||
@@ -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 = [
|
||||
|
||||
10418
superset-frontend/package-lock.json
generated
10418
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,14 @@
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/react": "^3.7.0",
|
||||
"@jsonforms/vanilla-renderers": "^3.7.0",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/antd": "^5.24.13",
|
||||
"@rjsf/core": "^5.24.13",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
"@rjsf/validator-ajv8": "^5.24.13",
|
||||
@@ -183,7 +190,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 +201,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 +218,6 @@
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-split": "^2.0.9",
|
||||
"react-table": "^7.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
@@ -244,14 +250,13 @@
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-env": "^7.29.3",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/register": "^7.23.7",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@babel/runtime-corejs3": "^7.29.2",
|
||||
"@babel/types": "^7.28.6",
|
||||
"@cypress/react": "^8.0.2",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
@@ -271,12 +276,11 @@
|
||||
"@storybook/test-runner": "^0.17.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.32",
|
||||
"@swc/plugin-emotion": "^14.8.0",
|
||||
"@swc/plugin-emotion": "^14.9.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -286,8 +290,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^17.0.83",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
@@ -306,7 +310,7 @@
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"baseline-browser-mapping": "^2.10.21",
|
||||
"baseline-browser-mapping": "^2.10.24",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
@@ -323,7 +327,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",
|
||||
@@ -373,7 +377,7 @@
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.3",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-sources": "^3.4.0",
|
||||
"webpack-sources": "^3.4.1",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "*"
|
||||
},
|
||||
|
||||
@@ -94,11 +94,20 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
|
||||
/**
|
||||
* Increment the color range with analogous colors
|
||||
*
|
||||
* @param forceMinimumExpansion When true, expand at least once even if the
|
||||
* ordinal domain is still shorter than the palette. Shared dashboard labels
|
||||
* can resolve from the global map without entering the scale domain, so
|
||||
* domain-based sizing alone would skip expansion while collision resolution
|
||||
* still needs analogous colors.
|
||||
*/
|
||||
incrementColorRange() {
|
||||
const multiple = Math.floor(
|
||||
incrementColorRange(forceMinimumExpansion = false) {
|
||||
const domainBasedMultiple = Math.floor(
|
||||
this.domain().length / this.originColors.length,
|
||||
);
|
||||
const multiple = forceMinimumExpansion
|
||||
? Math.max(domainBasedMultiple, 1)
|
||||
: domainBasedMultiple;
|
||||
// the domain has grown larger than the original range
|
||||
// increments the range with analogous colors
|
||||
if (multiple > this.multiple) {
|
||||
@@ -144,6 +153,7 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
if (isFeatureEnabled(FeatureFlag.UseAnalogousColors)) {
|
||||
this.incrementColorRange();
|
||||
}
|
||||
|
||||
if (
|
||||
// feature flag to be deprecated (will become standard behaviour)
|
||||
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
|
||||
@@ -154,6 +164,39 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
|
||||
source === LabelsColorMapSource.Dashboard &&
|
||||
(forcedColor || isExistingLabel)
|
||||
) {
|
||||
const colliding = [...this.chartLabelsColorMap.entries()].filter(
|
||||
([labelKey, c]) => c === color && labelKey !== cleanedValue,
|
||||
);
|
||||
if (
|
||||
colliding.length > 0 &&
|
||||
isFeatureEnabled(FeatureFlag.UseAnalogousColors)
|
||||
) {
|
||||
this.incrementColorRange(true);
|
||||
}
|
||||
for (const [otherLabel] of colliding) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(this.forcedColors, otherLabel)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const newColor = this.getNextAvailableColor(otherLabel, color);
|
||||
this.chartLabelsColorMap.set(otherLabel, newColor);
|
||||
if (sliceId) {
|
||||
this.labelsColorMapInstance.addSlice(
|
||||
otherLabel,
|
||||
newColor,
|
||||
sliceId,
|
||||
appliedColorScheme,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep track of values in this slice
|
||||
this.chartLabelsColorMap.set(cleanedValue, color);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Label } from '..';
|
||||
|
||||
// Define the prop types for DatasetTypeLabel
|
||||
interface DatasetTypeLabelProps {
|
||||
datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
|
||||
datasetType: 'physical' | 'virtual' | 'semantic_view';
|
||||
}
|
||||
|
||||
const SIZE = 's'; // Define the size as a constant
|
||||
@@ -32,6 +32,22 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
|
||||
datasetType,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if (datasetType === 'semantic_view') {
|
||||
return (
|
||||
<Label
|
||||
icon={
|
||||
<Icons.ApartmentOutlined
|
||||
iconSize={SIZE}
|
||||
iconColor={theme.colorInfo}
|
||||
/>
|
||||
}
|
||||
type="info"
|
||||
style={{ color: theme.colorInfo }}
|
||||
>
|
||||
{t('Semantic')}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
const isPhysical = datasetType === 'physical';
|
||||
const label: string = isPhysical ? t('Physical') : t('Virtual');
|
||||
const labelType = isPhysical ? 'primary' : 'default';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
|
||||
import { DatasourceType } from './types/Datasource';
|
||||
|
||||
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
|
||||
table: DatasourceType.Table,
|
||||
query: DatasourceType.Query,
|
||||
dataset: DatasourceType.Dataset,
|
||||
sl_table: DatasourceType.SlTable,
|
||||
saved_query: DatasourceType.SavedQuery,
|
||||
semantic_view: DatasourceType.SemanticView,
|
||||
};
|
||||
|
||||
export default class DatasourceKey {
|
||||
readonly id: number;
|
||||
|
||||
@@ -27,8 +36,7 @@ export default class DatasourceKey {
|
||||
constructor(key: string) {
|
||||
const [idStr, typeStr] = key.split('__');
|
||||
this.id = parseInt(idStr, 10);
|
||||
this.type = DatasourceType.Table; // default to SqlaTable model
|
||||
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
|
||||
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
|
||||
}
|
||||
|
||||
public toString() {
|
||||
|
||||
@@ -26,6 +26,7 @@ export enum DatasourceType {
|
||||
Dataset = 'dataset',
|
||||
SlTable = 'sl_table',
|
||||
SavedQuery = 'saved_query',
|
||||
SemanticView = 'semantic_view',
|
||||
}
|
||||
|
||||
export interface Currency {
|
||||
@@ -40,6 +41,13 @@ export interface Datasource {
|
||||
id: number;
|
||||
name: string;
|
||||
type: DatasourceType;
|
||||
/**
|
||||
* The parent resource that owns this datasource.
|
||||
* For SQL-based datasets this is the database; for semantic views it is the
|
||||
* semantic layer. Use this field instead of the legacy `database` field when
|
||||
* you only need the display name.
|
||||
*/
|
||||
parent?: { name: string };
|
||||
columns: Column[];
|
||||
metrics: Metric[];
|
||||
description?: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -61,6 +61,7 @@ export enum FeatureFlag {
|
||||
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
|
||||
Matrixify = 'MATRIXIFY',
|
||||
ScheduledQueries = 'SCHEDULED_QUERIES',
|
||||
SemanticLayers = 'SEMANTIC_LAYERS',
|
||||
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
|
||||
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
|
||||
SshTunneling = 'SSH_TUNNELING',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ScaleOrdinal } from 'd3-scale';
|
||||
import {
|
||||
CategoricalColorScale,
|
||||
FeatureFlag,
|
||||
getLabelsColorMap,
|
||||
LabelsColorMapSource,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
@@ -199,10 +200,42 @@ describe('CategoricalColorScale', () => {
|
||||
const returnedColor = scale.getColor(value, sliceId);
|
||||
expect(returnedColor).toBe(expectedColor);
|
||||
});
|
||||
test('reassigns colliding colors when no sliceId is provided', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.AvoidColorsCollision]: true,
|
||||
};
|
||||
const PALETTE = ['red', 'blue', 'green'];
|
||||
|
||||
const chartAScale = new CategoricalColorScale(PALETTE);
|
||||
const labelsColorMap = chartAScale.labelsColorMapInstance;
|
||||
labelsColorMap.reset();
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
|
||||
try {
|
||||
chartAScale.getColor('Trains', 101, 'testScheme');
|
||||
|
||||
const chartBScale = new CategoricalColorScale(PALETTE);
|
||||
// Call getColor without sliceId (or with undefined)
|
||||
chartBScale.getColor('Classic Cars', undefined, 'testScheme');
|
||||
chartBScale.getColor('Trains', undefined, 'testScheme');
|
||||
|
||||
const classicCarsColor =
|
||||
chartBScale.chartLabelsColorMap.get('Classic Cars');
|
||||
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
|
||||
|
||||
expect(trainsColor).toBe('red');
|
||||
expect(classicCarsColor).toBeDefined();
|
||||
expect(classicCarsColor).not.toBe('red');
|
||||
} finally {
|
||||
labelsColorMap.reset();
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
}
|
||||
});
|
||||
test('conditionally calls getNextAvailableColor', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.AvoidColorsCollision]: true,
|
||||
};
|
||||
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
|
||||
|
||||
scale.getColor('testValue1');
|
||||
scale.getColor('testValue2');
|
||||
@@ -225,6 +258,27 @@ describe('CategoricalColorScale', () => {
|
||||
|
||||
expect(getNextAvailableColorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
test('reassigns non-forced labels when a dashboard-synced label would duplicate their color', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.AvoidColorsCollision]: true,
|
||||
};
|
||||
|
||||
const dashScale = new CategoricalColorScale(['red', 'blue', 'green']);
|
||||
const sliceId = 501;
|
||||
const colorScheme = 'preset';
|
||||
|
||||
dashScale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
|
||||
jest
|
||||
.spyOn(dashScale.labelsColorMapInstance, 'getColorMap')
|
||||
.mockReturnValue(new Map([['Trains', 'red']]));
|
||||
|
||||
dashScale.getColor('Classic Cars', sliceId, colorScheme);
|
||||
dashScale.getColor('Trains', sliceId, colorScheme);
|
||||
|
||||
expect(dashScale.chartLabelsColorMap.get('Trains')).toBe('red');
|
||||
expect(dashScale.chartLabelsColorMap.get('Classic Cars')).not.toBe('red');
|
||||
expect(dashScale.chartLabelsColorMap.get('Classic Cars')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.setColor(value, forcedColor)', () => {
|
||||
@@ -479,6 +533,131 @@ describe('CategoricalColorScale', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard shared-dimension color collision', () => {
|
||||
let labelsColorMap: ReturnType<typeof getLabelsColorMap>;
|
||||
|
||||
beforeEach(() => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.AvoidColorsCollision]: true,
|
||||
};
|
||||
const sentinel = new CategoricalColorScale(['red', 'blue', 'green']);
|
||||
labelsColorMap = sentinel.labelsColorMapInstance;
|
||||
labelsColorMap.reset();
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
labelsColorMap.reset();
|
||||
});
|
||||
|
||||
test('reproduces the bug without the fix: Classic Cars and Trains would both be red', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.AvoidColorsCollision]: false,
|
||||
};
|
||||
|
||||
const PALETTE = ['red', 'blue', 'green'];
|
||||
|
||||
const chartAScale = new CategoricalColorScale(PALETTE);
|
||||
chartAScale.getColor('Trains', 101, 'testScheme');
|
||||
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
|
||||
|
||||
const chartBScale = new CategoricalColorScale(PALETTE);
|
||||
chartBScale.getColor('Classic Cars', 102, 'testScheme');
|
||||
chartBScale.getColor('Trains', 102, 'testScheme');
|
||||
|
||||
const classicCarsColor =
|
||||
chartBScale.chartLabelsColorMap.get('Classic Cars');
|
||||
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
|
||||
|
||||
expect(trainsColor).toBe('red');
|
||||
expect(classicCarsColor).toBe('red');
|
||||
});
|
||||
|
||||
test('fix: Classic Cars is reassigned when Trains locks red from the dashboard', () => {
|
||||
const PALETTE = ['red', 'blue', 'green'];
|
||||
|
||||
const chartAScale = new CategoricalColorScale(PALETTE);
|
||||
chartAScale.getColor('Trains', 101, 'testScheme');
|
||||
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
|
||||
|
||||
const chartBScale = new CategoricalColorScale(PALETTE);
|
||||
chartBScale.getColor('Classic Cars', 102, 'testScheme');
|
||||
chartBScale.getColor('Trains', 102, 'testScheme');
|
||||
|
||||
const classicCarsColor =
|
||||
chartBScale.chartLabelsColorMap.get('Classic Cars');
|
||||
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
|
||||
|
||||
expect(trainsColor).toBe('red');
|
||||
expect(classicCarsColor).toBeDefined();
|
||||
expect(classicCarsColor).not.toBe('red');
|
||||
});
|
||||
|
||||
test('fix: no series in Chart B share a color when palette has enough colors', () => {
|
||||
const PALETTE = ['red', 'blue', 'green'];
|
||||
|
||||
const chartAScale = new CategoricalColorScale(PALETTE);
|
||||
chartAScale.getColor('Trains', 101, 'testScheme');
|
||||
|
||||
const chartBScale = new CategoricalColorScale(PALETTE);
|
||||
chartBScale.getColor('Classic Cars', 102, 'testScheme');
|
||||
chartBScale.getColor('Trains', 102, 'testScheme');
|
||||
|
||||
const colors = Array.from(chartBScale.chartLabelsColorMap.values());
|
||||
const uniqueColors = new Set(colors);
|
||||
|
||||
expect(uniqueColors.size).toBe(colors.length);
|
||||
});
|
||||
|
||||
test('fix: increments analogous color range for dashboard collisions when UseAnalogousColors is enabled', () => {
|
||||
window.featureFlags = {
|
||||
[FeatureFlag.AvoidColorsCollision]: true,
|
||||
[FeatureFlag.UseAnalogousColors]: true,
|
||||
};
|
||||
|
||||
const PALETTE = ['red', 'blue', 'green'];
|
||||
|
||||
const chartAScale = new CategoricalColorScale(PALETTE);
|
||||
chartAScale.getColor('Trains', 101, 'testScheme');
|
||||
|
||||
const chartBScale = new CategoricalColorScale(PALETTE);
|
||||
const addSliceSpy = jest.spyOn(
|
||||
chartBScale.labelsColorMapInstance,
|
||||
'addSlice',
|
||||
);
|
||||
chartBScale.getColor('Classic Cars', 102, 'testScheme');
|
||||
chartBScale.getColor('Model T', 102, 'testScheme');
|
||||
chartBScale.getColor('Trains', 102, 'testScheme');
|
||||
|
||||
expect(chartBScale.chartLabelsColorMap.get('Trains')).toBe('red');
|
||||
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).toBeDefined();
|
||||
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).not.toBe(
|
||||
'red',
|
||||
);
|
||||
expect(chartBScale.range()).toHaveLength(6);
|
||||
expect(
|
||||
addSliceSpy.mock.calls.some(
|
||||
([label, color]) => label === 'Classic Cars' && color !== 'red',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('fix: forced colors (user-set in dashboard JSON) are never reassigned', () => {
|
||||
const PALETTE = ['red', 'blue', 'green'];
|
||||
const forcedColors = { 'Classic Cars': 'red' };
|
||||
|
||||
const chartAScale = new CategoricalColorScale(PALETTE);
|
||||
chartAScale.getColor('Trains', 101, 'testScheme');
|
||||
|
||||
const chartBScale = new CategoricalColorScale(PALETTE, forcedColors);
|
||||
chartBScale.getColor('Classic Cars', 102, 'testScheme');
|
||||
chartBScale.getColor('Trains', 102, 'testScheme');
|
||||
|
||||
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).toBe('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe("is compatible with D3's ScaleOrdinal", () => {
|
||||
test('passes type check', () => {
|
||||
const scale: ScaleOrdinal<{ toString(): string }, string> =
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
|
||||
import type { SupersetClientInterface } from '@superset-ui/core';
|
||||
import { LOGIN_GLOB } from './fixtures/constants';
|
||||
|
||||
beforeAll(() => fetchMock.mockGlobal());
|
||||
@@ -31,33 +32,46 @@ describe('SupersetClient', () => {
|
||||
|
||||
afterEach(() => SupersetClient.reset());
|
||||
|
||||
test('exposes reset, configure, init, get, post, postForm, isAuthenticated, and reAuthenticate methods', () => {
|
||||
const clientWithGetUrl = SupersetClient as SupersetClientInterface & {
|
||||
getUrl: (...args: unknown[]) => string;
|
||||
};
|
||||
|
||||
test('exposes configure, init, get, post, postForm, delete, put, request, reset, getGuestToken, getCSRFToken, getUrl, isAuthenticated, and reAuthenticate methods', () => {
|
||||
expect(typeof SupersetClient.configure).toBe('function');
|
||||
expect(typeof SupersetClient.init).toBe('function');
|
||||
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 clientWithGetUrl.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(clientWithGetUrl.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 +102,13 @@ describe('SupersetClient', () => {
|
||||
SupersetClientClass.prototype,
|
||||
'getGuestToken',
|
||||
);
|
||||
const getUrlSpy = jest.spyOn(SupersetClientClass.prototype, 'getUrl');
|
||||
|
||||
SupersetClient.configure({ appRoot: '/app' });
|
||||
expect(clientWithGetUrl.getUrl({ endpoint: '/some/path' })).toContain(
|
||||
'/app/some/path',
|
||||
);
|
||||
expect(getUrlSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
SupersetClient.configure({});
|
||||
await SupersetClient.init();
|
||||
@@ -141,6 +162,7 @@ describe('SupersetClient', () => {
|
||||
postSpy.mockRestore();
|
||||
authenticatedSpy.mockRestore();
|
||||
csrfSpy.mockRestore();
|
||||
getUrlSpy.mockRestore();
|
||||
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
});
|
||||
|
||||
@@ -28,10 +28,11 @@ test('DEFAULT_METRICS', () => {
|
||||
});
|
||||
|
||||
test('DatasourceType', () => {
|
||||
expect(Object.keys(DatasourceType).length).toBe(5);
|
||||
expect(Object.keys(DatasourceType).length).toBe(6);
|
||||
expect(DatasourceType.Table).toBe('table');
|
||||
expect(DatasourceType.Query).toBe('query');
|
||||
expect(DatasourceType.Dataset).toBe('dataset');
|
||||
expect(DatasourceType.SlTable).toBe('sl_table');
|
||||
expect(DatasourceType.SavedQuery).toBe('saved_query');
|
||||
expect(DatasourceType.SemanticView).toBe('semantic_view');
|
||||
});
|
||||
|
||||
@@ -67,6 +67,21 @@ 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 as unknown as number | Date)).toEqual(
|
||||
'2017',
|
||||
);
|
||||
});
|
||||
test('handles ISO-8601 string without misinterpreting it as a number', () => {
|
||||
expect(
|
||||
formatter.format(
|
||||
'2017-02-14T11:22:33.000Z' as unknown as number | Date,
|
||||
),
|
||||
).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
@@ -9,13 +9,13 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-MC", "NAME_1": "Moyen-Chari" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 19.932091369000148, 9.063330566000076 ], [ 19.914473917000066, 9.069266663000064 ], [ 19.889462525000056, 9.046374004000128 ], [ 19.784662720000085, 9.048751120000091 ], [ 19.640072062000087, 9.013998718000053 ], [ 19.613613729000122, 9.031672059000044 ], [ 19.521319621000117, 9.008831075000046 ], [ 19.420963989000057, 9.017435202000073 ], [ 19.36939091000005, 9.00038197800005 ], [ 19.253428996000139, 9.027951356000131 ], [ 19.192037394000067, 9.020174052000115 ], [ 19.168783000000133, 9.002216492000045 ], [ 19.100570109000103, 9.015264791000092 ], [ 19.021918579000044, 8.985214946000028 ], [ 18.971895793000044, 8.938215230000054 ], [ 18.922906535000038, 8.91821645100012 ], [ 18.917738892000045, 8.898811951000141 ], [ 18.892727498000056, 8.897933452000103 ], [ 18.869783162000118, 8.849409282000124 ], [ 18.88652632700007, 8.835844218000133 ], [ 18.902959432000017, 8.844810079000069 ], [ 18.929107707000128, 8.796544292000064 ], [ 19.048686971000109, 8.745668844000136 ], [ 19.124134562000108, 8.675078837000058 ], [ 19.061296021000146, 8.625624492000028 ], [ 19.02047163900005, 8.545577698000073 ], [ 18.813042440000089, 8.276421001000045 ], [ 18.638582804000094, 8.177641500000092 ], [ 18.618635702000063, 8.138651632000105 ], [ 18.617912231000076, 8.090127462000027 ], [ 18.589283488000092, 8.047881979000053 ], [ 18.508254842000071, 8.030673727000064 ], [ 18.175430591551454, 8.021952410863994 ], [ 18.126927773098259, 8.214065959676304 ], [ 18.128662000550889, 8.378817569475245 ], [ 18.090508995693597, 8.467263170458921 ], [ 18.088774768240967, 8.52102422238994 ], [ 18.031545262304007, 8.576519500874269 ], [ 18.012468759425701, 8.621609415542082 ], [ 18.014202986878331, 8.803703299867379 ], [ 17.937510613124289, 8.859727280789343 ], [ 17.961192253719503, 8.884466864054502 ], [ 17.967458529187752, 9.007391669190156 ], [ 17.98389733129676, 9.016628322638951 ], [ 18.004323764230321, 8.999457098266475 ], [ 18.091481966819288, 9.041205145700928 ], [ 18.165456576759595, 9.160101629286771 ], [ 18.141736600763522, 9.187974781510945 ], [ 17.995813352004575, 9.200135051791221 ], [ 17.795168885635576, 9.291337082040684 ], [ 17.73436753243567, 9.431180195209834 ], [ 17.713087058321094, 9.455500736669649 ], [ 17.624925096316133, 9.45246066932441 ], [ 17.579324080741742, 9.558863037199387 ], [ 17.536532016639228, 9.587063707224274 ], [ 17.561026646162759, 9.653622951837747 ], [ 17.543818394003665, 9.68395701700274 ], [ 17.427288039311577, 9.774804185564676 ], [ 17.329516228591046, 9.815835272779623 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.827160271829086, 10.47388296092987 ], [ 17.931288283027811, 10.382415676542223 ], [ 18.038826938912109, 10.344691881225344 ], [ 18.145125360447025, 10.341487942114838 ], [ 18.156752556685149, 10.311257228838031 ], [ 18.218505894431473, 10.288571275344054 ], [ 18.264756300562794, 10.247798570547559 ], [ 18.349919061315632, 10.236378078985126 ], [ 18.381596713617398, 10.204803779470808 ], [ 18.453116895628227, 10.179120592441393 ], [ 18.500762566639139, 10.10791046789376 ], [ 18.607060988174055, 10.020784003279118 ], [ 18.600394728433969, 9.913090317763931 ], [ 18.653466423936322, 9.900171210333099 ], [ 18.697701450261718, 9.866684882001664 ], [ 18.762710402163293, 9.884513250885789 ], [ 18.795266553608542, 9.994842433831252 ], [ 18.998871697870527, 10.007038071749605 ], [ 19.049462924674231, 10.035666816370451 ], [ 19.111991407877156, 10.120312812286443 ], [ 19.164132928291963, 10.137521064445536 ], [ 19.19358849521285, 10.202995103441253 ], [ 19.302677442909669, 10.299940090063103 ], [ 19.281593458970917, 10.34923940747268 ], [ 19.316371697595741, 10.357404283081166 ], [ 19.336835564809462, 10.419622707921633 ], [ 19.378590122436208, 10.469128730006844 ], [ 19.442203810357569, 10.502046617557369 ], [ 19.464372999914019, 10.482357895800249 ], [ 19.5337227715886, 10.495380356917906 ], [ 19.557803989062847, 10.552741197148521 ], [ 19.580128208250187, 10.541165675955142 ], [ 19.61428633014998, 10.595374253918692 ], [ 19.658366326844487, 10.618473619461952 ], [ 19.605914748067164, 10.359161282267337 ], [ 19.640951369110383, 10.225215968941825 ], [ 19.604519484086893, 10.10868561424968 ], [ 19.572066685429149, 10.075612697967642 ], [ 19.651028272636665, 9.92812816048621 ], [ 19.932091369000148, 9.063330566000076 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-MA", "NAME_1": "Mandoul" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.175430591551454, 8.021952410863994 ], [ 18.07045210800004, 8.019201558000049 ], [ 17.858785441000123, 7.960445455000084 ], [ 17.639884074000122, 7.985043437000073 ], [ 17.580766235000084, 7.940601706000095 ], [ 17.503664998000119, 7.926235657000092 ], [ 17.466354614000096, 7.884119364000057 ], [ 17.41912235500007, 7.898227030000086 ], [ 17.385739380000075, 7.870476787000129 ], [ 17.233790205553532, 7.810801425045724 ], [ 17.080177443034529, 8.136816311123823 ], [ 17.166735466868261, 8.158468735843485 ], [ 17.249469434866512, 8.290553697195378 ], [ 17.033461948010483, 8.632651679231572 ], [ 17.117384474413939, 8.783650214186025 ], [ 17.125911086127701, 8.995936997994022 ], [ 17.104827101289686, 9.069782619971932 ], [ 17.185649041370141, 9.156547349380673 ], [ 17.06322757329383, 9.263362534853115 ], [ 17.210040317206847, 9.3859907094037 ], [ 17.218256869658774, 9.507326971862256 ], [ 17.197637973713483, 9.555386054023188 ], [ 17.234224888367862, 9.592024643722311 ], [ 17.455193311721928, 9.641892401912799 ], [ 17.579324080741742, 9.558863037199387 ], [ 17.624925096316133, 9.45246066932441 ], [ 17.713087058321094, 9.455500736669649 ], [ 17.795168885635576, 9.291337082040684 ], [ 17.995813352004575, 9.200135051791221 ], [ 18.141736600763522, 9.187974781510945 ], [ 18.165456576759595, 9.160101629286771 ], [ 18.091481966819288, 9.041205145700928 ], [ 18.004323764230321, 8.999457098266475 ], [ 17.98389733129676, 9.016628322638951 ], [ 17.967458529187752, 9.007391669190156 ], [ 17.961192253719503, 8.884466864054502 ], [ 17.937510613124289, 8.859727280789343 ], [ 18.014202986878331, 8.803703299867379 ], [ 18.012468759425701, 8.621609415542082 ], [ 18.031545262304007, 8.576519500874269 ], [ 18.088774768240967, 8.52102422238994 ], [ 18.090508995693597, 8.467263170458921 ], [ 18.128662000550889, 8.378817569475245 ], [ 18.126927773098259, 8.214065959676304 ], [ 18.175430591551454, 8.021952410863994 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-LR", "NAME_1": "Logone Oriental" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.233790205553532, 7.810801425045724 ], [ 17.211073038000023, 7.768312480000077 ], [ 17.188645467000129, 7.764126689000051 ], [ 17.101932414000061, 7.677723694000065 ], [ 17.059351033000098, 7.696533916000078 ], [ 17.038990519000066, 7.662479147000028 ], [ 16.997546020000101, 7.66671661400008 ], [ 16.981939738000079, 7.648733215000092 ], [ 16.880653930000051, 7.632868551000087 ], [ 16.854815715000115, 7.611474508000072 ], [ 16.853885539000089, 7.567497864000117 ], [ 16.805516398000123, 7.543675029000056 ], [ 16.768619425000111, 7.55023793600013 ], [ 16.709811646000048, 7.627752584000092 ], [ 16.614210245000066, 7.679584046000073 ], [ 16.613073364000059, 7.748210347000025 ], [ 16.54930464600011, 7.794719137000058 ], [ 16.54868453000006, 7.870011699000116 ], [ 16.491737101000069, 7.84923777300007 ], [ 16.450912720000076, 7.792083638000051 ], [ 16.407401164000078, 7.796062724000123 ], [ 16.392208292000134, 7.783608703000127 ], [ 16.38714400200007, 7.69467356400007 ], [ 16.370814249000119, 7.672504374000056 ], [ 16.2827576090001, 7.660102031000079 ], [ 16.207206665000058, 7.613541565000034 ], [ 16.042978964000042, 7.583930970000083 ], [ 15.975696248000077, 7.515201315000112 ], [ 15.925260050000077, 7.488174541000063 ], [ 15.758345174000112, 7.455566711000102 ], [ 15.720001262000096, 7.468640849000067 ], [ 15.668531535000113, 7.516286519000104 ], [ 15.515569295000148, 7.512204081000093 ], [ 15.48104943900006, 7.523262838000093 ], [ 15.548952271000104, 7.630801493000121 ], [ 15.562904907000103, 7.792445374000039 ], [ 15.487870727000114, 7.804951071000048 ], [ 15.440741821000103, 7.83941925100001 ], [ 15.345347127000082, 8.135990296000088 ], [ 15.210706216000091, 8.421822448000057 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.431440870599658, 8.355149237047613 ], [ 15.481722039040847, 8.347914537425936 ], [ 15.520065951982076, 8.317012030580713 ], [ 15.519755893619561, 8.28362905503684 ], [ 15.558048129717406, 8.278358059277025 ], [ 15.570812209315989, 8.258152574481699 ], [ 15.664708285558902, 8.276394355415164 ], [ 15.728166944748693, 8.322696438389926 ], [ 15.813639763863989, 8.315823473074829 ], [ 15.85740970219598, 8.366466375822597 ], [ 15.867951693715668, 8.413543606052542 ], [ 16.004119093321492, 8.511005357511237 ], [ 16.145195754380552, 8.597511705400848 ], [ 16.365027297072061, 8.648568020197956 ], [ 16.409572380860652, 8.706548977153545 ], [ 16.513493687383743, 8.753161119390143 ], [ 16.5971061536261, 8.881111964745344 ], [ 16.596175977639234, 8.90266103847614 ], [ 16.569820997940667, 8.945345771190375 ], [ 16.549305453883562, 8.952167060561408 ], [ 16.542484165411906, 8.980124009815199 ], [ 16.520780063848861, 8.980124009815199 ], [ 16.489722528272068, 9.021465155392605 ], [ 16.435358920677629, 9.127608547296632 ], [ 16.568735793222288, 9.136083482166953 ], [ 16.91910200365453, 9.036141262009608 ], [ 16.998993767748175, 9.037484849146438 ], [ 17.104827101289686, 9.069782619971932 ], [ 17.125911086127701, 8.995936997994022 ], [ 17.117384474413939, 8.783650214186025 ], [ 17.033461948010483, 8.632651679231572 ], [ 17.249469434866512, 8.284559231023707 ], [ 17.166735466868261, 8.158468735843485 ], [ 17.080177443034529, 8.136816311123823 ], [ 17.233790205553532, 7.810801425045724 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-HL", "NAME_1": "Hadjer-Lamis" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.560692179000085, 12.766224467000143 ], [ 14.54901330600012, 12.81821095800008 ], [ 14.49010217300011, 12.873608094000119 ], [ 14.507672159000094, 12.952440491000047 ], [ 14.481040720000067, 13.000507963000089 ], [ 15.01405032666122, 13.00334096901048 ], [ 15.043195835219592, 13.097960517463889 ], [ 15.002578159154723, 13.188239244345652 ], [ 14.996635369826436, 13.25965607356892 ], [ 15.17404056222216, 13.203742174162016 ], [ 15.185512729728657, 13.173769843303603 ], [ 15.269900343226254, 13.102042954818501 ], [ 15.286540154604438, 13.143694158758422 ], [ 15.327984652969349, 13.144159246751826 ], [ 15.383175082863772, 13.19878123586534 ], [ 15.372012973719734, 13.221828925464536 ], [ 15.305918817099723, 13.241052558328875 ], [ 15.21450320775682, 13.372414049268968 ], [ 15.356820103165205, 13.405125230345107 ], [ 15.441052687031856, 13.31867055929888 ], [ 15.743049757840026, 13.182141425386476 ], [ 15.847642857032213, 13.168757229062862 ], [ 15.883609653162921, 13.187877509139696 ], [ 15.965258416442737, 13.121990058094696 ], [ 15.986807489274213, 13.051503403958918 ], [ 15.999984979123496, 13.047575995335876 ], [ 16.246894971925371, 13.051968491952323 ], [ 16.324926384944717, 13.074447739871289 ], [ 16.447812941014377, 13.050986640021392 ], [ 16.489412469010233, 13.081630764448221 ], [ 16.719372592970785, 13.113050035230913 ], [ 16.818694696403099, 13.159507147836564 ], [ 16.880137973988269, 13.143177394820896 ], [ 16.974034051130559, 13.16291779252208 ], [ 16.997288446304765, 13.182089749442355 ], [ 17.059661898977424, 13.170565904193097 ], [ 17.131698845825042, 13.144004218020257 ], [ 17.072684360095138, 13.063957424295666 ], [ 16.969848260089123, 12.970578111090958 ], [ 16.941116163580091, 12.908411363093933 ], [ 17.038887974300565, 12.829914862980502 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.122203616603599, 11.564674405499375 ], [ 16.989452205454882, 11.687214169498304 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.060192328313292, 12.463299342090977 ], [ 15.978499152014251, 12.371394519541411 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.069662568272918, 12.259066401793007 ], [ 14.90486270493879, 12.248664494924668 ], [ 14.908267863000049, 12.326897278000075 ], [ 14.877468709000084, 12.447044983000126 ], [ 14.849563436000096, 12.457018534000042 ], [ 14.863826131000053, 12.495465800000076 ], [ 14.819177694000132, 12.638816223000035 ], [ 14.768844849000061, 12.633235168000112 ], [ 14.734118286000125, 12.680415751000041 ], [ 14.713654419000051, 12.652510478000053 ], [ 14.70207889800011, 12.668995260000131 ], [ 14.709933716000137, 12.718242900000106 ], [ 14.664458455000101, 12.715969136000098 ], [ 14.618363078000073, 12.759196472000042 ], [ 14.575264933000142, 12.744907939000058 ], [ 14.560692179000085, 12.766224467000143 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-HL", "NAME_1": "Hadjer-Lamis" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.575264933000142, 12.744907939000058 ], [ 14.54901330600012, 12.81821095800008 ], [ 14.49010217300011, 12.873608094000119 ], [ 14.507672159000094, 12.952440491000047 ], [ 14.481040720000067, 13.000507963000089 ], [ 15.01405032666122, 13.00334096901048 ], [ 15.043195835219592, 13.097960517463889 ], [ 15.002578159154723, 13.188239244345652 ], [ 14.996635369826436, 13.25965607356892 ], [ 15.17404056222216, 13.203742174162016 ], [ 15.185512729728657, 13.173769843303603 ], [ 15.269900343226254, 13.102042954818501 ], [ 15.286540154604438, 13.143694158758422 ], [ 15.327984652969349, 13.144159246751826 ], [ 15.383175082863772, 13.19878123586534 ], [ 15.372012973719734, 13.221828925464536 ], [ 15.305918817099723, 13.241052558328875 ], [ 15.21450320775682, 13.372414049268968 ], [ 15.356820103165205, 13.405125230345107 ], [ 15.441052687031856, 13.31867055929888 ], [ 15.743049757840026, 13.182141425386476 ], [ 15.847642857032213, 13.168757229062862 ], [ 15.883609653162921, 13.187877509139696 ], [ 15.965258416442737, 13.121990058094696 ], [ 15.986807489274213, 13.051503403958918 ], [ 15.999984979123496, 13.047575995335876 ], [ 16.246894971925371, 13.051968491952323 ], [ 16.324926384944717, 13.074447739871289 ], [ 16.447812941014377, 13.050986640021392 ], [ 16.489412469010233, 13.081630764448221 ], [ 16.719372592970785, 13.113050035230913 ], [ 16.818694696403099, 13.159507147836564 ], [ 16.880137973988269, 13.143177394820896 ], [ 16.974034051130559, 13.16291779252208 ], [ 16.997288446304765, 13.182089749442355 ], [ 17.059661898977424, 13.170565904193097 ], [ 17.131698845825042, 13.144004218020257 ], [ 17.072684360095138, 13.063957424295666 ], [ 16.969848260089123, 12.970578111090958 ], [ 16.941116163580091, 12.908411363093933 ], [ 17.038887974300565, 12.829914862980502 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.122203616603599, 11.564674405499375 ], [ 16.989452205454882, 11.687214169498304 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.060192328313292, 12.463299342090977 ], [ 15.978499152014251, 12.371394519541411 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.069662568272918, 12.259066401793007 ], [ 14.90486270493879, 12.248664494924668 ], [ 14.908267863000049, 12.326897278000075 ], [ 14.877468709000084, 12.447044983000126 ], [ 14.849563436000096, 12.457018534000042 ], [ 14.863826131000053, 12.495465800000076 ], [ 14.819177694000132, 12.638816223000035 ], [ 14.768844849000061, 12.633235168000112 ], [ 14.734118286000125, 12.680415751000041 ], [ 14.713654419000051, 12.652510478000053 ], [ 14.70207889800011, 12.668995260000131 ], [ 14.709933716000137, 12.718242900000106 ], [ 14.664458455000101, 12.715969136000098 ], [ 14.618363078000073, 12.759196472000042 ], [ 14.575264933000142, 12.744907939000058 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-CB", "NAME_1": "Chari-Baguirmi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.964801879000106, 12.092441305000079 ], [ 14.898035929000059, 12.152799377000079 ], [ 14.911575154000047, 12.1801362100001 ], [ 14.898035929000059, 12.207473043000036 ], [ 14.90486270493879, 12.248664494924668 ], [ 15.069662568272918, 12.259066401793007 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.978499152014251, 12.371394519541411 ], [ 16.060192328313292, 12.463299342090977 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.989452205454882, 11.687214169498304 ], [ 17.122203616603599, 11.564674405499375 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.254955027752317, 11.237901701202532 ], [ 17.316224909751782, 10.74774264520687 ], [ 17.295801616351525, 10.584356293508108 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.329516228591046, 9.815835272779623 ], [ 17.227558627728456, 9.831854967432776 ], [ 17.156193475348573, 9.874332994572001 ], [ 17.130148553113202, 9.916966051342172 ], [ 17.028966098606531, 9.938980211267733 ], [ 16.936930373437974, 9.938515123274328 ], [ 16.879827914726491, 10.000113430490387 ], [ 16.73988813612857, 10.026468411088274 ], [ 16.632969597868623, 10.095611477187902 ], [ 16.607803175675997, 10.145427558535005 ], [ 16.605012647715512, 10.21312368381092 ], [ 16.568115675597937, 10.25167430322648 ], [ 16.500729607785161, 10.204131985003073 ], [ 16.4402165061868, 10.192866523071586 ], [ 16.219403109765665, 10.195863755707762 ], [ 16.196303745121725, 10.211935126305036 ], [ 16.177390170619844, 10.263043117945529 ], [ 16.129899530139198, 10.302627265236083 ], [ 16.108453810095227, 10.446029364463584 ], [ 16.13398196839313, 10.746476142559914 ], [ 16.07481245303228, 10.767301744080214 ], [ 16.063340284626406, 10.75123037438226 ], [ 16.001225212573502, 10.789419256793281 ], [ 15.921798537372581, 10.775776678950535 ], [ 15.745581903382117, 10.811226711143775 ], [ 15.665380080026637, 10.8888447130131 ], [ 15.591534458048784, 10.901298733349847 ], [ 15.548746371647724, 10.925948390705628 ], [ 15.513864780235394, 11.009974269896645 ], [ 15.438262159970691, 11.050281886699679 ], [ 15.432060988224009, 11.116272691431561 ], [ 15.342867466060682, 11.170377915708229 ], [ 15.23129804876595, 11.184537258387763 ], [ 15.150579460573624, 11.117926336930907 ], [ 15.028830280692887, 11.080218266259976 ], [ 15.021232544000043, 11.182548523000051 ], [ 15.033324829000037, 11.26026987700007 ], [ 15.063090454000104, 11.30993092900006 ], [ 15.049447876000102, 11.337267762000096 ], [ 15.060609985000042, 11.416125997000023 ], [ 15.135644165000116, 11.53082183800008 ], [ 15.069394979000037, 11.660788066000094 ], [ 15.076112915000039, 11.72145619700008 ], [ 15.097196899000068, 11.72765736900007 ], [ 15.083554321000065, 11.748172913000133 ], [ 15.110942831000102, 11.782899476000068 ], [ 15.079833618000066, 11.851215719000038 ], [ 15.044176880000066, 11.877312317000118 ], [ 15.042006469000086, 11.902375387000035 ], [ 15.063400512000101, 11.927283427000106 ], [ 15.048310995000094, 11.962785136000051 ], [ 15.081177205000103, 11.97167348300006 ], [ 15.049447876000102, 12.011929423000097 ], [ 15.040559529000092, 12.055647685000082 ], [ 15.104691864338065, 12.07118935600397 ], [ 15.125295796463831, 12.113568067412359 ], [ 15.1074616269525, 12.154100271010691 ], [ 15.050006169049425, 12.180917084955468 ], [ 15.021597762826502, 12.15508291524435 ], [ 15.011827432789801, 12.108616027907374 ], [ 14.964801879000106, 12.092441305000079 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-ND", "NAME_1": "Ville de N'Djamena" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.040559529000092, 12.055647685000082 ], [ 15.049447876000102, 12.084586487000138 ], [ 15.011827432789801, 12.108616027907374 ], [ 15.013491321567244, 12.134006169049428 ], [ 15.036189355654244, 12.17453837264776 ], [ 15.07432549192788, 12.179295797063389 ], [ 15.118810643996028, 12.134644813607224 ], [ 15.12252603384934, 12.09064481340738 ], [ 15.088478983618188, 12.061461626852577 ], [ 15.040559529000092, 12.055647685000082 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-ME", "NAME_1": "Mayo-Kebbi Est" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.382967570000119, 9.930196025000086 ], [ 15.681243937000147, 9.991277568000072 ], [ 15.476398560000092, 10.132741801000108 ], [ 15.439191528000094, 10.185245057000074 ], [ 15.301422160000072, 10.311748963000056 ], [ 15.278374471000092, 10.39427622500007 ], [ 15.241167440000083, 10.432904358000073 ], [ 15.218326457000074, 10.487216288000127 ], [ 15.138227986000118, 10.521658631000079 ], [ 15.132336873000014, 10.565402731000077 ], [ 15.149906860000101, 10.623125305000059 ], [ 15.065880981000078, 10.793114929000041 ], [ 15.079006795000026, 10.898147278000081 ], [ 15.035185181000116, 10.994627177000098 ], [ 15.028830280692887, 11.080218266259976 ], [ 15.150579460573624, 11.117926336930907 ], [ 15.236879102888281, 11.185777492737088 ], [ 15.342867466060682, 11.170377915708229 ], [ 15.421984083798407, 11.123972479945962 ], [ 15.438262159970691, 11.050281886699679 ], [ 15.513864780235394, 11.009974269896645 ], [ 15.548746371647724, 10.925948390705628 ], [ 15.591534458048784, 10.901298733349847 ], [ 15.665380080026637, 10.8888447130131 ], [ 15.745581903382117, 10.811226711143775 ], [ 15.921798537372581, 10.775776678950535 ], [ 16.001225212573502, 10.789419256793281 ], [ 16.063340284626406, 10.75123037438226 ], [ 16.07481245303228, 10.767301744080214 ], [ 16.13398196839313, 10.746476142559914 ], [ 16.108453810095227, 10.446029364463584 ], [ 16.126488884554362, 10.315029608729446 ], [ 16.177390170619844, 10.263043117945529 ], [ 16.196303745121725, 10.211935126305036 ], [ 16.246274855200397, 10.149716702363889 ], [ 16.234647658062954, 10.023781235915294 ], [ 16.1584249201739, 9.855729478432693 ], [ 15.973940056888125, 9.683233547490204 ], [ 15.912186720041063, 9.731189276863631 ], [ 15.830692987291513, 9.741317857233298 ], [ 15.71633304203624, 9.604065252909095 ], [ 15.67416507325953, 9.495906480299823 ], [ 15.523941684660997, 9.325839342111919 ], [ 15.438262159970691, 9.186829739500865 ], [ 15.29956135846993, 9.244814462817203 ], [ 15.08963651271489, 9.394434942318696 ], [ 15.071186992885373, 9.527896838568211 ], [ 15.022724939804107, 9.591270293289369 ], [ 15.026452789764448, 9.641596272250126 ], [ 14.994766062403869, 9.671419074630592 ], [ 14.94071223348277, 9.695650101171225 ], [ 14.792935054808936, 9.662967116539164 ], [ 14.737544393558892, 9.665827298790816 ], [ 14.722846171713456, 9.787266672119188 ], [ 14.732464640000074, 9.923813985000052 ], [ 14.77256555200006, 9.92174692800009 ], [ 14.898139282000074, 9.960478414000107 ], [ 15.033014770000136, 9.942856751000122 ], [ 15.109599244000037, 9.981536560000038 ], [ 15.214915812000072, 9.984094543000126 ], [ 15.382967570000119, 9.930196025000086 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-MO", "NAME_1": "Mayo-Kebbi Ouest" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.440492798000037, 9.995308329000068 ], [ 14.732464640000074, 9.923813985000052 ], [ 14.722846171713456, 9.787266672119188 ], [ 14.737544393558892, 9.665827298790816 ], [ 14.792935054808936, 9.662967116539164 ], [ 14.94071223348277, 9.695650101171225 ], [ 14.994766062403869, 9.671419074630592 ], [ 15.026452789764448, 9.641596272250126 ], [ 15.022724939804107, 9.591270293289369 ], [ 15.071186992885373, 9.527896838568211 ], [ 15.08963651271489, 9.394434942318696 ], [ 15.220104139594355, 9.292488794322367 ], [ 15.442189568593676, 9.18269562440355 ], [ 15.463893670156722, 9.02673615205174 ], [ 15.41779829275697, 9.004308580076895 ], [ 15.37712894074798, 8.883282375980855 ], [ 15.37542361840525, 8.746236477231605 ], [ 15.305918817099723, 8.671770738528721 ], [ 15.264939405828898, 8.502117011490895 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.210706216000091, 8.421822448000057 ], [ 15.183703247000068, 8.479147644000122 ], [ 15.051928345000078, 8.643788758000056 ], [ 14.955086710000074, 8.676215719000069 ], [ 14.940100545000092, 8.729649149000082 ], [ 14.899379516000124, 8.774323425000119 ], [ 14.846049438000136, 8.810987854000089 ], [ 14.793856242000061, 8.813623353000096 ], [ 14.349542277000126, 9.168356221000082 ], [ 14.321326945000067, 9.243157858000103 ], [ 14.036486450000069, 9.568771057000077 ], [ 13.947602987000039, 9.637759094000032 ], [ 14.006720825000087, 9.739277446000102 ], [ 14.119788859000096, 9.85200958300004 ], [ 14.173532348000094, 9.975025329000061 ], [ 14.440492798000037, 9.995308329000068 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-X01~", "NAME_1": "Ennedi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 20.893328898000107, 21.020940247000013 ], [ 23.981305786000036, 19.496123759000014 ], [ 23.98440637200008, 15.721160381000018 ], [ 23.972624146000072, 15.691084697000051 ], [ 23.707524048000067, 15.748858948000063 ], [ 23.592699016000068, 15.749013977000104 ], [ 23.395811808000076, 15.688345846000018 ], [ 23.320570923000105, 15.681317851000088 ], [ 23.166781860000128, 15.712943827000103 ], [ 23.119249041000103, 15.707223545000105 ], [ 23.066220330603755, 15.788442288081114 ], [ 22.997645705285038, 15.816502590122411 ], [ 22.957183057951738, 15.820946764481562 ], [ 22.875534294671922, 15.797485662833083 ], [ 22.75931399924167, 15.838413398159787 ], [ 22.56082482290725, 15.80957794796393 ], [ 22.470701124757113, 15.908900051396245 ], [ 22.370913934230657, 15.948432521843415 ], [ 22.311796094813928, 15.927606920323115 ], [ 22.270971714073312, 15.931430976158595 ], [ 22.1771273128752, 15.999798895902302 ], [ 22.144209426224052, 15.978921617538617 ], [ 22.018222283831335, 15.986001288428724 ], [ 21.915954623706909, 15.944453437276309 ], [ 21.731779818783593, 15.999850571846423 ], [ 21.388803337603917, 15.923782864487578 ], [ 21.312632276558304, 15.887919420245055 ], [ 21.096624789702275, 15.985174465229363 ], [ 20.9587003918096, 15.918718573403453 ], [ 20.896326939136941, 15.839446926034782 ], [ 20.832868279947149, 15.799449368493526 ], [ 20.660268996217212, 15.737024238078106 ], [ 20.567199741374964, 15.685451158444209 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.893328898000107, 21.020940247000013 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BO", "NAME_1": "Borkou" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 19.185837029855008, 21.864177058924099 ], [ 20.893328898000107, 21.020940247000013 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.291764356739634, 15.669018053540299 ], [ 20.11286054937483, 15.764102688188473 ], [ 19.863780145337444, 15.798674221238286 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.046982455975524, 15.614654445945916 ], [ 18.935413038680792, 15.521998603153008 ], [ 18.791235793097371, 15.4505300970863 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.270647413946961, 15.431461492953531 ], [ 18.084922316311861, 15.53166209642859 ], [ 17.84281823127634, 15.792524726335046 ], [ 15.468517057815973, 16.904909712513188 ], [ 15.490247843000077, 17.124537252000025 ], [ 17.359841679150179, 20.416379890019869 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.952986281791482, 20.83652398359493 ], [ 18.150701149038809, 20.935381417218593 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.917118774000073, 21.996849671000078 ], [ 19.185837029855008, 21.864177058924099 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BO", "NAME_1": "Borkou" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.917118774000073, 21.996849671000078 ], [ 20.893328898000107, 21.020940247000013 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.291764356739634, 15.669018053540299 ], [ 20.11286054937483, 15.764102688188473 ], [ 19.863780145337444, 15.798674221238286 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.046982455975524, 15.614654445945916 ], [ 18.935413038680792, 15.521998603153008 ], [ 18.791235793097371, 15.4505300970863 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.270647413946961, 15.431461492953531 ], [ 18.084922316311861, 15.53166209642859 ], [ 17.84281823127634, 15.792524726335046 ], [ 15.468517057815973, 16.904909712513188 ], [ 15.490247843000077, 17.124537252000025 ], [ 17.359841679150179, 20.416379890019869 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.952986281791482, 20.83652398359493 ], [ 18.150701149038809, 20.935381417218593 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.917118774000073, 21.996849671000078 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-TI", "NAME_1": "Tibesti" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.985101359000112, 23.444719951000067 ], [ 18.917118774000073, 21.996849671000078 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.150701149038809, 20.935381417218593 ], [ 17.952986281791482, 20.83652398359493 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.359841679150179, 20.416379890019869 ], [ 15.490247843000077, 17.124537252000025 ], [ 15.736020955000129, 19.903540751000023 ], [ 15.970321899000112, 20.336330871000101 ], [ 15.953992147000065, 20.374571432 ], [ 15.669461711000054, 20.671865946000068 ], [ 15.570242961000105, 20.751912740000122 ], [ 15.544198039000037, 20.798989970000022 ], [ 15.544301391000147, 20.890302226000117 ], [ 15.569002727000054, 20.928904521 ], [ 15.609310343000118, 20.950660299000035 ], [ 15.301008748000044, 21.401330465000072 ], [ 15.266592245000083, 21.440656230000045 ], [ 15.184530070000108, 21.491195780000069 ], [ 15.172024373000113, 21.993387350000134 ], [ 14.979909027000133, 22.995663760000085 ], [ 15.985101359000112, 23.444719951000067 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-KA", "NAME_1": "Kanem" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 13.608052034873481, 14.518266228003995 ], [ 13.665604695000098, 14.566889751000105 ], [ 13.648344767000111, 14.649417013000033 ], [ 13.709994751000067, 14.705124207000068 ], [ 13.764358358000038, 14.719076843000053 ], [ 13.772006469000104, 14.762872620000053 ], [ 13.755159952000042, 14.847208558000062 ], [ 13.833914836000133, 15.019601136000105 ], [ 14.368972615000104, 15.749634095000076 ], [ 15.468517057815973, 16.904909712513188 ], [ 16.671386334311876, 16.341868536703601 ], [ 16.489081502309489, 16.037375794157583 ], [ 16.131673857462545, 14.811978153269138 ], [ 16.111250563162969, 14.577110272421123 ], [ 16.049980681163504, 14.474993802721826 ], [ 15.774266211716281, 14.148221098424926 ], [ 15.590456565717943, 14.076739569275674 ], [ 15.6007221766871, 13.753926464834478 ], [ 15.71617801330467, 13.754923000895758 ], [ 15.758242629293875, 13.790166327513987 ], [ 15.841751742748727, 13.765775050777961 ], [ 15.85740970219598, 13.74510447798923 ], [ 15.865006137922933, 13.700921129406595 ], [ 15.840201450036886, 13.642630113189171 ], [ 15.8581848485519, 13.581238512447385 ], [ 15.898389112567486, 13.521500556305625 ], [ 15.899939406178646, 13.250457669186062 ], [ 15.871672397663019, 13.171496080179963 ], [ 15.776226026909626, 13.171857815385863 ], [ 15.560136878913795, 13.264834071335315 ], [ 15.375818277791268, 13.751985246721745 ], [ 15.213262974306815, 13.750582180223432 ], [ 14.915813428846661, 13.678751938950825 ], [ 14.839642367800991, 13.685883287583636 ], [ 14.805174188438059, 13.663817449915371 ], [ 14.643788689796168, 13.908918769385707 ], [ 14.477442253756976, 14.104514064972079 ], [ 14.356571079291882, 14.134951483823897 ], [ 14.30737511556913, 14.198823554163653 ], [ 14.261744826162783, 14.213189602418197 ], [ 14.149400261612755, 14.308480943540701 ], [ 14.131881951990465, 14.378347480052128 ], [ 14.087336867302554, 14.416691392094037 ], [ 14.017366978003622, 14.428060207712406 ], [ 13.995817905172146, 14.452348130761607 ], [ 13.950497674128371, 14.466145738235241 ], [ 13.78492638534442, 14.476377672291733 ], [ 13.735575391990722, 14.515703437163893 ], [ 13.608052034873481, 14.518266228003995 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-OD", "NAME_1": "Ouaddaï" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 22.180762493000145, 13.920554946000053 ], [ 22.073721964000129, 13.771356913000147 ], [ 22.112892700000089, 13.729834900000071 ], [ 22.132116333000113, 13.63865183500009 ], [ 22.195885050000129, 13.580464173000081 ], [ 22.215625448000083, 13.464993184000022 ], [ 22.275983521000086, 13.376316427000106 ], [ 22.267611939000119, 13.334561869000069 ], [ 22.139971151000054, 13.193511048000047 ], [ 22.016051066000074, 13.140232646000072 ], [ 21.964064575000037, 13.098348898000083 ], [ 21.852960246000066, 12.90572499600006 ], [ 21.809448689905082, 12.793664652701523 ], [ 21.574481710962175, 12.881976869836649 ], [ 21.482576888412666, 12.851341928387285 ], [ 21.380460417813993, 12.79007204638782 ], [ 21.288555595264484, 12.698167222938991 ], [ 21.237497360414807, 12.677743929538678 ], [ 21.094534302116358, 12.698167222938991 ], [ 20.982206185267216, 12.667532282388891 ], [ 20.849454774118499, 12.718590517238567 ], [ 20.839243126968711, 12.881976869836649 ], [ 20.686068421520417, 12.91261181038675 ], [ 20.675856774370629, 13.045363221535411 ], [ 20.410353952972514, 13.045363221535411 ], [ 20.294554884700176, 13.092327786498117 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.000050896831397, 14.155983791818528 ], [ 20.288043653691659, 14.441392727192579 ], [ 20.434959751291444, 14.437878729719614 ], [ 20.492113885047729, 14.464130357529939 ], [ 20.551541781927654, 14.470228176489115 ], [ 20.7355615581194, 14.411678778752616 ], [ 20.844185417822757, 14.355041408933857 ], [ 20.89896243656716, 14.264762682052094 ], [ 20.958080275084626, 14.204146225867589 ], [ 21.000299919805457, 14.196859849402529 ], [ 21.168196648556489, 14.101465155492519 ], [ 21.232275425370574, 14.093403631772162 ], [ 21.333096143772025, 14.142651272338355 ], [ 21.421721226053705, 14.151901353564597 ], [ 21.702272576817961, 14.110870266349707 ], [ 22.042251825361461, 13.999455877786488 ], [ 22.084626498813861, 13.973462633293877 ], [ 22.124520705366251, 13.920494290579029 ], [ 22.180762493000145, 13.920554946000053 ] ] ] } },
|
||||
@@ -24,6 +24,6 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-TA", "NAME_1": "Tandjilé" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 16.435358920677629, 9.127608547296632 ], [ 16.420527785329, 9.180835273329194 ], [ 16.357430861345165, 9.238712877497278 ], [ 16.304307488999427, 9.192410792723877 ], [ 16.204210239211136, 9.155927231756323 ], [ 16.146952751768083, 9.088541164842866 ], [ 16.042359652575897, 9.117118231721008 ], [ 16.001431919047832, 9.113965969453886 ], [ 15.818135614167261, 9.025082506552394 ], [ 15.7552970717025, 9.032213853386622 ], [ 15.569727003698233, 8.991389471746686 ], [ 15.489008416405284, 9.007719223863091 ], [ 15.452421501750905, 9.049990546326626 ], [ 15.438262159970691, 9.186829739500865 ], [ 15.523941684660997, 9.325839342111919 ], [ 15.67416507325953, 9.495906480299823 ], [ 15.71633304203624, 9.604065252909095 ], [ 15.830692987291513, 9.741317857233298 ], [ 15.912186720041063, 9.731189276863631 ], [ 15.973940056888125, 9.683233547490204 ], [ 16.166486443894257, 9.866788234789169 ], [ 16.211496615676253, 9.955206611495896 ], [ 16.243329299407606, 10.071581936557095 ], [ 16.246274855200397, 10.149716702363889 ], [ 16.219403109765665, 10.195863755707762 ], [ 16.4402165061868, 10.192866523071586 ], [ 16.500729607785161, 10.204131985003073 ], [ 16.568115675597937, 10.25167430322648 ], [ 16.605012647715512, 10.21312368381092 ], [ 16.607803175675997, 10.145427558535005 ], [ 16.632969597868623, 10.095611477187902 ], [ 16.73988813612857, 10.026468411088274 ], [ 16.879827914726491, 10.000113430490387 ], [ 16.936930373437974, 9.938515123274328 ], [ 17.028966098606531, 9.938980211267733 ], [ 17.130148553113202, 9.916966051342172 ], [ 17.156193475348573, 9.874332994572001 ], [ 17.227558627728456, 9.831854967432776 ], [ 17.347499627106117, 9.813974920805947 ], [ 17.427288039311577, 9.774804185564676 ], [ 17.558391147833163, 9.663183092325824 ], [ 17.536532016639228, 9.587063707224274 ], [ 17.443721144215431, 9.643752752987155 ], [ 17.306881951940511, 9.600137844286053 ], [ 17.254585401894758, 9.601481432322203 ], [ 17.204459263084516, 9.568201809565835 ], [ 17.197637973713483, 9.549701646213975 ], [ 17.218256869658774, 9.507326971862256 ], [ 17.210040317206847, 9.3859907094037 ], [ 17.06322757329383, 9.263362534853115 ], [ 17.185649041370141, 9.156547349380673 ], [ 17.104827101289686, 9.069782619971932 ], [ 16.998993767748175, 9.037484849146438 ], [ 16.91910200365453, 9.036141262009608 ], [ 16.568735793222288, 9.136083482166953 ], [ 16.435358920677629, 9.127608547296632 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-LO", "NAME_1": "Logone Occidental" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 16.357430861345165, 9.238712877497278 ], [ 16.420527785329, 9.180835273329194 ], [ 16.489722528272068, 9.021465155392605 ], [ 16.592455274591259, 8.917492173824769 ], [ 16.5971061536261, 8.881111964745344 ], [ 16.490497673728669, 8.732438869758028 ], [ 16.409572380860652, 8.706548977153545 ], [ 16.365027297072061, 8.648568020197956 ], [ 16.145195754380552, 8.597511705400848 ], [ 16.004119093321492, 8.511005357511237 ], [ 15.867951693715668, 8.413543606052542 ], [ 15.85740970219598, 8.366466375822597 ], [ 15.813639763863989, 8.315823473074829 ], [ 15.728166944748693, 8.322696438389926 ], [ 15.664708285558902, 8.276394355415164 ], [ 15.614065382811134, 8.26202830716062 ], [ 15.570812209315989, 8.258152574481699 ], [ 15.558048129717406, 8.278358059277025 ], [ 15.524251743023513, 8.279908351988865 ], [ 15.520065951982076, 8.317012030580713 ], [ 15.481722039040847, 8.347914537425936 ], [ 15.431440870599658, 8.355149237047613 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.264939405828898, 8.502117011490895 ], [ 15.305918817099723, 8.671770738528721 ], [ 15.37542361840525, 8.746236477231605 ], [ 15.372943148807281, 8.86426544779215 ], [ 15.428185256444408, 9.013455308515688 ], [ 15.463893670156722, 9.02673615205174 ], [ 15.518515659270236, 8.994851793275643 ], [ 15.569727003698233, 8.991389471746686 ], [ 15.7552970717025, 9.032213853386622 ], [ 15.818135614167261, 9.025082506552394 ], [ 16.022050815892442, 9.117273261351897 ], [ 16.146952751768083, 9.088541164842866 ], [ 16.204210239211136, 9.155927231756323 ], [ 16.304307488999427, 9.192410792723877 ], [ 16.357430861345165, 9.238712877497278 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-GR", "NAME_1": "Guéra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.265166674902105, 11.493192876350122 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.986685417597926, 12.482442532396931 ], [ 18.075258823036222, 12.569672348899758 ], [ 18.435288526744102, 12.763769029517107 ], [ 19.112611524602187, 12.751728420330323 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.990284050768309, 12.970578111090958 ], [ 20.136425002012231, 12.860455633720505 ], [ 20.170738152643594, 12.854409490705393 ], [ 20.169497918294269, 12.787850246991297 ], [ 20.122472364907708, 12.767903143715102 ], [ 20.048626742929798, 12.668477688394603 ], [ 20.094050326761135, 12.594838771991704 ], [ 20.043355747169983, 12.558871974961676 ], [ 19.926308627641049, 12.423117988304512 ], [ 19.885535922844554, 12.343381252043116 ], [ 19.821457146929788, 12.163185533485546 ], [ 19.820010207005453, 12.087014472439932 ], [ 19.642036573828818, 12.097659816747125 ], [ 19.3895972019493, 12.158948066500045 ], [ 19.219220005398938, 11.999371242988445 ], [ 19.293685744101765, 11.87286733575894 ], [ 19.190797967252365, 11.673654689911984 ], [ 19.061400181073509, 11.61572540890046 ], [ 19.012204217350757, 11.703213608721057 ], [ 18.886527134219875, 11.706365871887442 ], [ 18.867613559717995, 11.692361557939535 ], [ 18.84632287020429, 11.389485988887259 ], [ 18.860430536040383, 11.345354316248688 ], [ 19.136020949407282, 11.184175523181864 ], [ 19.255496859892162, 11.142575995186007 ], [ 19.315441521608932, 11.103611965519804 ], [ 19.360089959084348, 11.102578436745432 ], [ 19.392697788272301, 11.064234523804203 ], [ 19.460807325597557, 11.062580878304857 ], [ 19.628549024717699, 10.991680813019116 ], [ 19.6929378598943, 10.917731838253701 ], [ 19.666892937658929, 10.896027736690655 ], [ 19.677124871715421, 10.705083320139124 ], [ 19.658366326844487, 10.618473619461952 ], [ 19.61428633014998, 10.595374253918692 ], [ 19.580128208250187, 10.541165675955142 ], [ 19.557803989062847, 10.552741197148521 ], [ 19.5337227715886, 10.495380356917906 ], [ 19.464372999914019, 10.482357895800249 ], [ 19.442203810357569, 10.502046617557369 ], [ 19.378590122436208, 10.469128730006844 ], [ 19.336835564809462, 10.419622707921633 ], [ 19.316371697595741, 10.357404283081166 ], [ 19.281593458970917, 10.34923940747268 ], [ 19.302677442909669, 10.299940090063103 ], [ 19.19358849521285, 10.202995103441253 ], [ 19.164132928291963, 10.137521064445536 ], [ 19.111991407877156, 10.120312812286443 ], [ 19.049462924674231, 10.035666816370451 ], [ 18.998871697870527, 10.007038071749605 ], [ 18.795266553608542, 9.994842433831252 ], [ 18.756974318410073, 9.881774399768744 ], [ 18.69020836822159, 9.867304998726695 ], [ 18.653466423936322, 9.900171210333099 ], [ 18.602410109139214, 9.9109715851705 ], [ 18.607060988174055, 10.020784003279118 ], [ 18.500762566639139, 10.10791046789376 ], [ 18.453116895628227, 10.179120592441393 ], [ 18.381596713617398, 10.204803779470808 ], [ 18.349919061315632, 10.236378078985126 ], [ 18.264756300562794, 10.247798570547559 ], [ 18.218505894431473, 10.288571275344054 ], [ 18.156752556685149, 10.311257228838031 ], [ 18.145125360447025, 10.341487942114838 ], [ 18.038826938912109, 10.344691881225344 ], [ 17.931288283027811, 10.382415676542223 ], [ 17.827160271829086, 10.47388296092987 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.295801616351525, 10.584356293508108 ], [ 17.316224909751782, 10.74774264520687 ], [ 17.254955027752317, 11.237901701202532 ], [ 17.265166674902105, 11.493192876350122 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BA", "NAME_1": "Batha" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.131698845825042, 13.144004218020257 ], [ 17.202133823117435, 13.250974433123588 ], [ 17.197017856988509, 13.500623277042564 ], [ 17.238152296990904, 13.720609849365019 ], [ 17.551414828831298, 13.820086982428279 ], [ 17.992886590243927, 15.616411445132087 ], [ 18.084922316311861, 15.53166209642859 ], [ 18.209049106730902, 15.457248032770508 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.892418246704722, 15.495075181774212 ], [ 19.046982455975524, 15.614654445945916 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.863780145337444, 15.798674221238286 ], [ 20.000050896831397, 15.783843084990338 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.304321729863943, 13.118321030990785 ], [ 20.260396762800326, 13.043545233925386 ], [ 19.987648553338033, 12.986029364063882 ], [ 19.990284050768309, 12.970578111090958 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.096178419698276, 12.748679510850707 ], [ 18.435288526744102, 12.763769029517107 ], [ 18.075258823036222, 12.569672348899758 ], [ 17.986685417597926, 12.482442532396931 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.038887974300565, 12.829914862980502 ], [ 16.941116163580091, 12.908411363093933 ], [ 16.969848260089123, 12.970578111090958 ], [ 17.072684360095138, 13.063957424295666 ], [ 17.131698845825042, 13.144004218020257 ] ] ] } }
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BA", "NAME_1": "Batha" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.072684360095138, 13.063957424295666 ], [ 17.202133823117435, 13.250974433123588 ], [ 17.197017856988509, 13.500623277042564 ], [ 17.238152296990904, 13.720609849365019 ], [ 17.551414828831298, 13.820086982428279 ], [ 17.992886590243927, 15.616411445132087 ], [ 18.084922316311861, 15.53166209642859 ], [ 18.209049106730902, 15.457248032770508 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.892418246704722, 15.495075181774212 ], [ 19.046982455975524, 15.614654445945916 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.863780145337444, 15.798674221238286 ], [ 20.000050896831397, 15.783843084990338 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.304321729863943, 13.118321030990785 ], [ 20.260396762800326, 13.043545233925386 ], [ 19.987648553338033, 12.986029364063882 ], [ 19.990284050768309, 12.970578111090958 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.096178419698276, 12.748679510850707 ], [ 18.435288526744102, 12.763769029517107 ], [ 18.075258823036222, 12.569672348899758 ], [ 17.986685417597926, 12.482442532396931 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.038887974300565, 12.829914862980502 ], [ 16.941116163580091, 12.908411363093933 ], [ 16.969848260089123, 12.970578111090958 ], [ 17.072684360095138, 13.063957424295666 ] ] ] } }
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user