mirror of
https://github.com/apache/superset.git
synced 2026-09-11 17:54:25 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57ca29baf4 | ||
|
|
01c587361f | ||
|
|
fc64ac918a | ||
|
|
cd019bab3e | ||
|
|
a330fe6f7e | ||
|
|
fe1fddde05 | ||
|
|
7e67deead7 | ||
|
|
6e02603098 | ||
|
|
4518f6999c | ||
|
|
aff847b3af | ||
|
|
b24aca0304 | ||
|
|
2c453035e4 | ||
|
|
4fe11869fc | ||
|
|
a0a49f9300 | ||
|
|
29d2fac485 | ||
|
|
0c5da6cb5d | ||
|
|
da6947d295 | ||
|
|
2db8f809ba | ||
|
|
5912fad745 | ||
|
|
dc41c45bec | ||
|
|
88ee90c579 | ||
|
|
bbb2279644 | ||
|
|
1958df6b83 | ||
|
|
58bd3bfcf0 | ||
|
|
d6eb6e08d0 |
@@ -42,7 +42,7 @@ body:
|
||||
options:
|
||||
- master / latest-dev
|
||||
- "5.0.0"
|
||||
- "4.1.2"
|
||||
- "4.1.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Claude PR Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
allowed: ${{ steps.check.outputs.allowed }}
|
||||
steps:
|
||||
- name: Check if user is allowed
|
||||
id: check
|
||||
run: |
|
||||
# List of allowed users
|
||||
ALLOWED_USERS="mistercrunch,rusackas"
|
||||
|
||||
# Get the commenter's username
|
||||
COMMENTER="${{ github.event.comment.user.login }}"
|
||||
|
||||
echo "Checking permissions for user: $COMMENTER"
|
||||
|
||||
# Check if user is in allowed list
|
||||
if [[ ",$ALLOWED_USERS," == *",$COMMENTER,"* ]]; then
|
||||
echo "allowed=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ User $COMMENTER is allowed to use Claude"
|
||||
else
|
||||
echo "allowed=false" >> $GITHUB_OUTPUT
|
||||
echo "❌ User $COMMENTER is not allowed to use Claude"
|
||||
fi
|
||||
|
||||
deny-access:
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.allowed == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment access denied
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!
|
||||
|
||||
Thanks for trying to use Claude Code, but currently only certain team members have access to this feature.
|
||||
|
||||
If you believe you should have access, please contact a project maintainer.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: message
|
||||
});
|
||||
|
||||
claude-code-action:
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.allowed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
@@ -92,6 +92,7 @@ scripts/*.zip
|
||||
# IntelliJ
|
||||
*.iml
|
||||
venv
|
||||
.venv
|
||||
@eaDir/
|
||||
|
||||
# PyCharm
|
||||
@@ -127,3 +128,4 @@ docker/*local*
|
||||
test-report.html
|
||||
superset/static/stats/statistics.html
|
||||
.aider*
|
||||
CLAUDE.local.md
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1.3 (Thu May 29 02:31:07 2025 -0500)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#33522](https://github.com/apache/superset/pull/33522) fix(Sqllab): Autocomplete got stuck in UI when open it too fast (@rebenitez1802)
|
||||
- [#33425](https://github.com/apache/superset/pull/33425) fix(table-chart): time shift is not working (@justinpark)
|
||||
- [#32414](https://github.com/apache/superset/pull/32414) fix(api): Added uuid to list api calls (@withnale)
|
||||
- [#33354](https://github.com/apache/superset/pull/33354) fix: loading examples from raw.githubusercontent.com fails with 429 errors (@mistercrunch)
|
||||
- [#32382](https://github.com/apache/superset/pull/32382) fix(pinot): revert join and subquery flags (@yuribogomolov)
|
||||
- [#32473](https://github.com/apache/superset/pull/32473) fix(plugin-chart-echarts): remove erroneous upper bound value (@villebro)
|
||||
- [#33048](https://github.com/apache/superset/pull/33048) fix: improve error type on parse error (@justinpark)
|
||||
- [#32968](https://github.com/apache/superset/pull/32968) fix(pivot-table): Revert "fix(Pivot Table): Fix column width to respect currency config (#31414)" (@justinpark)
|
||||
- [#32795](https://github.com/apache/superset/pull/32795) fix(log): store navigation path to get correct logging path (@justinpark)
|
||||
- [#33216](https://github.com/apache/superset/pull/33216) fix: Downgrade to marshmallow<4 (@amotl)
|
||||
- [#32866](https://github.com/apache/superset/pull/32866) fix: make packages PEP 625 compliant (@sadpandajoe)
|
||||
- [#32035](https://github.com/apache/superset/pull/32035) fix(fe/dashboard-list): display modifier info for `Last modified` data (@hainenber)
|
||||
- [#32708](https://github.com/apache/superset/pull/32708) fix(logging): missing path in event data (@justinpark)
|
||||
- [#32699](https://github.com/apache/superset/pull/32699) fix: Signature of Celery pruner jobs (@michael-s-molina)
|
||||
- [#32681](https://github.com/apache/superset/pull/32681) fix(log): Update recent_activity by event name (@justinpark)
|
||||
- [#32608](https://github.com/apache/superset/pull/32608) fix(welcome): perf on distinct recent activities (@justinpark)
|
||||
- [#32572](https://github.com/apache/superset/pull/32572) fix: Log table retention policy (@michael-s-molina)
|
||||
- [#32406](https://github.com/apache/superset/pull/32406) fix(model/helper): represent RLS filter clause in proper textual SQL string (@hainenber)
|
||||
- [#32240](https://github.com/apache/superset/pull/32240) fix: upgrade to 3.11.11-slim-bookworm to address critical vulnerabilities (@gpchandran)
|
||||
- [#30858](https://github.com/apache/superset/pull/30858) fix(chart data): removing query from /chart/data payload when accessing as guest user (@fisjac)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#33612](https://github.com/apache/superset/pull/33612) chore: update Dockerfile - Upgrade to 3.11.12 (@gpchandran)
|
||||
- [#33435](https://github.com/apache/superset/pull/33435) docs: CVEs fixed on 4.1.2 (@sha174n)
|
||||
- [#33339](https://github.com/apache/superset/pull/33339) chore(🦾): bump python h11 0.14.0 -> 0.16.0 (@github-actions[bot])
|
||||
- [#32745](https://github.com/apache/superset/pull/32745) chore(🦾): bump python sqlglot 26.1.3 -> 26.11.1 (@github-actions[bot])
|
||||
- [#32782](https://github.com/apache/superset/pull/32782) chore: Revert "chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm`" (@sadpandajoe)
|
||||
- [#32780](https://github.com/apache/superset/pull/32780) chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm` (@gpchandran)
|
||||
+1
-1
@@ -23,7 +23,7 @@ This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
- [34204](https://github.com/apache/superset/pull/33603) OpenStreetView has been promoted as the new default for Deck.gl visualization since it can be enabled by default without requiring an API key. If you have Mapbox set up and want to disable OpenStreeView in your environment, please follow the steps documented here [https://superset.apache.org/docs/configuration/map-tiles].
|
||||
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
|
||||
There's a migration added that can potentially affect a significant number of existing charts.
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Map Tiles
|
||||
sidebar_position: 12
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Map tiles
|
||||
|
||||
Superset uses OSM and Mapbox tiles by default. OSM is free but you still need setting your MAPBOX_API_KEY if you want to use mapbox maps.
|
||||
|
||||
## Setting map tiles
|
||||
|
||||
Map tiles can be set with `DECKGL_BASE_MAP` in your `superset_config.py` or `superset_config_docker.py`
|
||||
For adding your own map tiles, you can use the following format.
|
||||
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['tile://https://your_personal_url/{z}/{x}/{y}.png', 'MyTile']
|
||||
]
|
||||
```
|
||||
Openstreetmap tiles url can be added without prefix.
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 'OpenStreetMap']
|
||||
]
|
||||
```
|
||||
|
||||
Default values are:
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
|
||||
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark'],
|
||||
['mapbox://styles/mapbox/light-v9', 'Light'],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets'],
|
||||
['mapbox://styles/mapbox/satellite-v9', 'Satellite'],
|
||||
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors'],
|
||||
]
|
||||
```
|
||||
|
||||
It is possible to set only mapbox by removing osm tiles and other way around.
|
||||
|
||||
:::warning
|
||||
Setting `DECKGL_BASE_MAP` overwrite default values
|
||||
:::
|
||||
|
||||
After defining your map tiles, set them in these variables:
|
||||
- `CORS_OPTIONS`
|
||||
- `connect-src` of `TALISMAN_CONFIG` and `TALISMAN_CONFIG_DEV` variables.
|
||||
|
||||
```python
|
||||
ENABLE_CORS = True
|
||||
CORS_OPTIONS: dict[Any, Any] = {
|
||||
"origins": [
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
]
|
||||
}
|
||||
|
||||
.
|
||||
.
|
||||
|
||||
TALISMAN_CONFIG = {
|
||||
"content_security_policy": {
|
||||
...
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://api.mapbox.com",
|
||||
"https://events.mapbox.com",
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
/* eslint-env node */
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
const typescriptEslintParser = require('@typescript-eslint/parser');
|
||||
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
|
||||
const eslintConfigPrettier = require('eslint-config-prettier');
|
||||
const prettierEslintPlugin = require('eslint-plugin-prettier');
|
||||
const js = require('@eslint/js');
|
||||
const ts = require('typescript-eslint');
|
||||
const react = require('eslint-plugin-react');
|
||||
const globals = require('globals');
|
||||
const { defineConfig, globalIgnores } = require('eslint/config');
|
||||
|
||||
module.exports = defineConfig([
|
||||
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
files: ['eslint.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
}
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptEslintParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
typescript: typescriptEslintPlugin,
|
||||
react,
|
||||
prettier: prettierEslintPlugin,
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
}
|
||||
])
|
||||
+8
-5
@@ -40,15 +40,18 @@
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"prettier": "^2.0.0",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.37.0",
|
||||
"webpack": "^5.99.9"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -111,7 +111,7 @@ const StyledTitleContainer = styled('div')`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Link as React.ComponentType<any>)`
|
||||
const StyledButton = styled(Link)`
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
+12
-8
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable no-undef */
|
||||
import { useEffect } from 'react';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
@@ -50,7 +49,9 @@ export default function Root({ children }) {
|
||||
|
||||
// Handle route changes for SPA
|
||||
const handleRouteChange = () => {
|
||||
devMode && console.log('Route changed to:', window.location.pathname);
|
||||
if (devMode) {
|
||||
console.log('Route changed to:', window.location.pathname);
|
||||
}
|
||||
|
||||
// Short timeout to ensure the page has fully rendered
|
||||
setTimeout(() => {
|
||||
@@ -58,11 +59,9 @@ export default function Root({ children }) {
|
||||
const currentTitle = document.title;
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
devMode &&
|
||||
console.log('Tracking page view:', currentPath, currentTitle);
|
||||
|
||||
// For testing: impersonate real domain - ONLY FOR DEVELOPMENT
|
||||
if (devMode) {
|
||||
console.log('Tracking page view:', currentPath, currentTitle);
|
||||
window._paq.push(['setDomains', ['superset.apache.org']]);
|
||||
window._paq.push([
|
||||
'setCustomUrl',
|
||||
@@ -85,17 +84,22 @@ export default function Root({ children }) {
|
||||
'routeDidUpdate',
|
||||
];
|
||||
|
||||
devMode && console.log('Setting up Docusaurus route listeners');
|
||||
if (devMode) {
|
||||
console.log('Setting up Docusaurus route listeners');
|
||||
}
|
||||
possibleEvents.forEach(eventName => {
|
||||
document.addEventListener(eventName, () => {
|
||||
devMode &&
|
||||
if (devMode) {
|
||||
console.log(`Docusaurus route update detected via ${eventName}`);
|
||||
}
|
||||
handleRouteChange();
|
||||
});
|
||||
});
|
||||
|
||||
// Also set up manual history tracking as fallback
|
||||
devMode && console.log('Setting up manual history tracking as fallback');
|
||||
if (devMode) {
|
||||
console.log('Setting up manual history tracking as fallback');
|
||||
}
|
||||
const originalPushState = window.history.pushState;
|
||||
window.history.pushState = function () {
|
||||
originalPushState.apply(this, arguments);
|
||||
|
||||
+284
-198
@@ -2157,30 +2157,71 @@
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
|
||||
"@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1":
|
||||
version "4.12.1"
|
||||
resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
|
||||
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
|
||||
|
||||
"@eslint/eslintrc@^2.1.4":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz"
|
||||
integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
|
||||
"@eslint/config-array@^0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636"
|
||||
integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==
|
||||
dependencies:
|
||||
"@eslint/object-schema" "^2.1.6"
|
||||
debug "^4.3.1"
|
||||
minimatch "^3.1.2"
|
||||
|
||||
"@eslint/config-helpers@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.0.tgz#3e09a90dfb87e0005c7694791e58e97077271286"
|
||||
integrity sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==
|
||||
|
||||
"@eslint/core@^0.15.0", "@eslint/core@^0.15.1":
|
||||
version "0.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.1.tgz#d530d44209cbfe2f82ef86d6ba08760196dd3b60"
|
||||
integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
|
||||
"@eslint/eslintrc@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
|
||||
integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
espree "^9.6.0"
|
||||
globals "^13.19.0"
|
||||
espree "^10.0.1"
|
||||
globals "^14.0.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.0"
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@8.57.1":
|
||||
version "8.57.1"
|
||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz"
|
||||
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
|
||||
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
|
||||
version "9.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
|
||||
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
|
||||
|
||||
"@eslint/object-schema@^2.1.6":
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
|
||||
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
|
||||
|
||||
"@eslint/plugin-kit@^0.3.1":
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
|
||||
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
|
||||
dependencies:
|
||||
"@eslint/core" "^0.15.1"
|
||||
levn "^0.4.1"
|
||||
|
||||
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
|
||||
version "9.3.0"
|
||||
@@ -2194,24 +2235,33 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz"
|
||||
integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
|
||||
"@humanfs/core@^0.19.1":
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
|
||||
integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
|
||||
|
||||
"@humanfs/node@^0.16.6":
|
||||
version "0.16.6"
|
||||
resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e"
|
||||
integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^2.0.3"
|
||||
debug "^4.3.1"
|
||||
minimatch "^3.0.5"
|
||||
"@humanfs/core" "^0.19.1"
|
||||
"@humanwhocodes/retry" "^0.3.0"
|
||||
|
||||
"@humanwhocodes/module-importer@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
|
||||
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
|
||||
|
||||
"@humanwhocodes/object-schema@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz"
|
||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||
"@humanwhocodes/retry@^0.3.0":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
|
||||
integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
|
||||
|
||||
"@humanwhocodes/retry@^0.4.2":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
|
||||
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
||||
|
||||
"@iconify/types@^2.0.0":
|
||||
version "2.0.0"
|
||||
@@ -2353,7 +2403,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
|
||||
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
|
||||
"@nodelib/fs.walk@^1.2.3":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
|
||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
||||
@@ -2361,6 +2411,11 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@pkgr/core@^0.2.4":
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058"
|
||||
integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==
|
||||
|
||||
"@pnpm/config.env-replace@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c"
|
||||
@@ -3599,11 +3654,6 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/semver@^7.3.12":
|
||||
version "7.5.8"
|
||||
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz"
|
||||
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
|
||||
|
||||
"@types/send@*":
|
||||
version "0.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
|
||||
@@ -3674,91 +3724,105 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.0.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz"
|
||||
integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
|
||||
"@typescript-eslint/eslint-plugin@8.37.0", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
|
||||
integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.62.0"
|
||||
"@typescript-eslint/type-utils" "5.62.0"
|
||||
"@typescript-eslint/utils" "5.62.0"
|
||||
debug "^4.3.4"
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.37.0"
|
||||
"@typescript-eslint/type-utils" "8.37.0"
|
||||
"@typescript-eslint/utils" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.0"
|
||||
natural-compare-lite "^1.4.0"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.0.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz"
|
||||
integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
|
||||
"@typescript-eslint/parser@8.37.0", "@typescript-eslint/parser@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
|
||||
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.62.0"
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/typescript-estree" "5.62.0"
|
||||
"@typescript-eslint/scope-manager" "8.37.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz"
|
||||
integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
|
||||
"@typescript-eslint/project-service@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
|
||||
integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/visitor-keys" "5.62.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz"
|
||||
integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.62.0"
|
||||
"@typescript-eslint/utils" "5.62.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.37.0"
|
||||
"@typescript-eslint/types" "^8.37.0"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz"
|
||||
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz"
|
||||
integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
|
||||
"@typescript-eslint/scope-manager@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
|
||||
integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/visitor-keys" "5.62.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
|
||||
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
|
||||
|
||||
"@typescript-eslint/type-utils@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
|
||||
integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
"@typescript-eslint/utils" "8.37.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
|
||||
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
|
||||
integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.37.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.37.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz"
|
||||
integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
|
||||
"@typescript-eslint/utils@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
|
||||
integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.62.0"
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/typescript-estree" "5.62.0"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.37.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.62.0":
|
||||
version "5.62.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz"
|
||||
integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
|
||||
"@typescript-eslint/visitor-keys@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
|
||||
integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
|
||||
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
|
||||
@@ -3914,11 +3978,16 @@ acorn-walk@^8.0.0:
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2, acorn@^8.9.0:
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
|
||||
address@^1.0.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e"
|
||||
@@ -5006,7 +5075,7 @@ cosmiconfig@^8.1.3, cosmiconfig@^8.3.5:
|
||||
parse-json "^5.2.0"
|
||||
path-type "^4.0.0"
|
||||
|
||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
cross-spawn@^7.0.3, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
@@ -5704,13 +5773,6 @@ doctrine@^2.1.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
doctrine@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz"
|
||||
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
docusaurus-plugin-less@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/docusaurus-plugin-less/-/docusaurus-plugin-less-2.0.2.tgz"
|
||||
@@ -6104,12 +6166,13 @@ eslint-config-prettier@^10.1.5:
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
|
||||
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
|
||||
|
||||
eslint-plugin-prettier@^4.0.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz"
|
||||
integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
|
||||
eslint-plugin-prettier@^5.5.1:
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15"
|
||||
integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
synckit "^0.11.7"
|
||||
|
||||
eslint-plugin-react@^7.37.5:
|
||||
version "7.37.5"
|
||||
@@ -6135,7 +6198,7 @@ eslint-plugin-react@^7.37.5:
|
||||
string.prototype.matchall "^4.0.12"
|
||||
string.prototype.repeat "^1.0.0"
|
||||
|
||||
eslint-scope@5.1.1, eslint-scope@^5.1.1:
|
||||
eslint-scope@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
|
||||
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
|
||||
@@ -6143,80 +6206,82 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^7.2.2:
|
||||
version "7.2.2"
|
||||
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz"
|
||||
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
|
||||
eslint-scope@^8.4.0:
|
||||
version "8.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
|
||||
integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
|
||||
eslint-visitor-keys@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
eslint@^8.0.0:
|
||||
version "8.57.1"
|
||||
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz"
|
||||
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
|
||||
eslint-visitor-keys@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@^9.31.0:
|
||||
version "9.31.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.31.0.tgz#9a488e6da75bbe05785cd62e43c5ea99356d21ba"
|
||||
integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.6.1"
|
||||
"@eslint/eslintrc" "^2.1.4"
|
||||
"@eslint/js" "8.57.1"
|
||||
"@humanwhocodes/config-array" "^0.13.0"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
"@eslint/config-array" "^0.21.0"
|
||||
"@eslint/config-helpers" "^0.3.0"
|
||||
"@eslint/core" "^0.15.0"
|
||||
"@eslint/eslintrc" "^3.3.1"
|
||||
"@eslint/js" "9.31.0"
|
||||
"@eslint/plugin-kit" "^0.3.1"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
"@ungap/structured-clone" "^1.2.0"
|
||||
"@humanwhocodes/retry" "^0.4.2"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
ajv "^6.12.4"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
cross-spawn "^7.0.6"
|
||||
debug "^4.3.2"
|
||||
doctrine "^3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^7.2.2"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
espree "^9.6.1"
|
||||
esquery "^1.4.2"
|
||||
eslint-scope "^8.4.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
espree "^10.4.0"
|
||||
esquery "^1.5.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
file-entry-cache "^8.0.0"
|
||||
find-up "^5.0.0"
|
||||
glob-parent "^6.0.2"
|
||||
globals "^13.19.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
is-path-inside "^3.0.3"
|
||||
js-yaml "^4.1.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.1.2"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
espree@^9.6.0, espree@^9.6.1:
|
||||
version "9.6.1"
|
||||
resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz"
|
||||
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
|
||||
espree@^10.0.1, espree@^10.4.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
|
||||
integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
|
||||
dependencies:
|
||||
acorn "^8.9.0"
|
||||
acorn "^8.15.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.4.2:
|
||||
esquery@^1.5.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
|
||||
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
|
||||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
@@ -6411,7 +6476,7 @@ fast-diff@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz"
|
||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
||||
|
||||
fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0:
|
||||
fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
@@ -6484,12 +6549,12 @@ figures@^3.2.0:
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
|
||||
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
|
||||
file-entry-cache@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
|
||||
integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
flat-cache "^4.0.0"
|
||||
|
||||
file-loader@^6.2.0:
|
||||
version "6.2.0"
|
||||
@@ -6548,14 +6613,13 @@ find-up@^6.3.0:
|
||||
locate-path "^7.1.0"
|
||||
path-exists "^5.0.0"
|
||||
|
||||
flat-cache@^3.0.4:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz"
|
||||
integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
|
||||
flat-cache@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
|
||||
integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
|
||||
dependencies:
|
||||
flatted "^3.2.9"
|
||||
keyv "^4.5.3"
|
||||
rimraf "^3.0.2"
|
||||
keyv "^4.5.4"
|
||||
|
||||
flat@^5.0.2:
|
||||
version "5.0.2"
|
||||
@@ -6590,12 +6654,14 @@ form-data-encoder@^2.1.2:
|
||||
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
format@^0.2.0:
|
||||
@@ -6781,18 +6847,21 @@ globals@^11.1.0:
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
globals@^13.19.0:
|
||||
version "13.24.0"
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz"
|
||||
integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
globals@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globals@^15.14.0:
|
||||
version "15.15.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
|
||||
integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
|
||||
|
||||
globals@^16.3.0:
|
||||
version "16.3.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-16.3.0.tgz#66118e765ddaf9e2d880f7e17658543f93f1f667"
|
||||
integrity sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==
|
||||
|
||||
globalthis@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz"
|
||||
@@ -7285,6 +7354,11 @@ ignore@^5.2.0, ignore@^5.2.4:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
ignore@^7.0.0:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
|
||||
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
|
||||
|
||||
image-size@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-2.0.2.tgz#84a7b43704db5736f364bf0d1b029821299b4bdc"
|
||||
@@ -7619,7 +7693,7 @@ is-obj@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
|
||||
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
|
||||
|
||||
is-path-inside@^3.0.2, is-path-inside@^3.0.3:
|
||||
is-path-inside@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||
@@ -7921,7 +7995,7 @@ katex@^0.16.9:
|
||||
dependencies:
|
||||
commander "^8.3.0"
|
||||
|
||||
keyv@^4.5.3:
|
||||
keyv@^4.5.3, keyv@^4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
|
||||
@@ -8949,7 +9023,7 @@ minimalistic-assert@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||
|
||||
minimatch@3.1.2, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
minimatch@3.1.2, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
@@ -8963,6 +9037,13 @@ minimatch@^7.4.3:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
@@ -9011,11 +9092,6 @@ nanoid@^3.3.11, nanoid@^3.3.8:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
natural-compare-lite@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"
|
||||
integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||
@@ -10165,10 +10241,10 @@ prettier-linter-helpers@^1.0.0:
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^2.0.0:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
prettier@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
|
||||
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
|
||||
|
||||
pretty-error@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -11438,6 +11514,11 @@ semver@^7.3.5, semver@^7.3.7, semver@^7.5.4:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
|
||||
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
|
||||
|
||||
semver@^7.6.0:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
send@0.19.0:
|
||||
version "0.19.0"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
|
||||
@@ -12058,6 +12139,13 @@ swagger-ui-react@^5.26.0:
|
||||
xml-but-prettier "^1.0.1"
|
||||
zenscroll "^4.0.2"
|
||||
|
||||
synckit@^0.11.7:
|
||||
version "0.11.8"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457"
|
||||
integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.4"
|
||||
|
||||
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
@@ -12084,11 +12172,6 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.31.1:
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
|
||||
throttle-debounce@^5.0.0, throttle-debounce@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz"
|
||||
@@ -12180,6 +12263,11 @@ trough@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f"
|
||||
integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
|
||||
|
||||
ts-api-utils@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
|
||||
ts-dedent@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
|
||||
@@ -12195,23 +12283,11 @@ ts-toolbelt@^9.6.0:
|
||||
resolved "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz"
|
||||
integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.3.0, tslib@^2.6.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
|
||||
@@ -12306,6 +12382,16 @@ types-ramda@^0.30.0:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.37.0:
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz#2235ddfa40cdbdadb1afb05f8bda688a2294b4c2"
|
||||
integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.37.0"
|
||||
"@typescript-eslint/parser" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
"@typescript-eslint/utils" "8.37.0"
|
||||
|
||||
typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.14.2
|
||||
version: 0.14.3
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 13.4.4
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ metadata:
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.init.jobAnnotations }}
|
||||
annotations: {{- toYaml .Values.init.jobAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -44,10 +44,10 @@ spec:
|
||||
{{- if or .Values.extraLabels .Values.init.podLabels }}
|
||||
labels:
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.init.podLabels }}
|
||||
{{- toYaml .Values.init.podLabels | nindent 8 }}
|
||||
{{- toYaml .Values.init.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
spec:
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetCeleryBeat.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetCeleryBeat.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetCeleryFlower.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetCeleryFlower.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetWorker.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetWorker.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetWebsockets.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetWebsockets.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetNode.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetNode.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
Generated
+326
-34
@@ -3251,20 +3251,6 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -3658,6 +3644,19 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4064,6 +4063,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.19",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
|
||||
@@ -4124,12 +4137,57 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -4590,6 +4648,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-readdir-recursive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
|
||||
@@ -4617,10 +4691,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
@@ -4640,6 +4717,30 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
@@ -4649,6 +4750,19 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -4711,6 +4825,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -4739,6 +4865,45 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -6927,6 +7092,15 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -10565,19 +10739,6 @@
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
@@ -10862,6 +11023,16 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -11140,6 +11311,17 @@
|
||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||
"dev": true
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.5.19",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
|
||||
@@ -11183,12 +11365,45 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true
|
||||
},
|
||||
"es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -11503,6 +11718,19 @@
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs-readdir-recursive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
|
||||
@@ -11523,9 +11751,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true
|
||||
},
|
||||
"gensync": {
|
||||
@@ -11540,12 +11768,40 @@
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -11588,6 +11844,12 @@
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -11609,6 +11871,30 @@
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -13219,6 +13505,12 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
||||
@@ -56,7 +56,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
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|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
'node_modules/(?!d3-(interpolate|color|time|scale)|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
Generated
+1332
-73
File diff suppressed because it is too large
Load Diff
@@ -104,12 +104,12 @@
|
||||
"@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-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@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",
|
||||
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
|
||||
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
@@ -203,6 +203,7 @@
|
||||
"use-event-callback": "^0.1.0",
|
||||
"use-immer": "^0.9.0",
|
||||
"use-query-params": "^1.1.9",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+16
-7
@@ -19,18 +19,27 @@
|
||||
|
||||
import { t } from '../translation';
|
||||
|
||||
const VALIDE_OSM_URLS = ['https://tile.osm', 'https://tile.openstreetmap'];
|
||||
|
||||
/**
|
||||
* Validate a [Mapbox styles URL](https://docs.mapbox.com/help/glossary/style-url/)
|
||||
* @param v
|
||||
*/
|
||||
export default function validateMapboxStylesUrl(v: unknown) {
|
||||
if (
|
||||
typeof v === 'string' &&
|
||||
v.trim().length > 0 &&
|
||||
v.trim().startsWith('mapbox://styles/')
|
||||
) {
|
||||
return false;
|
||||
if (typeof v === 'string') {
|
||||
const trimmed_v = v.trim();
|
||||
if (
|
||||
typeof v === 'string' &&
|
||||
trimmed_v.length > 0 &&
|
||||
(trimmed_v.startsWith('mapbox://styles/') ||
|
||||
trimmed_v.startsWith('tile://http') ||
|
||||
VALIDE_OSM_URLS.some(s => trimmed_v.startsWith(s)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return t('is expected to be a Mapbox URL');
|
||||
return t(
|
||||
'is expected to be a Mapbox/OSM URL (eg. mapbox://styles/...) or a tile server URL (eg. tile://http...)',
|
||||
);
|
||||
}
|
||||
|
||||
+6
-1
@@ -29,6 +29,11 @@ describe('validateMapboxStylesUrl', () => {
|
||||
'mapbox://styles/foobar/clp2dr5r4008a01pcg4ad45m8',
|
||||
),
|
||||
).toEqual(false);
|
||||
expect(
|
||||
validateMapboxStylesUrl(
|
||||
'tile://https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
[
|
||||
@@ -40,7 +45,7 @@ describe('validateMapboxStylesUrl', () => {
|
||||
].forEach(value => {
|
||||
it(`should not validate ${value}`, () => {
|
||||
expect(validateMapboxStylesUrl(value)).toEqual(
|
||||
'is expected to be a Mapbox URL',
|
||||
'is expected to be a Mapbox/OSM URL (eg. mapbox://styles/...) or a tile server URL (eg. tile://http...)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+122
-58
File diff suppressed because one or more lines are too long
+43
-119
File diff suppressed because one or more lines are too long
@@ -42,4 +42,8 @@ export default styled(WorldMapComponent)`
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
}
|
||||
}
|
||||
.hoverinfo {
|
||||
background-color: ${({ theme }) => theme.colorBgElevated};
|
||||
color: ${({ theme }) => theme.colorTextSecondary};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -24,10 +24,17 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.0.38",
|
||||
"@deck.gl/core": "^9.0.37",
|
||||
"@deck.gl/layers": "^9.0.38",
|
||||
"@deck.gl/react": "^9.1.4",
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/core": "^9.1.13",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/layers": "^9.1.13",
|
||||
"@deck.gl/react": "^9.1.13",
|
||||
"@luma.gl/constants": "^9.1.9",
|
||||
"@luma.gl/core": "^9.1.9",
|
||||
"@luma.gl/engine": "^9.1.9",
|
||||
"@luma.gl/shadertools": "^9.1.9",
|
||||
"@luma.gl/webgl": "^9.1.9",
|
||||
"@mapbox/tiny-sdf": "^2.0.6",
|
||||
"@mapbox/geojson-extent": "^1.0.1",
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
|
||||
@@ -39,6 +39,12 @@ import { JsonObject, JsonValue, styled, usePrevious } from '@superset-ui/core';
|
||||
import Tooltip, { TooltipProps } from './components/Tooltip';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
MAPBOX_LAYER_PREFIX,
|
||||
OSM_LAYER_KEYWORDS,
|
||||
TILE_LAYER_PREFIX,
|
||||
buildTileLayer,
|
||||
} from './utils';
|
||||
|
||||
const TICK = 250; // milliseconds
|
||||
|
||||
@@ -102,6 +108,20 @@ export const DeckGLContainer = memo(
|
||||
);
|
||||
|
||||
const layers = useCallback(() => {
|
||||
if (
|
||||
(props.mapStyle?.startsWith(TILE_LAYER_PREFIX) ||
|
||||
OSM_LAYER_KEYWORDS.some(tilek => props.mapStyle?.includes(tilek))) &&
|
||||
props.layers.some(
|
||||
l => typeof l !== 'function' && l?.id === 'tile-layer',
|
||||
) === false
|
||||
) {
|
||||
props.layers.unshift(
|
||||
buildTileLayer(
|
||||
(props.mapStyle ?? '').replace(TILE_LAYER_PREFIX, ''),
|
||||
'tile-layer',
|
||||
),
|
||||
);
|
||||
}
|
||||
// Support for layer factory
|
||||
if (props.layers.some(l => typeof l === 'function')) {
|
||||
return props.layers.map(l =>
|
||||
@@ -110,7 +130,7 @@ export const DeckGLContainer = memo(
|
||||
}
|
||||
|
||||
return props.layers as Layer[];
|
||||
}, [props.layers]);
|
||||
}, [props.layers, props.mapStyle]);
|
||||
|
||||
const { children = null, height, width } = props;
|
||||
|
||||
@@ -134,11 +154,13 @@ export const DeckGLContainer = memo(
|
||||
glContextRef.current = context.gl;
|
||||
}}
|
||||
>
|
||||
<StaticMap
|
||||
preserveDrawingBuffer
|
||||
mapStyle={props.mapStyle || 'light'}
|
||||
mapboxApiAccessToken={props.mapboxApiAccessToken}
|
||||
/>
|
||||
{props.mapStyle?.startsWith(MAPBOX_LAYER_PREFIX) && (
|
||||
<StaticMap
|
||||
preserveDrawingBuffer
|
||||
mapStyle={props.mapStyle || 'light'}
|
||||
mapboxApiAccessToken={props.mapboxApiAccessToken}
|
||||
/>
|
||||
)}
|
||||
</DeckGL>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
+27
-9
@@ -48,6 +48,30 @@ const sequentialSchemeRegistry = getSequentialSchemeRegistry();
|
||||
|
||||
export const DEFAULT_DECKGL_COLOR = { r: 158, g: 158, b: 158, a: 1 };
|
||||
|
||||
let deckglTiles: string[][];
|
||||
|
||||
export const DEFAULT_DECKGL_TILES = [
|
||||
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
|
||||
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets (Mapbox)'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark (Mapbox)'],
|
||||
['mapbox://styles/mapbox/light-v9', 'Light (Mapbox)'],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets (Mapbox)'],
|
||||
['mapbox://styles/mapbox/satellite-v9', 'Satellite (Mapbox)'],
|
||||
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors (Mapbox)'],
|
||||
];
|
||||
|
||||
const getDeckGLTiles = () => {
|
||||
if (!deckglTiles) {
|
||||
const appContainer = document.getElementById('app');
|
||||
const { common } = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
deckglTiles = common?.deckgl_tiles ?? DEFAULT_DECKGL_TILES;
|
||||
}
|
||||
return deckglTiles;
|
||||
};
|
||||
|
||||
const DEFAULT_VIEWPORT = {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
@@ -395,17 +419,11 @@ export const mapboxStyle = {
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
validators: [validateMapboxStylesUrl],
|
||||
choices: [
|
||||
['mapbox://styles/mapbox/streets-v9', t('Streets')],
|
||||
['mapbox://styles/mapbox/dark-v9', t('Dark')],
|
||||
['mapbox://styles/mapbox/light-v9', t('Light')],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', t('Satellite Streets')],
|
||||
['mapbox://styles/mapbox/satellite-v9', t('Satellite')],
|
||||
['mapbox://styles/mapbox/outdoors-v9', t('Outdoors')],
|
||||
],
|
||||
default: 'mapbox://styles/mapbox/light-v9',
|
||||
choices: getDeckGLTiles(),
|
||||
default: getDeckGLTiles()[0][0],
|
||||
description: t(
|
||||
'Base layer map style. See Mapbox documentation: %s',
|
||||
'Mapbox base layer map style (see Mapbox documentation: %s) or tile server URL.',
|
||||
'https://docs.mapbox.com/help/glossary/style-url/',
|
||||
),
|
||||
},
|
||||
|
||||
@@ -26,11 +26,17 @@ import {
|
||||
SequentialScheme,
|
||||
} from '@superset-ui/core';
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { GeoBoundingBox, TileLayer } from '@deck.gl/geo-layers';
|
||||
import { BitmapLayer, PathLayer } from '@deck.gl/layers';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { ColorBreakpointType } from './types';
|
||||
|
||||
const DEFAULT_NUM_BUCKETS = 10;
|
||||
|
||||
export const MAPBOX_LAYER_PREFIX = 'mapbox://';
|
||||
export const TILE_LAYER_PREFIX = 'tile://';
|
||||
export const OSM_LAYER_KEYWORDS = ['openstreetmap', 'osm'];
|
||||
|
||||
export type Buckets = {
|
||||
break_points: string[];
|
||||
num_buckets: string;
|
||||
@@ -215,3 +221,42 @@ export function getColorBreakpointsBuckets(
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
export function buildTileLayer(url: string, id: string) {
|
||||
interface TileLayerProps {
|
||||
id: string;
|
||||
data: string;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
tileSize: number;
|
||||
renderSubLayers: (props: any) => (BitmapLayer | PathLayer)[];
|
||||
}
|
||||
|
||||
interface RenderSubLayerProps {
|
||||
tile: {
|
||||
bbox: GeoBoundingBox;
|
||||
};
|
||||
data: any;
|
||||
}
|
||||
|
||||
return new TileLayer({
|
||||
data: url,
|
||||
id,
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
tileSize: 256,
|
||||
|
||||
renderSubLayers: (props: RenderSubLayerProps): BitmapLayer[] => {
|
||||
const { west, north, east, south } = props.tile.bbox as GeoBoundingBox;
|
||||
|
||||
// Ajouter une BitmapLayer
|
||||
const bitmapLayer = new BitmapLayer(props, {
|
||||
data: undefined,
|
||||
image: props.data,
|
||||
bounds: [west, south, east, north],
|
||||
});
|
||||
|
||||
return [bitmapLayer];
|
||||
},
|
||||
} as TileLayerProps);
|
||||
}
|
||||
|
||||
@@ -39,3 +39,10 @@ export interface TagType {
|
||||
css?: SerializedStyles;
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
export enum TagTypeEnum {
|
||||
Custom = 1,
|
||||
Type = 2,
|
||||
Owner = 3,
|
||||
FavoritedBy = 4,
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
DASHBOARD_POSITION_DATA_LIMIT,
|
||||
DASHBOARD_HEADER_ID,
|
||||
} from 'src/dashboard/util/constants';
|
||||
import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
import setPeriodicRunner, {
|
||||
stopPeriodicRender,
|
||||
} from 'src/dashboard/util/setPeriodicRunner';
|
||||
@@ -415,7 +416,9 @@ const Header = () => {
|
||||
owners: dashboardInfo.owners,
|
||||
roles: dashboardInfo.roles,
|
||||
slug,
|
||||
tags: dashboardInfo.tags,
|
||||
tags: (dashboardInfo.tags || []).filter(
|
||||
item => item.type === TagTypeEnum.Custom || !item.type,
|
||||
),
|
||||
metadata: {
|
||||
...dashboardInfo?.metadata,
|
||||
color_namespace: currentColorNamespace,
|
||||
@@ -460,6 +463,7 @@ const Header = () => {
|
||||
dashboardInfo.metadata,
|
||||
dashboardInfo.owners,
|
||||
dashboardInfo.roles,
|
||||
dashboardInfo.tags,
|
||||
dashboardTitle,
|
||||
layout,
|
||||
refreshFrequency,
|
||||
|
||||
@@ -59,6 +59,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
|
||||
formData: object;
|
||||
width: number;
|
||||
height: number;
|
||||
exportPivotExcel?: (arg0: string) => void;
|
||||
};
|
||||
|
||||
const annotationsLoading = t('Annotation layers are still loading.');
|
||||
@@ -167,6 +168,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
formData,
|
||||
width,
|
||||
height,
|
||||
exportPivotExcel = () => ({}),
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -344,6 +346,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
formData={formData}
|
||||
exploreUrl={exploreUrl}
|
||||
crossFiltersEnabled={isCrossFiltersEnabled}
|
||||
exportPivotExcel={exportPivotExcel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+15
@@ -33,6 +33,7 @@ const createProps = (viz_type = VizType.Sunburst) =>
|
||||
exportFullCSV: jest.fn(),
|
||||
exportXLSX: jest.fn(),
|
||||
exportFullXLSX: jest.fn(),
|
||||
exportPivotExcel: jest.fn(),
|
||||
forceRefresh: jest.fn(),
|
||||
handleToggleFullSize: jest.fn(),
|
||||
toggleExpandSlice: jest.fn(),
|
||||
@@ -254,6 +255,20 @@ test('Should not show export full Excel if report is not table', async () => {
|
||||
expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should export to pivoted Excel if report is pivot table', async () => {
|
||||
const props = createProps(VizType.PivotTable);
|
||||
renderWrapper(props);
|
||||
openMenu();
|
||||
expect(props.exportPivotExcel).toHaveBeenCalledTimes(0);
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
userEvent.click(await screen.findByText('Export to Pivoted Excel'));
|
||||
expect(props.exportPivotExcel).toHaveBeenCalledTimes(1);
|
||||
expect(props.exportPivotExcel).toHaveBeenCalledWith(
|
||||
'.pvtTable',
|
||||
props.slice.slice_name,
|
||||
);
|
||||
});
|
||||
|
||||
test('Should "Show chart description"', () => {
|
||||
const props = createProps();
|
||||
renderWrapper(props);
|
||||
|
||||
@@ -128,6 +128,7 @@ export interface SliceHeaderControlsProps {
|
||||
exportXLSX?: (sliceId: number) => void;
|
||||
exportFullXLSX?: (sliceId: number) => void;
|
||||
handleToggleFullSize: () => void;
|
||||
exportPivotExcel?: (tableSelector: string, sliceName: string) => void;
|
||||
|
||||
addDangerToast: (message: string) => void;
|
||||
addSuccessToast: (message: string) => void;
|
||||
@@ -255,6 +256,10 @@ const SliceHeaderControls = (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case MenuKeys.ExportPivotXlsx: {
|
||||
props.exportPivotExcel?.('.pvtTable', props.slice.slice_name);
|
||||
break;
|
||||
}
|
||||
case MenuKeys.CrossFilterScoping: {
|
||||
openScopingModal();
|
||||
break;
|
||||
@@ -468,6 +473,15 @@ const SliceHeaderControls = (
|
||||
{t('Export to Excel')}
|
||||
</Menu.Item>
|
||||
|
||||
{isPivotTable && (
|
||||
<Menu.Item
|
||||
key={MenuKeys.ExportPivotXlsx}
|
||||
icon={<Icons.FileOutlined css={dropdownIconsStyles} />}
|
||||
>
|
||||
{t('Export to Pivoted Excel')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{isFeatureEnabled(FeatureFlag.AllowFullCsvExport) &&
|
||||
props.supersetCanCSV &&
|
||||
isTable && (
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
|
||||
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
|
||||
|
||||
import SliceHeader from '../SliceHeader';
|
||||
import MissingChart from '../MissingChart';
|
||||
@@ -213,11 +214,15 @@ const Chart = props => {
|
||||
|
||||
const getHeaderHeight = useCallback(() => {
|
||||
if (headerRef.current) {
|
||||
const computedStyle = getComputedStyle(
|
||||
const computedMarginBottom = getComputedStyle(
|
||||
headerRef.current,
|
||||
).getPropertyValue('margin-bottom');
|
||||
const marginBottom = parseInt(computedStyle, 10) || 0;
|
||||
return headerRef.current.offsetHeight + marginBottom;
|
||||
const marginBottom = parseInt(computedMarginBottom, 10) || 0;
|
||||
const computedHeight = getComputedStyle(
|
||||
headerRef.current,
|
||||
).getPropertyValue('height');
|
||||
const height = parseInt(computedHeight, 10) || DEFAULT_HEADER_HEIGHT;
|
||||
return height + marginBottom;
|
||||
}
|
||||
return DEFAULT_HEADER_HEIGHT;
|
||||
}, [headerRef]);
|
||||
@@ -471,6 +476,7 @@ const Chart = props => {
|
||||
formData={formData}
|
||||
width={width}
|
||||
height={getHeaderHeight()}
|
||||
exportPivotExcel={exportPivotExcel}
|
||||
/>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function DownloadAsImage({
|
||||
text,
|
||||
logEvent,
|
||||
dashboardTitle,
|
||||
...props
|
||||
}: {
|
||||
text: string;
|
||||
dashboardTitle: string;
|
||||
@@ -50,6 +51,7 @@ export default function DownloadAsImage({
|
||||
onClick={e => {
|
||||
onDownloadImage(e.domEvent);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function DownloadAsPdf({
|
||||
text,
|
||||
logEvent,
|
||||
dashboardTitle,
|
||||
...props
|
||||
}: {
|
||||
text: string;
|
||||
dashboardTitle: string;
|
||||
@@ -50,6 +51,7 @@ export default function DownloadAsPdf({
|
||||
onClick={e => {
|
||||
onDownloadPdf(e.domEvent);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -289,4 +289,5 @@ export enum MenuKeys {
|
||||
ToggleFullscreen = 'toggle_fullscreen',
|
||||
ManageEmbedded = 'manage_embedded',
|
||||
ManageEmailReports = 'manage_email_reports',
|
||||
ExportPivotXlsx = 'export_pivot_xlsx',
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import Chart, { Slice } from 'src/types/Chart';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { type TagType } from 'src/components';
|
||||
import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
import { loadTags } from 'src/components/Tag/utils';
|
||||
|
||||
export type PropertiesModalProps = {
|
||||
@@ -125,7 +126,7 @@ function PropertiesModal({
|
||||
);
|
||||
if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
|
||||
const customTags = chart.tags?.filter(
|
||||
(tag: TagType) => tag.type === 1,
|
||||
(tag: TagType) => tag.type === TagTypeEnum.Custom,
|
||||
);
|
||||
setTags(customTags);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED,
|
||||
LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS,
|
||||
} from 'src/logger/LogUtils';
|
||||
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
|
||||
import ViewQueryModal from '../controls/ViewQueryModal';
|
||||
import EmbedCodeContent from '../EmbedCodeContent';
|
||||
import DashboardsSubMenu from './DashboardsSubMenu';
|
||||
@@ -67,6 +68,7 @@ const MENU_KEYS = {
|
||||
DELETE_REPORT: 'delete_report',
|
||||
VIEW_QUERY: 'view_query',
|
||||
RUN_IN_SQL_LAB: 'run_in_sql_lab',
|
||||
EXPORT_TO_PIVOT_XLSX: 'export_to_pivot_xlsx',
|
||||
};
|
||||
|
||||
const VIZ_TYPES_PIVOTABLE = [VizType.PivotTable];
|
||||
@@ -248,6 +250,16 @@ export const useExploreAdditionalActionsMenu = (
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case MENU_KEYS.EXPORT_TO_PIVOT_XLSX:
|
||||
exportPivotExcel('.pvtTable', slice?.slice_name ?? t('pivoted_xlsx'));
|
||||
setIsDropdownVisible(false);
|
||||
dispatch(
|
||||
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, {
|
||||
chartId: slice?.slice_id,
|
||||
chartName: slice?.slice_name,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case MENU_KEYS.DOWNLOAD_AS_IMAGE:
|
||||
downloadAsImage(
|
||||
'.panel-body .chart-container',
|
||||
@@ -365,6 +377,13 @@ export const useExploreAdditionalActionsMenu = (
|
||||
>
|
||||
{t('Export to Excel')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_PIVOT_XLSX}
|
||||
icon={<Icons.FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to Pivoted Excel')}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
|
||||
<Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
|
||||
|
||||
@@ -191,4 +191,32 @@ describe('ExtraOptions Component', () => {
|
||||
fireEvent.change(input, { target: { value: '1000' } });
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the collaps tab correctly and resets to default tab after closing', () => {
|
||||
const { rerender } = renderComponent();
|
||||
const sqlLabTab = screen.getByRole('tab', {
|
||||
name: /SQL Lab./i,
|
||||
});
|
||||
|
||||
expect(sqlLabTab).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(sqlLabTab);
|
||||
expect(sqlLabTab).toHaveAttribute('aria-expanded', 'true');
|
||||
const customDb = {
|
||||
...defaultDb,
|
||||
expose_in_sqllab: false,
|
||||
};
|
||||
|
||||
rerender(
|
||||
<ExtraOptions
|
||||
db={customDb as unknown as DatabaseObject}
|
||||
onInputChange={onInputChange}
|
||||
onTextChange={onTextChange}
|
||||
onEditorChange={onEditorChange}
|
||||
onExtraInputChange={onExtraInputChange}
|
||||
onExtraEditorChange={onExtraEditorChange}
|
||||
extraExtension={undefined}
|
||||
/>,
|
||||
);
|
||||
expect(sqlLabTab).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChangeEvent, EventHandler } from 'react';
|
||||
import { ChangeEvent, EventHandler, useState, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
t,
|
||||
@@ -88,12 +88,21 @@ const ExtraOptions = ({
|
||||
const isAllowRunAsyncDisabled = isFeatureEnabled(
|
||||
FeatureFlag.ForceSqlLabRunAsync,
|
||||
);
|
||||
const [activeKey, setActiveKey] = useState<string[] | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!expandableModalIsOpen && activeKey !== undefined) {
|
||||
setActiveKey(undefined);
|
||||
}
|
||||
}, [expandableModalIsOpen, activeKey]);
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
expandIconPosition="end"
|
||||
accordion
|
||||
modalMode
|
||||
activeKey={activeKey}
|
||||
onChange={key => setActiveKey(key)}
|
||||
items={[
|
||||
{
|
||||
key: 'sql-lab',
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { JsonObject, SupersetClient } from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import { TagType } from 'src/components';
|
||||
import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
|
||||
export const OBJECT_TYPES_VALUES = Object.freeze([
|
||||
'dashboard',
|
||||
@@ -93,7 +94,11 @@ export function fetchTags(
|
||||
endpoint: `/api/v1/${objectType}/${objectId}`,
|
||||
})
|
||||
.then(({ json }) =>
|
||||
callback(json.result.tags.filter((tag: TagType) => tag.type === 1)),
|
||||
callback(
|
||||
json.result.tags.filter(
|
||||
(tag: TagType) => tag.type === TagTypeEnum.Custom,
|
||||
),
|
||||
),
|
||||
)
|
||||
.catch(response => error(response));
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import Chart from 'src/types/Chart';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
|
||||
import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
import { loadTags } from 'src/components/Tag/utils';
|
||||
import ChartCard from 'src/features/charts/ChartCard';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
@@ -417,7 +418,8 @@ function ChartList(props: ChartListProps) {
|
||||
<TagsList
|
||||
tags={tags.filter((tag: TagType) =>
|
||||
tag.type
|
||||
? tag.type === 1 || tag.type === 'TagTypes.custom'
|
||||
? tag.type === TagTypeEnum.Custom ||
|
||||
tag.type === 'TagTypes.custom'
|
||||
: true,
|
||||
)}
|
||||
maxTags={3}
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
Dashboard as CRUDDashboard,
|
||||
QueryObjectColumns,
|
||||
} from 'src/views/CRUD/types';
|
||||
import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
import { loadTags } from 'src/components/Tag/utils';
|
||||
import DashboardCard from 'src/features/dashboards/DashboardCard';
|
||||
import { DashboardStatus } from 'src/features/dashboards/types';
|
||||
@@ -376,7 +377,8 @@ function DashboardList(props: DashboardListProps) {
|
||||
<TagsList
|
||||
tags={tags.filter(
|
||||
(tag: TagType) =>
|
||||
tag.type === 'TagTypes.custom' || tag.type === 1,
|
||||
tag.type === 'TagTypes.custom' ||
|
||||
tag.type === TagTypeEnum.Custom,
|
||||
)}
|
||||
maxTags={3}
|
||||
/>
|
||||
|
||||
@@ -58,6 +58,7 @@ import handleResourceExport from 'src/utils/export';
|
||||
import SubMenu, { ButtonProps, SubMenuProps } from 'src/features/home/SubMenu';
|
||||
import { commonMenuData } from 'src/features/home/commonMenuData';
|
||||
import { QueryObjectColumns, SavedQueryObject } from 'src/views/CRUD/types';
|
||||
import { TagTypeEnum } from 'src/components/Tag/TagType';
|
||||
import { loadTags } from 'src/components/Tag/utils';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
@@ -393,7 +394,11 @@ function SavedQueryList({
|
||||
},
|
||||
}: any) => (
|
||||
// Only show custom type tags
|
||||
<TagsList tags={tags.filter((tag: TagType) => tag.type === 1)} />
|
||||
<TagsList
|
||||
tags={tags.filter(
|
||||
(tag: TagType) => tag.type === TagTypeEnum.Custom,
|
||||
)}
|
||||
/>
|
||||
),
|
||||
Header: t('Tags'),
|
||||
accessor: 'tags',
|
||||
|
||||
@@ -101,13 +101,14 @@ export class ThemeController {
|
||||
const {
|
||||
storage = new LocalStorageAdapter(),
|
||||
modeStorageKey = STORAGE_KEYS.THEME_MODE,
|
||||
themeObject: fallbackThemeObject = supersetThemeObject,
|
||||
themeObject = supersetThemeObject,
|
||||
defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {},
|
||||
onChange = null,
|
||||
} = options;
|
||||
|
||||
this.storage = storage;
|
||||
this.modeStorageKey = modeStorageKey;
|
||||
this.themeObject = themeObject;
|
||||
|
||||
// Initialize bootstrap data and themes
|
||||
const {
|
||||
@@ -139,16 +140,14 @@ export class ThemeController {
|
||||
|
||||
// Initialize theme and mode
|
||||
this.currentMode = this.determineInitialMode();
|
||||
const { theme, themeObject } =
|
||||
this.createInitialThemeObject(fallbackThemeObject);
|
||||
|
||||
this.themeObject = themeObject;
|
||||
const initialTheme =
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme;
|
||||
|
||||
// Setup change callback
|
||||
if (onChange) this.onChangeCallbacks.add(onChange);
|
||||
|
||||
// Apply initial theme and persist mode
|
||||
this.applyTheme(theme);
|
||||
this.applyTheme(initialTheme);
|
||||
this.persistMode();
|
||||
}
|
||||
|
||||
@@ -158,7 +157,12 @@ export class ThemeController {
|
||||
* Cleans up listeners and references. Should be called when the controller is no longer needed.
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.mediaQuery.removeEventListener('change', this.handleSystemThemeChange);
|
||||
if (this.mediaQuery)
|
||||
this.mediaQuery.removeEventListener(
|
||||
'change',
|
||||
this.handleSystemThemeChange,
|
||||
);
|
||||
|
||||
this.onChangeCallbacks.clear();
|
||||
}
|
||||
|
||||
@@ -329,8 +333,12 @@ export class ThemeController {
|
||||
* Initializes media query listeners if OS preference is allowed
|
||||
*/
|
||||
private initializeMediaQueryListener(): void {
|
||||
this.mediaQuery = window.matchMedia(MEDIA_QUERY_DARK_SCHEME);
|
||||
this.mediaQuery.addEventListener('change', this.handleSystemThemeChange);
|
||||
try {
|
||||
this.mediaQuery = window.matchMedia(MEDIA_QUERY_DARK_SCHEME);
|
||||
this.mediaQuery.addEventListener('change', this.handleSystemThemeChange);
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize media query listener:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,9 +355,9 @@ export class ThemeController {
|
||||
settings: themeSettings,
|
||||
} = theme;
|
||||
|
||||
const hasValidDefault: boolean = this.hasKeys(defaultTheme);
|
||||
const hasValidDark: boolean = this.hasKeys(darkTheme);
|
||||
const hasValidSettings: boolean = this.hasKeys(themeSettings);
|
||||
const hasValidDefault: boolean = this.isNonEmptyObject(defaultTheme);
|
||||
const hasValidDark: boolean = this.isNonEmptyObject(darkTheme);
|
||||
const hasValidSettings: boolean = this.isNonEmptyObject(themeSettings);
|
||||
|
||||
return {
|
||||
bootstrapDefaultTheme: hasValidDefault ? defaultTheme : null,
|
||||
@@ -360,9 +368,11 @@ export class ThemeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an object has keys (not empty).
|
||||
* Checks if an object is non-empty (has at least one property).
|
||||
*/
|
||||
private hasKeys(obj: Record<string, any> | undefined | null): boolean {
|
||||
private isNonEmptyObject(
|
||||
obj: Record<string, any> | undefined | null,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
obj && typeof obj === 'object' && Object.keys(obj).length > 0,
|
||||
);
|
||||
@@ -417,28 +427,7 @@ export class ThemeController {
|
||||
? this.darkTheme || this.defaultTheme
|
||||
: this.defaultTheme;
|
||||
|
||||
return normalizeThemeConfig(selectedTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the initial theme object.
|
||||
* This sets the theme based on the current mode and ensures it has the correct algorithm.
|
||||
* @param defaultThemeObject - The fallback theme object to use if no theme is set
|
||||
* @returns An object containing the theme and the themeObject
|
||||
*/
|
||||
private createInitialThemeObject(defaultThemeObject: Theme): {
|
||||
theme: AnyThemeConfig;
|
||||
themeObject: Theme;
|
||||
} {
|
||||
let theme: AnyThemeConfig | null = this.getThemeForMode(this.currentMode);
|
||||
theme = theme || (defaultThemeObject.theme as AnyThemeConfig);
|
||||
|
||||
const normalizedTheme = this.normalizeTheme(theme);
|
||||
|
||||
return {
|
||||
theme: normalizedTheme,
|
||||
themeObject: Theme.fromConfig(normalizedTheme),
|
||||
};
|
||||
return selectedTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -230,13 +230,13 @@ describe('ThemeController', () => {
|
||||
expect(controller.getTheme()).toBe(mockThemeObject);
|
||||
});
|
||||
|
||||
it('should use BootsrapData themes when available', () => {
|
||||
it('should use BootstrapData themes when available', () => {
|
||||
controller = new ThemeController({
|
||||
themeObject: mockThemeObject,
|
||||
});
|
||||
|
||||
expect(mockThemeFromConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockThemeFromConfig).toHaveBeenCalledWith(
|
||||
expect(mockSetConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: expect.objectContaining({
|
||||
colorBgBase: '#ededed',
|
||||
@@ -246,7 +246,7 @@ describe('ThemeController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to Superset default theme when BootsrapData themes are empty', () => {
|
||||
it('should fallback to Superset default theme when BootstrapData themes are empty', () => {
|
||||
mockGetBootstrapData.mockReturnValue(
|
||||
createMockBootstrapData({
|
||||
default: {},
|
||||
@@ -267,8 +267,8 @@ describe('ThemeController', () => {
|
||||
defaultTheme: fallbackTheme,
|
||||
});
|
||||
|
||||
expect(mockThemeFromConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockThemeFromConfig).toHaveBeenCalledWith(
|
||||
expect(mockSetConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...fallbackTheme,
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-env node */
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
@@ -17,31 +16,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'react', 'prettier'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['build/**/*', '.docusaurus/**/*', 'node_modules/**/*'],
|
||||
};
|
||||
import { utils, writeFile } from 'xlsx';
|
||||
|
||||
export default function exportPivotExcel(
|
||||
tableSelector: string,
|
||||
fileName: string,
|
||||
) {
|
||||
const table = document.querySelector(tableSelector);
|
||||
const workbook = utils.table_to_book(table);
|
||||
writeFile(workbook, `${fileName}.xlsx`);
|
||||
}
|
||||
+15
-15
@@ -13,7 +13,7 @@
|
||||
"express": "~5.1.0",
|
||||
"http-errors": "~2.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "~1.10.0",
|
||||
"morgan": "~1.10.1",
|
||||
"pug": "~3.0.3"
|
||||
}
|
||||
},
|
||||
@@ -863,15 +863,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"dependencies": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-headers": "~1.0.2"
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -936,9 +936,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -2045,15 +2045,15 @@
|
||||
}
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"requires": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-headers": "~1.0.2"
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
@@ -2100,9 +2100,9 @@
|
||||
}
|
||||
},
|
||||
"on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"express": "~5.1.0",
|
||||
"http-errors": "~2.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "~1.10.0",
|
||||
"morgan": "~1.10.1",
|
||||
"pug": "~3.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,40 +87,40 @@ def cidr_translate_filter_func( # noqa: C901
|
||||
if operator in (FilterOperator.IN, FilterOperator.NOT_IN):
|
||||
dict_items = [val for val in values if isinstance(val, dict)]
|
||||
single_values = [val for val in values if not isinstance(val, dict)]
|
||||
if operator == FilterOperator.IN.value:
|
||||
if operator == FilterOperator.IN:
|
||||
cond = col.in_(single_values)
|
||||
for dictionary in dict_items:
|
||||
cond = cond | (col <= dictionary["end"]) & (col >= dictionary["start"])
|
||||
elif operator == FilterOperator.NOT_IN.value:
|
||||
elif operator == FilterOperator.NOT_IN:
|
||||
cond = ~(col.in_(single_values))
|
||||
for dictionary in dict_items:
|
||||
cond = cond & (col > dictionary["end"]) & (col < dictionary["start"])
|
||||
return_expression = cond
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if operator == FilterOperator.EQUALS.value:
|
||||
if operator == FilterOperator.EQUALS:
|
||||
return_expression = (
|
||||
col == value
|
||||
if not isinstance(value, dict)
|
||||
else (col <= value["end"]) & (col >= value["start"])
|
||||
)
|
||||
if operator == FilterOperator.GREATER_THAN_OR_EQUALS.value:
|
||||
if operator == FilterOperator.GREATER_THAN_OR_EQUALS:
|
||||
return_expression = (
|
||||
col >= value if not isinstance(value, dict) else col >= value["end"]
|
||||
)
|
||||
if operator == FilterOperator.GREATER_THAN.value:
|
||||
if operator == FilterOperator.GREATER_THAN:
|
||||
return_expression = (
|
||||
col > value if not isinstance(value, dict) else col > value["end"]
|
||||
)
|
||||
if operator == FilterOperator.LESS_THAN.value:
|
||||
if operator == FilterOperator.LESS_THAN:
|
||||
return_expression = (
|
||||
col < value if not isinstance(value, dict) else col < value["start"]
|
||||
)
|
||||
if operator == FilterOperator.LESS_THAN_OR_EQUALS.value:
|
||||
if operator == FilterOperator.LESS_THAN_OR_EQUALS:
|
||||
return_expression = (
|
||||
col <= value if not isinstance(value, dict) else col <= value["start"]
|
||||
)
|
||||
if operator == FilterOperator.NOT_EQUALS.value:
|
||||
if operator == FilterOperator.NOT_EQUALS:
|
||||
return_expression = (
|
||||
col != value
|
||||
if not isinstance(value, dict)
|
||||
|
||||
@@ -114,25 +114,25 @@ def port_translate_filter_func( # noqa: C901
|
||||
return_expression: Any
|
||||
if operator in (FilterOperator.IN, FilterOperator.NOT_IN):
|
||||
vals_list = itertools.chain.from_iterable(values)
|
||||
if operator == FilterOperator.IN.value:
|
||||
if operator == FilterOperator.IN:
|
||||
cond = col.in_(vals_list)
|
||||
elif operator == FilterOperator.NOT_IN.value:
|
||||
elif operator == FilterOperator.NOT_IN:
|
||||
cond = ~(col.in_(vals_list))
|
||||
return_expression = cond
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
value.sort()
|
||||
if operator == FilterOperator.EQUALS.value:
|
||||
if operator == FilterOperator.EQUALS:
|
||||
return_expression = col.in_(value)
|
||||
if operator == FilterOperator.GREATER_THAN_OR_EQUALS.value:
|
||||
if operator == FilterOperator.GREATER_THAN_OR_EQUALS:
|
||||
return_expression = col >= value[0]
|
||||
if operator == FilterOperator.GREATER_THAN.value:
|
||||
if operator == FilterOperator.GREATER_THAN:
|
||||
return_expression = col > value[0]
|
||||
if operator == FilterOperator.LESS_THAN.value:
|
||||
if operator == FilterOperator.LESS_THAN:
|
||||
return_expression = col < value[-1]
|
||||
if operator == FilterOperator.LESS_THAN_OR_EQUALS.value:
|
||||
if operator == FilterOperator.LESS_THAN_OR_EQUALS:
|
||||
return_expression = col <= value[-1]
|
||||
if operator == FilterOperator.NOT_EQUALS.value:
|
||||
if operator == FilterOperator.NOT_EQUALS:
|
||||
return_expression = ~col.in_(value)
|
||||
return return_expression
|
||||
|
||||
|
||||
@@ -541,9 +541,7 @@ class QueryContextProcessor:
|
||||
# Lets find the first temporal filter in the filters array and change
|
||||
# its val to be the result of get_since_until with the offset
|
||||
for flt in query_object_clone.filter:
|
||||
if flt.get(
|
||||
"op"
|
||||
) == FilterOperator.TEMPORAL_RANGE.value and isinstance(
|
||||
if flt.get("op") == FilterOperator.TEMPORAL_RANGE and isinstance(
|
||||
flt.get("val"), str
|
||||
):
|
||||
time_range = cast(str, flt.get("val"))
|
||||
|
||||
@@ -66,7 +66,7 @@ def get_since_until_from_query_object(
|
||||
|
||||
time_range = None
|
||||
for flt in query_object.filter:
|
||||
if flt.get("op") == FilterOperator.TEMPORAL_RANGE.value and isinstance(
|
||||
if flt.get("op") == FilterOperator.TEMPORAL_RANGE and isinstance(
|
||||
flt.get("val"), str
|
||||
):
|
||||
time_range = cast(str, flt.get("val"))
|
||||
|
||||
+33
-2
@@ -422,6 +422,28 @@ class D3Format(TypedDict, total=False):
|
||||
|
||||
D3_FORMAT: D3Format = {}
|
||||
|
||||
# Override the default mapbox tiles
|
||||
# Default values are equivalent to
|
||||
# DECKGL_BASE_MAP = [
|
||||
# ['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
|
||||
# ['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
|
||||
# ['mapbox://styles/mapbox/streets-v9', 'Streets'],
|
||||
# ['mapbox://styles/mapbox/dark-v9', 'Dark'],
|
||||
# ['mapbox://styles/mapbox/light-v9', 'Light'],
|
||||
# ['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets'],
|
||||
# ['mapbox://styles/mapbox/satellite-v9', 'Satellite'],
|
||||
# ['mapbox://styles/mapbox/outdoors-v9', 'Outdoors'],
|
||||
# ]
|
||||
# for adding your own map tiles, you can use the following format:
|
||||
# - tile:// + your_personal_url or openstreetmap_url
|
||||
# example:
|
||||
# DECKGL_BASE_MAP = [
|
||||
# ['tile://https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 'OpenStreetMap']
|
||||
# ]
|
||||
# Enable CORS and set map url in origins option.
|
||||
# Add also map url in connect-src of TALISMAN_CONFIG variable
|
||||
DECKGL_BASE_MAP: list[list[str, str]] = None
|
||||
|
||||
|
||||
# Override the default d3 locale for time format
|
||||
# Default values are equivalent to
|
||||
@@ -877,8 +899,13 @@ STORE_CACHE_KEYS_IN_METADATA_DB = False
|
||||
# CORS Options
|
||||
# NOTE: enabling this requires installing the cors-related python dependencies
|
||||
# `pip install .[cors]` or `pip install apache_superset[cors]`, depending
|
||||
ENABLE_CORS = False
|
||||
CORS_OPTIONS: dict[Any, Any] = {}
|
||||
ENABLE_CORS = True
|
||||
CORS_OPTIONS: dict[Any, Any] = {
|
||||
"origins": [
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
]
|
||||
}
|
||||
|
||||
# Sanitizes the HTML content used in markdowns to allow its rendering in a safe manner.
|
||||
# Disabling this option is not recommended for security reasons. If you wish to allow
|
||||
@@ -1707,6 +1734,8 @@ TALISMAN_CONFIG = {
|
||||
"'self'",
|
||||
"https://api.mapbox.com",
|
||||
"https://events.mapbox.com",
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
],
|
||||
"object-src": "'none'",
|
||||
"style-src": [
|
||||
@@ -1739,6 +1768,8 @@ TALISMAN_DEV_CONFIG = {
|
||||
"'self'",
|
||||
"https://api.mapbox.com",
|
||||
"https://events.mapbox.com",
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
],
|
||||
"object-src": "'none'",
|
||||
"style-src": [
|
||||
|
||||
@@ -164,7 +164,9 @@ class DatasourceKind(StrEnum):
|
||||
PHYSICAL = "physical"
|
||||
|
||||
|
||||
class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable=too-many-public-methods
|
||||
class BaseDatasource(
|
||||
AuditMixinNullable, ImportExportMixin
|
||||
): # pylint: disable=too-many-public-methods
|
||||
"""A common interface to objects that are queryable
|
||||
(tables and datasources)"""
|
||||
|
||||
@@ -1778,7 +1780,9 @@ class SqlaTable(
|
||||
def default_query(qry: Query) -> Query:
|
||||
return qry.filter_by(is_sqllab_view=False)
|
||||
|
||||
def has_extra_cache_key_calls(self, query_obj: QueryObjectDict) -> bool: # noqa: C901
|
||||
def has_extra_cache_key_calls(
|
||||
self, query_obj: QueryObjectDict
|
||||
) -> bool: # noqa: C901
|
||||
"""
|
||||
Detects the presence of calls to `ExtraCache` methods in items in query_obj that
|
||||
can be templated. If any are present, the query must be evaluated to extract
|
||||
|
||||
@@ -75,12 +75,11 @@ class DatasetDAO(BaseDAO[SqlaTable]):
|
||||
database: Database,
|
||||
table: Table,
|
||||
) -> bool:
|
||||
try:
|
||||
database.get_table(table)
|
||||
return True
|
||||
except SQLAlchemyError as ex: # pragma: no cover
|
||||
logger.warning("Got an error %s validating table: %s", str(ex), table)
|
||||
return False
|
||||
with database.get_inspector(
|
||||
catalog=table.catalog,
|
||||
schema=table.schema,
|
||||
) as inspector:
|
||||
return database.db_engine_spec.has_table(database, inspector, table)
|
||||
|
||||
@staticmethod
|
||||
def validate_uniqueness(
|
||||
|
||||
@@ -30,6 +30,7 @@ from typing import (
|
||||
cast,
|
||||
ContextManager,
|
||||
NamedTuple,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
TypedDict,
|
||||
Union,
|
||||
@@ -54,7 +55,7 @@ from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.engine.url import URL
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql import literal_column, quoted_name, text
|
||||
from sqlalchemy.sql.expression import ColumnClause, Select, TextClause
|
||||
from sqlalchemy.sql.expression import BinaryExpression, ColumnClause, Select, TextClause
|
||||
from sqlalchemy.types import TypeEngine
|
||||
|
||||
from superset import db
|
||||
@@ -62,6 +63,10 @@ from superset.constants import QUERY_CANCEL_KEY, TimeGrain as TimeGrainConstants
|
||||
from superset.databases.utils import get_table_metadata, make_url_safe
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import OAuth2Error, OAuth2RedirectError
|
||||
from superset.extensions.semantic_layer import (
|
||||
get_sqla_type_from_dimension_type,
|
||||
SemanticLayer,
|
||||
)
|
||||
from superset.sql.parse import (
|
||||
BaseSQLStatement,
|
||||
LimitMethod,
|
||||
@@ -85,7 +90,7 @@ from superset.utils.network import is_hostname_valid, is_port_open
|
||||
from superset.utils.oauth2 import encode_oauth2_state
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.connectors.sqla.models import TableColumn
|
||||
from superset.connectors.sqla.models import SqlaTable, TableColumn
|
||||
from superset.databases.schemas import TableMetadataResponse
|
||||
from superset.models.core import Database
|
||||
from superset.models.sql_lab import Query
|
||||
@@ -106,6 +111,15 @@ logger = logging.getLogger()
|
||||
GenericDBException = Exception
|
||||
|
||||
|
||||
class ValidColumnsType(TypedDict):
|
||||
"""
|
||||
Type for valid columns returned by `get_valid_metrics_and_dimensions`.
|
||||
"""
|
||||
|
||||
dimensions: set[str]
|
||||
metrics: set[str]
|
||||
|
||||
|
||||
def convert_inspector_columns(cols: list[SQLAColumnType]) -> list[ResultSetColumnType]:
|
||||
result_set_columns: list[ResultSetColumnType] = []
|
||||
for col in cols:
|
||||
@@ -143,7 +157,9 @@ builtin_time_grains: dict[str | None, str] = {
|
||||
}
|
||||
|
||||
|
||||
class TimestampExpression(ColumnClause): # pylint: disable=abstract-method, too-many-ancestors
|
||||
class TimestampExpression(
|
||||
ColumnClause
|
||||
): # pylint: disable=abstract-method, too-many-ancestors
|
||||
def __init__(self, expr: str, col: ColumnClause, **kwargs: Any) -> None:
|
||||
"""Sqlalchemy class that can be used to render native column elements respecting
|
||||
engine-specific quoting rules as part of a string-based expression.
|
||||
@@ -214,6 +230,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
"engine+driver://user:password@host:port/dbname[?key=value&key=value...]"
|
||||
)
|
||||
|
||||
# databases can optionally specify a semantic layer
|
||||
semantic_layer: Type[SemanticLayer] | None = None
|
||||
|
||||
disable_ssh_tunneling = False
|
||||
|
||||
_date_trunc_functions: dict[str, str] = {}
|
||||
@@ -377,6 +396,10 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
disallow_uri_query_params: dict[str, set[str]] = {}
|
||||
# A Dict of query parameters that will always be used on every connection
|
||||
# by driver name
|
||||
|
||||
# Whether to use equality operators (= true/false) instead of IS operators
|
||||
# for boolean filters. Some databases like Snowflake don't support IS true/false
|
||||
use_equality_for_boolean_filters = False
|
||||
enforce_uri_query_params: dict[str, dict[str, Any]] = {}
|
||||
|
||||
force_column_alias_quotes = False
|
||||
@@ -384,9 +407,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
max_column_name_length: int | None = None
|
||||
try_remove_schema_from_table_name = True # pylint: disable=invalid-name
|
||||
run_multiple_statements_as_one = False
|
||||
custom_errors: dict[
|
||||
Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]
|
||||
] = {}
|
||||
custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]] = (
|
||||
{}
|
||||
)
|
||||
|
||||
# List of JSON path to fields in `encrypted_extra` that should be masked when the
|
||||
# database is edited. By default everything is masked.
|
||||
@@ -1202,6 +1225,78 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def handle_boolean_filter(
|
||||
cls, sqla_col: Any, op: str, value: bool
|
||||
) -> BinaryExpression:
|
||||
"""
|
||||
Handle boolean filter operations with engine-specific logic.
|
||||
|
||||
By default, uses SQLAlchemy's IS operator (column IS true/false).
|
||||
Engines that don't support IS for boolean values can override
|
||||
use_equality_for_boolean_filters to use equality operators instead.
|
||||
|
||||
:param sqla_col: SQLAlchemy column element
|
||||
:param op: Filter operator (IS_TRUE or IS_FALSE)
|
||||
:param value: Boolean value (True or False)
|
||||
:return: SQLAlchemy expression for the boolean filter
|
||||
"""
|
||||
if cls.use_equality_for_boolean_filters:
|
||||
return sqla_col == value
|
||||
else:
|
||||
return sqla_col.is_(value)
|
||||
|
||||
@classmethod
|
||||
def handle_null_filter(
|
||||
cls,
|
||||
sqla_col: Any,
|
||||
op: utils.FilterOperator,
|
||||
) -> BinaryExpression:
|
||||
"""
|
||||
Handle null/not null filter operations.
|
||||
|
||||
:param sqla_col: SQLAlchemy column element
|
||||
:param op: Filter operator (IS_NULL or IS_NOT_NULL)
|
||||
:return: SQLAlchemy expression for the null filter
|
||||
"""
|
||||
from superset.utils import core as utils
|
||||
|
||||
if op == utils.FilterOperator.IS_NULL:
|
||||
return sqla_col.is_(None)
|
||||
elif op == utils.FilterOperator.IS_NOT_NULL:
|
||||
return sqla_col.isnot(None)
|
||||
else:
|
||||
raise ValueError(f"Invalid null filter operator: {op}")
|
||||
|
||||
@classmethod
|
||||
def handle_comparison_filter(
|
||||
cls, sqla_col: Any, op: utils.FilterOperator, value: Any
|
||||
) -> BinaryExpression:
|
||||
"""
|
||||
Handle comparison filter operations (=, !=, >, <, >=, <=).
|
||||
|
||||
:param sqla_col: SQLAlchemy column element
|
||||
:param op: Filter operator
|
||||
:param value: Filter value
|
||||
:return: SQLAlchemy expression for the comparison filter
|
||||
"""
|
||||
from superset.utils import core as utils
|
||||
|
||||
if op == utils.FilterOperator.EQUALS:
|
||||
return sqla_col == value
|
||||
elif op == utils.FilterOperator.NOT_EQUALS:
|
||||
return sqla_col != value
|
||||
elif op == utils.FilterOperator.GREATER_THAN:
|
||||
return sqla_col > value
|
||||
elif op == utils.FilterOperator.LESS_THAN:
|
||||
return sqla_col < value
|
||||
elif op == utils.FilterOperator.GREATER_THAN_OR_EQUALS:
|
||||
return sqla_col >= value
|
||||
elif op == utils.FilterOperator.LESS_THAN_OR_EQUALS:
|
||||
return sqla_col <= value
|
||||
else:
|
||||
raise ValueError(f"Invalid comparison filter operator: {op}")
|
||||
|
||||
@classmethod
|
||||
def handle_cursor(cls, cursor: Any, query: Query) -> None:
|
||||
"""Handle a live cursor between the execute and fetchall calls
|
||||
@@ -1385,8 +1480,32 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
|
||||
if schema and cls.try_remove_schema_from_table_name:
|
||||
tables = {re.sub(f"^{schema}\\.", "", table) for table in tables}
|
||||
|
||||
# add semantic views as tables too
|
||||
if cls.semantic_layer:
|
||||
semantic_layer = cls.semantic_layer(inspector.engine)
|
||||
tables.update(
|
||||
semantic_view.name
|
||||
for semantic_view in semantic_layer.get_semantic_views()
|
||||
)
|
||||
|
||||
return tables
|
||||
|
||||
@classmethod
|
||||
def has_table(
|
||||
cls,
|
||||
database: Database,
|
||||
inspector: Inspector,
|
||||
table: Table,
|
||||
) -> bool:
|
||||
if cls.semantic_layer:
|
||||
semantic_layer = cls.semantic_layer(inspector.engine)
|
||||
semantic_views = semantic_layer.get_semantic_views()
|
||||
if table.table in {semantic_view.name for semantic_view in semantic_views}:
|
||||
return True
|
||||
|
||||
return inspector.has_table(table.table, table.schema)
|
||||
|
||||
@classmethod
|
||||
def get_view_names( # pylint: disable=unused-argument
|
||||
cls,
|
||||
@@ -1460,6 +1579,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
@classmethod
|
||||
def get_columns( # pylint: disable=unused-argument
|
||||
cls,
|
||||
database: Database,
|
||||
inspector: Inspector,
|
||||
table: Table,
|
||||
options: dict[str, Any] | None = None,
|
||||
@@ -1467,7 +1587,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Get all columns from a given schema and table.
|
||||
|
||||
The inspector will be bound to a catalog, if one was specified.
|
||||
The inspector will be bound to a catalog, if one was specified. If the database
|
||||
supports semantic layers the method will check if the table is a semantic view,
|
||||
and return columns (metrics and dimensions) from it instead.
|
||||
|
||||
:param inspector: SqlAlchemy Inspector instance
|
||||
:param table: Table instance
|
||||
@@ -1475,6 +1597,26 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
some databases
|
||||
:return: All columns in table
|
||||
"""
|
||||
if cls.semantic_layer:
|
||||
semantic_layer = cls.semantic_layer(inspector.engine)
|
||||
semantic_views = {
|
||||
semantic_view.name: semantic_view
|
||||
for semantic_view in semantic_layer.get_semantic_views()
|
||||
}
|
||||
if semantic_view := semantic_views.get(table.table):
|
||||
dialect = database.get_dialect()
|
||||
return [
|
||||
{
|
||||
"name": dimension.name,
|
||||
"column_name": dimension.name,
|
||||
"type": cls.column_datatype_to_string(
|
||||
get_sqla_type_from_dimension_type(dimension.type),
|
||||
dialect,
|
||||
),
|
||||
}
|
||||
for dimension in semantic_layer.get_dimensions(semantic_view)
|
||||
]
|
||||
|
||||
return convert_inspector_columns(
|
||||
cast(
|
||||
list[SQLAColumnType],
|
||||
@@ -1492,6 +1634,22 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Get all metrics from a given schema and table.
|
||||
"""
|
||||
if cls.semantic_layer:
|
||||
semantic_layer = cls.semantic_layer(inspector.engine)
|
||||
semantic_views = {
|
||||
semantic_view.name: semantic_view
|
||||
for semantic_view in semantic_layer.get_semantic_views()
|
||||
}
|
||||
if semantic_view := semantic_views.get(table.table):
|
||||
return [
|
||||
{
|
||||
"metric_name": metric.name,
|
||||
"verbose_name": metric.name,
|
||||
"expression": metric.sql,
|
||||
}
|
||||
for metric in semantic_layer.get_metrics(semantic_view)
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
"metric_name": "count",
|
||||
@@ -1501,6 +1659,62 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
}
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_valid_metrics_and_dimensions(
|
||||
cls,
|
||||
database: Database,
|
||||
table: SqlaTable,
|
||||
dimensions: set[str],
|
||||
metrics: set[str],
|
||||
) -> ValidColumnsType:
|
||||
"""
|
||||
Get valid metrics and dimensions.
|
||||
|
||||
Given a datasource, and sets of selected metrics and dimensions, return the
|
||||
sets of valid metrics and dimensions that can further be selected.
|
||||
"""
|
||||
if cls.semantic_layer:
|
||||
with database.get_sqla_engine() as engine:
|
||||
semantic_layer = cls.semantic_layer(engine)
|
||||
semantic_views = {
|
||||
semantic_view.name: semantic_view
|
||||
for semantic_view in semantic_layer.get_semantic_views()
|
||||
}
|
||||
if semantic_view := semantic_views.get(table.table):
|
||||
selected_metrics = {
|
||||
metric
|
||||
for metric in semantic_layer.get_metrics(semantic_view)
|
||||
if metric.name in metrics
|
||||
}
|
||||
selected_dimensions = {
|
||||
dimension
|
||||
for dimension in semantic_layer.get_dimensions(semantic_view)
|
||||
if dimension.name in dimensions
|
||||
}
|
||||
return {
|
||||
"metrics": {
|
||||
metric.name
|
||||
for metric in semantic_layer.get_valid_metrics(
|
||||
semantic_view,
|
||||
selected_metrics,
|
||||
selected_dimensions,
|
||||
)
|
||||
},
|
||||
"dimensions": {
|
||||
dimension.name
|
||||
for dimension in semantic_layer.get_valid_dimensions(
|
||||
semantic_view,
|
||||
selected_metrics,
|
||||
selected_dimensions,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"dimensions": {column.column_name for column in table.columns},
|
||||
"metrics": {metric.metric_name for metric in table.metrics},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def where_latest_partition( # pylint: disable=unused-argument
|
||||
cls,
|
||||
@@ -1767,6 +1981,11 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
:param kwargs: kwargs to be passed to cursor.execute()
|
||||
:return:
|
||||
"""
|
||||
if cls.semantic_layer:
|
||||
with cls.get_engine(database, schema="tpcds_sf10tcl") as engine:
|
||||
semantic_layer = cls.semantic_layer(engine)
|
||||
query = semantic_layer.get_query_from_standard_sql(query).sql
|
||||
|
||||
if cls.arraysize:
|
||||
cursor.arraysize = cls.arraysize
|
||||
try:
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from re import Pattern
|
||||
from typing import Any, Optional, TYPE_CHECKING, TypedDict
|
||||
from typing import Any, Iterator, Optional, TYPE_CHECKING, TypedDict
|
||||
from urllib import parse
|
||||
|
||||
from apispec import APISpec
|
||||
@@ -30,20 +32,48 @@ from cryptography.hazmat.primitives import serialization
|
||||
from flask import current_app
|
||||
from flask_babel import gettext as __
|
||||
from marshmallow import fields, Schema
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy import text, types
|
||||
from sqlalchemy.engine.interfaces import Dialect
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.engine.url import URL
|
||||
from sqlglot import exp, parse_one
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.databases.utils import make_url_safe
|
||||
from superset.db_engine_specs.base import BaseEngineSpec, BasicPropertiesType
|
||||
from superset.db_engine_specs.postgres import PostgresBaseEngineSpec
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.extensions.semantic_layer import (
|
||||
BINARY,
|
||||
BOOLEAN,
|
||||
Column as SemanticColumn,
|
||||
DATE,
|
||||
DATETIME,
|
||||
DECIMAL,
|
||||
Dimension as SemanticDimension,
|
||||
Filter as SemanticFilter,
|
||||
INTEGER,
|
||||
Metric as SemanticMetric,
|
||||
NoSort,
|
||||
NUMBER,
|
||||
OBJECT,
|
||||
Query as SemanticQuery,
|
||||
SemanticView,
|
||||
Sort as SemanticSort,
|
||||
SortDirectionEnum,
|
||||
STRING,
|
||||
Table as SemanticTable,
|
||||
TIME,
|
||||
Type as SemanticType,
|
||||
)
|
||||
from superset.models.sql_lab import Query
|
||||
from superset.sql.parse import Table
|
||||
from superset.utils import json
|
||||
from superset.utils.core import get_user_agent, QuerySource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine.base import Engine
|
||||
|
||||
from superset.models.core import Database
|
||||
|
||||
# Regular expressions to catch custom errors
|
||||
@@ -77,16 +107,318 @@ class SnowflakeParametersType(TypedDict):
|
||||
warehouse: str
|
||||
|
||||
|
||||
class SnowflakeSemanticLayer:
|
||||
def __init__(self, engine: Engine) -> None:
|
||||
self.engine = engine
|
||||
|
||||
def execute(
|
||||
self,
|
||||
sql: str,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
with self.engine.connect() as connection:
|
||||
for row in connection.execute(text(sql), kwargs).mappings():
|
||||
yield dict(row)
|
||||
|
||||
def get_semantic_views(self) -> set[SemanticView]:
|
||||
sql = """
|
||||
SHOW SEMANTIC VIEWS
|
||||
->> SELECT "name" FROM $1;
|
||||
"""
|
||||
return {SemanticView(row["name"]) for row in self.execute(sql)}
|
||||
|
||||
def get_type(self, snowflake_type: str | None) -> type[SemanticType]:
|
||||
if snowflake_type is None:
|
||||
return STRING
|
||||
|
||||
type_map = {
|
||||
STRING: {r"VARCHAR\(\d+\)$", "STRING$", "TEXT$", r"CHAR\(\d+\)$"},
|
||||
INTEGER: {r"NUMBER\(38,\s?0\)$", "INT$", "INTEGER$", "BIGINT$"},
|
||||
DECIMAL: {r"NUMBER\(10,\s?2\)$"},
|
||||
NUMBER: {r"NUMBER\(\d+,\s?\d+\)$", "FLOAT$", "DOUBLE$"},
|
||||
BOOLEAN: {"BOOLEAN$"},
|
||||
DATE: {"DATE$"},
|
||||
DATETIME: {"TIMESTAMP_TZ$", "TIMESTAMP__NTZ$"},
|
||||
TIME: {"TIME$"},
|
||||
OBJECT: {"OBJECT$"},
|
||||
BINARY: {r"BINARY\(\d+\)$", r"VARBINARY\(\d+\)$"},
|
||||
}
|
||||
for semantic_type, patterns in type_map.items():
|
||||
if any(
|
||||
re.match(pattern, snowflake_type, re.IGNORECASE) for pattern in patterns
|
||||
):
|
||||
return semantic_type
|
||||
|
||||
return STRING
|
||||
|
||||
@classmethod
|
||||
def quote_table(cls, table: Table, dialect: Dialect) -> str:
|
||||
"""
|
||||
Fully quote a table name, including the schema and catalog.
|
||||
"""
|
||||
quoters = {
|
||||
"catalog": dialect.identifier_preparer.quote_schema,
|
||||
"schema": dialect.identifier_preparer.quote_schema,
|
||||
"table": dialect.identifier_preparer.quote,
|
||||
}
|
||||
|
||||
return ".".join(
|
||||
function(getattr(table, key))
|
||||
for key, function in quoters.items()
|
||||
if getattr(table, key)
|
||||
)
|
||||
|
||||
def get_metrics(self, semantic_view: SemanticView) -> set[SemanticMetric]:
|
||||
quoted_semantic_view_name = self.quote_table(
|
||||
Table(semantic_view.name),
|
||||
self.engine.dialect,
|
||||
)
|
||||
sql = f"""
|
||||
DESC SEMANTIC VIEW {quoted_semantic_view_name}
|
||||
->> SELECT "object_name", "property", "property_value"
|
||||
FROM $1
|
||||
WHERE
|
||||
"object_kind" = 'METRIC' AND
|
||||
"property" IN ('DATA_TYPE', 'TABLE');
|
||||
""" # noqa: S608 (semantic_view.name is quoted)
|
||||
rows = self.execute(sql)
|
||||
|
||||
metrics: set[SemanticMetric] = set()
|
||||
for name, group in itertools.groupby(rows, key=lambda x: x["object_name"]):
|
||||
attributes = defaultdict(set)
|
||||
for row in group:
|
||||
attributes[row["property"]].add(row["property_value"])
|
||||
|
||||
table = next(iter(attributes["TABLE"]))
|
||||
metric_name = table + "." + name
|
||||
type_ = self.get_type(next(iter(attributes["DATA_TYPE"])))
|
||||
sql = self.engine.dialect.identifier_preparer.quote(metric_name)
|
||||
tables = frozenset(attributes["TABLE"])
|
||||
join_columns = frozenset()
|
||||
|
||||
metrics.add(SemanticMetric(metric_name, type_, sql, tables, join_columns))
|
||||
|
||||
return metrics
|
||||
|
||||
def get_dimensions(self, semantic_view: SemanticView) -> set[SemanticDimension]:
|
||||
quoted_semantic_view_name = self.quote_table(
|
||||
Table(semantic_view.name),
|
||||
self.engine.dialect,
|
||||
)
|
||||
sql = f"""
|
||||
DESC SEMANTIC VIEW {quoted_semantic_view_name}
|
||||
->> SELECT "object_name", "property", "property_value"
|
||||
FROM $1
|
||||
WHERE
|
||||
"object_kind" = 'DIMENSION' AND
|
||||
"property" IN ('DATA_TYPE', 'TABLE');
|
||||
""" # noqa: S608 (semantic_view.name is quoted)
|
||||
rows = self.execute(sql)
|
||||
|
||||
dimensions: set[SemanticDimension] = set()
|
||||
for name, group in itertools.groupby(rows, key=lambda x: x["object_name"]):
|
||||
attributes = defaultdict(set)
|
||||
for row in group:
|
||||
attributes[row["property"]].add(row["property_value"])
|
||||
|
||||
table = next(iter(attributes["TABLE"]))
|
||||
dimension_name = table + "." + name
|
||||
column = SemanticColumn(SemanticTable(table), name)
|
||||
type_ = self.get_type(next(iter(attributes["DATA_TYPE"])))
|
||||
|
||||
dimensions.add(SemanticDimension(column, dimension_name, type_))
|
||||
|
||||
return dimensions
|
||||
|
||||
def get_valid_metrics(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[SemanticMetric],
|
||||
dimensions: set[SemanticDimension],
|
||||
) -> set[SemanticMetric]:
|
||||
# all metrics and dimensions are valid inside a given semantic view
|
||||
return self.get_metrics(semantic_view)
|
||||
|
||||
def get_valid_dimensions(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[SemanticMetric],
|
||||
dimensions: set[SemanticDimension],
|
||||
) -> set[SemanticDimension]:
|
||||
# all metrics and dimensions are valid inside a given semantic view
|
||||
return self.get_dimensions(semantic_view)
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[SemanticMetric],
|
||||
dimensions: set[SemanticDimension],
|
||||
filters: set[SemanticFilter],
|
||||
sort: SemanticSort = NoSort,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> SemanticQuery:
|
||||
ast = self.build_query(
|
||||
semantic_view,
|
||||
metrics,
|
||||
dimensions,
|
||||
filters,
|
||||
sort,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
return SemanticQuery(sql=ast.sql(dialect="snowflake", pretty=True))
|
||||
|
||||
def build_query(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[SemanticMetric],
|
||||
dimensions: set[SemanticDimension],
|
||||
filters: set[SemanticFilter],
|
||||
sort: SemanticSort = NoSort,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> exp.Select:
|
||||
semantic_view = exp.SemanticView(
|
||||
this=exp.Table(this=exp.Identifier(this=semantic_view.name, quoted=True)),
|
||||
dimensions=[
|
||||
exp.Column(
|
||||
this=exp.Identifier(this=dimension.column.name, quoted=True),
|
||||
table=exp.Identifier(
|
||||
this=dimension.column.relation.name,
|
||||
quoted=True,
|
||||
),
|
||||
)
|
||||
for dimension in dimensions
|
||||
],
|
||||
metrics=[
|
||||
exp.Column(
|
||||
this=exp.Identifier(this=column, quoted=True),
|
||||
table=exp.Identifier(this=table, quoted=True),
|
||||
)
|
||||
for table, column in (
|
||||
metric.name.split(".", 1)
|
||||
for metric in metrics
|
||||
if "." in metric.name
|
||||
)
|
||||
],
|
||||
)
|
||||
query = exp.Select(
|
||||
expressions=[exp.Star()],
|
||||
**{"from": exp.From(this=exp.Table(this=semantic_view))},
|
||||
)
|
||||
|
||||
if sort.items:
|
||||
order = [
|
||||
exp.Ordered(
|
||||
this=exp.Column(this=exp.Identifier(this=item.field.name)),
|
||||
desc=item.direction == SortDirectionEnum.DESC,
|
||||
nulls_first=item.nulls_first,
|
||||
)
|
||||
for item in sort.items
|
||||
]
|
||||
query.args["order"] = exp.Order(expressions=order)
|
||||
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query
|
||||
|
||||
def get_query_from_standard_sql(self, sql: str) -> SemanticQuery:
|
||||
"""
|
||||
Convert the Explore query into a proper query.
|
||||
|
||||
Explore will produce a pseudo-SQL query that references metrics and dimensions
|
||||
as if they were columns in a table. This method replaces the table name with a
|
||||
call to `SEMANTIC_VIEW`, and removes the `GROUP BY` clause, since all the
|
||||
aggregations happen inside the `SEMANTIC_VIEW` call.
|
||||
"""
|
||||
ast = parse_one(sql, "snowflake")
|
||||
table = ast.find(exp.Table)
|
||||
if not table:
|
||||
return SemanticQuery(sql=sql)
|
||||
|
||||
semantic_views = self.get_semantic_views()
|
||||
if table.name not in {semantic_view.name for semantic_view in semantic_views}:
|
||||
return SemanticQuery(sql=sql)
|
||||
|
||||
# collect all metric and dimensions
|
||||
semantic_view = SemanticView(table.name)
|
||||
all_metrics = self.get_metrics(semantic_view)
|
||||
all_dimensions = self.get_dimensions(semantic_view)
|
||||
|
||||
# collect metrics and dimensions used in the query
|
||||
columns = {column.name for column in ast.find_all(exp.Column)}
|
||||
metrics = [metric for metric in all_metrics if metric.name in columns]
|
||||
dimensions = [
|
||||
dimension for dimension in all_dimensions if dimension.name in columns
|
||||
]
|
||||
|
||||
# now replace table with a call to `SEMANTIC_VIEW`
|
||||
udtf = exp.Table(
|
||||
this=exp.SemanticView(
|
||||
this=exp.Table(
|
||||
this=exp.Identifier(this=semantic_view.name, quoted=True)
|
||||
),
|
||||
metrics=[
|
||||
exp.Column(
|
||||
this=exp.Identifier(this=column, quoted=True),
|
||||
table=exp.Identifier(this=table, quoted=True),
|
||||
)
|
||||
for table, column in (
|
||||
metric.name.split(".", 1)
|
||||
for metric in metrics
|
||||
if "." in metric.name
|
||||
)
|
||||
],
|
||||
dimensions=[
|
||||
exp.Column(
|
||||
this=exp.Identifier(this=column, quoted=True),
|
||||
table=exp.Identifier(this=table, quoted=True),
|
||||
)
|
||||
for table, column in (
|
||||
dimension.name.split(".", 1)
|
||||
for dimension in dimensions
|
||||
if "." in dimension.name
|
||||
)
|
||||
],
|
||||
),
|
||||
alias=exp.TableAlias(
|
||||
this=exp.Identifier(this="table_alias", quoted=False),
|
||||
columns=[
|
||||
exp.Identifier(this=column.name, quoted=True)
|
||||
for column in metrics + dimensions
|
||||
],
|
||||
),
|
||||
)
|
||||
table.replace(udtf)
|
||||
|
||||
# remove group by, since aggregations are done inside the `SEMANTIC_VIEW` call
|
||||
del ast.args["group"]
|
||||
|
||||
print("BETO")
|
||||
print(ast.sql(dialect="snowflake", pretty=True))
|
||||
return SemanticQuery(sql=ast.sql(dialect="snowflake", pretty=True))
|
||||
|
||||
|
||||
class SnowflakeEngineSpec(PostgresBaseEngineSpec):
|
||||
engine = "snowflake"
|
||||
engine_name = "Snowflake"
|
||||
force_column_alias_quotes = True
|
||||
max_column_name_length = 256
|
||||
|
||||
# Snowflake doesn't support IS true/false syntax, use = true/false instead
|
||||
use_equality_for_boolean_filters = True
|
||||
|
||||
parameters_schema = SnowflakeParametersSchema()
|
||||
default_driver = "snowflake"
|
||||
sqlalchemy_uri_placeholder = "snowflake://"
|
||||
|
||||
semantic_layer = SnowflakeSemanticLayer
|
||||
|
||||
supports_dynamic_schema = True
|
||||
supports_catalog = supports_dynamic_catalog = supports_cross_catalog_queries = True
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
"datasource": "5__table",
|
||||
"granularity_sqla": None,
|
||||
"groupby": [],
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"multiplier": 10,
|
||||
"point_radius_fixed": {"type": "metric", "value": "count"},
|
||||
"point_unit": "square_m",
|
||||
@@ -229,7 +229,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
"point_unit": "square_m",
|
||||
"row_limit": 5000,
|
||||
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
|
||||
"mapbox_style": "mapbox://styles/mapbox/dark-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"granularity_sqla": None,
|
||||
"size": "count",
|
||||
"viz_type": "deck_screengrid",
|
||||
@@ -263,7 +263,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
slice_data = {
|
||||
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
|
||||
"row_limit": 5000,
|
||||
"mapbox_style": "mapbox://styles/mapbox/streets-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"granularity_sqla": None,
|
||||
"size": "count",
|
||||
"viz_type": "deck_hex",
|
||||
@@ -300,7 +300,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
"autozoom": False,
|
||||
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
|
||||
"row_limit": 5000,
|
||||
"mapbox_style": "mapbox://styles/mapbox/satellite-streets-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"granularity_sqla": None,
|
||||
"size": "count",
|
||||
"viz_type": "deck_grid",
|
||||
@@ -367,7 +367,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
},
|
||||
"line_type": "json",
|
||||
"linear_color_scheme": "oranges",
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"viewport": {
|
||||
"longitude": -122.43388541747726,
|
||||
"latitude": 37.752020331384834,
|
||||
@@ -442,7 +442,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
"lonCol": "LONGITUDE_DEST",
|
||||
},
|
||||
"row_limit": 5000,
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"viewport": {
|
||||
"altitude": 1.5,
|
||||
"bearing": 8.546256357301871,
|
||||
@@ -486,7 +486,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
|
||||
"line_column": "path_json",
|
||||
"line_type": "json",
|
||||
"row_limit": 5000,
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"viewport": {
|
||||
"longitude": -122.18885402582598,
|
||||
"latitude": 37.73671752604488,
|
||||
|
||||
@@ -110,15 +110,15 @@ def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None
|
||||
"viz_type": "mapbox",
|
||||
"all_columns_x": "LON",
|
||||
"all_columns_y": "LAT",
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"mapbox_style": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"all_columns": ["occupancy"],
|
||||
"row_limit": 500000,
|
||||
}
|
||||
|
||||
logger.debug("Creating a slice")
|
||||
slc = Slice(
|
||||
slice_name="Mapbox Long/Lat",
|
||||
viz_type="mapbox",
|
||||
slice_name="OSM Long/Lat",
|
||||
viz_type="osm",
|
||||
datasource_type=DatasourceType.TABLE,
|
||||
datasource_id=tbl.id,
|
||||
params=get_slice_json(slice_data),
|
||||
|
||||
@@ -47,7 +47,7 @@ def load_misc_dashboard() -> None:
|
||||
"meta": {
|
||||
"chartId": 3969,
|
||||
"height": 69,
|
||||
"sliceName": "Mapbox Long/Lat",
|
||||
"sliceName": "OSM Long/Lat",
|
||||
"uuid": "164efe31-295b-4408-aaa6-2f4bfb58a212",
|
||||
"width": 4
|
||||
},
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import total_ordering
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.engine.base import Engine
|
||||
|
||||
|
||||
class Type:
|
||||
"""
|
||||
Base class for types.
|
||||
"""
|
||||
|
||||
|
||||
class INTEGER(Type):
|
||||
"""
|
||||
Represents an integer type.
|
||||
"""
|
||||
|
||||
|
||||
class NUMBER(Type):
|
||||
"""
|
||||
Represents a number type.
|
||||
"""
|
||||
|
||||
|
||||
class DECIMAL(Type):
|
||||
"""
|
||||
Represents a decimal type.
|
||||
"""
|
||||
|
||||
|
||||
class STRING(Type):
|
||||
"""
|
||||
Represents a string type.
|
||||
"""
|
||||
|
||||
|
||||
class BOOLEAN(Type):
|
||||
"""
|
||||
Represents a boolean type.
|
||||
"""
|
||||
|
||||
|
||||
class DATE(Type):
|
||||
"""
|
||||
Represents a date type.
|
||||
"""
|
||||
|
||||
|
||||
class TIME(Type):
|
||||
"""
|
||||
Represents a time type.
|
||||
"""
|
||||
|
||||
|
||||
class DATETIME(DATE, TIME):
|
||||
"""
|
||||
Represents a datetime type.
|
||||
"""
|
||||
|
||||
|
||||
class INTERVAL(Type):
|
||||
"""
|
||||
Represents an interval type.
|
||||
"""
|
||||
|
||||
|
||||
class OBJECT(Type):
|
||||
"""
|
||||
Represents an object type.
|
||||
"""
|
||||
|
||||
|
||||
class BINARY(Type):
|
||||
"""
|
||||
Represents a binary type.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemanticView:
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Relation:
|
||||
name: str
|
||||
schema: str | None = None
|
||||
catalog: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Table:
|
||||
name: str
|
||||
schema: str | None = None
|
||||
catalog: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class View:
|
||||
name: str
|
||||
sql: str
|
||||
schema: str | None = None
|
||||
catalog: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Virtual:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metric:
|
||||
name: str
|
||||
type: type[Type]
|
||||
sql: str
|
||||
tables: frozenset[Table]
|
||||
join_columns: frozenset[str]
|
||||
|
||||
|
||||
@total_ordering
|
||||
class ComparableEnum(enum.Enum):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, enum.Enum):
|
||||
return self.value == other.value
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
if isinstance(other, enum.Enum):
|
||||
return self.value < other.value
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.__class__, self.name))
|
||||
|
||||
|
||||
class TimeGrain(ComparableEnum):
|
||||
second = timedelta(seconds=1)
|
||||
minute = timedelta(minutes=1)
|
||||
hour = timedelta(hours=1)
|
||||
|
||||
|
||||
class DateGrain(ComparableEnum):
|
||||
day = timedelta(days=1)
|
||||
week = timedelta(weeks=1)
|
||||
month = timedelta(days=30)
|
||||
quarter = timedelta(days=90)
|
||||
year = timedelta(days=365)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Column:
|
||||
relation: Table | View | Virtual
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dimension:
|
||||
column: Column
|
||||
name: str
|
||||
type: type[Type]
|
||||
grain: TimeGrain | DateGrain | None = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
metadata = f"[{self.grain.name}]" if self.grain else ""
|
||||
return f"{self.type.__name__} {self.name} {metadata}".strip()
|
||||
|
||||
|
||||
class FilterTypeEnum(enum.Enum):
|
||||
WHERE = enum.auto()
|
||||
HAVING = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Filter:
|
||||
type: FilterTypeEnum
|
||||
expression: str
|
||||
|
||||
|
||||
class SortDirectionEnum(enum.Enum):
|
||||
ASC = enum.auto()
|
||||
DESC = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SortField:
|
||||
field: Metric | Dimension
|
||||
direction: SortDirectionEnum
|
||||
nulls_first: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Sort:
|
||||
items: list[SortField]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Query:
|
||||
sql: str
|
||||
|
||||
|
||||
NoSort = Sort(items=[])
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SemanticLayer(Protocol):
|
||||
"""
|
||||
A generic protocol for semantic layers.
|
||||
"""
|
||||
|
||||
def __init__(self, engine: Engine) -> None: ...
|
||||
|
||||
def get_semantic_views(self) -> set[SemanticView]:
|
||||
"""
|
||||
Return a set of the semantic views.
|
||||
|
||||
A semantic view is an organizational group of metrics and dimensions. It's not a
|
||||
logical grouping, since metrics and dimensions from a given semantic view might
|
||||
not be compatible. An implementation might expose a single semantic view for
|
||||
exploration of available metric and dimesnions, and smaller curated semantic
|
||||
views that are domain specific.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_metrics(self, semantic_view: SemanticView) -> set[Metric]:
|
||||
"""
|
||||
Return a set of metrics from a given semantic views.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_dimensions(self, semantic_view: SemanticView) -> set[Dimension]:
|
||||
"""
|
||||
Return a set of dimensions from a given semantic views.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_valid_metrics(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[Metric],
|
||||
dimensions: set[Dimension],
|
||||
) -> set[Metric]:
|
||||
"""
|
||||
Return compatible metrics for the given metrics and dimensions.
|
||||
|
||||
For metrics to be valid they must be compatible with all the provided
|
||||
dimensions.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_valid_dimensions(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[Metric],
|
||||
dimensions: set[Dimension],
|
||||
) -> set[Dimension]:
|
||||
"""
|
||||
Return compatible dimensions for the given metrics.
|
||||
|
||||
For dimensions to be valid they must be compatible with all the provided
|
||||
metrics.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
metrics: set[Metric],
|
||||
dimensions: set[Dimension],
|
||||
# populations: set[Population],
|
||||
filters: set[Filter],
|
||||
sort: Sort = NoSort,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> Query:
|
||||
"""
|
||||
Build a SQL query from the given metrics, dimensions, filters, and sort order.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_query_from_standard_sql(
|
||||
self,
|
||||
semantic_view: SemanticView,
|
||||
sql: str,
|
||||
) -> Query:
|
||||
"""
|
||||
Build a SQL query from a pseudo-query referencing metrics and dimensions.
|
||||
|
||||
For example, given `metric1` having the expression `COUNT(*)`, this query:
|
||||
|
||||
SELECT metric1, dim1
|
||||
FROM semantic_layer
|
||||
GROUP BY dim1
|
||||
|
||||
Becomes:
|
||||
|
||||
SELECT metric1, dim1
|
||||
FROM (
|
||||
SELECT COUNT(*) AS metric1, dim1
|
||||
FROM fact_table
|
||||
JOIN dim_table
|
||||
ON fact_table.dim_id = dim_table.id
|
||||
GROUP BY dim1
|
||||
) AS semantic_view
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
TYPE_MAPPING: dict[Type, type[sqltypes.TypeEngine]] = {
|
||||
# Numeric types
|
||||
INTEGER: sqltypes.Integer,
|
||||
NUMBER: sqltypes.Numeric,
|
||||
DECIMAL: sqltypes.DECIMAL,
|
||||
# String types
|
||||
STRING: sqltypes.String,
|
||||
# Boolean type
|
||||
BOOLEAN: sqltypes.Boolean,
|
||||
# Date/time types
|
||||
DATE: sqltypes.Date,
|
||||
TIME: sqltypes.Time,
|
||||
DATETIME: sqltypes.DateTime,
|
||||
INTERVAL: sqltypes.Interval,
|
||||
# Complex types
|
||||
OBJECT: sqltypes.JSON,
|
||||
BINARY: sqltypes.LargeBinary,
|
||||
}
|
||||
|
||||
|
||||
def get_sqla_type_from_dimension_type(
|
||||
dimension_type: Type,
|
||||
) -> sqltypes.TypeEngine:
|
||||
"""
|
||||
Get the SQLAlchemy type corresponding to the given dimension type.
|
||||
"""
|
||||
return TYPE_MAPPING.get(dimension_type, sqltypes.String)()
|
||||
@@ -409,8 +409,8 @@ class ExtraCache:
|
||||
# IS_NULL and IS_NOT_NULL operators do not have a value
|
||||
or op
|
||||
in (
|
||||
FilterOperator.IS_NULL.value,
|
||||
FilterOperator.IS_NOT_NULL.value,
|
||||
FilterOperator.IS_NULL,
|
||||
FilterOperator.IS_NOT_NULL,
|
||||
)
|
||||
)
|
||||
):
|
||||
@@ -421,8 +421,8 @@ class ExtraCache:
|
||||
self.applied_filters.append(column)
|
||||
|
||||
if op in (
|
||||
FilterOperator.IN.value,
|
||||
FilterOperator.NOT_IN.value,
|
||||
FilterOperator.IN,
|
||||
FilterOperator.NOT_IN,
|
||||
) and not isinstance(val, list):
|
||||
val = [val]
|
||||
|
||||
|
||||
+13
-8
@@ -126,7 +126,9 @@ class ConfigurationMethod(StrEnum):
|
||||
DYNAMIC_FORM = "dynamic_form"
|
||||
|
||||
|
||||
class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable=too-many-public-methods
|
||||
class Database(
|
||||
Model, AuditMixinNullable, ImportExportMixin
|
||||
): # pylint: disable=too-many-public-methods
|
||||
"""An ORM object that stores Database related information"""
|
||||
|
||||
__tablename__ = "dbs"
|
||||
@@ -400,9 +402,7 @@ class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable
|
||||
return (
|
||||
username
|
||||
if (username := get_username())
|
||||
else object_url.username
|
||||
if self.impersonate_user
|
||||
else None
|
||||
else object_url.username if self.impersonate_user else None
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
@@ -987,7 +987,10 @@ class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable
|
||||
schema=table.schema,
|
||||
) as inspector:
|
||||
return self.db_engine_spec.get_columns(
|
||||
inspector, table, self.schema_options
|
||||
self,
|
||||
inspector,
|
||||
table,
|
||||
self.schema_options,
|
||||
)
|
||||
|
||||
def get_metrics(
|
||||
@@ -1076,9 +1079,11 @@ class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable
|
||||
return self.perm
|
||||
|
||||
def has_table(self, table: Table) -> bool:
|
||||
with self.get_sqla_engine(catalog=table.catalog, schema=table.schema) as engine:
|
||||
# do not pass "" as an empty schema; force null
|
||||
return engine.has_table(table.table, table.schema or None)
|
||||
with self.get_inspector(
|
||||
catalog=table.catalog,
|
||||
schema=table.schema,
|
||||
) as inspector:
|
||||
return self.db_engine_spec.has_table(self, inspector, table)
|
||||
|
||||
def has_view(self, table: Table) -> bool:
|
||||
with self.get_sqla_engine(catalog=table.catalog, schema=table.schema) as engine:
|
||||
|
||||
+37
-31
@@ -1756,7 +1756,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
flt_col = flt["col"]
|
||||
val = flt.get("val")
|
||||
flt_grain = flt.get("grain")
|
||||
op = flt["op"].upper()
|
||||
op = utils.FilterOperator(flt["op"].upper())
|
||||
col_obj: Optional["TableColumn"] = None
|
||||
sqla_col: Optional[Column] = None
|
||||
if flt_col == utils.DTTM_ALIAS and is_timeseries and dttm_col:
|
||||
@@ -1790,8 +1790,8 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
col_type = col_obj.type if col_obj else None
|
||||
col_spec = db_engine_spec.get_column_spec(native_type=col_type)
|
||||
is_list_target = op in (
|
||||
utils.FilterOperator.IN.value,
|
||||
utils.FilterOperator.NOT_IN.value,
|
||||
utils.FilterOperator.IN,
|
||||
utils.FilterOperator.NOT_IN,
|
||||
)
|
||||
|
||||
col_advanced_data_type = col_obj.advanced_data_type if col_obj else ""
|
||||
@@ -1850,23 +1850,30 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
cond = is_null_cond
|
||||
else:
|
||||
cond = sqla_col.in_(eq)
|
||||
if op == utils.FilterOperator.NOT_IN.value:
|
||||
if op == utils.FilterOperator.NOT_IN:
|
||||
cond = ~cond
|
||||
where_clause_and.append(cond)
|
||||
elif op == utils.FilterOperator.IS_NULL.value:
|
||||
where_clause_and.append(sqla_col.is_(None))
|
||||
elif op == utils.FilterOperator.IS_NOT_NULL.value:
|
||||
where_clause_and.append(sqla_col.isnot(None))
|
||||
elif op == utils.FilterOperator.IS_TRUE.value:
|
||||
where_clause_and.append(sqla_col.is_(True))
|
||||
elif op == utils.FilterOperator.IS_FALSE.value:
|
||||
where_clause_and.append(sqla_col.is_(False))
|
||||
elif op in {
|
||||
utils.FilterOperator.IS_NULL,
|
||||
utils.FilterOperator.IS_NOT_NULL,
|
||||
}:
|
||||
where_clause_and.append(
|
||||
db_engine_spec.handle_null_filter(sqla_col, op)
|
||||
)
|
||||
elif op == utils.FilterOperator.IS_TRUE:
|
||||
where_clause_and.append(
|
||||
db_engine_spec.handle_boolean_filter(sqla_col, op, True)
|
||||
)
|
||||
elif op == utils.FilterOperator.IS_FALSE:
|
||||
where_clause_and.append(
|
||||
db_engine_spec.handle_boolean_filter(sqla_col, op, False)
|
||||
)
|
||||
else:
|
||||
if (
|
||||
op
|
||||
not in {
|
||||
utils.FilterOperator.EQUALS.value,
|
||||
utils.FilterOperator.NOT_EQUALS.value,
|
||||
utils.FilterOperator.EQUALS,
|
||||
utils.FilterOperator.NOT_EQUALS,
|
||||
}
|
||||
and eq is None
|
||||
):
|
||||
@@ -1876,36 +1883,35 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
"with comparison operators"
|
||||
)
|
||||
)
|
||||
if op == utils.FilterOperator.EQUALS.value:
|
||||
where_clause_and.append(sqla_col == eq)
|
||||
elif op == utils.FilterOperator.NOT_EQUALS.value:
|
||||
where_clause_and.append(sqla_col != eq)
|
||||
elif op == utils.FilterOperator.GREATER_THAN.value:
|
||||
where_clause_and.append(sqla_col > eq)
|
||||
elif op == utils.FilterOperator.LESS_THAN.value:
|
||||
where_clause_and.append(sqla_col < eq)
|
||||
elif op == utils.FilterOperator.GREATER_THAN_OR_EQUALS.value:
|
||||
where_clause_and.append(sqla_col >= eq)
|
||||
elif op == utils.FilterOperator.LESS_THAN_OR_EQUALS.value:
|
||||
where_clause_and.append(sqla_col <= eq)
|
||||
if op in {
|
||||
utils.FilterOperator.EQUALS,
|
||||
utils.FilterOperator.NOT_EQUALS,
|
||||
utils.FilterOperator.GREATER_THAN,
|
||||
utils.FilterOperator.LESS_THAN,
|
||||
utils.FilterOperator.GREATER_THAN_OR_EQUALS,
|
||||
utils.FilterOperator.LESS_THAN_OR_EQUALS,
|
||||
}:
|
||||
where_clause_and.append(
|
||||
db_engine_spec.handle_comparison_filter(sqla_col, op, eq)
|
||||
)
|
||||
elif op in {
|
||||
utils.FilterOperator.ILIKE.value,
|
||||
utils.FilterOperator.LIKE.value,
|
||||
utils.FilterOperator.ILIKE,
|
||||
utils.FilterOperator.LIKE,
|
||||
}:
|
||||
if target_generic_type != GenericDataType.STRING:
|
||||
sqla_col = sa.cast(sqla_col, sa.String)
|
||||
|
||||
if op == utils.FilterOperator.LIKE.value:
|
||||
if op == utils.FilterOperator.LIKE:
|
||||
where_clause_and.append(sqla_col.like(eq))
|
||||
else:
|
||||
where_clause_and.append(sqla_col.ilike(eq))
|
||||
elif op in {utils.FilterOperator.NOT_LIKE.value}:
|
||||
elif op in {utils.FilterOperator.NOT_LIKE}:
|
||||
if target_generic_type != GenericDataType.STRING:
|
||||
sqla_col = sa.cast(sqla_col, sa.String)
|
||||
|
||||
where_clause_and.append(sqla_col.not_like(eq))
|
||||
elif (
|
||||
op == utils.FilterOperator.TEMPORAL_RANGE.value
|
||||
op == utils.FilterOperator.TEMPORAL_RANGE
|
||||
and isinstance(eq, str)
|
||||
and col_obj is not None
|
||||
):
|
||||
|
||||
@@ -55,6 +55,7 @@ from superset.views.filters import (
|
||||
BaseFilterRelatedRoles,
|
||||
BaseFilterRelatedUsers,
|
||||
FilterRelatedOwners,
|
||||
FilterRelatedTables,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -135,6 +136,7 @@ class RLSRestApi(BaseSupersetModelRestApi):
|
||||
|
||||
allowed_rel_fields = {"tables", "roles", "created_by", "changed_by"}
|
||||
related_field_filters = {
|
||||
"tables": RelatedFieldFilter("table_name", FilterRelatedTables),
|
||||
"changed_by": RelatedFieldFilter("first_name", FilterRelatedOwners),
|
||||
}
|
||||
base_related_field_filters = {
|
||||
|
||||
@@ -410,6 +410,7 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument
|
||||
"d3_format": conf.get("D3_FORMAT"),
|
||||
"d3_time_format": conf.get("D3_TIME_FORMAT"),
|
||||
"currencies": conf.get("CURRENCIES"),
|
||||
"deckgl_tiles": conf.get("DECKGL_BASE_MAP"),
|
||||
"feature_flags": get_feature_flags(),
|
||||
"extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"],
|
||||
"extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"],
|
||||
|
||||
@@ -102,3 +102,22 @@ class BaseFilterRelatedRoles(BaseFilter): # pylint: disable=too-few-public-meth
|
||||
return extra_filters(query)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class FilterRelatedTables(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
A filter to allow searching for related tables.
|
||||
Use in the api by adding something like:
|
||||
related_field_filters = {
|
||||
"tables": RelatedFieldFilter("table_name", FilterRelatedTables),
|
||||
}
|
||||
"""
|
||||
|
||||
name = lazy_gettext("Table")
|
||||
arg_name = "tables"
|
||||
|
||||
def apply(self, query: Query, value: Optional[Any]) -> Query:
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
|
||||
like_value = "%" + cast(str, value) + "%"
|
||||
return query.filter(SqlaTable.table_name.ilike(like_value))
|
||||
|
||||
@@ -555,6 +555,30 @@ class TestRowLevelSecurityWithRelatedAPI(SupersetTestCase):
|
||||
assert len(result) == len(db_tables)
|
||||
assert db_table_names == received_tables
|
||||
|
||||
def test_rls_tables_related_api_with_filter_matching_birth(self):
|
||||
self.login(ADMIN_USERNAME)
|
||||
# Test with filter that should match 'birth_names'
|
||||
params = prison.dumps({"filter": "birth", "page": 0, "page_size": 100})
|
||||
rv = self.client.get(f"/api/v1/rowlevelsecurity/related/tables?q={params}")
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
result = data["result"]
|
||||
received_tables = {table["text"] for table in result}
|
||||
# Should only return tables with 'birth' in the name
|
||||
assert all("birth" in table_name.lower() for table_name in received_tables)
|
||||
assert len(result) >= 1 # At least birth_names should be returned
|
||||
|
||||
def test_rls_tables_related_api_with_filter_no_matches(self):
|
||||
self.login(ADMIN_USERNAME)
|
||||
# Test with filter that should match nothing
|
||||
params = prison.dumps({"filter": "nonexistent", "page": 0, "page_size": 100})
|
||||
rv = self.client.get(f"/api/v1/rowlevelsecurity/related/tables?q={params}")
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
result = data["result"]
|
||||
assert len(result) == 0
|
||||
assert data["count"] == 0
|
||||
|
||||
def test_rls_roles_related_api(self):
|
||||
self.login(ADMIN_USERNAME)
|
||||
params = prison.dumps({"page": 0, "page_size": 100})
|
||||
|
||||
@@ -749,12 +749,12 @@ def test_should_generate_closed_and_open_time_filter_range(login_as_admin):
|
||||
def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
||||
expected_results = [
|
||||
{
|
||||
"operator": FilterOperator.EQUALS.value,
|
||||
"operator": FilterOperator.EQUALS,
|
||||
"count": 10,
|
||||
"sql_should_contain": "COL4 IS NULL",
|
||||
},
|
||||
{
|
||||
"operator": FilterOperator.NOT_EQUALS.value,
|
||||
"operator": FilterOperator.NOT_EQUALS,
|
||||
"count": 0,
|
||||
"sql_should_contain": "COL4 IS NOT NULL",
|
||||
},
|
||||
@@ -1122,12 +1122,12 @@ def test__temporal_range_operator_in_adhoc_filter(physical_dataset):
|
||||
{
|
||||
"col": "col5",
|
||||
"val": "2000-01-05 : 2000-01-06",
|
||||
"op": FilterOperator.TEMPORAL_RANGE.value,
|
||||
"op": FilterOperator.TEMPORAL_RANGE,
|
||||
},
|
||||
{
|
||||
"col": "col6",
|
||||
"val": "2002-05-11 : 2002-05-12",
|
||||
"op": FilterOperator.TEMPORAL_RANGE.value,
|
||||
"op": FilterOperator.TEMPORAL_RANGE,
|
||||
},
|
||||
],
|
||||
"is_timeseries": False,
|
||||
|
||||
@@ -423,3 +423,119 @@ def test_impersonate_user_no_database(mocker: MockerFixture) -> None:
|
||||
"alice",
|
||||
"SECRET",
|
||||
)
|
||||
|
||||
|
||||
def test_handle_boolean_filter_default_behavior() -> None:
|
||||
"""
|
||||
Test that BaseEngineSpec uses IS operators for boolean filters by default.
|
||||
"""
|
||||
from sqlalchemy import Boolean, Column
|
||||
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
|
||||
# Create a mock SQLAlchemy column
|
||||
bool_col = Column("test_col", Boolean)
|
||||
|
||||
# Test IS_TRUE filter - should use IS operator by default
|
||||
result_true = BaseEngineSpec.handle_boolean_filter(bool_col, "IS TRUE", True)
|
||||
assert hasattr(result_true, "left") # IS comparison has left/right attributes
|
||||
assert hasattr(result_true, "right")
|
||||
|
||||
# Test IS_FALSE filter - should use IS operator by default
|
||||
result_false = BaseEngineSpec.handle_boolean_filter(bool_col, "IS FALSE", False)
|
||||
assert hasattr(result_false, "left")
|
||||
assert hasattr(result_false, "right")
|
||||
|
||||
|
||||
def test_handle_boolean_filter_with_equality() -> None:
|
||||
"""
|
||||
Test that BaseEngineSpec can use equality operators when configured.
|
||||
"""
|
||||
from sqlalchemy import Boolean, Column
|
||||
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
|
||||
# Create a test engine spec that uses equality
|
||||
class TestEngineSpec(BaseEngineSpec):
|
||||
use_equality_for_boolean_filters = True
|
||||
|
||||
bool_col = Column("test_col", Boolean)
|
||||
|
||||
# Test with equality enabled
|
||||
result_true = TestEngineSpec.handle_boolean_filter(bool_col, "IS TRUE", True)
|
||||
# Equality comparison should have different structure than IS comparison
|
||||
assert str(type(result_true)).endswith("BinaryExpression'>")
|
||||
|
||||
result_false = TestEngineSpec.handle_boolean_filter(bool_col, "IS FALSE", False)
|
||||
assert str(type(result_false)).endswith("BinaryExpression'>")
|
||||
|
||||
|
||||
def test_handle_null_filter() -> None:
|
||||
"""
|
||||
Test null/not null filter handling.
|
||||
"""
|
||||
from sqlalchemy import Boolean, Column
|
||||
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
|
||||
bool_col = Column("test_col", Boolean)
|
||||
|
||||
# Test IS_NULL - use actual FilterOperator values
|
||||
from superset.utils.core import FilterOperator
|
||||
|
||||
result_null = BaseEngineSpec.handle_null_filter(bool_col, FilterOperator.IS_NULL)
|
||||
assert hasattr(result_null, "left")
|
||||
assert hasattr(result_null, "right")
|
||||
|
||||
# Test IS_NOT_NULL
|
||||
result_not_null = BaseEngineSpec.handle_null_filter(
|
||||
bool_col, FilterOperator.IS_NOT_NULL
|
||||
)
|
||||
assert hasattr(result_not_null, "left")
|
||||
assert hasattr(result_not_null, "right")
|
||||
|
||||
# Test invalid operator
|
||||
with pytest.raises(ValueError, match="Invalid null filter operator"):
|
||||
BaseEngineSpec.handle_null_filter(bool_col, "INVALID") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_handle_comparison_filter() -> None:
|
||||
"""
|
||||
Test comparison filter handling for all operators.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer
|
||||
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
|
||||
int_col = Column("test_col", Integer)
|
||||
|
||||
# Test all comparison operators - use actual FilterOperator values
|
||||
from superset.utils.core import FilterOperator
|
||||
|
||||
operators_and_values = [
|
||||
(FilterOperator.EQUALS, 5),
|
||||
(FilterOperator.NOT_EQUALS, 5),
|
||||
(FilterOperator.GREATER_THAN, 5),
|
||||
(FilterOperator.LESS_THAN, 5),
|
||||
(FilterOperator.GREATER_THAN_OR_EQUALS, 5),
|
||||
(FilterOperator.LESS_THAN_OR_EQUALS, 5),
|
||||
]
|
||||
|
||||
for op, value in operators_and_values:
|
||||
result = BaseEngineSpec.handle_comparison_filter(int_col, op, value)
|
||||
# All comparison operators should return binary expressions
|
||||
assert str(type(result)).endswith("BinaryExpression'>")
|
||||
|
||||
# Test invalid operator
|
||||
with pytest.raises(ValueError, match="Invalid comparison filter operator"):
|
||||
BaseEngineSpec.handle_comparison_filter(int_col, "INVALID", 5) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_use_equality_for_boolean_filters_property() -> None:
|
||||
"""
|
||||
Test that BaseEngineSpec has the correct default value for boolean filter property.
|
||||
"""
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
|
||||
# Default should be False (use IS operators)
|
||||
assert BaseEngineSpec.use_equality_for_boolean_filters is False
|
||||
|
||||
@@ -352,6 +352,48 @@ def test_mask_encrypted_extra_no_fields() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_handle_boolean_filter() -> None:
|
||||
"""
|
||||
Test that Snowflake uses equality operators for boolean filters instead of IS.
|
||||
"""
|
||||
from sqlalchemy import Boolean, Column
|
||||
|
||||
from superset.db_engine_specs.snowflake import SnowflakeEngineSpec
|
||||
|
||||
# Create a mock SQLAlchemy column
|
||||
bool_col = Column("test_col", Boolean)
|
||||
|
||||
# Test IS_TRUE filter - use actual FilterOperator values
|
||||
from superset.utils.core import FilterOperator
|
||||
|
||||
result_true = SnowflakeEngineSpec.handle_boolean_filter(
|
||||
bool_col, FilterOperator.IS_TRUE, True
|
||||
)
|
||||
# The result should be a equality comparison, not an IS comparison
|
||||
assert (
|
||||
str(result_true.compile(compile_kwargs={"literal_binds": True}))
|
||||
== "test_col = true"
|
||||
)
|
||||
|
||||
# Test IS_FALSE filter
|
||||
result_false = SnowflakeEngineSpec.handle_boolean_filter(
|
||||
bool_col, FilterOperator.IS_FALSE, False
|
||||
)
|
||||
assert (
|
||||
str(result_false.compile(compile_kwargs={"literal_binds": True}))
|
||||
== "test_col = false"
|
||||
)
|
||||
|
||||
|
||||
def test_use_equality_for_boolean_filters_property() -> None:
|
||||
"""
|
||||
Test that Snowflake has the use_equality_for_boolean_filters property set to True.
|
||||
"""
|
||||
from superset.db_engine_specs.snowflake import SnowflakeEngineSpec
|
||||
|
||||
assert SnowflakeEngineSpec.use_equality_for_boolean_filters is True
|
||||
|
||||
|
||||
def test_unmask_encrypted_extra() -> None:
|
||||
"""
|
||||
Test that the private keys can be reused from the previous `encrypted_extra`.
|
||||
|
||||
Reference in New Issue
Block a user