diff --git a/docs/docs/security/security.mdx b/docs/docs/security/security.mdx index 5425e7368c05a..b2e805c2b57e2 100644 --- a/docs/docs/security/security.mdx +++ b/docs/docs/security/security.mdx @@ -253,6 +253,9 @@ You can get current nonce value by calling jinja macro `csp_nonce()`. connect-src 'self' https://api.mapbox.com https://events.mapbox.com ``` +- Cartodiagram charts request map data (image and json) from external resources that can be edited by users, +and therefore either require a list of allowed domains to request from or a wildcard (`'*'`) for `img-src` and `connect-src`. + * Other CSP directives default to `'self'` to limit content to the same origin as the Superset server. In order to adjust provided CSP configuration to your needs, follow the instructions and examples provided in diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index 22e5db98b312d..23482723a4ed0 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -56,7 +56,7 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], snapshotSerializers: ['@emotion/jest/enzyme-serializer'], transformIgnorePatterns: [ - 'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms)', + 'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol)', ], globals: { __DEV__: true, diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index c19eafc75c2c4..a3f41c75b5438 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -44,6 +44,7 @@ "@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map", "@superset-ui/legacy-preset-chart-deckgl": "file:./plugins/legacy-preset-chart-deckgl", "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3", + "@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram", "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts", "@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars", "@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table", @@ -71,11 +72,17 @@ "d3-scale": "^2.1.2", "dayjs": "^1.11.13", "dom-to-image-more": "^3.2.0", + "echarts": "^5.4.1", "emotion-rgba": "0.0.12", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", "geolib": "^2.0.24", + "geostyler": "^12.0.2", + "geostyler-data": "^1.0.0", + "geostyler-openlayers-parser": "^4.3.0", + "geostyler-style": "^7.5.0", + "geostyler-wfs-parser": "^2.0.3", "googleapis": "^130.0.0", "html-webpack-plugin": "^5.3.2", "immer": "^10.1.1", @@ -95,6 +102,7 @@ "mousetrap": "^1.6.5", "mustache": "^4.2.0", "nanoid": "^5.0.7", + "ol": "^7.5.2", "polished": "^4.3.1", "prop-types": "^15.8.1", "query-string": "^6.13.7", @@ -3509,27 +3517,28 @@ "node": ">=0.1.95" } }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", - "dev": true, + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "license": "MIT", - "peer": true, "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { "node": ">=12" } }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "license": "MIT", @@ -4548,6 +4557,83 @@ "node": ">=10.0.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/accessibility/node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" + }, + "node_modules/@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core/node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable/node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities/node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" + }, "node_modules/@emnapi/core": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.2.0.tgz", @@ -5586,6 +5672,15 @@ "node": ">=6.9.0" } }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@inquirer/figures": { "version": "1.0.3", "devOptional": true, @@ -7452,6 +7547,28 @@ "node": ">= 0.6" } }, + "node_modules/@mapbox/mapbox-gl-style-spec": { + "version": "13.28.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz", + "integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/unitbezier": "^0.0.0", + "csscolorparser": "~1.0.2", + "json-stringify-pretty-compact": "^2.0.0", + "minimist": "^1.2.6", + "rw": "^1.3.3", + "sort-object": "^0.3.2" + }, + "bin": { + "gl-style-composite": "bin/gl-style-composite.js", + "gl-style-format": "bin/gl-style-format.js", + "gl-style-migrate": "bin/gl-style-migrate.js", + "gl-style-validate": "bin/gl-style-validate.js" + } + }, "node_modules/@mapbox/mapbox-gl-supported": { "version": "2.0.1", "license": "BSD-3-Clause" @@ -7583,6 +7700,32 @@ "version": "1.0.1", "license": "MIT" }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -8900,6 +9043,12 @@ "@octokit/openapi-types": "^11.2.0" } }, + "node_modules/@petamoriken/float16": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.7.tgz", + "integrity": "sha512-/Ri4xDDpe12NT6Ex/DRgHzLlobiQXEW/hmG08w1wj/YU7hLemk97c+zHQFp0iZQ9r7YqgLEXZR2sls4HxBf9NA==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -12488,6 +12637,10 @@ "resolved": "plugins/legacy-preset-chart-nvd3", "link": true }, + "node_modules/@superset-ui/plugin-chart-cartodiagram": { + "resolved": "plugins/plugin-chart-cartodiagram", + "link": true + }, "node_modules/@superset-ui/plugin-chart-echarts": { "resolved": "plugins/plugin-chart-echarts", "link": true @@ -12908,7 +13061,6 @@ }, "node_modules/@swc/core": { "version": "1.4.0", - "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "optional": true, @@ -12963,14 +13115,12 @@ }, "node_modules/@swc/counter": { "version": "0.1.3", - "dev": true, "license": "Apache-2.0", "optional": true, "peer": true }, "node_modules/@swc/types": { "version": "0.1.5", - "dev": true, "license": "Apache-2.0", "optional": true, "peer": true @@ -13157,27 +13307,19 @@ }, "node_modules/@tsconfig/node10": { "version": "1.0.8", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.9", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", @@ -13369,10 +13511,40 @@ "@types/node": "*" } }, + "node_modules/@types/chroma-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.4.tgz", + "integrity": "sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==", + "license": "MIT" + }, "node_modules/@types/classnames": { "version": "2.2.10", "license": "MIT" }, + "node_modules/@types/color": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz", + "integrity": "sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==", + "license": "MIT", + "dependencies": { + "@types/color-convert": "*" + } + }, + "node_modules/@types/color-convert": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz", + "integrity": "sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==", + "license": "MIT", + "dependencies": { + "@types/color-name": "^1.1.0" + } + }, + "node_modules/@types/color-name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.5.tgz", + "integrity": "sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==", + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.35", "license": "MIT", @@ -13580,8 +13752,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "license": "MIT" + }, "node_modules/@types/geojson": { - "version": "7946.0.8", + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", "license": "MIT" }, "node_modules/@types/glob": { @@ -13676,12 +13856,10 @@ }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.3", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -13689,7 +13867,6 @@ }, "node_modules/@types/istanbul-reports": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*", @@ -13762,8 +13939,9 @@ "license": "MIT" }, "node_modules/@types/json-schema": { - "version": "7.0.9", - "devOptional": true, + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, "node_modules/@types/json5": { @@ -13781,7 +13959,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.182", + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==", "license": "MIT" }, "node_modules/@types/lodash-es": { @@ -13892,6 +14072,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-color": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.12.tgz", + "integrity": "sha512-pr3uKE3lSvf7GFo1Rn2K3QktiZQFFrSgSGJ/3iMvSOYWt2pPAJ97rVdVfhWxYJZ8prAEXzoP2XX//3qGSQgu7Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, "node_modules/@types/react-dom": { "version": "16.9.8", "license": "MIT", @@ -14002,6 +14192,15 @@ "@types/react": "*" } }, + "node_modules/@types/reactcss": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.12.tgz", + "integrity": "sha512-BrXUQ86/wbbFiZv8h/Q1/Q1XOsaHneYmCb/tHe9+M8XBAAUc2EHfdY0DY22ZZjVSaXr5ix7j+zsqO2eGZub8lQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/redux-localstorage": { "version": "1.0.8", "dev": true, @@ -14259,7 +14458,6 @@ }, "node_modules/@types/yargs-parser": { "version": "15.0.0", - "dev": true, "license": "MIT" }, "node_modules/@types/yauzl": { @@ -14711,6 +14909,12 @@ "version": "1.2.0", "license": "ISC" }, + "node_modules/@ungap/url-search-params": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@ungap/url-search-params/-/url-search-params-0.2.2.tgz", + "integrity": "sha512-qQsguKXZVKdCixOHX9jqnX/K/1HekPDpGKyEcXHT+zR6EjGA7S4boSuelL4uuPv6YfhN0n8c4UxW+v/Z3gM2iw==", + "license": "ISC" + }, "node_modules/@visx/annotation": { "version": "3.3.0", "license": "MIT", @@ -16977,9 +17181,7 @@ }, "node_modules/arg": { "version": "4.1.3", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/argparse": { "version": "1.0.10", @@ -18290,6 +18492,15 @@ "node": ">= 6" } }, + "node_modules/blob": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.1.0.tgz", + "integrity": "sha512-k+GwK+4Rj+MPNT4qu+y6+kHp+mPmmNd+28zdrIo69QM9UvypK5Vhcw7jnRiY4KaOMAiOdn0NtPQGTb+Ox1Dtng==", + "license": "MIT", + "dependencies": { + "esm": "^3.2.25" + } + }, "node_modules/blob-util": { "version": "2.0.2", "dev": true, @@ -19311,6 +19522,12 @@ "dev": true, "license": "ISC" }, + "node_modules/chroma-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", + "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/chrome-trace-event": { "version": "1.0.2", "devOptional": true, @@ -20473,9 +20690,7 @@ }, "node_modules/create-require": { "version": "1.1.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-env": { "version": "7.0.3", @@ -20611,6 +20826,12 @@ "postcss": "^8.0.9" } }, + "node_modules/css-font-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/css-font-parser/-/css-font-parser-2.0.1.tgz", + "integrity": "sha512-C4aQOpCmQL/Arl68chQatNh7/Nfyty15kbLNZezGudjcKSqHHVoHQEeb9IJcjgQ6CiurrHZoEt47yce891vjGw==", + "license": "BSD-3-Clause" + }, "node_modules/css-in-js-utils": { "version": "2.0.1", "license": "MIT", @@ -22491,9 +22712,7 @@ }, "node_modules/diff": { "version": "4.0.2", - "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.3.1" } @@ -24883,6 +25102,15 @@ "node": ">= 8" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "dev": true, @@ -25954,6 +26182,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/file-system-cache": { "version": "2.3.0", "license": "MIT", @@ -26853,6 +27087,15 @@ "node": ">=6.9.0" } }, + "node_modules/geojson": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/geojson/-/geojson-0.5.0.tgz", + "integrity": "sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/geojson-flatten": { "version": "1.0.4", "license": "BSD-2-Clause", @@ -26878,6 +27121,411 @@ "node_modules/geolib": { "version": "2.0.24" }, + "node_modules/geostyler": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/geostyler/-/geostyler-12.0.2.tgz", + "integrity": "sha512-H5Jgszl+UU6/mrMznRTh815ZQpv4zkPwv4Nomi3jS68I6Afmx01s+403UGPTufiT/j7g/ZOSyOH3avBhAYvbaw==", + "license": "BSD-2-Clause", + "dependencies": { + "@babel/polyfill": "^7.12.1", + "@dnd-kit/core": "^6.0.6", + "@dnd-kit/sortable": "^7.0.1", + "@dnd-kit/utilities": "^3.2.1", + "@monaco-editor/react": "^4.4.6", + "@types/chroma-js": "^2.1.4", + "@types/color": "^3.0.3", + "@types/file-saver": "^2.0.5", + "@types/geojson": "^7946.0.10", + "@types/lodash": "^4.14.188", + "@types/react-color": "^3.0.6", + "@ungap/url-search-params": "^0.2.2", + "blob": "^0.1.0", + "chroma-js": "^2.4.2", + "color": "^4.2.3", + "csstype": "^3.1.1", + "file-saver": "^2.0.5", + "geostyler-cql-parser": "^3.0.1", + "geostyler-data": "^1.0.0", + "geostyler-geojson-parser": "^1.0.1", + "geostyler-openlayers-parser": "^4.1.0", + "geostyler-sld-parser": "^5.0.0", + "geostyler-style": "^7.2.0", + "geostyler-wfs-parser": "^2.0.0", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "monaco-editor": "^0.34.1", + "react-color": "^2.19.3", + "react-rnd": "^10.3.7", + "typescript-json-schema": "^0.55.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "@ant-design/icons": ">=4.x", + "@types/react": ">=16.x", + "@types/react-dom": ">=16.x", + "antd": "4.x", + "ol": ">=6.x", + "react": ">=16.x", + "react-dom": ">=16.x" + } + }, + "node_modules/geostyler-cql-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/geostyler-cql-parser/-/geostyler-cql-parser-3.0.2.tgz", + "integrity": "sha512-nfQOnWwFSrWtgl7DRmhwoa3r0BnpsMvEv7rb5D8ihpCUQS/YP09C2C1j6zKQVSnYRMl0545VnhtYtqejMgPaBg==", + "license": "BSD-2-Clause", + "dependencies": { + "geostyler-style": "^7.2.0", + "lodash": "^4.17.21" + } + }, + "node_modules/geostyler-data": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/geostyler-data/-/geostyler-data-1.0.0.tgz", + "integrity": "sha512-ctmk6OsunL427Uaa1HME/blTyBbl0Ihu+vPV1Irqz3ip80qvNLwDEr46xI5HwMeyrsWH8o76kfA0sF6oecW1BA==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/geojson": "7946.0.7", + "@types/json-schema": "7.0.3" + } + }, + "node_modules/geostyler-data/node_modules/@types/geojson": { + "version": "7946.0.7", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", + "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==", + "license": "MIT" + }, + "node_modules/geostyler-data/node_modules/@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "license": "MIT" + }, + "node_modules/geostyler-geojson-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/geostyler-geojson-parser/-/geostyler-geojson-parser-1.0.1.tgz", + "integrity": "sha512-b7eJ2sCrYCC7fIDItxfbDH9r55dE58OXTQjPb/kIlXgH+7A2o2xp7pQlRXu5xCqM5lucQAAM9A7IfLLhbflznw==", + "license": "BSD-2-Clause", + "dependencies": { + "@babel/polyfill": "^7.4.4", + "@types/geojson": "^7946.0.7", + "@types/jest": "^24.0.18", + "@types/json-schema": "^7.0.3", + "@types/node": "^12.7.3", + "geostyler-data": "^1.0.0" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/@types/jest": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", + "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", + "license": "MIT", + "dependencies": { + "jest-diff": "^24.3.0" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/geostyler-geojson-parser/node_modules/@types/yargs": { + "version": "13.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.12.tgz", + "integrity": "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/geostyler-geojson-parser/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/geostyler-openlayers-parser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/geostyler-openlayers-parser/-/geostyler-openlayers-parser-4.3.0.tgz", + "integrity": "sha512-9LoctzoxorZogOpp6nXT6SZGnISRpNR9M+MLyhYVTFlOtDi4Gb4fWbUnpqVzDxqMNl8anMmv7cpMsmw1bjWsLw==", + "license": "BSD-2-Clause", + "dependencies": { + "css-font-parser": "^2.0.0", + "geostyler-style": "^8.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + }, + "peerDependencies": { + "ol": ">=7.4" + } + }, + "node_modules/geostyler-openlayers-parser/node_modules/geostyler-style": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", + "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/lodash": "^4.14.201", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, + "node_modules/geostyler-sld-parser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-5.4.0.tgz", + "integrity": "sha512-TY/gwMoE/M8Xv8ykviC1NqygykrojvrFSCRIc/+CNww5VS0Qf9pu9JxiSQONGfR6QWDPtQrfFIoazyH67c0jTg==", + "license": "BSD-2-Clause", + "dependencies": { + "fast-xml-parser": "^4.2.2", + "geostyler-style": "^8.1.0", + "i18next": "^23.11.5", + "lodash": "^4.17.21" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, + "node_modules/geostyler-sld-parser/node_modules/geostyler-style": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", + "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/lodash": "^4.14.201", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, + "node_modules/geostyler-style": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-7.5.0.tgz", + "integrity": "sha512-0qlFOwDl9cdiiNUk6ijRSpBq+W3Kpry1aZS++BE8EPBxvbFgoTsKU6JIriKLmCqPqBmJIVnMFMUEk6Sf3n0ZIA==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/lodash": "^4.14.201", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, + "node_modules/geostyler-wfs-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/geostyler-wfs-parser/-/geostyler-wfs-parser-2.0.3.tgz", + "integrity": "sha512-23gmufyveYB/jhVzxPj/eb/zkI+xWSzTKqGO7gcAh0NYUFPI+UG3WTJhzT4Dj9G+ZTpSUnXOSmkHr4M7jZsakA==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/geojson": "^7946.0.14", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.4", + "fast-xml-parser": "^4.4.0", + "geostyler-data": "^1.0.0", + "lodash": "^4.17.21" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, + "node_modules/geostyler/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/geostyler/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/geostyler/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/geotiff": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", + "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", + "license": "MIT", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2", + "zstddec": "^0.1.0" + }, + "engines": { + "node": ">=10.19" + } + }, + "node_modules/geotiff/node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, + "node_modules/geotiff/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/geotiff/node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "license": "ISC", @@ -28928,6 +29576,29 @@ "version": "1.0.4", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "23.16.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.0.tgz", + "integrity": "sha512-Ni3CG6c14teOogY19YNRl+kYaE/Rb59khy0VyHVn4uOZ97E2E/Yziyi6r3C3s9+wacjdLZiq/LLYyx+Cgd+FCw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -34690,9 +35361,7 @@ }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/make-fetch-happen": { "version": "13.0.1", @@ -34802,6 +35471,12 @@ "kdbush": "^3.0.0" } }, + "node_modules/mapbox-to-css-font": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.5.tgz", + "integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==", + "license": "BSD-2-Clause" + }, "node_modules/markdown-extensions": { "version": "1.1.1", "dev": true, @@ -40293,6 +40968,12 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz", + "integrity": "sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==", + "license": "MIT" + }, "node_modules/moo": { "version": "0.4.3", "dev": true, @@ -42143,6 +42824,34 @@ "dev": true, "license": "MIT" }, + "node_modules/ol": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/ol/-/ol-7.5.2.tgz", + "integrity": "sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ==", + "license": "BSD-2-Clause", + "dependencies": { + "earcut": "^2.2.3", + "geotiff": "^2.0.7", + "ol-mapbox-style": "^10.1.0", + "pbf": "3.2.1", + "rbush": "^3.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openlayers" + } + }, + "node_modules/ol-mapbox-style": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-10.7.0.tgz", + "integrity": "sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw==", + "license": "BSD-2-Clause", + "dependencies": { + "@mapbox/mapbox-gl-style-spec": "^13.23.1", + "mapbox-to-css-font": "^2.4.1", + "ol": "^7.3.0" + } + }, "node_modules/omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -42973,6 +43682,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "4.0.0", "dev": true, @@ -43074,6 +43789,12 @@ "dev": true, "license": "MIT" }, + "node_modules/path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "3.0.0", "dev": true, @@ -44854,6 +45575,15 @@ "node": ">= 0.8" } }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/rc": { "version": "1.2.8", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", @@ -45565,14 +46295,21 @@ } }, "node_modules/react-color": { - "version": "2.14.1", + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", "license": "MIT", "dependencies": { - "lodash": "^4.0.1", + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", "material-colors": "^1.2.1", "prop-types": "^15.5.10", "reactcss": "^1.2.0", "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" } }, "node_modules/react-colorful": { @@ -46048,6 +46785,27 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-rnd": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.13.tgz", + "integrity": "sha512-Vgbf0iihspcQ6nkaFhpOGWfmnuVbhkhoB0hBbYl8aRDA4horsQHESc4E1z7O/P27kFFjK2aqM0u5CGzfr9gEZA==", + "license": "MIT", + "dependencies": { + "re-resizable": "6.10.0", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/react-router": { "version": "5.3.4", "license": "MIT", @@ -48691,7 +49449,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "dev": true, "engines": { "node": ">=10" } @@ -49501,6 +50258,22 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sort-asc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", + "integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-desc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz", + "integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -49513,6 +50286,18 @@ "node": ">=4" } }, + "node_modules/sort-object": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz", + "integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==", + "dependencies": { + "sort-asc": "^0.1.0", + "sort-desc": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sort-object-keys": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", @@ -50007,6 +50792,12 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/static-eval": { "version": "2.1.0", "license": "MIT", @@ -51572,12 +52363,12 @@ "license": "ISC" }, "node_modules/ts-node": { - "version": "10.7.0", - "dev": true, + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "license": "MIT", - "peer": true, "dependencies": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -51588,7 +52379,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "bin": { @@ -51616,9 +52407,7 @@ }, "node_modules/ts-node/node_modules/acorn-walk": { "version": "8.2.0", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4.0" } @@ -51831,6 +52620,44 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-json-schema": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.55.0.tgz", + "integrity": "sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", + "path-equal": "^1.1.2", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~4.8.2", + "yargs": "^17.1.1" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/node_modules/@types/node": { + "version": "16.18.114", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.114.tgz", + "integrity": "sha512-7oAtnxrgkMNzyzT443UDWwzkmYew81F1ZSPm3/lsITJfW/WludaSOpegTvUG+UdapcbrtWOtY/E4LyTkhPGJ5Q==", + "license": "MIT" + }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/ua-parser-js": { "version": "0.7.33", "funding": [ @@ -52454,10 +53281,10 @@ } }, "node_modules/v8-compile-cache-lib": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "peer": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" }, "node_modules/v8-to-istanbul": { "version": "9.2.0", @@ -52779,6 +53606,12 @@ "node": ">= 8" } }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "license": "Apache-2.0" + }, "node_modules/webdriver": { "version": "7.31.1", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-7.31.1.tgz", @@ -54188,6 +55021,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/xml-utils": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.1.tgz", + "integrity": "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ==", + "license": "CC0-1.0" + }, "node_modules/xmlbuilder": { "version": "15.0.0", "dev": true, @@ -54280,9 +55119,7 @@ }, "node_modules/yn": { "version": "3.1.1", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -54330,6 +55167,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==", + "license": "MIT AND BSD-3-Clause" + }, "node_modules/zwitch": { "version": "2.0.4", "license": "MIT", @@ -58114,19 +58957,43 @@ "version": "3.1.0", "license": "(MPL-2.0 OR Apache-2.0)" }, + "plugins/plugin-chart-cartodiagram": { + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@types/geojson": "^7946.0.10", + "geojson": "^0.5.0", + "lodash": "^4.17.21", + "ol": "^7.1.0" + }, + "peerDependencies": { + "@ant-design/icons": "^5.0.1", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "antd": "^4.10.3", + "geostyler": "^12.0.0", + "geostyler-data": "^1.0.0", + "geostyler-openlayers-parser": "^4.0.0", + "geostyler-style": "^7.2.0", + "geostyler-wfs-parser": "^2.0.0", + "polished": "*", + "react": "^16.13.1", + "react-dom": "^16.13.0" + } + }, "plugins/plugin-chart-echarts": { "name": "@superset-ui/plugin-chart-echarts", "version": "0.18.25", "license": "Apache-2.0", "dependencies": { "d3-array": "^1.2.0", - "echarts": "^5.4.1", "lodash": "^4.17.21", "moment": "^2.30.1" }, "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "echarts": "*", "memoize-one": "*", "react": "^16.13.1" } @@ -60392,17 +61259,23 @@ "minimist": "^1.2.0" } }, - "@cspotcode/source-map-consumer": { - "version": "0.8.0", - "dev": true, - "peer": true - }, "@cspotcode/source-map-support": { - "version": "0.7.0", - "dev": true, - "peer": true, + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "requires": { - "@cspotcode/source-map-consumer": "0.8.0" + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } } }, "@ctrl/tinycolor": { @@ -61178,6 +62051,69 @@ "version": "0.5.7", "dev": true }, + "@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + } + } + }, + "@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "requires": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + } + } + }, + "@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "requires": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + } + } + }, + "@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + } + } + }, "@emnapi/core": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.2.0.tgz", @@ -61862,6 +62798,12 @@ "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", "dev": true }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "requires": {} + }, "@inquirer/figures": { "version": "1.0.3", "devOptional": true @@ -63214,6 +64156,21 @@ "@mapbox/jsonlint-lines-primitives": { "version": "2.0.2" }, + "@mapbox/mapbox-gl-style-spec": { + "version": "13.28.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz", + "integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==", + "requires": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/unitbezier": "^0.0.0", + "csscolorparser": "~1.0.2", + "json-stringify-pretty-compact": "^2.0.0", + "minimist": "^1.2.6", + "rw": "^1.3.3", + "sort-object": "^0.3.2" + } + }, "@mapbox/mapbox-gl-supported": { "version": "2.0.1" }, @@ -63319,6 +64276,22 @@ "@mihkeleidast/storybook-addon-source": { "version": "1.0.1" }, + "@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "requires": { + "@monaco-editor/loader": "^1.4.0" + } + }, "@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -64256,6 +65229,11 @@ "@octokit/openapi-types": "^11.2.0" } }, + "@petamoriken/float16": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.7.tgz", + "integrity": "sha512-/Ri4xDDpe12NT6Ex/DRgHzLlobiQXEW/hmG08w1wj/YU7hLemk97c+zHQFp0iZQ9r7YqgLEXZR2sls4HxBf9NA==" + }, "@pkgjs/parseargs": { "version": "0.11.0", "optional": true @@ -68702,11 +69680,19 @@ } } }, + "@superset-ui/plugin-chart-cartodiagram": { + "version": "file:plugins/plugin-chart-cartodiagram", + "requires": { + "@types/geojson": "^7946.0.10", + "geojson": "^0.5.0", + "lodash": "^4.17.21", + "ol": "^7.1.0" + } + }, "@superset-ui/plugin-chart-echarts": { "version": "file:plugins/plugin-chart-echarts", "requires": { "d3-array": "^1.2.0", - "echarts": "^5.4.1", "lodash": "^4.17.21", "moment": "^2.30.1" } @@ -69054,7 +70040,6 @@ }, "@swc/core": { "version": "1.4.0", - "dev": true, "optional": true, "peer": true, "requires": { @@ -69080,13 +70065,11 @@ }, "@swc/counter": { "version": "0.1.3", - "dev": true, "optional": true, "peer": true }, "@swc/types": { "version": "0.1.5", - "dev": true, "optional": true, "peer": true }, @@ -69210,24 +70193,16 @@ "dev": true }, "@tsconfig/node10": { - "version": "1.0.8", - "dev": true, - "peer": true + "version": "1.0.8" }, "@tsconfig/node12": { - "version": "1.0.9", - "dev": true, - "peer": true + "version": "1.0.9" }, "@tsconfig/node14": { - "version": "1.0.1", - "dev": true, - "peer": true + "version": "1.0.1" }, "@tsconfig/node16": { - "version": "1.0.2", - "dev": true, - "peer": true + "version": "1.0.2" }, "@tufjs/canonical-json": { "version": "2.0.0", @@ -69396,9 +70371,35 @@ "@types/node": "*" } }, + "@types/chroma-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.4.tgz", + "integrity": "sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==" + }, "@types/classnames": { "version": "2.2.10" }, + "@types/color": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz", + "integrity": "sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==", + "requires": { + "@types/color-convert": "*" + } + }, + "@types/color-convert": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz", + "integrity": "sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==", + "requires": { + "@types/color-name": "^1.1.0" + } + }, + "@types/color-name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.5.tgz", + "integrity": "sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==" + }, "@types/connect": { "version": "3.4.35", "requires": { @@ -69573,8 +70574,15 @@ "version": "7.3.5", "dev": true }, + "@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==" + }, "@types/geojson": { - "version": "7946.0.8" + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" }, "@types/glob": { "version": "8.1.0", @@ -69659,19 +70667,16 @@ } }, "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "dev": true + "version": "2.0.3" }, "@types/istanbul-lib-report": { "version": "3.0.0", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "*" } }, "@types/istanbul-reports": { "version": "1.1.2", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "*", "@types/istanbul-lib-report": "*" @@ -69728,8 +70733,9 @@ "version": "1.0.4" }, "@types/json-schema": { - "version": "7.0.9", - "devOptional": true + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "@types/json5": { "version": "0.0.29", @@ -69745,7 +70751,9 @@ } }, "@types/lodash": { - "version": "4.14.182" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==" }, "@types/lodash-es": { "version": "4.17.12", @@ -69841,6 +70849,15 @@ "csstype": "^3.0.2" } }, + "@types/react-color": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.12.tgz", + "integrity": "sha512-pr3uKE3lSvf7GFo1Rn2K3QktiZQFFrSgSGJ/3iMvSOYWt2pPAJ97rVdVfhWxYJZ8prAEXzoP2XX//3qGSQgu7Q==", + "requires": { + "@types/react": "^16.9.53", + "@types/reactcss": "*" + } + }, "@types/react-dom": { "version": "16.9.8", "requires": { @@ -69940,6 +70957,14 @@ "@types/react": "^16.9.53" } }, + "@types/reactcss": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.12.tgz", + "integrity": "sha512-BrXUQ86/wbbFiZv8h/Q1/Q1XOsaHneYmCb/tHe9+M8XBAAUc2EHfdY0DY22ZZjVSaXr5ix7j+zsqO2eGZub8lQ==", + "requires": { + "@types/react": "^16.9.53" + } + }, "@types/redux-localstorage": { "version": "1.0.8", "dev": true, @@ -70156,8 +71181,7 @@ } }, "@types/yargs-parser": { - "version": "15.0.0", - "dev": true + "version": "15.0.0" }, "@types/yauzl": { "version": "2.9.2", @@ -70403,6 +71427,11 @@ "@ungap/structured-clone": { "version": "1.2.0" }, + "@ungap/url-search-params": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@ungap/url-search-params/-/url-search-params-0.2.2.tgz", + "integrity": "sha512-qQsguKXZVKdCixOHX9jqnX/K/1HekPDpGKyEcXHT+zR6EjGA7S4boSuelL4uuPv6YfhN0n8c4UxW+v/Z3gM2iw==" + }, "@visx/annotation": { "version": "3.3.0", "requires": { @@ -72018,9 +73047,7 @@ "peer": true }, "arg": { - "version": "4.1.3", - "dev": true, - "peer": true + "version": "4.1.3" }, "argparse": { "version": "1.0.10", @@ -72903,6 +73930,14 @@ } } }, + "blob": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.1.0.tgz", + "integrity": "sha512-k+GwK+4Rj+MPNT4qu+y6+kHp+mPmmNd+28zdrIo69QM9UvypK5Vhcw7jnRiY4KaOMAiOdn0NtPQGTb+Ox1Dtng==", + "requires": { + "esm": "^3.2.25" + } + }, "blob-util": { "version": "2.0.2", "dev": true, @@ -73579,6 +74614,11 @@ "version": "1.1.4", "dev": true }, + "chroma-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", + "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==" + }, "chrome-trace-event": { "version": "1.0.2", "devOptional": true, @@ -74376,9 +75416,7 @@ } }, "create-require": { - "version": "1.1.1", - "dev": true, - "peer": true + "version": "1.1.1" }, "cross-env": { "version": "7.0.3", @@ -74463,6 +75501,11 @@ "dev": true, "requires": {} }, + "css-font-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/css-font-parser/-/css-font-parser-2.0.1.tgz", + "integrity": "sha512-C4aQOpCmQL/Arl68chQatNh7/Nfyty15kbLNZezGudjcKSqHHVoHQEeb9IJcjgQ6CiurrHZoEt47yce891vjGw==" + }, "css-in-js-utils": { "version": "2.0.1", "requires": { @@ -75715,9 +76758,7 @@ "dev": true }, "diff": { - "version": "4.0.2", - "dev": true, - "peer": true + "version": "4.0.2" }, "diff-match-patch": { "version": "1.0.5" @@ -77253,6 +78294,11 @@ "version": "3.4.3", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "espree": { "version": "9.6.1", "dev": true, @@ -77995,6 +79041,11 @@ "flat-cache": "^3.0.4" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-system-cache": { "version": "2.3.0", "requires": { @@ -78566,6 +79617,11 @@ "gensync": { "version": "1.0.0-beta.2" }, + "geojson": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/geojson/-/geojson-0.5.0.tgz", + "integrity": "sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==" + }, "geojson-flatten": { "version": "1.0.4", "requires": { @@ -78584,6 +79640,302 @@ "geolib": { "version": "2.0.24" }, + "geostyler": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/geostyler/-/geostyler-12.0.2.tgz", + "integrity": "sha512-H5Jgszl+UU6/mrMznRTh815ZQpv4zkPwv4Nomi3jS68I6Afmx01s+403UGPTufiT/j7g/ZOSyOH3avBhAYvbaw==", + "requires": { + "@babel/polyfill": "^7.12.1", + "@dnd-kit/core": "^6.0.6", + "@dnd-kit/sortable": "^7.0.1", + "@dnd-kit/utilities": "^3.2.1", + "@monaco-editor/react": "^4.4.6", + "@types/chroma-js": "^2.1.4", + "@types/color": "^3.0.3", + "@types/file-saver": "^2.0.5", + "@types/geojson": "^7946.0.10", + "@types/lodash": "^4.14.188", + "@types/react-color": "^3.0.6", + "@ungap/url-search-params": "^0.2.2", + "blob": "^0.1.0", + "chroma-js": "^2.4.2", + "color": "^4.2.3", + "csstype": "^3.1.1", + "file-saver": "^2.0.5", + "geostyler-cql-parser": "^3.0.1", + "geostyler-data": "^1.0.0", + "geostyler-geojson-parser": "^1.0.1", + "geostyler-openlayers-parser": "^4.1.0", + "geostyler-sld-parser": "^5.0.0", + "geostyler-style": "^7.2.0", + "geostyler-wfs-parser": "^2.0.0", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "monaco-editor": "^0.34.1", + "react-color": "^2.19.3", + "react-rnd": "^10.3.7", + "typescript-json-schema": "^0.55.0" + }, + "dependencies": { + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "geostyler-cql-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/geostyler-cql-parser/-/geostyler-cql-parser-3.0.2.tgz", + "integrity": "sha512-nfQOnWwFSrWtgl7DRmhwoa3r0BnpsMvEv7rb5D8ihpCUQS/YP09C2C1j6zKQVSnYRMl0545VnhtYtqejMgPaBg==", + "requires": { + "geostyler-style": "^7.2.0", + "lodash": "^4.17.21" + } + }, + "geostyler-data": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/geostyler-data/-/geostyler-data-1.0.0.tgz", + "integrity": "sha512-ctmk6OsunL427Uaa1HME/blTyBbl0Ihu+vPV1Irqz3ip80qvNLwDEr46xI5HwMeyrsWH8o76kfA0sF6oecW1BA==", + "requires": { + "@types/geojson": "7946.0.7", + "@types/json-schema": "7.0.3" + }, + "dependencies": { + "@types/geojson": { + "version": "7946.0.7", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", + "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==" + }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" + } + } + }, + "geostyler-geojson-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/geostyler-geojson-parser/-/geostyler-geojson-parser-1.0.1.tgz", + "integrity": "sha512-b7eJ2sCrYCC7fIDItxfbDH9r55dE58OXTQjPb/kIlXgH+7A2o2xp7pQlRXu5xCqM5lucQAAM9A7IfLLhbflznw==", + "requires": { + "@babel/polyfill": "^7.4.4", + "@types/geojson": "^7946.0.7", + "@types/jest": "^24.0.18", + "@types/json-schema": "^7.0.3", + "@types/node": "^12.7.3", + "geostyler-data": "^1.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/jest": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", + "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", + "requires": { + "jest-diff": "^24.3.0" + } + }, + "@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "@types/yargs": { + "version": "13.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.12.tgz", + "integrity": "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==" + }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==" + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "geostyler-openlayers-parser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/geostyler-openlayers-parser/-/geostyler-openlayers-parser-4.3.0.tgz", + "integrity": "sha512-9LoctzoxorZogOpp6nXT6SZGnISRpNR9M+MLyhYVTFlOtDi4Gb4fWbUnpqVzDxqMNl8anMmv7cpMsmw1bjWsLw==", + "requires": { + "css-font-parser": "^2.0.0", + "geostyler-style": "^8.1.0" + }, + "dependencies": { + "geostyler-style": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", + "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", + "requires": { + "@types/lodash": "^4.14.201", + "lodash": "^4.17.21" + } + } + } + }, + "geostyler-sld-parser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-5.4.0.tgz", + "integrity": "sha512-TY/gwMoE/M8Xv8ykviC1NqygykrojvrFSCRIc/+CNww5VS0Qf9pu9JxiSQONGfR6QWDPtQrfFIoazyH67c0jTg==", + "requires": { + "fast-xml-parser": "^4.2.2", + "geostyler-style": "^8.1.0", + "i18next": "^23.11.5", + "lodash": "^4.17.21" + }, + "dependencies": { + "geostyler-style": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", + "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", + "requires": { + "@types/lodash": "^4.14.201", + "lodash": "^4.17.21" + } + } + } + }, + "geostyler-style": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-7.5.0.tgz", + "integrity": "sha512-0qlFOwDl9cdiiNUk6ijRSpBq+W3Kpry1aZS++BE8EPBxvbFgoTsKU6JIriKLmCqPqBmJIVnMFMUEk6Sf3n0ZIA==", + "requires": { + "@types/lodash": "^4.14.201", + "lodash": "^4.17.21" + } + }, + "geostyler-wfs-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/geostyler-wfs-parser/-/geostyler-wfs-parser-2.0.3.tgz", + "integrity": "sha512-23gmufyveYB/jhVzxPj/eb/zkI+xWSzTKqGO7gcAh0NYUFPI+UG3WTJhzT4Dj9G+ZTpSUnXOSmkHr4M7jZsakA==", + "requires": { + "@types/geojson": "^7946.0.14", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.4", + "fast-xml-parser": "^4.4.0", + "geostyler-data": "^1.0.0", + "lodash": "^4.17.21" + } + }, + "geotiff": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", + "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", + "requires": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2", + "zstddec": "^0.1.0" + }, + "dependencies": { + "lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" + }, + "pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==" + } + } + }, "get-caller-file": { "version": "2.0.5" }, @@ -79915,6 +81267,14 @@ "hyphenate-style-name": { "version": "1.0.4" }, + "i18next": { + "version": "23.16.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.0.tgz", + "integrity": "sha512-Ni3CG6c14teOogY19YNRl+kYaE/Rb59khy0VyHVn4uOZ97E2E/Yziyi6r3C3s9+wacjdLZiq/LLYyx+Cgd+FCw==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, "iconv-lite": { "version": "0.4.24", "requires": { @@ -83680,9 +85040,7 @@ } }, "make-error": { - "version": "1.3.6", - "dev": true, - "peer": true + "version": "1.3.6" }, "make-fetch-happen": { "version": "13.0.1", @@ -83770,6 +85128,11 @@ } } }, + "mapbox-to-css-font": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.5.tgz", + "integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==" + }, "markdown-extensions": { "version": "1.1.1", "dev": true @@ -86670,6 +88033,11 @@ "moment": "^2.29.4" } }, + "monaco-editor": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz", + "integrity": "sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==" + }, "moo": { "version": "0.4.3", "dev": true @@ -87918,6 +89286,28 @@ "version": "1.1.3", "dev": true }, + "ol": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/ol/-/ol-7.5.2.tgz", + "integrity": "sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ==", + "requires": { + "earcut": "^2.2.3", + "geotiff": "^2.0.7", + "ol-mapbox-style": "^10.1.0", + "pbf": "3.2.1", + "rbush": "^3.0.1" + } + }, + "ol-mapbox-style": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-10.7.0.tgz", + "integrity": "sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw==", + "requires": { + "@mapbox/mapbox-gl-style-spec": "^13.23.1", + "mapbox-to-css-font": "^2.4.1", + "ol": "^7.3.0" + } + }, "omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -88452,6 +89842,11 @@ } } }, + "parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" + }, "parse-json": { "version": "4.0.0", "dev": true, @@ -88524,6 +89919,11 @@ "version": "1.0.1", "dev": true }, + "path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==" + }, "path-exists": { "version": "3.0.0", "dev": true @@ -89628,6 +91028,14 @@ } } }, + "rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "requires": { + "quickselect": "^2.0.0" + } + }, "rc": { "version": "1.2.8", "requires": { @@ -90072,9 +91480,13 @@ } }, "react-color": { - "version": "2.14.1", + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", "requires": { - "lodash": "^4.0.1", + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", "material-colors": "^1.2.1", "prop-types": "^15.5.10", "reactcss": "^1.2.0", @@ -90371,6 +91783,23 @@ "version": "2.1.1", "requires": {} }, + "react-rnd": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.13.tgz", + "integrity": "sha512-Vgbf0iihspcQ6nkaFhpOGWfmnuVbhkhoB0hBbYl8aRDA4horsQHESc4E1z7O/P27kFFjK2aqM0u5CGzfr9gEZA==", + "requires": { + "re-resizable": "6.10.0", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "react-router": { "version": "5.3.4", "requires": { @@ -92024,8 +93453,7 @@ "safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "dev": true + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" }, "safer-buffer": { "version": "2.1.2" @@ -92615,6 +94043,16 @@ "atomic-sleep": "^1.0.0" } }, + "sort-asc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", + "integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==" + }, + "sort-desc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz", + "integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==" + }, "sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -92624,6 +94062,15 @@ "is-plain-obj": "^1.0.0" } }, + "sort-object": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz", + "integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==", + "requires": { + "sort-asc": "^0.1.0", + "sort-desc": "^0.1.1" + } + }, "sort-object-keys": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", @@ -92961,6 +94408,11 @@ } } }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "static-eval": { "version": "2.1.0", "requires": { @@ -93969,11 +95421,11 @@ } }, "ts-node": { - "version": "10.7.0", - "dev": true, - "peer": true, + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "requires": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -93984,14 +95436,12 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "dependencies": { "acorn-walk": { - "version": "8.2.0", - "dev": true, - "peer": true + "version": "8.2.0" } } }, @@ -94134,6 +95584,33 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" }, + "typescript-json-schema": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.55.0.tgz", + "integrity": "sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==", + "requires": { + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", + "path-equal": "^1.1.2", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~4.8.2", + "yargs": "^17.1.1" + }, + "dependencies": { + "@types/node": { + "version": "16.18.114", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.114.tgz", + "integrity": "sha512-7oAtnxrgkMNzyzT443UDWwzkmYew81F1ZSPm3/lsITJfW/WludaSOpegTvUG+UdapcbrtWOtY/E4LyTkhPGJ5Q==" + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==" + } + } + }, "ua-parser-js": { "version": "0.7.33" }, @@ -94502,9 +95979,9 @@ } }, "v8-compile-cache-lib": { - "version": "3.0.0", - "dev": true, - "peer": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" }, "v8-to-istanbul": { "version": "9.2.0", @@ -94733,6 +96210,11 @@ "version": "3.3.2", "dev": true }, + "web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" + }, "webdriver": { "version": "7.31.1", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-7.31.1.tgz", @@ -95642,6 +97124,11 @@ "version": "3.0.0", "dev": true }, + "xml-utils": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.1.tgz", + "integrity": "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ==" + }, "xmlbuilder": { "version": "15.0.0", "dev": true @@ -95700,9 +97187,7 @@ } }, "yn": { - "version": "3.1.1", - "dev": true, - "peer": true + "version": "3.1.1" }, "yocto-queue": { "version": "0.1.0" @@ -95732,6 +97217,11 @@ } } }, + "zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" + }, "zwitch": { "version": "2.0.4" } diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 736c294de0ea6..43725d07ffa49 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -110,6 +110,7 @@ "@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map", "@superset-ui/legacy-preset-chart-deckgl": "file:./plugins/legacy-preset-chart-deckgl", "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3", + "@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram", "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts", "@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars", "@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table", @@ -137,11 +138,17 @@ "d3-scale": "^2.1.2", "dayjs": "^1.11.13", "dom-to-image-more": "^3.2.0", + "echarts": "^5.4.1", "emotion-rgba": "0.0.12", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", "geolib": "^2.0.24", + "geostyler": "^12.0.2", + "geostyler-data": "^1.0.0", + "geostyler-openlayers-parser": "^4.3.0", + "geostyler-style": "^7.5.0", + "geostyler-wfs-parser": "^2.0.3", "googleapis": "^130.0.0", "html-webpack-plugin": "^5.3.2", "immer": "^10.1.1", @@ -161,6 +168,7 @@ "mousetrap": "^1.6.5", "mustache": "^4.2.0", "nanoid": "^5.0.7", + "ol": "^7.5.2", "polished": "^4.3.1", "prop-types": "^15.8.1", "query-string": "^6.13.7", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/index.ts index fed32cae3a287..f97f24ca7f7c3 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/index.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/index.ts @@ -33,6 +33,7 @@ export * from './components/Dropdown'; export * from './components/Menu'; export * from './components/MetricOption'; export * from './components/Tooltip'; +export { default as ControlHeader } from './components/ControlHeader'; export * from './shared-controls'; export * from './types'; diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/QueryResponse.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/QueryResponse.ts index d5b12e89c28fc..563c0a2d2491d 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/types/QueryResponse.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/types/QueryResponse.ts @@ -31,6 +31,14 @@ export interface TimeseriesDataRecord extends DataRecord { __timestamp: number | string | Date | null; } +export const isTimeseriesDataRecord = ( + item: any, +): item is TimeseriesDataRecord => Object.keys(item).includes('__timestamp'); + +export const isTimeseriesDataRecordList = ( + items: any[], +): items is TimeseriesDataRecord[] => items.every(isTimeseriesDataRecord); + // data record value filters from FilterBox export interface DataRecordFilters { [key: string]: DataRecordValue[]; diff --git a/superset-frontend/packages/superset-ui-core/test/chart/types/QueryResponse.test.ts b/superset-frontend/packages/superset-ui-core/test/chart/types/QueryResponse.test.ts new file mode 100644 index 0000000000000..38007e11cea5f --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/chart/types/QueryResponse.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { + isTimeseriesDataRecord, + isTimeseriesDataRecordList, +} from '@superset-ui/core'; + +describe('QueryResponse', () => { + describe('TypeGuards', () => { + it('correctly determines a TimeseriesDataRecord', () => { + const timeseriesDataRecord = { + foo: 'bar', + __timestamp: 0, + }; + expect(isTimeseriesDataRecord(timeseriesDataRecord)).toBe(true); + }); + + it('correctly determines if a DataRecord is not a TimeseriesDataRecord', () => { + const timeseriesDataRecord = { + foo: 'bar', + }; + expect(isTimeseriesDataRecord(timeseriesDataRecord)).toBe(false); + }); + + it('correctly determines a TimeseriesDataRecordList', () => { + const timeseriesDataRecordList = [ + { + foo: 'bar', + __timestamp: 0, + }, + { + foo: 'baz', + __timestamp: 1, + }, + ]; + expect(isTimeseriesDataRecordList(timeseriesDataRecordList)).toBe(true); + }); + + it('correctly determines if a DataRecordList is not a TimeseriesDataRecordList', () => { + const timeseriesDataRecordList = [ + { + foo: 'bar', + }, + { + foo: 'baz', + }, + ]; + expect(isTimeseriesDataRecordList(timeseriesDataRecordList)).toBe(false); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/README.md b/superset-frontend/plugins/plugin-chart-cartodiagram/README.md new file mode 100644 index 0000000000000..8037296234f0c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/README.md @@ -0,0 +1,67 @@ + + +## @superset-ui/plugin-chart-cartodiagram + +This plugin allows visualizing charts on a map. To do so, the plugin makes use of existing charts and renders them on the +provided locations. + +Configuring the charts: Charts will be configured in their respective editors. So all configuration options of any chart are supported. + +Configuring the map: For the map, an arbitrary number of background layers (WMS, WFS, XYZ), the initial map extent, the chart background color and border radius, as well as the chart size (per zoom level) can be configured. + +### Usage + +The plugin is configured in `superset-frontend/src/visualizations/presets/MainPreset.js`. + +```js +import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; + +new CartodiagramPlugin().configure({ key: 'cartodiagram' }).register(); +``` + +Default layers can be added to the constructor. These layers will be added to each chart by default (but can be removed by editors). See also `./src/types.ts` for the definitions of types `WmsLayerConf`, `WfsLayerConf` and `XyzLayerConf`. + +Example for an XYZ default layer: + +```js +import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; + +const opts = { + defaultLayers: [ + { + type: 'XYZ', + url: 'example.com/path/to/xyz/layer', + title: 'my default layer title', + attribution: 'my default layer attribution', + }, + ], +}; + +new CartodiagramPlugin(opts).configure({ key: 'cartodiagram' }).register(); +``` + +Please note that by default, Superset rejects requests to third-party domains. If you want to include +layers from those, you have to adjust the CSP settings. See also docs/docs/security/security.mdx. + +### Geometry Column + +The plugin requires the selection of a geometry column for a dataset. +This is expected to be a GeoJSON-Point-Geometry string in WGS 84/Pseudo-Mercator (EPSG:3857). Other formats and projections +will be supported in the future. diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json new file mode 100644 index 0000000000000..585884f2116dd --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json @@ -0,0 +1,51 @@ +{ + "name": "@superset-ui/plugin-chart-cartodiagram", + "version": "0.0.1", + "description": "An OpenLayers map that displays charts for single features.", + "sideEffects": false, + "main": "lib/index.js", + "module": "esm/index.js", + "files": [ + "esm", + "lib" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/apache-superset/superset-ui.git" + }, + "keywords": [ + "superset" + ], + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/apache-superset/superset-ui/issues" + }, + "homepage": "https://github.com/apache-superset/superset-ui#readme", + "contributors": [ + "terrestris GmbH & Co. KG (https://www.terrestris.de)", + "meggsimum - Büro für Geoinformatik (https://meggsimum.de)" + ], + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@types/geojson": "^7946.0.10", + "geojson": "^0.5.0", + "lodash": "^4.17.21", + "ol": "^7.1.0" + }, + "peerDependencies": { + "@ant-design/icons": "^5.0.1", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "antd": "^4.10.3", + "geostyler": "^12.0.0", + "geostyler-data": "^1.0.0", + "geostyler-openlayers-parser": "^4.0.0", + "geostyler-style": "^7.2.0", + "geostyler-wfs-parser": "^2.0.0", + "polished": "*", + "react": "^16.13.1", + "react-dom": "^16.13.0" + } +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/CartodiagramPlugin.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/CartodiagramPlugin.tsx new file mode 100644 index 0000000000000..6598bbd5a2761 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/CartodiagramPlugin.tsx @@ -0,0 +1,59 @@ +/** + * 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 { createRef, useState } from 'react'; +import { styled, useTheme } from '@superset-ui/core'; +import OlMap from 'ol/Map'; +import { + CartodiagramPluginProps, + CartodiagramPluginStylesProps, +} from './types'; + +import OlChartMap from './components/OlChartMap'; + +import 'ol/ol.css'; + +// The following Styles component is a
element, which has been styled using Emotion +// For docs, visit https://emotion.sh/docs/styled + +// Theming variables are provided for your use via a ThemeProvider +// imported from @superset-ui/core. For variables available, please visit +// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/style/index.ts + +const Styles = styled.div` + height: ${({ height }) => height}px; + width: ${({ width }) => width}px; +`; + +export default function CartodiagramPlugin(props: CartodiagramPluginProps) { + const { height, width } = props; + const theme = useTheme(); + + const rootElem = createRef(); + + const [mapId] = useState( + `cartodiagram-plugin-${Math.floor(Math.random() * 1000)}`, + ); + const [olMap] = useState(new OlMap({})); + + return ( + + + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/ChartLayer.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/ChartLayer.tsx new file mode 100644 index 0000000000000..8800f3da38c96 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/ChartLayer.tsx @@ -0,0 +1,282 @@ +/** + * 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 Layer from 'ol/layer/Layer'; +import { FrameState } from 'ol/Map'; +import { apply as applyTransform } from 'ol/transform'; +import ReactDOM from 'react-dom'; +import { SupersetTheme } from '@superset-ui/core'; +import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types'; +import { createChartComponent } from '../util/chartUtil'; +import { getProjectedCoordinateFromPointGeoJson } from '../util/geometryUtil'; + +import Loader from '../images/loading.gif'; + +/** + * Custom OpenLayers layer that displays charts on given locations. + */ +export class ChartLayer extends Layer { + charts: any[] = []; + + chartConfigs: ChartConfig = { + type: 'FeatureCollection', + features: [], + }; + + chartSizeValues: ChartSizeValues = {}; + + chartVizType: string; + + div: HTMLDivElement; + + loadingMask: HTMLDivElement; + + chartBackgroundCssColor = ''; + + chartBackgroundBorderRadius = 0; + + theme: SupersetTheme; + + /** + * Create a ChartLayer. + * + * @param {ChartLayerOptions} options The options to create a ChartLayer + * @param {ChartHtmlElement[]} options.charts An array with the chart objects containing the HTML element and the coordinate + * @param {ChartConfig} options.chartConfigs The chart configuration for the charts + * @param {ChartSizeValues} options.chartSizeValues The values for the chart sizes + * @param {String} options.chartVizType The viztype of the charts + * @param {String} options.chartBackgroundCssColor The color of the additionally added chart background + * @param {Number} options.chartBackgroundBorderRadius The border radius in percent of the additionally added chart background + * @param {Function} options.onMouseOver The handler function to execute when the mouse entering a HTML element + * @param {Function} options.onMouseOut The handler function to execute when the mouse leaves a HTML element + * @param {SupersetTheme} options.theme The superset theme + */ + constructor(options: ChartLayerOptions) { + super(options); + + this.chartVizType = options.chartVizType; + + if (options.chartConfigs) { + this.chartConfigs = options.chartConfigs; + } + + if (options.chartSizeValues) { + this.chartSizeValues = options.chartSizeValues; + } + + if (options.chartBackgroundCssColor) { + this.chartBackgroundCssColor = options.chartBackgroundCssColor; + } + + if (options.chartBackgroundBorderRadius) { + this.chartBackgroundBorderRadius = options.chartBackgroundBorderRadius; + } + + if (options.theme) { + this.theme = options.theme; + } + + const spinner = document.createElement('img'); + spinner.src = Loader; + spinner.style.position = 'relative'; + spinner.style.width = '50px'; + spinner.style.top = '50%'; + spinner.style.left = '50%'; + spinner.style.transform = 'translate(-50%, -50%)'; + + this.loadingMask = document.createElement('div'); + this.loadingMask.style.position = 'relative'; + this.loadingMask.style.height = '100%'; + this.loadingMask.appendChild(spinner); + + this.div = document.createElement('div'); + + // TODO: consider creating an OpenLayers event + if (options.onMouseOver) { + this.div.onmouseover = options.onMouseOver; + } + + // TODO: consider creating an OpenLayers event + if (options.onMouseOut) { + this.div.onmouseout = options.onMouseOut; + } + } + + setChartConfig(chartConfigs: ChartConfig, silent = false) { + this.chartConfigs = chartConfigs; + if (!silent) { + this.changed(); + } + } + + setChartVizType(chartVizType: string, silent = false) { + this.chartVizType = chartVizType; + if (!silent) { + this.changed(); + } + } + + setChartSizeValues(chartSizeValues: ChartSizeValues, silent = false) { + this.chartSizeValues = chartSizeValues; + if (!silent) { + this.changed(); + } + } + + setChartBackgroundCssColor(chartBackgroundCssColor: string, silent = false) { + this.chartBackgroundCssColor = chartBackgroundCssColor; + if (!silent) { + this.changed(); + } + } + + setChartBackgroundBorderRadius( + chartBackgroundBorderRadius: number, + silent = false, + ) { + this.chartBackgroundBorderRadius = chartBackgroundBorderRadius; + if (!silent) { + this.changed(); + } + } + + /** + * Unmount and remove all created chart elements from the DOM. + */ + removeAllChartElements() { + this.charts.forEach(chart => { + ReactDOM.unmountComponentAtNode(chart.htmlElement); + chart.htmlElement.remove(); + }); + this.charts = []; + } + + createCharts(zoom: number) { + const charts = this.chartConfigs.features.map(feature => { + const container = document.createElement('div'); + + let chartWidth = 0; + let chartHeight = 0; + if (this.chartSizeValues[zoom]) { + chartWidth = this.chartSizeValues[zoom].width; + chartHeight = this.chartSizeValues[zoom].height; + } + + const chartComponent = createChartComponent( + this.chartVizType, + feature, + chartWidth, + chartHeight, + this.theme, + ); + ReactDOM.render(chartComponent, container); + + return { + htmlElement: container, + coordinate: getProjectedCoordinateFromPointGeoJson(feature.geometry), + width: chartWidth, + height: chartHeight, + feature, + }; + }); + + this.charts = charts; + } + + updateCharts(zoom: number) { + const charts = this.charts.map(chart => { + let chartWidth = 0; + let chartHeight = 0; + if (this.chartSizeValues[zoom]) { + chartWidth = this.chartSizeValues[zoom].width; + chartHeight = this.chartSizeValues[zoom].height; + } + + // only rerender chart if size changes + if (chartWidth === chart.width && chartHeight === chart.height) { + return chart; + } + + const chartComponent = createChartComponent( + this.chartVizType, + chart.feature, + chartWidth, + chartHeight, + this.theme, + ); + ReactDOM.render(chartComponent, chart.htmlElement); + + return { + ...chart, + width: chartWidth, + height: chartHeight, + }; + }); + + this.charts = charts; + } + + render(frameState: FrameState | null) { + if (!frameState) { + return this.div; + } + + const { viewState } = frameState; + const currentZoom = Math.round(viewState.zoom); + + // nextResolution is only defined while an animation + // is in action. For this time we show a loading mask + // to keep the amount of chart rerenderings as low as possible. + if (viewState.nextResolution) { + return this.loadingMask; + } + + if (this.charts.length === 0) { + this.createCharts(currentZoom); + } else { + this.updateCharts(currentZoom); + } + + this.charts.forEach(chartObject => { + const { htmlElement, coordinate, width, height } = chartObject; + + // clone, because applyTransform modifies in place + const coordCopy = [...coordinate]; + + const [x, y] = applyTransform( + frameState.coordinateToPixelTransform, + coordCopy, + ); + + // left and top are corrected to place the center of the chart to its location + htmlElement.style.left = `${x - width / 2}px`; + htmlElement.style.top = `${y - height / 2}px`; + htmlElement.style.position = 'absolute'; + htmlElement.style['background-color' as any] = + this.chartBackgroundCssColor; + htmlElement.style['border-radius' as any] = + `${this.chartBackgroundBorderRadius}%`; + }); + + // TODO should we always replace the html elements or is there a better way? + const htmlElements = this.charts.map(c => c.htmlElement); + this.div.replaceChildren(...htmlElements); + + return this.div; + } +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/ChartWrapper.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/ChartWrapper.tsx new file mode 100644 index 0000000000000..ed441a7181ec3 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/ChartWrapper.tsx @@ -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 { getChartComponentRegistry, ThemeProvider } from '@superset-ui/core'; +import { FC, useEffect, useState } from 'react'; +import { ChartWrapperProps } from '../types'; + +export const ChartWrapper: FC = ({ + vizType, + theme, + height, + width, + chartConfig, +}) => { + const [Chart, setChart] = useState(); + + const getChartFromRegistry = async (vizType: string) => { + const registry = getChartComponentRegistry(); + const c = await registry.getAsPromise(vizType); + setChart(() => c); + }; + + useEffect(() => { + getChartFromRegistry(vizType); + }, [vizType]); + + return ( + + {Chart === undefined ? ( + <> + ) : ( + + )} + + ); +}; + +export default ChartWrapper; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/OlChartMap.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/OlChartMap.tsx new file mode 100644 index 0000000000000..4982c5b95e44c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/components/OlChartMap.tsx @@ -0,0 +1,409 @@ +/** + * 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 { useEffect, useState } from 'react'; + +import Point from 'ol/geom/Point'; +import { View } from 'ol'; +import BaseEvent from 'ol/events/Event'; +import { unByKey } from 'ol/Observable'; +import { toLonLat } from 'ol/proj'; +import { debounce } from 'lodash'; +import { fitMapToCharts } from '../util/mapUtil'; +import { ChartLayer } from './ChartLayer'; +import { createLayer } from '../util/layerUtil'; +import { + ChartConfig, + LayerConf, + MapViewConfigs, + OlChartMapProps, +} from '../types'; +import { isChartConfigEqual } from '../util/chartUtil'; + +/** The name to reference the chart layer */ +const CHART_LAYER_NAME = 'openlayers-chart-layer'; + +export const OlChartMap = (props: OlChartMapProps) => { + const { + height, + width, + mapId, + olMap, + chartConfigs, + chartSize, + chartVizType, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + setControlValue, + theme, + } = props; + + const [currentChartConfigs, setCurrentChartConfigs] = + useState(chartConfigs); + const [currentMapView, setCurrentMapView] = useState(mapView); + + /** + * Add map to correct DOM element. + */ + useEffect(() => { + olMap.setTarget(mapId); + }, [olMap, mapId]); + + /** + * Update map size if size of parent container changes. + */ + useEffect(() => { + olMap.updateSize(); + }, [olMap, width, height]); + + /** + * The prop chartConfigs will always be created on the fly, + * therefore the shallow comparison of the effect hooks will + * always trigger. In this hook, we make a 'deep comparison' + * between the incoming prop and the state. Only if the objects + * differ will we set the state to the new object. All other + * effect hooks that depend on chartConfigs should now depend + * on currentChartConfigs instead. + */ + useEffect(() => { + setCurrentChartConfigs(oldCurrentChartConfigs => { + if (isChartConfigEqual(chartConfigs, oldCurrentChartConfigs)) { + return oldCurrentChartConfigs; + } + return chartConfigs; + }); + }, [chartConfigs]); + + /** + * The prop mapView will always be created on the fly, + * therefore the shallow comparison of the effect hooks will + * always trigger. In this hook, we compare only those props + * that might be changed from outside of the component, i.e the + * fixed properties and the mode. Only if these values differ will + * we set the state to the new object. All other effect hooks that + * depend on mapView should now depend on currentMapView instead. + */ + useEffect(() => { + setCurrentMapView(oldCurrentMapView => { + const sameFixedZoom = oldCurrentMapView.fixedZoom === mapView.fixedZoom; + const sameFixedLon = + oldCurrentMapView.fixedLongitude === mapView.fixedLongitude; + const sameFixedLat = + oldCurrentMapView.fixedLatitude === mapView.fixedLatitude; + const sameMode = oldCurrentMapView.mode === mapView.mode; + if (sameFixedZoom && sameFixedLon && sameFixedLat && sameMode) { + return oldCurrentMapView; + } + return mapView; + }); + }, [mapView]); + + /** + * Set initial map extent. + */ + useEffect(() => { + const view = olMap.getView(); + const { mode, fixedLatitude, fixedLongitude, fixedZoom } = mapView; + + switch (mode) { + case 'CUSTOM': { + const fixedCenter = new Point([fixedLongitude, fixedLatitude]); + fixedCenter.transform('EPSG:4326', 'EPSG:3857'); // in-place + + view.setZoom(fixedZoom); + view.setCenter(fixedCenter.getCoordinates()); + break; + } + default: { + fitMapToCharts(olMap, chartConfigs); + + const zoom = view.getZoom(); + const centerCoord = view.getCenter(); + if (!centerCoord) return; + + const centerPoint = new Point(centerCoord); + centerPoint.transform('EPSG:3857', 'EPSG:4326'); // in-place + + const [longitude, latitude] = centerPoint.getCoordinates(); + + setControlValue('map_view', { + ...mapView, + zoom, + longitude, + latitude, + fixedLatitude: latitude, + fixedLongitude: longitude, + fixedZoom: zoom, + }); + + break; + } + } + }, []); + + /** + * Update non-chart layers + */ + useEffect(() => { + // clear existing layers + // We first filter the layers we want to remove, + // because removing items from an array during a loop can be erroneous. + const layersToRemove = olMap + .getLayers() + .getArray() + .filter(layer => !(layer instanceof ChartLayer)); + + layersToRemove.forEach(layer => { + olMap.removeLayer(layer); + }); + + const addLayers = async (configs: LayerConf[]) => { + // Loop through layer configs, create layers and add them to map. + // The first layer in the list will be the upmost layer on the map. + // With insertAt(0) we ensure that the chart layer will always + // stay on top, though. + const createdLayersPromises = configs.map(createLayer); + const createdLayers = await Promise.allSettled(createdLayersPromises); + createdLayers.forEach((createdLayer, idx) => { + if (createdLayer.status === 'fulfilled' && createdLayer.value) { + olMap.getLayers().insertAt(0, createdLayer.value); + } else { + console.warn(`Layer could not be created: ${configs[idx]}`); + } + }); + }; + + addLayers(layerConfigs); + }, [olMap, layerConfigs]); + + /** + * Create listener on map movement + */ + useEffect(() => { + const { fixedLatitude, fixedLongitude, fixedZoom } = currentMapView; + + const view = olMap.getView(); + + const onViewChange = (event: BaseEvent) => { + const targetView: View = event.target as unknown as View; + + const center = targetView.getCenter(); + const zoom = targetView.getZoom(); + if (!center) { + return; + } + const [longitude, latitude] = toLonLat(center); + + setControlValue('map_view', { + ...currentMapView, + zoom, + longitude, + latitude, + fixedLatitude, + fixedLongitude, + fixedZoom, + }); + }; + + // TODO: maybe replace with debounce from lodash + // timeout=100ms seems to work well, 1000ms has other side-effects + function debounce(func: Function, timeout = 100) { + let timer: number; + return function (this: any, ...args: any) { + clearTimeout(timer); + timer = window.setTimeout(() => func.apply(this, args), timeout); + }; + } + + const debouncedOnViewChange = debounce((event: BaseEvent) => { + onViewChange(event); + }); + + const listenerKey = view.on('change', debouncedOnViewChange); + + // this is executed before the next render, + // here we cleanup the listener + return () => { + unByKey(listenerKey); + }; + }, [olMap, setControlValue, currentMapView, currentChartConfigs]); + + useEffect(() => { + if (currentMapView.mode === 'FIT_DATA') { + const layers = olMap.getLayers(); + const chartLayer = layers + .getArray() + .find(layer => layer instanceof ChartLayer) as ChartLayer; + + if (!chartLayer) { + return; + } + const extent = chartLayer.getExtent(); + if (!extent) { + return; + } + const view = olMap.getView(); + view.fit(extent, { + size: [250, 250], + }); + } + }, [olMap, currentMapView.mode]); + + /** + * Send updated zoom to chart config control. + */ + useEffect(() => { + const view = olMap.getView(); + + const onViewChange = (event: BaseEvent) => { + const targetView: View = event.target as unknown as View; + + // ensure only zoom has changed + const zoom = targetView.getZoom(); + + // needed for TypeScript + if (!zoom) return; + + // round zoom to full integer + const previousZoom = Math.round(chartSize.configs.zoom); + const newZoom = Math.round(zoom); + + // if zoom has not changed, we return and do not update the controls + if (previousZoom === newZoom) return; + + const updatedChartSizeConf = { + ...chartSize, + configs: { + ...chartSize.configs, + zoom: newZoom, + }, + }; + + setControlValue('chart_size', updatedChartSizeConf); + }; + + const debouncedOnZoomChange = debounce((event: BaseEvent) => { + onViewChange(event); + }, 100); + + const listenerKey = view.on('change:resolution', debouncedOnZoomChange); + + // This is executed before the next render, + // here we cleanup our listener. + return () => { + unByKey(listenerKey); + }; + }, [olMap, setControlValue, chartSize]); + + /** + * Handle changes that trigger changes of charts. Also instantiate + * the chart layer, if it does not exist yet. + */ + useEffect(() => { + const layers = olMap.getLayers(); + const chartLayer = layers + .getArray() + .find(layer => layer instanceof ChartLayer) as ChartLayer; + + const { r, g, b, a } = chartBackgroundColor; + const cssColor = `rgba(${r}, ${g}, ${b}, ${a})`; + + if (!chartLayer) { + layers.forEach(layer => { + if (!(layer instanceof ChartLayer)) { + return; + } + // remove all chart elements from dom. + layer.removeAllChartElements(); + // delete previous chart layers + olMap.removeLayer(layer); + }); + + // prevent map interactions when mouse is over chart element + // inspired by https://gis.stackexchange.com/questions/303331 + const deactivateInteractions = () => { + olMap.getInteractions().forEach(interaction => { + interaction.setActive(false); + }); + }; + + const activateInteractions = () => { + olMap.getInteractions().forEach(interaction => { + interaction.setActive(true); + }); + }; + + const newChartLayer = new ChartLayer({ + name: CHART_LAYER_NAME, + chartConfigs: currentChartConfigs, + chartVizType, + chartSizeValues: chartSize.values, + chartBackgroundCssColor: cssColor, + chartBackgroundBorderRadius, + onMouseOver: deactivateInteractions, + onMouseOut: activateInteractions, + theme, + }); + + olMap.addLayer(newChartLayer); + } else { + let recreateCharts = false; + if (chartVizType !== chartLayer.chartVizType) { + chartLayer.setChartVizType(chartVizType, true); + recreateCharts = true; + } + if (!isChartConfigEqual(currentChartConfigs, chartLayer.chartConfigs)) { + chartLayer.setChartConfig(currentChartConfigs, true); + recreateCharts = true; + } + // Only the last setter triggers rerendering of charts + chartLayer.setChartBackgroundBorderRadius( + chartBackgroundBorderRadius, + true, + ); + chartLayer.setChartBackgroundCssColor(cssColor, true); + chartLayer.setChartSizeValues(chartSize.values, true); + if (recreateCharts) { + chartLayer.removeAllChartElements(); + } + chartLayer.changed(); + } + }, [ + olMap, + theme, + currentChartConfigs, + chartVizType, + chartSize.values, + chartBackgroundColor, + chartBackgroundBorderRadius, + ]); + + return ( +
+ ); +}; + +export default OlChartMap; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/example1.png b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/example1.png new file mode 100644 index 0000000000000..6fe034477b8a4 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/example1.png differ diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/example2.png b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/example2.png new file mode 100644 index 0000000000000..7d42dc459e4bb Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/example2.png differ diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/loading.gif b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/loading.gif new file mode 100644 index 0000000000000..d82fc5d9244e2 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/loading.gif differ diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/thumbnail.png new file mode 100644 index 0000000000000..bfd9539337169 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-cartodiagram/src/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.ts new file mode 100644 index 0000000000000..ca175475ac38c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ +// eslint-disable-next-line import/prefer-default-export +export { default as CartodiagramPlugin } from './plugin'; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts new file mode 100644 index 0000000000000..23405382e6198 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts @@ -0,0 +1,68 @@ +/** + * 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 { QueryFormData, getChartBuildQueryRegistry } from '@superset-ui/core'; + +/** + * The buildQuery function is used to create an instance of QueryContext that's + * sent to the chart data endpoint. In addition to containing information of which + * datasource to use, it specifies the type (e.g. full payload, samples, query) and + * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from + * the datasource as opposed to using a cached copy of the data, if available. + * + * More importantly though, QueryContext contains a property `queries`, which is an array of + * QueryObjects specifying individual data requests to be made. A QueryObject specifies which + * columns, metrics and filters, among others, to use during the query. Usually it will be enough + * to specify just one query based on the baseQueryObject, but for some more advanced use cases + * it is possible to define post processing operations in the QueryObject, or multiple queries + * if a viz needs multiple different result sets. + */ +export default function buildQuery(formData: QueryFormData) { + const { + selected_chart: selectedChartString, + geom_column: geometryColumn, + extra_form_data: extraFormData, + } = formData; + const selectedChart = JSON.parse(selectedChartString); + const vizType = selectedChart.viz_type; + const chartFormData = JSON.parse(selectedChart.params); + // Pass extra_form_data to chartFormData so that + // dashboard filters will also be applied to the charts + // on the map. + chartFormData.extra_form_data = { + ...chartFormData.extra_form_data, + ...extraFormData, + }; + + // adapt groupby property to ensure geometry column always exists + // and is always at first position + let { groupby } = chartFormData; + if (!groupby) { + groupby = []; + } + // add geometry column at the first place + groupby?.unshift(geometryColumn); + chartFormData.groupby = groupby; + + // TODO: find way to import correct type "InclusiveLoaderResult" + const buildQueryRegistry = getChartBuildQueryRegistry(); + const chartQueryBuilder = buildQueryRegistry.get(vizType) as any; + + const chartQuery = chartQueryBuilder(chartFormData); + return chartQuery; +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/controlPanel.ts new file mode 100644 index 0000000000000..772d4ef689612 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/controlPanel.ts @@ -0,0 +1,203 @@ +/** + * 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 { t, validateNonEmpty } from '@superset-ui/core'; +import { ControlPanelConfig } from '@superset-ui/chart-controls'; +import { selectedChartMutator } from '../util/controlPanelUtil'; + +import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../util/zoomUtil'; + +const config: ControlPanelConfig = { + /** + * The control panel is split into two tabs: "Data" and + * "Customize". The controls that define the inputs to + * the chart data request, such as columns and metrics, usually + * reside within "Data", while controls that affect the visual + * appearance or functionality of the chart are under the + * "Customize" section. + */ + + // For control input types, see: superset-frontend/src/explore/components/controls/index.js + controlPanelSections: [ + { + label: t('Configuration'), + expanded: true, + controlSetRows: [ + [ + { + name: 'selected_chart', + config: { + type: 'SelectAsyncControl', + mutator: selectedChartMutator, + multi: false, + label: t('Chart'), + validators: [validateNonEmpty], + description: t('Choose a chart for displaying on the map'), + placeholder: t('Select chart'), + onAsyncErrorMessage: t('Error while fetching charts'), + mapStateToProps: state => { + if (state?.datasource?.id) { + const datasourceId = state.datasource.id; + const query = { + columns: ['id', 'slice_name', 'params', 'viz_type'], + filters: [ + { + col: 'datasource_id', + opr: 'eq', + value: datasourceId, + }, + ], + page: 0, + // TODO check why we only retrieve 100 items, even though there are more + page_size: 999, + }; + + const dataEndpoint = `/api/v1/chart/?q=${JSON.stringify( + query, + )}`; + + return { dataEndpoint }; + } + // could not extract datasource from map + return {}; + }, + }, + }, + ], + [ + { + name: 'geom_column', + config: { + type: 'SelectControl', + label: t('Geometry Column'), + renderTrigger: false, + description: t('The name of the geometry column'), + mapStateToProps: state => ({ + choices: state.datasource?.columns.map(c => [ + c.column_name, + c.column_name, + ]), + }), + validators: [validateNonEmpty], + }, + }, + ], + ], + }, + { + label: t('Map Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'map_view', + config: { + type: 'MapViewControl', + renderTrigger: true, + description: t( + 'The extent of the map on application start. FIT DATA automatically sets the extent so that all data points are included in the viewport. CUSTOM allows users to define the extent manually.', + ), + label: t('Extent'), + dontRefreshOnChange: true, + default: { + mode: 'FIT_DATA', + }, + }, + }, + ], + [ + { + // name is referenced in 'index.ts' for setting default value + name: 'layer_configs', + config: { + type: 'LayerConfigsControl', + renderTrigger: true, + label: t('Layers'), + default: [], + description: t('The configuration for the map layers'), + }, + }, + ], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'chart_background_color', + config: { + label: t('Background Color'), + description: t('The background color of the charts.'), + type: 'ColorPickerControl', + default: { r: 255, g: 255, b: 255, a: 0.2 }, + renderTrigger: true, + }, + }, + ], + [ + { + name: 'chart_background_border_radius', + config: { + label: t('Corner Radius'), + description: t('The corner radius of the chart background'), + type: 'SliderControl', + default: 10, + min: 0, + step: 1, + max: 100, + renderTrigger: true, + }, + }, + ], + [ + { + name: 'chart_size', + config: { + type: 'ZoomConfigControl', + // set this to true, if we are able to render it fast + renderTrigger: true, + default: { + type: 'FIXED', + configs: { + zoom: 6, + width: 100, + height: 100, + slope: 30, + exponent: 2, + }, + // create an object with keys MIN_ZOOM_LEVEL - MAX_ZOOM_LEVEL + // that all contain the same initial value + values: { + ...Array.from( + { length: MAX_ZOOM_LEVEL - MIN_ZOOM_LEVEL + 1 }, + () => ({ width: 100, height: 100 }), + ), + }, + }, + label: t('Chart size'), + description: t('Configure the chart size for each zoom level'), + }, + }, + ], + ], + }, + ], +}; +export default config; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/index.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/index.ts new file mode 100644 index 0000000000000..17164f7f69844 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/index.ts @@ -0,0 +1,77 @@ +/** + * 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 { t, ChartMetadata, ChartPlugin } from '@superset-ui/core'; +import buildQuery from './buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from './transformProps'; +import thumbnail from '../images/thumbnail.png'; +import example1 from '../images/example1.png'; +import example2 from '../images/example2.png'; +import { CartodiagramPluginConstructorOpts } from '../types'; +import { getLayerConfig } from '../util/controlPanelUtil'; + +export default class CartodiagramPlugin extends ChartPlugin { + /** + * The constructor is used to pass relevant metadata and callbacks that get + * registered in respective registries that are used throughout the library + * and application. A more thorough description of each property is given in + * the respective imported file. + * + * It is worth noting that `buildQuery` and is optional, and only needed for + * advanced visualizations that require either post processing operations + * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. + */ + constructor(opts: CartodiagramPluginConstructorOpts) { + console.log('hello world'); + const metadata = new ChartMetadata({ + description: + 'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.', + name: t('Cartodiagram'), + thumbnail, + tags: [t('Geo'), t('2D'), t('Spatial'), t('Experimental')], + category: t('Map'), + exampleGallery: [ + { url: example1, caption: t('Pie charts on a map') }, + { url: example2, caption: t('Line charts on a map') }, + ], + }); + + if (opts.defaultLayers) { + const layerConfig = getLayerConfig(controlPanel); + + // set defaults for layer config if found + if (layerConfig) { + layerConfig.config.default = opts.defaultLayers; + } else { + // eslint-disable-next-line no-console + console.warn( + 'Cannot set defaultLayers. layerConfig not found in control panel. Please check if the path to layerConfig should be adjusted.', + ); + } + } + + super({ + buildQuery, + controlPanel, + loadChart: () => import('../CartodiagramPlugin'), + metadata, + transformProps, + }); + } +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/transformProps.ts new file mode 100644 index 0000000000000..95b7a4adc45ab --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/transformProps.ts @@ -0,0 +1,92 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ChartProps, getChartTransformPropsRegistry } from '@superset-ui/core'; +import { + getChartConfigs, + parseSelectedChart, +} from '../util/transformPropsUtil'; + +export default function transformProps(chartProps: ChartProps) { + /** + * This function is called after a successful response has been + * received from the chart data endpoint, and is used to transform + * the incoming data prior to being sent to the Visualization. + * + * The transformProps function is also quite useful to return + * additional/modified props to your data viz component. The formData + * can also be accessed from your CartodiagramPlugin.tsx file, but + * doing supplying custom props here is often handy for integrating third + * party libraries that rely on specific props. + * + * A description of properties in `chartProps`: + * - `height`, `width`: the height/width of the DOM element in which + * the chart is located + * - `formData`: the chart data request payload that was sent to the + * backend. + * - `queriesData`: the chart data response payload that was received + * from the backend. Some notable properties of `queriesData`: + * - `data`: an array with data, each row with an object mapping + * the column/alias to its value. Example: + * `[{ col1: 'abc', metric1: 10 }, { col1: 'xyz', metric1: 20 }]` + * - `rowcount`: the number of rows in `data` + * - `query`: the query that was issued. + * + * Please note: the transformProps function gets cached when the + * application loads. When making changes to the `transformProps` + * function during development with hot reloading, changes won't + * be seen until restarting the development server. + */ + const { width, height, formData, hooks, theme } = chartProps; + const { + geomColumn, + selectedChart: selectedChartString, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + } = formData; + const { setControlValue = () => {} } = hooks; + const selectedChart = parseSelectedChart(selectedChartString); + const transformPropsRegistry = getChartTransformPropsRegistry(); + const chartTransformer = transformPropsRegistry.get(selectedChart.viz_type); + + const chartConfigs = getChartConfigs( + selectedChart, + geomColumn, + chartProps, + chartTransformer, + ); + + return { + width, + height, + geomColumn, + selectedChart, + chartConfigs, + chartVizType: selectedChart.viz_type, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + setControlValue, + theme, + }; +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/typeguards.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/typeguards.ts new file mode 100644 index 0000000000000..a65e0f124b960 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/typeguards.ts @@ -0,0 +1,31 @@ +/** + * 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 { LayerConf, WfsLayerConf, WmsLayerConf, XyzLayerConf } from './types'; + +export const isWmsLayerConf = ( + layerConf: LayerConf, +): layerConf is WmsLayerConf => layerConf.type === 'WMS'; + +export const isWfsLayerConf = ( + layerConf: LayerConf, +): layerConf is WfsLayerConf => layerConf.type === 'WFS'; + +export const isXyzLayerConf = ( + layerConf: LayerConf, +): layerConf is XyzLayerConf => layerConf.type === 'XYZ'; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/types.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/types.ts new file mode 100644 index 0000000000000..a875103d9e092 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/types.ts @@ -0,0 +1,210 @@ +/** + * 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 { + DataRecord, + SupersetTheme, + TimeseriesDataRecord, +} from '@superset-ui/core'; +import { RenderFunction } from 'ol/layer/Layer'; +import { Extent } from 'ol/extent'; +import Source from 'ol/source/Source'; +import { Coordinate } from 'ol/coordinate'; +import { Map } from 'ol'; +import { Feature, FeatureCollection, Point } from 'geojson'; +import { Style } from 'geostyler-style'; + +export interface CartodiagramPluginStylesProps { + height: number; + width: number; + theme: SupersetTheme; +} + +// TODO find a way to reference props from other charts +export type ChartConfigProperties = any; + +export type ChartConfigFeature = Feature; +export type ChartConfig = FeatureCollection< + ChartConfigFeature['geometry'], + ChartConfigFeature['properties'] +>; + +interface CartodiagramPluginCustomizeProps { + geomColumn: string; + selectedChart: string; + chartConfigs: ChartConfig; + chartSize: ZoomConfigs; + chartVizType: string; + layerConfigs: LayerConf[]; + mapView: MapViewConfigs; + chartBackgroundColor: { + r: number; + g: number; + b: number; + a: number; + }; + chartBackgroundBorderRadius: number; + setControlValue: Function; +} + +export type CartodiagramPluginProps = CartodiagramPluginStylesProps & + CartodiagramPluginCustomizeProps & { + data: TimeseriesDataRecord[]; + }; + +export interface OlChartMapProps extends CartodiagramPluginProps { + mapId: string; + olMap: Map; +} + +export interface BaseLayerConf { + title: string; + url: string; + type: string; + attribution?: string; +} + +export interface WfsLayerConf extends BaseLayerConf { + type: 'WFS'; + typeName: string; + version: string; + maxFeatures?: number; + style?: Style; +} + +export interface XyzLayerConf extends BaseLayerConf { + type: 'XYZ'; +} + +export interface WmsLayerConf extends BaseLayerConf { + type: 'WMS'; + version: string; + layersParam: string; +} + +export type LayerConf = WmsLayerConf | WfsLayerConf | XyzLayerConf; + +export type EventHandlers = Record; + +export type SelectedChartConfig = { + viz_type: string; + params: { + [key: string]: any; + }; +}; + +export type LocationConfigMapping = { + [key: string]: DataRecord[]; +}; + +export type MapViewConfigs = { + mode: 'FIT_DATA' | 'CUSTOM'; + zoom: number; + latitude: number; + longitude: number; + fixedZoom: number; + fixedLatitude: number; + fixedLongitude: number; +}; + +export type ZoomConfigs = ZoomConfigsFixed | ZoomConfigsLinear | ZoomConfigsExp; + +export type ChartSizeValues = { + [index: number]: { width: number; height: number }; +}; + +export interface ZoomConfigsBase { + type: string; + configs: { + zoom: number; + width: number; + height: number; + slope?: number; + exponent?: number; + }; + values: ChartSizeValues; +} + +export interface ZoomConfigsFixed extends ZoomConfigsBase { + type: 'FIXED'; +} + +export interface ZoomConfigsLinear extends ZoomConfigsBase { + type: 'LINEAR'; + configs: { + zoom: number; + width: number; + height: number; + slope: number; + exponent?: number; + }; +} + +export interface ZoomConfigsExp extends ZoomConfigsBase { + type: 'EXP'; + configs: { + zoom: number; + width: number; + height: number; + slope?: number; + exponent: number; + }; +} + +export type ChartHtmlElement = { + htmlElement: HTMLDivElement; + coordinate: Coordinate; + width: number; + height: number; +}; + +export type ChartLayerOptions = { + chartSizeValues?: ChartSizeValues; + chartConfigs?: ChartConfig; + chartVizType: string; + onMouseOver?: (this: GlobalEventHandlers, ev: MouseEvent) => any | undefined; + onMouseOut?: (this: GlobalEventHandlers, ev: MouseEvent) => any | undefined; + [key: string]: any; // allow custom types like 'name' + // these properties are copied from OpenLayers + // TODO: consider extending the OpenLayers options type + className?: string | undefined; + opacity?: number | undefined; + visible?: boolean | undefined; + extent?: Extent | undefined; + zIndex?: number | undefined; + minResolution?: number | undefined; + maxResolution?: number | undefined; + minZoom?: number | undefined; + maxZoom?: number | undefined; + source?: Source | undefined; + map?: Map | null | undefined; + render?: RenderFunction | undefined; + properties?: { [x: string]: any } | undefined; +}; + +export type CartodiagramPluginConstructorOpts = { + defaultLayers?: LayerConf[]; +}; + +export type ChartWrapperProps = { + vizType: string; + theme: SupersetTheme; + width: number; + height: number; + chartConfig: ChartConfigFeature; +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/chartUtil.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/chartUtil.tsx new file mode 100644 index 0000000000000..e0b4932f32cb4 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/chartUtil.tsx @@ -0,0 +1,86 @@ +/** + * 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 { SupersetTheme } from '@superset-ui/core'; +import { ChartConfig, ChartConfigFeature } from '../types'; +import ChartWrapper from '../components/ChartWrapper'; + +/** + * Create a chart component for a location. + * + * @param chartVizType The superset visualization type + * @param chartConfigs The chart configurations + * @param chartWidth The chart width + * @param chartHeight The chart height + * @param chartTheme The chart theme + * @returns The chart as React component + */ +export const createChartComponent = ( + chartVizType: string, + chartConfig: ChartConfigFeature, + chartWidth: number, + chartHeight: number, + chartTheme: SupersetTheme, +) => ( + +); + +/** + * Simplifies a chart configuration by removing + * non-serializable properties. + * + * @param config The chart configuration to simplify. + * @returns The simplified chart configuration. + */ +export const simplifyConfig = (config: ChartConfig) => { + const simplifiedConfig: ChartConfig = { + type: config.type, + features: config.features.map(f => ({ + type: f.type, + geometry: f.geometry, + properties: Object.keys(f.properties) + .filter(k => k !== 'refs') + .reduce((prev, cur) => ({ ...prev, [cur]: f.properties[cur] }), {}), + })), + }; + return simplifiedConfig; +}; + +/** + * Check if two chart configurations are equal (deep equality). + * + * @param configA The first chart config for comparison. + * @param configB The second chart config for comparison. + * @returns True, if configurations are equal. False otherwise. + */ +export const isChartConfigEqual = ( + configA: ChartConfig, + configB: ChartConfig, +) => { + const simplifiedConfigA = simplifyConfig(configA); + const simplifiedConfigB = simplifyConfig(configB); + return ( + JSON.stringify(simplifiedConfigA) === JSON.stringify(simplifiedConfigB) + ); +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/controlPanelUtil.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/controlPanelUtil.tsx new file mode 100644 index 0000000000000..1fc79f4aa7520 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/controlPanelUtil.tsx @@ -0,0 +1,128 @@ +/** + * 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 { t } from '@superset-ui/core'; +import { SelectValue } from 'antd/lib/select'; +import { ControlPanelConfig } from '@superset-ui/chart-controls'; + +/** + * Get the layer configuration object from the control panel. + * + * @param controlPanel The control panel + * @returns The layer configuration object or undefined if not found + */ +export const getLayerConfig = (controlPanel: ControlPanelConfig) => { + let layerConfig: any; + controlPanel.controlPanelSections.forEach(section => { + if (!section) { + return; + } + const { controlSetRows } = section; + controlSetRows.forEach((row: any[]) => { + const configObject = row[0] as any; + if (configObject && configObject.name === 'layer_configs') { + layerConfig = configObject; + } + }); + }); + + return layerConfig; +}; + +/** + * Mutates response of chart request into select options. + * + * If a currently selected value is not included in the response, + * it will be added explicitly, in order to prevent antd from creating + * a non-user-friendly select option. + * + * @param response Response json from resolved http request. + * @param value The currently selected value of the select input. + * @returns The list of options for the select input. + */ +export const selectedChartMutator = ( + response: Record, + value: SelectValue | undefined, +) => { + if (!response?.result) { + if (value && typeof value === 'string') { + return [ + { + label: JSON.parse(value).slice_name, + value, + }, + ]; + } + return []; + } + + const data: Record = []; + if (value && typeof value === 'string') { + const parsedValue = JSON.parse(value); + let itemFound = false; + response.result.forEach((config: any) => { + const configString = JSON.stringify(config); + const sameId = config.id === parsedValue.id; + const isUpdated = configString !== value; + const label = config.slice_name; + + if (sameId) { + itemFound = true; + } + if (!sameId || !isUpdated) { + data.push({ + value: configString, + label, + }); + } else { + data.push({ + value: configString, + label: ( + + ({t('updated')}) + {label} + + ), + }); + data.push({ + value, + label, + }); + } + }); + + if (!itemFound) { + data.push({ + value, + label: parsedValue.slice_name, + }); + } + } else { + response.result.forEach((config: any) => { + const configString = JSON.stringify(config); + const label = config.slice_name; + + data.push({ + value: configString, + label, + }); + }); + } + + return data; +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/geometryUtil.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/geometryUtil.ts new file mode 100644 index 0000000000000..fee05069465a6 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/geometryUtil.ts @@ -0,0 +1,60 @@ +/** + * 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. + */ + +/** + * Util for geometry related operations. + */ + +import GeoJSON from 'ol/format/GeoJSON'; +import Feature from 'ol/Feature'; +import { Point as OlPoint } from 'ol/geom'; +import VectorSource from 'ol/source/Vector'; +import { Point as GeoJsonPoint } from 'geojson'; + +/** + * Extracts the coordinate from a Point GeoJSON in the current map projection. + * + * @param geoJsonPoint The GeoJSON string for the point + * + * @returns The coordinate + */ +export const getProjectedCoordinateFromPointGeoJson = ( + geoJsonPoint: GeoJsonPoint, +) => { + const geom: OlPoint = new GeoJSON().readGeometry(geoJsonPoint, { + // TODO: adapt to map projection + featureProjection: 'EPSG:3857', + }) as OlPoint; + return geom.getCoordinates(); +}; + +/** + * Computes the extent for an array of features. + * + * @param features An Array of OpenLayers features + * @returns The OpenLayers extent or undefined + */ +export const getExtentFromFeatures = (features: Feature[]) => { + if (features.length === 0) { + return undefined; + } + const source = new VectorSource(); + source.addFeatures(features); + return source.getExtent(); +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx new file mode 100644 index 0000000000000..65e608bd757a2 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx @@ -0,0 +1,160 @@ +/** + * 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. + */ + +/** + * Util for layer related operations. + */ + +import OlParser from 'geostyler-openlayers-parser'; +import TileLayer from 'ol/layer/Tile'; +import TileWMS from 'ol/source/TileWMS'; +import { bbox as bboxStrategy } from 'ol/loadingstrategy'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import XyzSource from 'ol/source/XYZ'; +import GeoJSON from 'ol/format/GeoJSON'; +import { WmsLayerConf, WfsLayerConf, LayerConf, XyzLayerConf } from '../types'; +import { isWfsLayerConf, isWmsLayerConf, isXyzLayerConf } from '../typeguards'; +import { isVersionBelow } from './serviceUtil'; + +/** + * Create a WMS layer. + * + * @param wmsLayerConf The layer configuration + * + * @returns The created WMS layer + */ +export const createWmsLayer = (wmsLayerConf: WmsLayerConf) => { + const { url, layersParam, version, attribution } = wmsLayerConf; + return new TileLayer({ + source: new TileWMS({ + url, + params: { + LAYERS: layersParam, + VERSION: version, + }, + attributions: attribution, + }), + }); +}; + +/** + * Create a XYZ layer. + * + * @param xyzLayerConf The layer configuration + * + * @returns The created XYZ layer + */ +export const createXyzLayer = (xyzLayerConf: XyzLayerConf) => { + const { url, attribution } = xyzLayerConf; + return new TileLayer({ + source: new XyzSource({ + url, + attributions: attribution, + }), + }); +}; + +/** + * Create a WFS layer. + * + * @param wfsLayerConf The layer configuration + * + * @returns The created WFS layer + */ +export const createWfsLayer = async (wfsLayerConf: WfsLayerConf) => { + const { + url, + typeName, + maxFeatures, + version = '1.1.0', + style, + attribution, + } = wfsLayerConf; + + const wfsSource = new VectorSource({ + format: new GeoJSON(), + attributions: attribution, + url: extent => { + const requestUrl = new URL(url); + const params = requestUrl.searchParams; + params.append('service', 'wfs'); + params.append('request', 'GetFeature'); + params.append('outputFormat', 'application/json'); + // TODO: make CRS configurable or take it from Ol Map + params.append('srsName', 'EPSG:3857'); + params.append('version', version); + + let typeNameQuery = 'typeNames'; + if (isVersionBelow(version, '2.0.0', 'WFS')) { + typeNameQuery = 'typeName'; + } + params.append(typeNameQuery, typeName); + + params.append('bbox', extent.join(',')); + if (maxFeatures) { + let maxFeaturesQuery = 'count'; + if (isVersionBelow(version, '2.0.0', 'WFS')) { + maxFeaturesQuery = 'maxFeatures'; + } + params.append(maxFeaturesQuery, maxFeatures.toString()); + } + + return requestUrl.toString(); + }, + strategy: bboxStrategy, + }); + + let writeStyleResult; + if (style) { + const olParser = new OlParser(); + writeStyleResult = await olParser.writeStyle(style); + if (writeStyleResult.errors) { + console.warn('Could not create ol-style', writeStyleResult.errors); + return undefined; + } + } + + return new VectorLayer({ + source: wfsSource, + // @ts-ignore + style: writeStyleResult?.output, + }); +}; + +/** + * Create a layer instance with the provided configuration. + * + * @param layerConf The layer configuration + * + * @returns The created layer + */ +export const createLayer = async (layerConf: LayerConf) => { + let layer; + if (isWmsLayerConf(layerConf)) { + layer = createWmsLayer(layerConf); + } else if (isWfsLayerConf(layerConf)) { + layer = await createWfsLayer(layerConf); + } else if (isXyzLayerConf(layerConf)) { + layer = createXyzLayer(layerConf); + } else { + console.warn('Provided layerconfig is not recognized'); + } + return layer; +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/mapUtil.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/mapUtil.tsx new file mode 100644 index 0000000000000..56af025f7f37c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/mapUtil.tsx @@ -0,0 +1,52 @@ +/** + * 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. + */ + +/** + * Util for map related operations. + */ +import { Map } from 'ol'; +import GeoJSON from 'ol/format/GeoJSON'; +import { ChartConfig } from '../types'; +import { getExtentFromFeatures } from './geometryUtil'; + +// default map extent of world if no features are found +// TODO: move to generic config file or plugin configuration +// TODO: adapt to CRS other than Web Mercator +const defaultExtent = [-16000000, -7279000, 20500000, 11000000]; + +/** + * Fits map to the spatial extent of provided charts. + * + * @param olMap The OpenLayers map + * @param chartConfigs The chart configuration + */ +export const fitMapToCharts = (olMap: Map, chartConfigs: ChartConfig) => { + const view = olMap.getView(); + const features = new GeoJSON().readFeatures(chartConfigs, { + // TODO: adapt to map projection + featureProjection: 'EPSG:3857', + }); + + const extent = getExtentFromFeatures(features) || defaultExtent; + + view.fit(extent, { + // tested for a desktop size monitor + size: [250, 250], + }); +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/serviceUtil.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/serviceUtil.ts new file mode 100644 index 0000000000000..b4d79485d1fe0 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/serviceUtil.ts @@ -0,0 +1,59 @@ +/** + * 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. + */ + +/** + * Get the available versions of WFS and WMS. + * + * @returns the versions + */ +export const getServiceVersions = () => ({ + WMS: ['1.3.0', '1.1.1'], + WFS: ['2.0.2', '2.0.0', '1.1.0'], +}); + +/** + * Checks if a given version is below the comparer version. + * + * @param version The version to check. + * @param below The version to compare to. + * @param serviceType The service type. + * @returns True, if the version is below comparer version. False, otherwise. + */ +export const isVersionBelow = ( + version: string, + below: string, + serviceType: 'WFS' | 'WMS', +) => { + const versions = getServiceVersions()[serviceType]; + // versions is ordered from newest to oldest, so we invert the order + // to improve the readability of this function. + versions.reverse(); + const versionIdx = versions.indexOf(version); + if (versionIdx === -1) { + // TODO: consider throwing an error instead + return false; + } + const belowIdx = versions.indexOf(below); + if (belowIdx === -1) { + // TODO: consider throwing an error instead + return false; + } + + return versionIdx < belowIdx; +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/transformPropsUtil.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/transformPropsUtil.ts new file mode 100644 index 0000000000000..1987a75da7130 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/transformPropsUtil.ts @@ -0,0 +1,340 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ChartProps, + convertKeysToCamelCase, + DataRecord, +} from '@superset-ui/core'; +import { isObject } from 'lodash'; +import { + LocationConfigMapping, + SelectedChartConfig, + ChartConfig, + ChartConfigFeature, +} from '../types'; + +const COLUMN_SEPARATOR = ', '; + +/** + * Get the indices of columns where the title is a geojson. + * + * @param columns List of column names. + * @returns List of indices containing geojsonColumns. + */ +export const getGeojsonColumns = (columns: string[]) => + columns.reduce((prev, current, idx) => { + let parsedColName; + try { + parsedColName = JSON.parse(current); + } catch { + parsedColName = undefined; + } + if (!parsedColName || !isObject(parsedColName)) { + return [...prev]; + } + if (!('type' in parsedColName) || !('coordinates' in parsedColName)) { + return [...prev]; + } + return [...prev, idx]; + }, []); + +/** + * Create a column name ignoring provided indices. + * + * @param columns List of column names. + * @param ignoreIdx List of indices to ignore. + * @returns Column name. + */ +export const createColumnName = (columns: string[], ignoreIdx: number[]) => + columns.filter((l, idx) => !ignoreIdx.includes(idx)).join(COLUMN_SEPARATOR); + +/** + * Group data by location for data providing a generic + * x-axis. + * + * @param data The data to group. + * @param params The data params. + * @returns Data grouped by location. + */ +export const groupByLocationGenericX = ( + data: DataRecord[], + params: SelectedChartConfig['params'], + queryData: any, +) => { + const locations: LocationConfigMapping = {}; + if (!data) { + return locations; + } + data.forEach(d => { + Object.keys(d) + .filter(k => k !== params.x_axis) + .forEach(k => { + const labelMap: string[] = queryData.label_map?.[k]; + + if (!labelMap) { + console.log( + 'Cannot extract location from queryData. label_map not defined', + ); + return; + } + + const geojsonCols = getGeojsonColumns(labelMap); + + if (geojsonCols.length > 1) { + // TODO what should we do, if there is more than one geom column? + console.log( + 'More than one geometry column detected. Using first found.', + ); + } + const location = labelMap[geojsonCols[0]]; + const filter = geojsonCols.length ? [geojsonCols[0]] : []; + const leftOverKey = createColumnName(labelMap, filter); + + if (!Object.keys(locations).includes(location)) { + locations[location] = []; + } + + let dataAtX = locations[location].find( + i => i[params.x_axis] === d[params.x_axis], + ); + + if (!dataAtX) { + dataAtX = { + // add the x_axis value explicitly, since we + // filtered it out for the rest of the computation. + [params.x_axis]: d[params.x_axis], + }; + locations[location].push(dataAtX); + } + dataAtX[leftOverKey] = d[k]; + }); + }); + + return locations; +}; + +/** + * Group data by location. + * + * @param data The incoming dataset + * @param geomColumn The name of the geometry column + * @returns The grouped data + */ +export const groupByLocation = (data: DataRecord[], geomColumn: string) => { + const locations: LocationConfigMapping = {}; + + data.forEach(d => { + const loc = d[geomColumn] as string; + if (!loc) { + return; + } + + if (!Object.keys(locations).includes(loc)) { + locations[loc] = []; + } + + const newData = { + ...d, + }; + delete newData[geomColumn]; + + locations[loc].push(newData); + }); + + return locations; +}; + +/** + * Strips the geom from colnames and coltypes. + * + * @param queryData The querydata. + * @param geomColumn Name of the geom column. + * @returns colnames and coltypes without the geom. + */ +export const stripGeomFromColnamesAndTypes = ( + queryData: any, + geomColumn: string, +) => { + const newColnames: string[] = []; + const newColtypes: number[] = []; + queryData.colnames?.forEach((colname: string, idx: number) => { + if (colname === geomColumn) { + return; + } + + const parts = colname.split(COLUMN_SEPARATOR); + const geojsonColumns = getGeojsonColumns(parts); + const filter = geojsonColumns.length ? [geojsonColumns[0]] : []; + + const newColname = createColumnName(parts, filter); + if (newColnames.includes(newColname)) { + return; + } + newColnames.push(newColname); + newColtypes.push(queryData.coltypes[idx]); + }); + + return { + colnames: newColnames, + coltypes: newColtypes, + }; +}; + +/** + * Strips the geom from labelMap. + * + * @param queryData The querydata. + * @param geomColumn Name of the geom column. + * @returns labelMap without the geom column. + */ +export const stripGeomColumnFromLabelMap = ( + labelMap: { [key: string]: string[] }, + geomColumn: string, +) => { + const newLabelMap = {}; + Object.entries(labelMap).forEach(([key, value]) => { + if (key === geomColumn) { + return; + } + const geojsonCols = getGeojsonColumns(value); + const filter = geojsonCols.length ? [geojsonCols[0]] : []; + const columnName = createColumnName(value, filter); + const restItems = value.filter((v, idx) => !geojsonCols.includes(idx)); + newLabelMap[columnName] = restItems; + }); + return newLabelMap; +}; + +/** + * Strip occurrences of the geom column from the query data. + * + * @param queryDataClone The query data + * @param geomColumn The name of the geom column + * @returns query data without geom column. + */ +export const stripGeomColumnFromQueryData = ( + queryData: any, + geomColumn: string, +) => { + const queryDataClone = { + ...structuredClone(queryData), + ...stripGeomFromColnamesAndTypes(queryData, geomColumn), + }; + if (queryDataClone.label_map) { + queryDataClone.label_map = stripGeomColumnFromLabelMap( + queryData.label_map, + geomColumn, + ); + } + return queryDataClone; +}; + +/** + * Create the chart configurations depending on the referenced Superset chart. + * + * @param selectedChart The configuration of the referenced Superset chart + * @param geomColumn The name of the geometry column + * @param chartProps The properties provided within this OL plugin + * @param chartTransformer The transformer function + * @returns The chart configurations + */ +export const getChartConfigs = ( + selectedChart: SelectedChartConfig, + geomColumn: string, + chartProps: ChartProps, + chartTransformer: any, +) => { + const chartFormDataSnake = selectedChart.params; + const chartFormData = convertKeysToCamelCase(chartFormDataSnake); + + const baseConfig = { + ...chartProps, + // We overwrite width and height, which are not needed + // here, but leads to unnecessary updating of the UI. + width: null, + height: null, + formData: chartFormData, + rawFormData: chartFormDataSnake, + datasource: {}, + }; + + const { queriesData } = chartProps; + const [queryData] = queriesData; + + const data = queryData.data as DataRecord[]; + let dataByLocation: LocationConfigMapping; + + const chartConfigs: ChartConfig = { + type: 'FeatureCollection', + features: [], + }; + + if (!data) { + return chartConfigs; + } + + if ('x_axis' in selectedChart.params) { + dataByLocation = groupByLocationGenericX( + data, + selectedChart.params, + queryData, + ); + } else { + dataByLocation = groupByLocation(data, geomColumn); + } + + const strippedQueryData = stripGeomColumnFromQueryData(queryData, geomColumn); + + Object.keys(dataByLocation).forEach(location => { + const config = { + ...baseConfig, + queriesData: [ + { + ...strippedQueryData, + data: dataByLocation[location], + }, + ], + }; + const transformedProps = chartTransformer(config); + + const feature: ChartConfigFeature = { + type: 'Feature', + geometry: JSON.parse(location), + properties: { + ...transformedProps, + }, + }; + + chartConfigs.features.push(feature); + }); + return chartConfigs; +}; + +/** + * Return the same chart configuration with parsed values for of the stringified "params" object. + * + * @param selectedChart Incoming chart configuration + * @returns Chart configuration with parsed values for "params" + */ +export const parseSelectedChart = (selectedChart: string) => { + const selectedChartParsed = JSON.parse(selectedChart); + selectedChartParsed.params = JSON.parse(selectedChartParsed.params); + return selectedChartParsed; +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/zoomUtil.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/zoomUtil.ts new file mode 100644 index 0000000000000..c21fda764e55a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/zoomUtil.ts @@ -0,0 +1,21 @@ +/** + * 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. + */ + +export const MAX_ZOOM_LEVEL = 28; +export const MIN_ZOOM_LEVEL = 0; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/components/chartLayer.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/components/chartLayer.test.ts new file mode 100644 index 0000000000000..050af2803eded --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/components/chartLayer.test.ts @@ -0,0 +1,48 @@ +/** + * 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 { ChartLayer } from '../../src/components/ChartLayer'; +import { ChartLayerOptions } from '../../src/types'; + +describe('ChartLayer', () => { + it('creates div and loading mask', () => { + const options: ChartLayerOptions = { + chartVizType: 'pie', + }; + const chartLayer = new ChartLayer(options); + + expect(chartLayer.loadingMask).toBeDefined(); + expect(chartLayer.div).toBeDefined(); + }); + + it('can remove chart elements', () => { + const options: ChartLayerOptions = { + chartVizType: 'pie', + }; + const chartLayer = new ChartLayer(options); + chartLayer.charts = [ + { + htmlElement: document.createElement('div'), + }, + ]; + + chartLayer.removeAllChartElements(); + expect(chartLayer.charts).toEqual([]); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/index.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/index.test.ts new file mode 100644 index 0000000000000..a43ce12cade75 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/index.test.ts @@ -0,0 +1,33 @@ +/** + * 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 { CartodiagramPlugin } from '../src'; + +/** + * The example tests in this file act as a starting point, and + * we encourage you to build more. These tests check that the + * plugin loads properly, and focus on `transformProps` + * to ake sure that data, controls, and props are all + * treated correctly (e.g. formData from plugin controls + * properly transform the data and/or any resulting props). + */ +describe('CartodiagramPlugin', () => { + it('exists', () => { + expect(CartodiagramPlugin).toBeDefined(); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts new file mode 100644 index 0000000000000..7b0da000e58fb --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts @@ -0,0 +1,67 @@ +/** + * 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 { getChartBuildQueryRegistry } from '@superset-ui/core'; +import buildQuery from '../../src/plugin/buildQuery'; + +describe('CartodiagramPlugin buildQuery', () => { + const selectedChartParams = { + extra_form_data: {}, + groupby: [], + }; + + const selectedChart = { + viz_type: 'pie', + params: JSON.stringify(selectedChartParams), + }; + + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + series: 'foo', + viz_type: 'my_chart', + selected_chart: JSON.stringify(selectedChart), + geom_column: 'geom', + }; + + let chartQueryBuilderMock: jest.MockedFunction; + beforeEach(() => { + chartQueryBuilderMock = jest.fn(); + + const registry = getChartBuildQueryRegistry(); + registry.registerValue('pie', chartQueryBuilderMock); + }); + + afterEach(() => { + // remove registered buildQuery + const registry = getChartBuildQueryRegistry(); + registry.clear(); + }); + + it('should call the buildQuery function of the referenced chart', () => { + buildQuery(formData); + expect(chartQueryBuilderMock.mock.calls).toHaveLength(1); + }); + + it('should build groupby with geom in form data', () => { + const expectedParams = { ...selectedChartParams, groupby: ['geom'] }; + + buildQuery(formData); + expect(chartQueryBuilderMock.mock.calls[0][0]).toEqual(expectedParams); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts new file mode 100644 index 0000000000000..04563b081b8fa --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts @@ -0,0 +1,26 @@ +/** + * 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 CartodiagramPlugin from '../../src/CartodiagramPlugin'; + +describe('CartodiagramPlugin', () => { + it('exists', () => { + expect(CartodiagramPlugin).toBeDefined(); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts new file mode 100644 index 0000000000000..98cfc4977efda --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts @@ -0,0 +1,150 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + ChartProps, + getChartTransformPropsRegistry, + supersetTheme, +} from '@superset-ui/core'; +import { LayerConf, MapViewConfigs, ZoomConfigs } from '../../src/types'; +import transformProps from '../../src/plugin/transformProps'; +import { + groupedTimeseriesChartData, + groupedTimeseriesLabelMap, +} from '../testData'; + +describe('CartodiagramPlugin transformProps', () => { + const chartSize: ZoomConfigs = { + type: 'FIXED', + configs: { + height: 10, + width: 10, + zoom: 1, + }, + values: { + 1: { + height: 10, + width: 10, + }, + }, + }; + const layerConfigs: LayerConf[] = [ + { + type: 'XYZ', + title: 'foo', + url: 'example.com', + }, + ]; + const mapView: MapViewConfigs = { + mode: 'FIT_DATA', + zoom: 1, + latitude: 0, + longitude: 0, + fixedZoom: 1, + fixedLatitude: 0, + fixedLongitude: 0, + }; + + // only minimal subset of actual params + const selectedChartParams = { + groupby: ['bar'], + x_axis: 'mydate', + }; + + const selectedChart = { + id: 1, + viz_type: 'pie', + slice_name: 'foo', + params: JSON.stringify(selectedChartParams), + }; + + const formData = { + viz_type: 'cartodiagram', + geomColumn: 'geom', + selectedChart: JSON.stringify(selectedChart), + chartSize, + layerConfigs, + mapView, + chartBackgroundColor: '#000000', + chartBackgroundBorderRadius: 5, + }; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData: [ + { + data: groupedTimeseriesChartData, + label_map: groupedTimeseriesLabelMap, + }, + ], + theme: supersetTheme, + }); + + let chartTransformPropsPieMock: jest.MockedFunction; + let chartTransformPropsTimeseriesMock: jest.MockedFunction; + beforeEach(() => { + chartTransformPropsPieMock = jest.fn(); + chartTransformPropsTimeseriesMock = jest.fn(); + const registry = getChartTransformPropsRegistry(); + registry.registerValue('pie', chartTransformPropsPieMock); + registry.registerValue( + 'echarts_timeseries', + chartTransformPropsTimeseriesMock, + ); + }); + + afterEach(() => { + // remove registered transformProps + const registry = getChartTransformPropsRegistry(); + registry.clear(); + }); + + it('should call the transform props function of the referenced chart', () => { + transformProps(chartProps); + expect(chartTransformPropsPieMock).toHaveBeenCalled(); + expect(chartTransformPropsTimeseriesMock).not.toHaveBeenCalled(); + }); + + it('should transform chart props for viz', () => { + const transformedProps = transformProps(chartProps); + expect(transformedProps).toEqual( + expect.objectContaining({ + width: chartProps.width, + height: chartProps.height, + geomColumn: formData.geomColumn, + selectedChart: expect.objectContaining({ + viz_type: selectedChart.viz_type, + params: selectedChartParams, + }), + // The actual test for the created chartConfigs + // will be done in transformPropsUtil.test.ts + chartConfigs: expect.objectContaining({ + type: 'FeatureCollection', + }), + chartVizType: selectedChart.viz_type, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor: formData.chartBackgroundColor, + chartBackgroundBorderRadius: formData.chartBackgroundBorderRadius, + }), + ); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/testData.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/testData.ts new file mode 100644 index 0000000000000..5b5a3a0e7d2c0 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/testData.ts @@ -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. + */ + +const coord1 = '[1,2]'; +const coord2 = '[3,4]'; +export const geom1 = `{"type":"Point","coordinates":${coord1}}`; +export const geom2 = `{"type":"Point","coordinates":${coord2}}`; + +export const nonTimeSeriesChartData: any = [ + { + geom: geom1, + my_value: 'apple', + my_count: 347, + }, + { + geom: geom1, + my_value: 'apple', + my_count: 360, + }, + { + geom: geom1, + my_value: 'lemon', + my_count: 335, + }, + { + geom: geom1, + my_value: 'lemon', + my_count: 333, + }, + { + geom: geom1, + my_value: 'lemon', + my_count: 353, + }, + { + geom: geom1, + my_value: 'lemon', + my_count: 359, + }, + { + geom: geom2, + my_value: 'lemon', + my_count: 347, + }, + { + geom: geom2, + my_value: 'apple', + my_count: 335, + }, + { + geom: geom2, + my_value: 'apple', + my_count: 356, + }, + { + geom: geom2, + my_value: 'banana', + my_count: 218, + }, +]; + +export const timeseriesChartData = [ + { + [geom1]: 347, + [geom2]: 360, + mydate: 1564275000000, + }, + { + [geom1]: 353, + [geom2]: 328, + mydate: 1564272000000, + }, +]; + +export const groupedTimeseriesChartData = [ + { + [`${geom1}, apple`]: 347, + [`${geom2}, apple`]: 360, + [`${geom1}, lemon`]: 352, + [`${geom2}, lemon`]: 364, + mydate: 1564275000000, + }, + { + [`${geom1}, apple`]: 353, + [`${geom2}, apple`]: 328, + [`${geom1}, lemon`]: 346, + [`${geom2}, lemon`]: 333, + mydate: 1564272000000, + }, +]; + +export const groupedTimeseriesLabelMap = { + [`${geom1}, apple`]: [geom1, 'apple'], + [`${geom2}, apple`]: [geom2, 'apple'], + [`${geom1}, lemon`]: [geom1, 'lemon'], + [`${geom2}, lemon`]: [geom2, 'lemon'], +}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-cartodiagram/test/tsconfig.json new file mode 100644 index 0000000000000..07351e5038c30 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": false, + "noEmit": true, + "rootDir": "." + }, + "extends": "../../../tsconfig.json", + "include": [ + "**/*", + "../types/**/*", + "../../../types/**/*" + ], + "references": [ + { + "path": "../../../packages/superset-ui-chart-controls" + }, + { + "path": "../../../packages/superset-ui-core" + }, + ] +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/chartUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/chartUtil.test.ts new file mode 100644 index 0000000000000..891ac9541bf3d --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/chartUtil.test.ts @@ -0,0 +1,77 @@ +/** + * 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 { ChartConfig } from '../../src/types'; +import { isChartConfigEqual, simplifyConfig } from '../../src/util/chartUtil'; + +describe('chartUtil', () => { + const configA: ChartConfig = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [], + }, + properties: { + refs: 'foo', + }, + }, + ], + }; + + const configB: ChartConfig = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [], + }, + properties: { + refs: 'foo', + foo: 'bar', + }, + }, + ], + }; + + describe('simplifyConfig', () => { + it('removes the refs property from a feature', () => { + const simplifiedConfig = simplifyConfig(configA); + const propKeys = Object.keys(simplifiedConfig.features[0].properties); + + expect(propKeys).toHaveLength(0); + }); + }); + + describe('isChartConfigEqual', () => { + it('returns true, if configurations are equal', () => { + const isEqual = isChartConfigEqual(configA, structuredClone(configA)); + expect(isEqual).toBe(true); + }); + + it('returns false if configurations are not equal', () => { + const isEqual = isChartConfigEqual(configA, configB); + expect(isEqual).toBe(false); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/controlPanelUtil.test.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/controlPanelUtil.test.tsx new file mode 100644 index 0000000000000..88e1e3b9e8323 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/controlPanelUtil.test.tsx @@ -0,0 +1,212 @@ +/** + * 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 { + ControlPanelConfig, + CustomControlItem, +} from '@superset-ui/chart-controls'; +import { + getLayerConfig, + selectedChartMutator, +} from '../../src/util/controlPanelUtil'; + +describe('controlPanelUtil', () => { + describe('getLayerConfig', () => { + it('returns the correct layer config', () => { + const layerConfigs: CustomControlItem = { + name: 'layer_configs', + config: { + type: 'dummy', + renderTrigger: true, + label: 'Layers', + default: [ + { + type: 'XYZ', + url: 'http://example.com/', + title: 'dummy title', + attribution: 'dummy attribution', + }, + ], + description: 'The configuration for the map layers', + }, + }; + const controlPanel: ControlPanelConfig = { + controlPanelSections: [ + { + label: 'Configuration', + expanded: true, + controlSetRows: [], + }, + { + label: 'Map Options', + expanded: true, + controlSetRows: [ + [ + { + name: 'map_view', + config: { + type: 'dummy', + }, + }, + ], + [layerConfigs], + ], + }, + { + label: 'Chart Options', + expanded: true, + controlSetRows: [], + }, + ], + }; + const extractedLayerConfigs = getLayerConfig(controlPanel); + + expect(extractedLayerConfigs).toEqual(layerConfigs); + }); + }); + + describe('selectedChartMutator', () => { + it('returns empty array for empty inputs', () => { + const response = {}; + const value = undefined; + const result = selectedChartMutator(response, value); + expect(result).toEqual([]); + }); + + it('returns parsed value if response is empty', () => { + const response = {}; + + const sliceName = 'foobar'; + const value = JSON.stringify({ + id: 278, + params: '', + slice_name: sliceName, + viz_type: 'pie', + }); + + const result = selectedChartMutator(response, value); + + expect(result[0].label).toEqual(sliceName); + }); + + it('returns response options if no value is chosen', () => { + const sliceName1 = 'foo'; + const sliceName2 = 'bar'; + const response = { + result: [ + { + id: 1, + params: '{}', + slice_name: sliceName1, + viz_type: 'viz1', + }, + { + id: 2, + params: '{}', + slice_name: sliceName2, + viz_type: 'viz2', + }, + ], + }; + const value = undefined; + + const result = selectedChartMutator(response, value); + expect(result[0].label).toEqual(sliceName1); + expect(result[1].label).toEqual(sliceName2); + }); + + it('returns correct result if id of chosen config does not exist in response', () => { + const response = { + result: [ + { + id: 1, + params: '{}', + slice_name: 'foo', + viz_type: 'viz1', + }, + { + id: 2, + params: '{}', + slice_name: 'bar', + viz_type: 'viz2', + }, + ], + }; + + const value = JSON.stringify({ + id: 3, + params: '{}', + slice_name: 'my-slice', + viz_type: 'pie', + }); + + const result = selectedChartMutator(response, value); + + // collect all ids in a set to prevent double entries + const ids = new Set(); + result.forEach((item: any) => { + const config = JSON.parse(item.value); + const { id } = config; + ids.add(id); + }); + + const threeDifferentIds = ids.size === 3; + + expect(threeDifferentIds).toEqual(true); + }); + + it('returns correct result if id of chosen config already exists', () => { + const response = { + result: [ + { + id: 1, + params: '{}', + slice_name: 'foo', + viz_type: 'viz1', + }, + { + id: 2, + params: '{}', + slice_name: 'bar', + viz_type: 'viz2', + }, + ], + }; + + const value = JSON.stringify({ + id: 1, + params: '{}', + slice_name: 'my-slice', + viz_type: 'pie', + }); + + const result = selectedChartMutator(response, value); + + const itemsIdWithId1 = result.filter((item: any) => { + const config = JSON.parse(item.value); + const { id } = config; + return id === 1; + }); + expect(itemsIdWithId1.length).toEqual(2); + + const labelsEqual = itemsIdWithId1[0].label === itemsIdWithId1[1].label; + expect(labelsEqual).toEqual(false); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/geometryUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/geometryUtil.test.ts new file mode 100644 index 0000000000000..4f070f4b78350 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/geometryUtil.test.ts @@ -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. + */ +import GeoJSON from 'ol/format/GeoJSON'; +import { Point } from 'geojson'; +import { + getExtentFromFeatures, + getProjectedCoordinateFromPointGeoJson, +} from '../../src/util/geometryUtil'; +import { ChartConfig } from '../../src/types'; + +describe('geometryUtil', () => { + describe('getProjectedCoordinateFromPointGeoJson', () => { + it('returns a plausible result', () => { + const pointGeoJson: Point = { + type: 'Point', + coordinates: [6.6555, 49.74283], + }; + const result = getProjectedCoordinateFromPointGeoJson(pointGeoJson); + + expect(result.length).toEqual(2); + + const valuesAreNumbers = + !Number.isNaN(result[0]) && !Number.isNaN(result[1]); + expect(valuesAreNumbers).toEqual(true); + }); + }); + + describe('getExtentFromFeatures', () => { + it('computes correct extent with valid input', () => { + const expectedExtent = [1, 2, 3, 4]; + + const chartConfig: ChartConfig = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [expectedExtent[0], expectedExtent[1]], + }, + properties: { + setDataMask: '', + labelMap: '', + labelMapB: '', + groupby: '', + selectedValues: '', + formData: '', + groupbyB: '', + seriesBreakdown: '', + legendData: '', + echartOptions: '', + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [expectedExtent[2], expectedExtent[3]], + }, + properties: { + setDataMask: '', + labelMap: '', + labelMapB: '', + groupby: '', + selectedValues: '', + formData: '', + groupbyB: '', + seriesBreakdown: '', + legendData: '', + echartOptions: '', + }, + }, + ], + }; + + const features = new GeoJSON().readFeatures(chartConfig); + const extent = getExtentFromFeatures(features); + expect(extent).toEqual(expectedExtent); + }); + + it('returns undefined on invalid input', () => { + const emptyExtent = getExtentFromFeatures([]); + expect(emptyExtent).toBeUndefined(); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts new file mode 100644 index 0000000000000..f5b55bdf77911 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts @@ -0,0 +1,95 @@ +/** + * 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 { WfsLayerConf } from '../../src/types'; +import { + createLayer, + createWfsLayer, + createWmsLayer, + createXyzLayer, +} from '../../src/util/layerUtil'; + +describe('layerUtil', () => { + describe('createWmsLayer', () => { + it('exists', () => { + // function is trivial + expect(createWmsLayer).toBeDefined(); + }); + }); + + describe('createWfsLayer', () => { + it('properly applies style', async () => { + const colorToExpect = '#123456'; + + const wfsLayerConf: WfsLayerConf = { + title: 'osm:osm-fuel', + url: 'https://ows-demo.terrestris.de/geoserver/osm/wfs', + type: 'WFS', + version: '2.0.2', + typeName: 'osm:osm-fuel', + style: { + name: 'Default Style', + rules: [ + { + name: 'Default Rule', + symbolizers: [ + { + kind: 'Line', + color: '#000000', + width: 2, + }, + { + kind: 'Mark', + wellKnownName: 'circle', + color: colorToExpect, + }, + { + kind: 'Fill', + color: '#000000', + }, + ], + }, + ], + }, + }; + + const wfsLayer = await createWfsLayer(wfsLayerConf); + + const style = wfsLayer!.getStyle(); + // @ts-ignore + expect(style!.length).toEqual(3); + + const colorAtLayer = style![1].getImage().getFill().getColor(); + expect(colorToExpect).toEqual(colorAtLayer); + }); + }); + + describe('createXyzLayer', () => { + it('exists', () => { + // function is trivial + expect(createXyzLayer).toBeDefined(); + }); + }); + + describe('createLayer', () => { + it('exists', () => { + expect(createLayer).toBeDefined(); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/mapUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/mapUtil.test.ts new file mode 100644 index 0000000000000..0447b03afb861 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/mapUtil.test.ts @@ -0,0 +1,116 @@ +/** + * 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 Map from 'ol/Map.js'; +import OSM from 'ol/source/OSM.js'; +import TileLayer from 'ol/layer/Tile.js'; +import View from 'ol/View.js'; +import { ChartConfig } from '../../src/types'; +import { fitMapToCharts } from '../../src/util/mapUtil'; + +describe('mapUtil', () => { + describe('fitMapToCharts', () => { + it('changes the center of the map', () => { + const chartConfig: ChartConfig = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [8.793, 53.04117], + }, + properties: { + setDataMask: '', + labelMap: '', + labelMapB: '', + groupby: '', + selectedValues: '', + formData: '', + groupbyB: '', + seriesBreakdown: '', + legendData: '', + echartOptions: '', + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [10.61833, 51.8], + }, + properties: { + setDataMask: '', + labelMap: '', + labelMapB: '', + groupby: '', + selectedValues: '', + formData: '', + groupbyB: '', + seriesBreakdown: '', + legendData: '', + echartOptions: '', + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [6.86883, 50.35667], + }, + properties: { + setDataMask: '', + labelMap: '', + labelMapB: '', + groupby: '', + selectedValues: '', + formData: '', + groupbyB: '', + seriesBreakdown: '', + legendData: '', + echartOptions: '', + }, + }, + ], + }; + + const initialCenter = [0, 0]; + + const olMap = new Map({ + layers: [ + new TileLayer({ + source: new OSM(), + }), + ], + target: 'map', + view: new View({ + center: initialCenter, + zoom: 2, + }), + }); + + // should set center + fitMapToCharts(olMap, chartConfig); + + const updatedCenter = olMap.getView().getCenter(); + + expect(initialCenter).not.toEqual(updatedCenter); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/serviceUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/serviceUtil.test.ts new file mode 100644 index 0000000000000..0728839b92b50 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/serviceUtil.test.ts @@ -0,0 +1,46 @@ +/** + * 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 { isVersionBelow } from '../../src/util/serviceUtil'; + +describe('serviceUtil', () => { + describe('isVersionBelow', () => { + describe('WMS', () => { + it('recognizes the higher version', () => { + const result = isVersionBelow('1.3.0', '1.1.0', 'WMS'); + expect(result).toEqual(false); + }); + it('recognizes the lower version', () => { + const result = isVersionBelow('1.1.1', '1.3.0', 'WMS'); + expect(result).toEqual(true); + }); + }); + + describe('WFS', () => { + it('recognizes the higher version', () => { + const result = isVersionBelow('2.0.2', '1.1.0', 'WFS'); + expect(result).toEqual(false); + }); + it('recognizes the lower version', () => { + const result = isVersionBelow('1.1.0', '2.0.2', 'WFS'); + expect(result).toEqual(true); + }); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/transformPropsUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/transformPropsUtil.test.ts new file mode 100644 index 0000000000000..6190990e0d7e3 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/transformPropsUtil.test.ts @@ -0,0 +1,249 @@ +/** + * 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 { + groupByLocation, + getChartConfigs, + parseSelectedChart, + getGeojsonColumns, + createColumnName, + groupByLocationGenericX, + stripGeomFromColnamesAndTypes, + stripGeomColumnFromLabelMap, +} from '../../src/util/transformPropsUtil'; +import { + nonTimeSeriesChartData, + groupedTimeseriesChartData, + geom1, + geom2, + groupedTimeseriesLabelMap, +} from '../testData'; + +describe('transformPropsUtil', () => { + const groupedTimeseriesParams = { + x_axis: 'mydate', + }; + + const groupedTimeseriesQueryData = { + label_map: groupedTimeseriesLabelMap, + }; + + describe('getGeojsonColumns', () => { + it('gets the GeoJSON columns', () => { + const columns = ['foo', 'bar', geom1]; + const result = getGeojsonColumns(columns); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(2); + }); + + it('gets multiple GeoJSON columns', () => { + const columns = ['foo', geom2, 'bar', geom1]; + const result = getGeojsonColumns(columns); + expect(result).toHaveLength(2); + expect(result[0]).toEqual(1); + expect(result[1]).toEqual(3); + }); + + it('returns empty array when no GeoJSON is included', () => { + const columns = ['foo', 'bar']; + const result = getGeojsonColumns(columns); + expect(result).toHaveLength(0); + }); + }); + + describe('createColumnName', () => { + it('creates a columns name', () => { + const columns = ['foo', 'bar']; + const result = createColumnName(columns, []); + expect(result).toEqual('foo, bar'); + }); + + it('ignores items provided by ignoreIdx', () => { + const columns = ['foo', 'bar', 'baz']; + const ignoreIdx = [1]; + const result = createColumnName(columns, ignoreIdx); + expect(result).toEqual('foo, baz'); + }); + }); + + describe('groupByLocationGenericX', () => { + it('groups in the correct count of geometries', () => { + const result = groupByLocationGenericX( + groupedTimeseriesChartData, + groupedTimeseriesParams, + groupedTimeseriesQueryData, + ); + const countOfGeometries = Object.keys(result).length; + expect(countOfGeometries).toEqual(2); + }); + + it('groups items by same geometry', () => { + const result = groupByLocationGenericX( + groupedTimeseriesChartData, + groupedTimeseriesParams, + groupedTimeseriesQueryData, + ); + const allGeom1 = result[geom1].length === 2; + const allGeom2 = result[geom2].length === 2; + expect(allGeom1 && allGeom2).toBe(true); + }); + }); + + describe('groupByLocation', () => { + it('groups in the correct count of geometries', () => { + const geometryColumn = 'geom'; + const result = groupByLocation(nonTimeSeriesChartData, geometryColumn); + const countOfGeometries = Object.keys(result).length; + expect(countOfGeometries).toEqual(2); + }); + + it('groups items by same geometry', () => { + const geometryColumn = 'geom'; + const result = groupByLocation(nonTimeSeriesChartData, geometryColumn); + const allGeom1 = result[geom1].length === 6; + const allGeom2 = result[geom2].length === 4; + expect(allGeom1 && allGeom2).toBe(true); + }); + }); + + describe('stripGeomFromColnamesAndTypes', () => { + it('strips the geom from colnames with geom column', () => { + const queryData = { + colnames: ['foo', 'geom'], + coltypes: [0, 0], + }; + const result = stripGeomFromColnamesAndTypes(queryData, 'geom'); + expect(result).toEqual({ + colnames: ['foo'], + coltypes: [0], + }); + }); + + it('strips the geom from colnames with grouped columns', () => { + const queryData = { + colnames: ['foo', `bar, ${geom1}`], + coltypes: [0, 0], + }; + const result = stripGeomFromColnamesAndTypes(queryData, 'geom'); + expect(result).toEqual({ + colnames: ['foo', 'bar'], + coltypes: [0, 0], + }); + }); + + it('strips the geom from colnames with grouped columns without geom', () => { + const queryData = { + colnames: ['foo', `bar, baz`], + coltypes: [0, 0], + }; + const result = stripGeomFromColnamesAndTypes(queryData, 'geom'); + expect(result).toEqual({ + colnames: ['foo', 'bar, baz'], + coltypes: [0, 0], + }); + }); + }); + + describe('stripGeomColumnFromLabelMap', () => { + it('strips the geom column from label_map', () => { + const labelMap = { + [`apple, ${geom1}`]: ['apple', geom1], + [`${geom2}, lemon`]: [geom2, 'lemon'], + geom: ['geom'], + }; + const result = stripGeomColumnFromLabelMap(labelMap, 'geom'); + expect(result).toEqual({ + apple: ['apple'], + lemon: ['lemon'], + }); + }); + }); + + describe('getChartConfigs', () => { + let chartTransformer: jest.MockedFunction; + const geomColumn = 'geom'; + const pieChartConfig = { + params: {}, + viz_type: 'pie', + }; + const chartProps: any = { + queriesData: [ + { + data: nonTimeSeriesChartData, + }, + ], + }; + beforeEach(() => { + chartTransformer = jest.fn(); + }); + + it('calls the transformProps function for every location', () => { + getChartConfigs(pieChartConfig, geomColumn, chartProps, chartTransformer); + + expect(chartTransformer).toHaveBeenCalledTimes(2); + }); + it('returns a geojson', () => { + const result = getChartConfigs( + pieChartConfig, + geomColumn, + chartProps, + chartTransformer, + ); + + expect(result).toEqual( + expect.objectContaining({ + type: 'FeatureCollection', + features: expect.arrayContaining([ + expect.objectContaining({ + type: 'Feature', + }), + ]), + }), + ); + }); + it('returns a feature for each location', () => { + const result = getChartConfigs( + pieChartConfig, + geomColumn, + chartProps, + chartTransformer, + ); + expect(result.features).toHaveLength(2); + expect(result.features[0].geometry).toEqual(JSON.parse(geom1)); + expect(result.features[1].geometry).toEqual(JSON.parse(geom2)); + }); + }); + + describe('parseSelectedChart', () => { + it('parses the inline stringified JSON', () => { + const selectedChartObject = { + id: 278, + params: + '{"adhoc_filters":[],"applied_time_extras":{},"datasource":"24__table","viz_type":"pie","time_range":"No filter","groupby":["nuclide"],"metric":{"expressionType":"SIMPLE","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"nuclide","description":null,"expression":null,"filterable":true,"groupby":true,"id":772,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"TEXT","type_generic":1,"verbose_name":null,"warning_markdown":null},"aggregate":"COUNT","sqlExpression":null,"isNew":false,"datasourceWarning":false,"hasCustomLabel":false,"label":"COUNT(nuclide)","optionName":"metric_k6d9mt9zujc_7v9szd1i0pl"},"dashboards":[]}', + slice_name: 'pie', + viz_type: 'pie', + }; + + const selectedChartString = JSON.stringify(selectedChartObject); + const result = parseSelectedChart(selectedChartString); + const expectedParams = JSON.parse(selectedChartObject.params); + + expect(result.params).toEqual(expectedParams); + }); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json b/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json new file mode 100644 index 0000000000000..019b367ef9e3c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declarationDir": "lib", + "outDir": "lib", + "rootDir": "src" + }, + "exclude": [ + "lib", + "test" + ], + "extends": "../../tsconfig.json", + "include": [ + "src/**/*", + "types/**/*", + "../../types/**/*" + ], + "references": [ + { + "path": "../../packages/superset-ui-chart-controls" + }, + { + "path": "../../packages/superset-ui-core" + }, + ] +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/types/external.d.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/types/external.d.ts new file mode 100644 index 0000000000000..4bb0ad129964f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/types/external.d.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +declare module '*.png' { + const value: any; + export default value; +} + +declare module '*.gif' { + const value: any; + export default value; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json index 6c0e5fa63f781..39da8723cd788 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/package.json +++ b/superset-frontend/plugins/plugin-chart-echarts/package.json @@ -25,13 +25,13 @@ ], "dependencies": { "d3-array": "^1.2.0", - "echarts": "^5.4.1", "lodash": "^4.17.21", "moment": "^2.30.1" }, "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "echarts": "*", "memoize-one": "*", "react": "^16.13.1" }, diff --git a/superset-frontend/plugins/plugin-chart-table/src/utils/extent.ts b/superset-frontend/plugins/plugin-chart-table/src/utils/extent.ts index 4b326800d2fca..5721a2a5057c1 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/utils/extent.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/utils/extent.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { isNil } from 'lodash'; export default function extent( diff --git a/superset-frontend/src/explore/components/controls/LayerConfigsControl/FlatLayerTree.tsx b/superset-frontend/src/explore/components/controls/LayerConfigsControl/FlatLayerTree.tsx new file mode 100644 index 0000000000000..56de1914d482a --- /dev/null +++ b/superset-frontend/src/explore/components/controls/LayerConfigsControl/FlatLayerTree.tsx @@ -0,0 +1,146 @@ +/** + * 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 { PlusOutlined } from '@ant-design/icons'; +import { css, styled, t } from '@superset-ui/core'; +import { Button, Tree } from 'antd'; +import { TreeProps } from 'antd/lib/tree'; +import { forwardRef } from 'react'; +import { FlatLayerDataNode, FlatLayerTreeProps, LayerConf } from './types'; +import { handleDrop } from './dragDropUtil'; +import LayerTreeItem from './LayerTreeItem'; + +export const StyledLayerTreeItem = styled(LayerTreeItem)` + ${({ theme }) => css` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + padding: unset; + + border: none; + border-radius: ${theme.borderRadius}px; + background-color: ${theme.colors.grayscale.light3}; + font-size: ${theme.typography.sizes.s}px; + font-weight: ${theme.typography.weights.normal}; + + &:hover { + background-color: ${theme.colors.grayscale.light3}; + } + + & .layer-tree-item-close { + border-right: solid; + border-right-width: 1px; + border-right-color: ${theme.colors.grayscale.light2}; + } + + & .layer-tree-item-edit { + border-left: solid; + border-left-width: 1px; + border-left-color: ${theme.colors.grayscale.light2}; + } + + & .layer-tree-item-title { + flex: 1; + padding-left: 4px; + } + + & .layer-tree-item-type { + padding-left: 4px; + font-size: ${theme.typography.sizes.xs}px; + font-family: ${theme.typography.families.monospace}; + } + + & > button { + border: none; + background-color: unset; + color: ${theme.colors.grayscale.light1}; + } + + & > button:hover { + background-color: unset; + color: ${theme.colors.grayscale.light1}; + } + `} +`; + +// forwardRef is needed here in order for emotion and antd tree to work properly +export const FlatLayerTree = forwardRef( + ( + { + layerConfigs, + onAddLayer = () => {}, + onRemoveLayer = () => {}, + onEditLayer = () => {}, + onMoveLayer = () => {}, + draggable, + className, + }, + ref, + ) => { + const layerConfigsToTreeData = ( + configs: LayerConf[], + ): FlatLayerDataNode[] => + configs.map((config, idx) => ({ + layerConf: config, + key: idx, + title: ( + onEditLayer(config, idx)} + onRemoveClick={() => onRemoveLayer(idx)} + /> + ), + selectable: false, + isLeaf: true, + checkable: false, + })); + + const treeDataToLayerConfigs = ( + treeData: FlatLayerDataNode[], + ): LayerConf[] => treeData.map(data => data.layerConf); + + const treeData = layerConfigsToTreeData(layerConfigs); + + const onDrop: TreeProps['onDrop'] = info => { + const data = handleDrop(info, treeData); + const movedLayerConfigs = treeDataToLayerConfigs(data); + onMoveLayer(movedLayerConfigs); + }; + + const addLayerLabel = t('Click to add new layer'); + + return ( +
+ + +
+ ); + }, +); + +export default FlatLayerTree; diff --git a/superset-frontend/src/explore/components/controls/LayerConfigsControl/GeoStylerWrapper.tsx b/superset-frontend/src/explore/components/controls/LayerConfigsControl/GeoStylerWrapper.tsx new file mode 100644 index 0000000000000..b9e3db4d00f93 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/LayerConfigsControl/GeoStylerWrapper.tsx @@ -0,0 +1,38 @@ +/** + * 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. + */ + +/** + * This component is needed to be able to style GeoStyler + * via emotion. Emotion can only be used on a component that + * accepts a className property. + */ +import CardStyle from 'geostyler/dist/Component/CardStyle/CardStyle'; +import { FC } from 'react'; +import { GeoStylerWrapperProps } from './types'; + +export const GeoStylerWrapper: FC = ({ + className, + ...passThroughProps +}) => ( +
+ +
+); + +export default GeoStylerWrapper; diff --git a/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerConfigsControl.tsx b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerConfigsControl.tsx new file mode 100644 index 0000000000000..89fa40d3c9091 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerConfigsControl.tsx @@ -0,0 +1,193 @@ +/** + * 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 { ControlHeader } from '@superset-ui/chart-controls'; +import { css, styled, t } from '@superset-ui/core'; +import { Popover } from 'antd'; +import { FC, useState } from 'react'; +import { EditItem, LayerConf, LayerConfigsControlProps } from './types'; +import LayerConfigsPopoverContent from './LayerConfigsPopoverContent'; +import FlatLayerTree from './FlatLayerTree'; + +export const StyledFlatLayerTree = styled(FlatLayerTree)` + ${({ theme }) => css` + display: flex; + flex-direction: column; + + border: solid; + border-width: 1px; + border-radius: ${theme.borderRadius}px; + border-color: ${theme.colors.grayscale.light2}; + + & .add-layer-btn { + display: flex; + align-items: center; + + margin: 4px; + + color: ${theme.colors.grayscale.light1}; + font-size: ${theme.typography.sizes.s}px; + font-weight: ${theme.typography.weights.normal}; + + &:hover { + background-color: ${theme.colors.grayscale.light4}; + border-color: ${theme.colors.grayscale.light2}; + } + } + + & .ant-tree .ant-tree-treenode { + display: block; + } + + & .ant-tree-list-holder-inner { + display: block !important; + } + + & .ant-tree-node-content-wrapper { + display: block; + } + + & .ant-tree-node-content-wrapper:hover { + background-color: unset; + } + `} +`; + +const getEmptyEditItem = (): EditItem => ({ + idx: NaN, + layerConf: { + type: 'WMS', + version: '1.3.0', + title: '', + url: '', + layersParam: '', + }, +}); + +export const LayerConfigsControl: FC = ({ + value, + onChange = () => {}, + name, + label, + description, + renderTrigger, + hovered, + validationErrors, +}) => { + const [popoverVisible, setPopoverVisible] = useState(false); + const [editItem, setEditItem] = useState(getEmptyEditItem()); + + const onAddClick = () => { + setEditItem(getEmptyEditItem()); + setPopoverVisible(true); + }; + + const onEditClick = (layerConf: LayerConf, idx: number) => { + if (popoverVisible) { + return; + } + setEditItem({ + idx, + layerConf: { ...layerConf }, + }); + setPopoverVisible(true); + }; + + const onRemoveClick = (idx: number) => { + const newValue = value ? [...value] : []; + newValue.splice(idx, 1); + onChange(newValue); + }; + + const onPopoverClose = () => { + setPopoverVisible(false); + }; + + const computeNewValue = (layerConf: LayerConf) => { + const newValue = value ? [...value] : []; + if (!editItem) { + return undefined; + } + if (Number.isNaN(editItem.idx)) { + newValue.unshift(layerConf); + } else if (editItem) { + newValue[editItem.idx] = layerConf; + } + return newValue; + }; + + const onPopoverSave = (layerConf: LayerConf) => { + const newValue = computeNewValue(layerConf); + setPopoverVisible(false); + if (!newValue) { + return; + } + onChange(newValue); + }; + + const onMoveLayer = (newConfigs: LayerConf[]) => { + onChange(newConfigs); + }; + + const popoverTitle = editItem.layerConf.title + ? editItem.layerConf.title + : t('Add Layer'); + const controlHeaderProps = { + name, + label, + description, + renderTrigger, + hovered, + validationErrors, + }; + + return ( +
+ + + } + > + + +
+ ); +}; + +export default LayerConfigsControl; diff --git a/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerConfigsPopoverContent.tsx b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerConfigsPopoverContent.tsx new file mode 100644 index 0000000000000..3ab51c5c9441b --- /dev/null +++ b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerConfigsPopoverContent.tsx @@ -0,0 +1,506 @@ +/** + * 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 { css, JsonValue, styled, t } from '@superset-ui/core'; +import { Button, Form, Tabs } from 'antd'; +import { mix } from 'polished'; +import { Data as GsData } from 'geostyler-data'; +import { Style as GsStyle } from 'geostyler-style'; +import WfsDataParser, { + RequestParams1_1_0, + RequestParams2_0_0, +} from 'geostyler-wfs-parser'; +import { FC, useEffect, useState } from 'react'; +import { isWfsLayerConf, isWmsLayerConf, isXyzLayerConf } from './typeguards'; +import { + BaseLayerConf, + LayerConf, + LayerConfigsPopoverContentProps, + WfsLayerConf, + WmsLayerConf, + XyzLayerConf, +} from './types'; +import { getServiceVersions, hasAllRequiredWfsParams } from './serviceUtil'; +import { ControlFormItem } from '../ColumnConfigControl/ControlForm'; +import GeoStylerWrapper from './GeoStylerWrapper'; + +// Enum for the different tabs +const LAYER_CONFIG_TABS = { + LAYER: '1', + GEOSTYLER: '2', +}; + +export const StyledButtonContainer = styled.div` + display: flex; + margin: 8px; +`; + +export const StyledCloseButton = styled(Button)` + ${({ theme }) => css` + flex: 1; + margin-right: 4px; + line-height: 1.5715; + border-radius: ${theme.borderRadius}px; + background-color: ${theme.colors.primary.light4}; + color: ${theme.colors.primary.dark1}; + font-size: ${theme.typography.sizes.s}px; + font-weight: ${theme.typography.weights.bold}; + text-transform: uppercase; + min-width: ${theme.gridUnit * 36}; + min-height: ${theme.gridUnit * 8}; + box-shadow: none; + border-width: 0px; + border-style: none; + border-color: transparent; + &:hover { + background-color: ${mix( + 0.1, + theme.colors.primary.base, + theme.colors.primary.light4, + )}; + color: ${theme.colors.primary.dark1}; + } + `} +`; + +export const StyledControlFormItem = styled(ControlFormItem)` + ${({ theme }) => css` + border-radius: ${theme.borderRadius}px; + `} +`; + +export const StyledControlNumberFormItem = styled(ControlFormItem)` + ${({ theme }) => css` + border-radius: ${theme.borderRadius}px; + width: 100%; + `} +`; + +export const StyledGeoStyler = styled(GeoStylerWrapper)` + ${({ theme }) => css` + h2 { + font-weight: ${theme.typography.weights.normal}; + font-size: ${theme.typography.sizes.xl}px; + } + .ant-form-item-control { + flex: unset; + } + `} +`; + +export const StyledSaveButton = styled(Button)` + ${({ theme }) => css` + flex: 1; + margin-left: 4px; + line-height: 1.5715; + border-radius: ${theme.borderRadius}px; + background-color: ${theme.colors.primary.base}; + color: ${theme.colors.grayscale.light5}; + font-size: ${theme.typography.sizes.s}px; + font-weight: ${theme.typography.weights.bold}; + text-transform: uppercase; + min-width: ${theme.gridUnit * 36}; + min-height: ${theme.gridUnit * 8}; + box-shadow: none; + border-width: 0px; + border-style: none; + border-color: transparent; + &:hover { + background-color: ${theme.colors.primary.dark1}; + } + `} +`; + +export const LayerConfigsPopoverContent: FC< + LayerConfigsPopoverContentProps +> = ({ onClose = () => {}, onSave = () => {}, layerConf }) => { + const [currentLayerConf, setCurrentLayerConf] = + useState(layerConf); + const initialWmsVersion = + layerConf.type === 'WMS' ? layerConf.version : undefined; + const [wmsVersion, setWmsVersion] = useState( + initialWmsVersion, + ); + const initialWfsVersion = + layerConf.type === 'WFS' ? layerConf.version : undefined; + const [wfsVersion, setWfsVersion] = useState( + initialWfsVersion, + ); + const [geostylerData, setGeoStylerData] = useState( + undefined, + ); + + const serviceVersions = getServiceVersions(); + + // This is needed to force mounting the form every time + // we get a new layerConf prop. Otherwise the input fields + // will not be updated properly, since ControlFormItem only + // recognises the `value` property once and then handles the + // values in its on state. Remounting creates a new component + // and thereby starts with a fresh state. + const [formKey, setFormKey] = useState(0); + + useEffect(() => { + setCurrentLayerConf({ ...layerConf }); + setFormKey(oldFormKey => oldFormKey + 1); + }, [layerConf]); + + const onFieldValueChange = (value: JsonValue, key: string) => { + setCurrentLayerConf({ + ...currentLayerConf, + [key]: value, + }); + }; + + const onLayerTypeChange = (value: LayerConf['type']) => { + if (value === 'WFS') { + setCurrentLayerConf({ + ...currentLayerConf, + type: value, + version: serviceVersions[value][0], + style: { + name: 'Default Style', + rules: [ + { + name: 'Default Rule', + symbolizers: [ + { + kind: 'Line', + // eslint-disable-next-line theme-colors/no-literal-colors + color: '#000000', + width: 2, + }, + { + kind: 'Mark', + wellKnownName: 'circle', + // eslint-disable-next-line theme-colors/no-literal-colors + color: '#000000', + }, + { + kind: 'Fill', + // eslint-disable-next-line theme-colors/no-literal-colors + color: '#000000', + }, + ], + }, + ], + }, + } as WfsLayerConf); + } else if (value === 'XYZ') { + setCurrentLayerConf({ + ...currentLayerConf, + type: value, + } as XyzLayerConf); + } else { + setCurrentLayerConf({ + ...currentLayerConf, + type: value, + version: serviceVersions[value][0], + } as WmsLayerConf); + } + }; + + const onLayerTitleChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'title'); + }; + + const onLayerUrlChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'url'); + }; + + const onLayersParamChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'layersParam'); + }; + + const onTypeNameChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'typeName'); + }; + + const onWmsVersionChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'version'); + setWmsVersion(fieldValue); + }; + + const onWfsVersionChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'version'); + setWfsVersion(fieldValue); + }; + + const onMaxFeaturesChange = (fieldValue: number) => { + onFieldValueChange(fieldValue, 'maxFeatures'); + }; + + const onStyleChange = (fieldValue: GsStyle) => { + onFieldValueChange(fieldValue, 'style'); + }; + + const onAttributionChange = (fieldValue: string) => { + onFieldValueChange(fieldValue, 'attribution'); + }; + + const onCloseClick = () => { + onClose(); + }; + + const onSaveClick = () => { + const baseConfs: BaseLayerConf = { + title: currentLayerConf.title, + url: currentLayerConf.url, + type: currentLayerConf.type, + attribution: currentLayerConf.attribution, + }; + + let conf: LayerConf; + if (isWmsLayerConf(currentLayerConf)) { + conf = { + ...baseConfs, + version: currentLayerConf.version, + type: currentLayerConf.type, + layersParam: currentLayerConf.layersParam, + }; + } else if (isXyzLayerConf(currentLayerConf)) { + conf = { + ...baseConfs, + type: currentLayerConf.type, + }; + } else { + conf = { + ...baseConfs, + type: currentLayerConf.type, + version: currentLayerConf.version, + typeName: currentLayerConf.typeName, + maxFeatures: currentLayerConf.maxFeatures, + style: currentLayerConf.style, + }; + } + + onSave(conf); + }; + + useEffect(() => { + if ( + !isWfsLayerConf(currentLayerConf) || + !hasAllRequiredWfsParams(currentLayerConf) + ) { + setGeoStylerData(undefined); + return undefined; + } + + const readWfsData = async (conf: WfsLayerConf) => { + const wfsParser = new WfsDataParser(); + try { + let requestParams: RequestParams1_1_0 | RequestParams2_0_0 = {} as + | RequestParams1_1_0 + | RequestParams2_0_0; + if (conf.version.startsWith('1.')) { + requestParams = { + version: conf.version as RequestParams1_1_0['version'], + maxFeatures: conf.maxFeatures, + typeName: conf.typeName, + }; + } + if (conf.version.startsWith('2.')) { + requestParams = { + version: conf.version as RequestParams2_0_0['version'], + count: conf.maxFeatures, + typeNames: conf.typeName, + }; + } + + const gsData = await wfsParser.readData({ + url: conf.url, + requestParams, + }); + setGeoStylerData(gsData); + } catch { + console.warn('Could not read geostyler data'); + setGeoStylerData(undefined); + } + }; + + // debounce function + const timer = setTimeout(() => readWfsData(currentLayerConf), 500); + + return () => { + clearTimeout(timer); + }; + }, [currentLayerConf]); + + const layerTabLabel = t('Layer'); + const styleTabLabel = t('Style'); + const layerTypeLabel = t('Layer type'); + const layerTypeDescription = t('The type of the layer'); + const serviceVersionLabel = t('Service version'); + const serviceVersionDescription = t('The version of the service'); + const layersParamLabel = t('Layer Name'); + const layersParamDescription = t( + 'The name of the layer as described in GetCapabilities', + ); + const layersParamPlaceholder = t('Layer Name'); + const layerTitleLabel = t('Layer title'); + const layerTitleDescription = t('The visible title of the layer'); + const layerTitlePlaceholder = t('Insert Layer title'); + const layerUrlLabel = t('Layer URL'); + const layerUrlDescription = t('The service url of the layer'); + const layerUrlPlaceholder = t('Insert Layer URL'); + const maxFeaturesLabel = t('Max. features'); + const maxFeaturesDescription = t( + 'Maximum number of features to fetch from service', + ); + const maxFeaturesPlaceholder = t('10000'); + const attributionLabel = t('Attribution'); + const attributionDescription = t('The layer attribution'); + const attributionPlaceholder = t('© Layer attribution'); + + const wmsVersionOptions: { value: any; label: string }[] = + serviceVersions.WMS.map(version => ({ value: version, label: version })); + const wfsVersionOptions: { value: any; label: string }[] = + serviceVersions.WFS.map(version => ({ value: version, label: version })); + + return ( +
+
+ + + + + {isWmsLayerConf(currentLayerConf) && ( + + )} + {isWfsLayerConf(currentLayerConf) && ( + + )} + {isWmsLayerConf(currentLayerConf) && ( + + )} + {isWfsLayerConf(currentLayerConf) && ( + + )} + + {isWfsLayerConf(currentLayerConf) && ( + + )} + + + + {isWfsLayerConf(currentLayerConf) && ( + + )} + + + + + {t('Close')} + + + {t('Save')} + + +
+
+ ); +}; + +export default LayerConfigsPopoverContent; diff --git a/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx new file mode 100644 index 0000000000000..a06b317813bb9 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx @@ -0,0 +1,72 @@ +/** + * 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 { CloseOutlined, RightOutlined } from '@ant-design/icons'; +import { Button, Tag } from 'antd'; +import { FC } from 'react'; +import { LayerTreeItemProps } from './types'; + +export const LayerTreeItem: FC = ({ + layerConf, + onEditClick = () => {}, + onRemoveClick = () => {}, + className, +}) => { + const onCloseTag = () => { + onRemoveClick(); + }; + + const onEditTag = () => { + onEditClick(); + }; + + return ( + +