Compare commits

..

16 Commits

Author SHA1 Message Date
hainenber
efd9141a38 test(frontend/db): add test to cover for executeQuery function
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-26 12:50:00 +07:00
hainenber
d0b422638e chore(ci): remove nyc usage for merging coverage results as Codecov natively supports the action
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-26 11:57:45 +07:00
dependabot[bot]
e3d3262e97 chore(deps-dev): bump grpcio from 1.81.1 to 1.83.0 (#42414)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-07-25 18:47:24 -07:00
Evan Rusackas
6c2ccc44bd chore(table): drop dead th.ant-column-cell selector (#42407)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:31:07 -07:00
dependabot[bot]
e1ffa53d9b chore(deps): bump pyparsing from 3.2.3 to 3.3.2 (#42420)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-25 21:53:51 +07:00
dependabot[bot]
4443357f33 chore(deps): bump flask-caching from 2.3.1 to 2.4.1 (#42413)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-25 21:49:15 +07:00
dependabot[bot]
dd08c4f36c chore(deps-dev): update flightsql-dbapi requirement from <0.3,>=0.2.0 to >=0.2.2,<0.3 (#42416)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 21:36:04 +07:00
dependabot[bot]
4f3a1a5058 chore(deps-dev): update snowflake-sqlalchemy requirement from <2,>=1.10.2 to >=1.11.0,<2 (#42418)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 21:35:44 +07:00
dependabot[bot]
9d6fbdceff chore(deps-dev): update ibm-db-sa requirement from <=0.4.4,>0.3.8 to <=0.4.4,>=0.4.4 (#42419)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 20:15:32 +07:00
dependabot[bot]
8864b7a0a3 chore(deps-dev): update oracledb requirement from <5,>=2.0.0 to >=4.0.2,<5 (#42421)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 20:04:34 +07:00
dependabot[bot]
d4652460ee chore(deps-dev): bump google-cloud-bigquery from 3.42.1 to 3.42.2 (#42422)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-25 20:04:07 +07:00
Gordon Cowie
8c37d627aa fix: allow clearing database/schema in DatasourceModal (#39918) 2026-07-24 16:17:25 -07:00
dependabot[bot]
5775bdbe5e chore(deps-dev): bump postcss from 8.5.15 to 8.5.23 in /superset-embedded-sdk (#42394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 16:14:38 -07:00
Evan Rusackas
c65bedf2d7 fix(extensions): make LOCAL_EXTENSIONS hot reload reliable in Docker (#40084)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-24 15:30:04 -07:00
dependabot[bot]
2a07f7967d chore(deps): bump brace-expansion, @cypress/code-coverage and eslint in /superset-frontend/cypress-base (#42275)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-07-24 14:24:22 -07:00
Joe Li
b82ff27f9c fix(sqllab): render each query error once on async fallback (#42318)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-24 14:13:35 -07:00
24 changed files with 3616 additions and 3629 deletions

View File

@@ -122,25 +122,13 @@ jobs:
pattern: coverage-artifacts-*
path: coverage/
- name: Reorganize test result reports
run: |
find coverage/
for i in {1..8}; do
mv coverage/coverage-artifacts-${i}/coverage-final.json coverage/coverage-shard-${i}.json
done
shell: bash
- name: Merge Code Coverage
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: javascript
use_oidc: true
verbose: true
disable_search: true
files: merged-output/coverage-summary.json
directory: coverage
slug: apache/superset
lint-frontend:

1
.gitignore vendored
View File

@@ -57,6 +57,7 @@ local_config.py
/superset_text.yml
superset.egg-info/
superset/bin/supersetc
superset/extensions/.reload_trigger
tmp
rat-results.txt
superset/app/

View File

@@ -34,6 +34,7 @@ x-superset-volumes: &superset-volumes
- superset_home:/app/superset_home
- ./tests:/app/tests
- superset_data:/app/data
- ./local_extensions:/app/local_extensions
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`

View File

@@ -98,7 +98,9 @@ case "${1}" in
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 \
--extra-files "/app/superset/extensions/.reload_trigger" \
--exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*:*/superset/__init__.py"
;;
app-gunicorn)
echo "Starting web app..."

View File

@@ -54,7 +54,7 @@ dependencies = [
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.2, <6.0.0",
"flask-caching>=2.1.0, <3",
"flask-caching>=2.4.1, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0",
@@ -92,7 +92,7 @@ dependencies = [
"Pillow>=11.0.0, <13",
"polyline>=2.0.0, <3.0",
"pydantic>=2.8.0",
"pyparsing>=3.0.6, <4",
"pyparsing>=3.3.2, <4",
"python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
"pygeohash",
@@ -127,7 +127,7 @@ aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.0",
"sqlalchemy-bigquery>=1.17.0",
"google-cloud-bigquery>=3.42.1",
"google-cloud-bigquery>=3.42.2",
]
clickhouse = ["clickhouse-connect>=1.4.2, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
@@ -142,8 +142,8 @@ databricks = [
"databricks-sql-connector>=4.2.6, <4.4.0",
"databricks-sqlalchemy==1.0.5",
]
datafusion = ["flightsql-dbapi>=0.2.0, <0.3"]
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
db2 = ["ibm-db-sa<=0.4.4, >=0.4.4"]
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.10, <2"]
@@ -189,7 +189,7 @@ ocient = [
"shapely",
"geojson",
]
oracle = ["oracledb>=2.0.0, <5"]
oracle = ["oracledb>=4.0.2, <5"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.61.0, <2"]
@@ -201,7 +201,7 @@ redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
spark = [
"pyhive[hive_pure_sasl]>=0.7",
@@ -227,7 +227,7 @@ development = [
"docker",
"flask-testing",
"freezegun",
"grpcio>=1.81.1",
"grpcio>=1.82.1",
"openapi-spec-validator",
"parameterized",
"pip",

View File

@@ -130,7 +130,7 @@ flask-appbuilder==5.2.2
# apache-superset-core
flask-babel==3.1.0
# via flask-appbuilder
flask-caching==2.3.1
flask-caching==2.4.1
# via apache-superset (pyproject.toml)
flask-compress==1.24
# via apache-superset (pyproject.toml)
@@ -329,7 +329,7 @@ pyopenssl==26.3.0
# via
# -r requirements/base.in
# shillelagh
pyparsing==3.2.3
pyparsing==3.3.2
# via apache-superset (pyproject.toml)
pysocks==1.7.1
# via urllib3

View File

@@ -276,7 +276,7 @@ flask-babel==3.1.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
flask-caching==2.3.1
flask-caching==2.4.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -341,7 +341,7 @@ geopy==2.4.1
# apache-superset
gevent==26.4.0
# via apache-superset
google-api-core==2.23.0
google-api-core==2.33.0
# via
# google-cloud-bigquery
# google-cloud-core
@@ -362,7 +362,7 @@ google-auth-oauthlib==1.2.1
# via
# pandas-gbq
# pydata-google-auth
google-cloud-bigquery==3.42.1
google-cloud-bigquery==3.42.2
# via
# apache-superset
# pandas-gbq
@@ -386,7 +386,7 @@ greenlet==3.5.3
# sqlalchemy
griffelib==2.0.2
# via fastmcp-slim
grpcio==1.81.1
grpcio==1.83.0
# via
# apache-superset
# google-api-core
@@ -795,7 +795,7 @@ pyopenssl==26.3.0
# -c requirements/base-constraint.txt
# google-auth
# shillelagh
pyparsing==3.2.3
pyparsing==3.3.2
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -3937,9 +3937,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
@@ -4114,9 +4114,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"dev": true,
"funding": [
{
@@ -4134,7 +4134,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},

File diff suppressed because it is too large Load Diff

View File

@@ -3967,16 +3967,16 @@
}
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@eslint/config-array/node_modules/minimatch": {
@@ -5233,9 +5233,9 @@
}
},
"node_modules/@jest/reporters/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6871,16 +6871,16 @@
}
},
"node_modules/@npmcli/arborist/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@npmcli/arborist/node_modules/lru-cache": {
@@ -7137,16 +7137,16 @@
}
},
"node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@npmcli/map-workspaces/node_modules/glob": {
@@ -7297,16 +7297,16 @@
}
},
"node_modules/@npmcli/package-json/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@npmcli/package-json/node_modules/glob": {
@@ -7559,16 +7559,16 @@
}
},
"node_modules/@nx/devkit/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@nx/devkit/node_modules/minimatch": {
@@ -11561,16 +11561,16 @@
}
},
"node_modules/@tufjs/models/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@tufjs/models/node_modules/minimatch": {
@@ -15756,9 +15756,9 @@
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
"integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15872,9 +15872,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16142,16 +16142,16 @@
}
},
"node_modules/cacache/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/cacache/node_modules/glob": {
@@ -20280,16 +20280,16 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/eslint/node_modules/eslint-scope": {
@@ -21037,9 +21037,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -24068,16 +24068,16 @@
}
},
"node_modules/ignore-walk/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/ignore-walk/node_modules/minimatch": {
@@ -25615,9 +25615,9 @@
}
},
"node_modules/jest-config/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -26552,9 +26552,9 @@
}
},
"node_modules/jest-runtime/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -29496,15 +29496,15 @@
}
},
"node_modules/mem-fs-editor/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/mem-fs-editor/node_modules/ejs": {
@@ -30908,15 +30908,15 @@
}
},
"node_modules/multimatch/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/multimatch/node_modules/minimatch": {
@@ -31590,16 +31590,16 @@
}
},
"node_modules/nx/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/nx/node_modules/ignore": {
@@ -39719,9 +39719,9 @@
}
},
"node_modules/tar": {
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"version": "7.5.21",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz",
"integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -42252,9 +42252,9 @@
}
},
"node_modules/webpack-dev-server": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz",
"integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==",
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz",
"integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -42276,7 +42276,7 @@
"graceful-fs": "^4.2.6",
"http-proxy-middleware": "^2.0.9",
"ipaddr.js": "^2.1.0",
"launch-editor": "^2.6.1",
"launch-editor": "^2.14.1",
"open": "^10.0.3",
"p-retry": "^6.2.0",
"schema-utils": "^4.2.0",

View File

@@ -66,12 +66,6 @@ const StyledTable = styled(Table)<{
showRowCount?: boolean;
}>`
${({ theme, isPaginationSticky, showRowCount }) => `
th.ant-column-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
opacity: 0;
font-size: ${theme.fontSizeXL}px;

View File

@@ -355,6 +355,35 @@ describe('ResultSet', () => {
expect(errorMessages).toHaveLength(errors.length);
});
test('deduplicates errors present in both query.errors and query.extra.errors', async () => {
// The async fallback path (QueryAutoRefresh) populates both
// `query.errors` (via queryFailed) and `query.extra.errors` (via
// refreshQueries) with the same errors. ResultSet should render each
// error only once, not twice.
const duplicatedErrorQuery = {
...failedQueryWithErrors,
extra: { errors: failedQueryWithErrors.errors },
};
const duplicatedErrorState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[duplicatedErrorQuery.id]: duplicatedErrorQuery,
},
},
};
await waitFor(() => {
setup(
{ ...mockedProps, queryId: duplicatedErrorQuery.id },
mockStore(duplicatedErrorState),
);
});
const errorMessages = screen.getAllByTestId('error-message');
expect(errorMessages).toHaveLength(failedQueryWithErrors.errors.length);
});
test('should render a timeout error with a retrial button', async () => {
await waitFor(() => {
setup(

View File

@@ -618,7 +618,26 @@ const ResultSet = ({
}
if (query.state === QueryState.Failed) {
const errors = [...(query.extra?.errors || []), ...(query.errors || [])];
// A failed query can carry the same error in both `query.errors` (set by
// the queryFailed action) and `query.extra.errors` (set by the async
// refresh). When falling back to async polling both get populated with
// identical errors, so deduplicate to avoid rendering each error twice.
const seenErrors = new Set<string>();
const errors = [
...(query.extra?.errors || []),
...(query.errors || []),
].filter(error => {
// json-bigint parsing can leave native BigInt values in payloads,
// which JSON.stringify rejects; serialize them as strings.
const key = JSON.stringify(error, (_, value) =>
typeof value === 'bigint' ? value.toString() : value,
);
if (seenErrors.has(key)) {
return false;
}
seenErrors.add(key);
return true;
});
return (
<ResultlessStyles>

View File

@@ -119,9 +119,10 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
filter_select_enabled: datasource.filter_select_enabled,
fetch_values_predicate: datasource.fetch_values_predicate,
schema:
datasource.tableSelector?.schema ||
datasource.databaseSelector?.schema ||
datasource.schema,
datasource.tableSelector?.schema ??
datasource.databaseSelector?.schema ??
datasource.schema ??
null,
description: datasource.description,
main_dttm_col: datasource.main_dttm_col,
currency_code_column: datasource.currency_code_column ?? null,

View File

@@ -0,0 +1,67 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { executeQuery } from './actions';
import fetchMock from 'fetch-mock';
fetchMock.post('glob:*/sqllab/execute', { result: [] });
afterAll(() => {
fetchMock.clearHistory().removeRoutes();
});
test('executeQuery', async () => {
const mockDispatch = jest.fn();
const mockedQueryExecutePayload = {
client_id: 'client_id_1',
database_id: 1,
runAsync: false,
catalog: null,
schema: 'schema_1',
sql: '1',
tmp_table_name: 'tmp_table_1',
select_as_cta: false,
ctas_method: 'SELECT',
queryLimit: 10,
expand_data: false,
};
const returnedDispatchFunc = executeQuery(mockedQueryExecutePayload);
await returnedDispatchFunc(mockDispatch);
const [
[setQueryIsLoadingActionObject],
[setQueryResultActionObject],
[setQueryIsNotLoadingActionObject],
] = mockDispatch.mock.calls;
expect(setQueryIsLoadingActionObject).toStrictEqual({
type: 'SET_QUERY_IS_LOADING',
payload: true,
});
expect(setQueryResultActionObject).toStrictEqual({
type: 'SET_QUERY_RESULT',
payload: {
result: [],
},
});
expect(setQueryIsNotLoadingActionObject).toStrictEqual({
type: 'SET_QUERY_IS_LOADING',
payload: false,
});
});

View File

@@ -67,7 +67,7 @@ export function executeQuery(payload: QueryExecutePayload) {
const result = await executeQueryApi(payload);
dispatch(setQueryResult(result as QueryExecuteResponse));
} catch (error) {
dispatch(setQueryError(error.message));
dispatch(setQueryError((error as Error).message));
} finally {
dispatch(setQueryIsLoading(false));
}

View File

@@ -169,7 +169,7 @@ class ExtensionsRestApi(BaseApi):
@protect()
@safe
@expose("/<publisher>/<name>/<file>", methods=("GET",))
@expose("/<publisher>/<name>/<path:file>", methods=("GET",))
def content(self, publisher: str, name: str, file: str) -> Response:
"""Get a frontend chunk of an extension.
---

View File

@@ -23,9 +23,11 @@ from typing import Callable, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
# Matches only the static asset endpoint: /api/v1/extensions/<publisher>/<name>/<file>
# Matches only the static asset endpoint:
# /api/v1/extensions/<publisher>/<name>/<path:file>, where the file portion may
# contain nested segments (worker / WASM / chunk subfolders).
# Does not match the list (/), get (/<publisher>/<name>), or info (/_info) endpoints.
_ASSET_PATH_RE = re.compile(r"^/api/v1/extensions/[^/]+/[^/]+/[^/]+$")
_ASSET_PATH_RE: re.Pattern[str] = re.compile(r"^/api/v1/extensions/[^/]+/[^/]+/.+$")
class ExtensionCacheMiddleware:

View File

@@ -21,7 +21,6 @@ from __future__ import annotations
import logging
import os
import threading
import time
from pathlib import Path
from typing import Any
@@ -29,37 +28,207 @@ from flask import Flask
logger = logging.getLogger(__name__)
# Sentinel file Flask watches via --extra-files. Touching it on a real change
# triggers a server reload without depending on cwd or the location of any
# Python source file.
RELOAD_TRIGGER: Path = Path(__file__).resolve().parent / ".reload_trigger"
# Guard to prevent multiple initializations
_watcher_initialized = False
_watcher_lock = threading.Lock()
def _get_file_handler_class() -> Any:
def _get_file_handler_class() -> Any: # noqa: C901
"""Get the file handler class, importing watchdog only when needed."""
try:
from watchdog.events import FileSystemEventHandler
import hashlib
from watchdog.events import (
FileCreatedEvent,
FileDeletedEvent,
FileModifiedEvent,
FileMovedEvent,
FileSystemEventHandler,
)
class LocalExtensionFileHandler(FileSystemEventHandler):
"""Custom file system event handler for LOCAL_EXTENSIONS directories."""
"""Custom file system event handler for LOCAL_EXTENSIONS directories.
Only reacts to genuine content changes (create / modify / move) in the
dist directory, verified by comparing a SHA-256 of the file's content.
This avoids the Docker VirtioFS / osxfs problem where reading a file
generates inotify events that watchdog surfaces as modifications.
"""
def __init__(self) -> None:
super().__init__()
# sha256 of last-seen content, keyed by absolute path. Populated
# from existing files in watched `dist` dirs at startup (see
# `prime_baseline`) so that startup-noise inotify events from
# Docker VirtioFS reads don't get treated as the first real edit.
self._file_hashes: dict[str, str] = {}
self._lock: threading.Lock = threading.Lock()
# Trailing debounce: schedule a single reload after a quiet
# window so simultaneous webpack writes coalesce into one
# restart that fires *after* the build settles.
self._debounce_seconds: float = 1.0
self._pending_timer: threading.Timer | None = None
# Monotonically increasing token identifying the most recently
# scheduled timer. Guards the timer-already-fired race where
# `Timer.cancel()` can't stop a callback that has begun running.
self._reload_generation: int = 0
# ── helpers ──────────────────────────────────────────────────────
@staticmethod
def _sha256(path: str) -> str | None:
try:
with open(path, "rb") as fh:
return hashlib.sha256(fh.read()).hexdigest()
except OSError:
return None
def prime_baseline(self, watch_dirs: set[str]) -> None:
"""Pre-populate content hashes for existing files in watched
`dist` directories. Called once at watcher startup so a
developer's first real edit registers as a content change
rather than as the file's 'first observation'."""
for root_dir in watch_dirs:
root = Path(root_dir)
for path in root.rglob("*"):
if not path.is_file():
continue
if "dist" not in path.parts:
continue
digest = self._sha256(str(path))
if digest is not None:
self._file_hashes[str(path)] = digest
def _content_changed(self, path: str) -> bool:
"""Return True when the file's content differs from last seen.
With `prime_baseline` called at startup, the baseline reflects
what was on disk when the watcher started. A first observation
that differs (or doesn't exist in baseline) is treated as a
genuine change.
"""
digest = self._sha256(path)
if digest is None:
return False
old_digest = self._file_hashes.get(path)
self._file_hashes[path] = digest
# New file (not in baseline) is a real change; otherwise compare.
return old_digest != digest
def _trigger_reload(self, source_path: str, generation: int) -> None:
"""Touch the reload-trigger sentinel; Flask's --extra-files
watcher reloads on its mtime change."""
# A newer event may have superseded this timer after it began
# running (`cancel()` can't stop an in-flight callback), so only
# the most recently scheduled generation is allowed to fire. The
# check and the sentinel touch happen inside the same critical
# section so a `_schedule_reload` call racing in from another
# thread can't bump the generation between the check and the
# write and let this stale callback still fire.
with self._lock:
if generation != self._reload_generation:
return
logger.info(
"File change settled in LOCAL_EXTENSIONS: %s", source_path
)
logger.info("Triggering restart by touching %s", RELOAD_TRIGGER)
try:
RELOAD_TRIGGER.touch()
except OSError as e:
logger.warning(
"Failed to touch reload trigger %s: %s", RELOAD_TRIGGER, e
)
def _schedule_reload(self, source_path: str) -> None:
"""Trailing-debounce: cancel any pending reload and schedule a
new one for `_debounce_seconds` from now. Each new event resets
the timer, so the reload fires only after a quiet window."""
with self._lock:
if self._pending_timer is not None:
self._pending_timer.cancel()
self._reload_generation += 1
timer = threading.Timer(
self._debounce_seconds,
self._trigger_reload,
args=(source_path, self._reload_generation),
)
timer.daemon = True
self._pending_timer = timer
timer.start()
def _handle_moved(self, event: Any) -> None:
"""Moves into/out of `dist` are explicit signals — trigger
regardless of content match (the source may already be gone
or the destination may not have a meaningful hash yet).
Atomic-build workflows rename tmp -> dist (dest in dist),
while removing an artifact renames dist -> elsewhere (src in
dist); either side touching `dist` is a real signal."""
dest = getattr(event, "dest_path", None)
src = getattr(event, "src_path", None)
# The file no longer lives at the source path; evict its
# hash entry so the index only tracks paths that exist.
if isinstance(src, str):
self._file_hashes.pop(src, None)
dist_side = next(
(
p
for p in (dest, src)
if isinstance(p, str) and "dist" in Path(p).parts
),
None,
)
if dist_side is not None:
self._schedule_reload(dist_side)
# ── event handler ─────────────────────────────────────────────────
def on_any_event(self, event: Any) -> None:
"""Handle any file system event in the watched directories."""
"""Handle file system events in the watched directories."""
if event.is_directory:
return
# Only trigger on changes to files in `dist` directory
src = getattr(event, "src_path", None)
if not isinstance(src, str) or "dist" not in Path(src).parts:
# Deletions don't trigger a reload (webpack clean steps delete
# old chunks right before writing new ones, which trigger via
# the subsequent create/modify), but the stale hash entry must
# be evicted so `_file_hashes` doesn't grow without bound as
# hashed chunk filenames churn across rebuilds.
if isinstance(event, FileDeletedEvent):
src = getattr(event, "src_path", None)
if isinstance(src, str):
self._file_hashes.pop(src, None)
return
logger.info(
"File change detected in LOCAL_EXTENSIONS: %s", event.src_path
)
# Only react to true write events; skip access / close / open etc.
if not isinstance(
event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)
):
return
# Touch superset/__init__.py to trigger Flask's file watcher
superset_init = Path("superset/__init__.py")
logger.info("Triggering restart by touching %s", superset_init)
os.utime(superset_init, (time.time(), time.time()))
if isinstance(event, FileMovedEvent):
self._handle_moved(event)
return
# For Create/Modify events watchdog only sets src_path.
target = getattr(event, "src_path", None)
if not isinstance(target, str):
return
# Only care about paths inside a `dist` directory.
if "dist" not in Path(target).parts:
return
# For Create/Modify, verify the content actually changed to
# ignore spurious inotify events generated by Docker bind-mount
# reads.
if not self._content_changed(target):
return
self._schedule_reload(target)
return LocalExtensionFileHandler
except ImportError:
@@ -130,11 +299,23 @@ def setup_local_extensions_watcher(app: Flask) -> None: # noqa: C901
if not watch_dirs:
return
# Ensure the sentinel exists so touch() and Flask's --extra-files watcher
# both have a real path to operate on.
try:
RELOAD_TRIGGER.touch(exist_ok=True)
except OSError as e:
logger.warning("Could not create reload trigger %s: %s", RELOAD_TRIGGER, e)
return
try:
from watchdog.observers import Observer
# Set up and start the file watcher
event_handler = handler_class()
# Pre-populate baseline hashes from existing dist files so the
# developer's first real edit isn't silently dropped as a "first
# observation".
event_handler.prime_baseline(watch_dirs)
observer = Observer()
for watch_dir in watch_dirs:

View File

@@ -35,12 +35,17 @@ from superset.utils.core import check_is_safe_zip
logger = logging.getLogger(__name__)
FRONTEND_REGEX = re.compile(r"^frontend/dist/([^/]+)$")
# Accept nested paths inside frontend/dist so extensions can serve
# worker / WASM / chunk subfolders. Reject any entry whose path contains "..",
# conservatively excluding parent traversal segments so a crafted entry name
# cannot escape the bundle directory (defense in depth; check_is_safe_zip runs
# first).
FRONTEND_REGEX: re.Pattern[str] = re.compile(r"^frontend/dist/(?!.*\.\.)(.+)$")
# Reject any entry whose path contains "..", conservatively excluding parent
# traversal segments along with the (in practice nonexistent) case of a module
# path embedding consecutive dots, so a crafted entry name cannot produce a
# traversal-style module path (defense in depth; check_is_safe_zip runs first).
BACKEND_REGEX = re.compile(r"^backend/src/(?!.*\.\.)(.+)$")
BACKEND_REGEX: re.Pattern[str] = re.compile(r"^backend/src/(?!.*\.\.)(.+)$")
class InMemoryLoader(importlib.abc.Loader):

View File

@@ -614,15 +614,8 @@ class SemanticLayerRestApi(BaseSupersetApi):
warning: str | None = None
try:
schema = cls.get_configuration_schema(parsed_config)
except Exception: # pylint: disable=broad-except
# Show a stable, user-friendly message in the UI; the exception
# detail goes to the server log below.
warning = str(
t(
"Could not load metadata for this configuration; "
"showing the default form. See the server logs for details."
)
)
except Exception as ex: # pylint: disable=broad-except
warning = str(ex)
logger.exception(
"Error enriching semantic layer configuration schema for type %s",
sl_type,

View File

@@ -64,6 +64,21 @@ def test_asset_path_is_intercepted() -> None:
assert "Cookie" not in vary
def test_nested_asset_path_is_intercepted() -> None:
headers: ResponseHeaders = call_middleware(
"/api/v1/extensions/acme/my-ext/workers/nested/chunk.wasm",
[("Vary", "Accept-Encoding, Cookie")],
)
vary: str = dict(headers).get("Vary", "")
assert "Cookie" not in vary
def test_get_endpoint_with_trailing_slash_is_not_intercepted() -> None:
upstream: ResponseHeaders = [("Vary", "Accept-Encoding, Cookie")]
headers = call_middleware("/api/v1/extensions/acme/my-ext/", upstream)
assert headers == upstream
def test_list_endpoint_is_not_intercepted() -> None:
upstream = [("Vary", "Accept-Encoding, Cookie")]
headers = call_middleware("/api/v1/extensions/", upstream)

View File

@@ -0,0 +1,59 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Any
from watchdog.events import FileDeletedEvent, FileMovedEvent
from superset.extensions.local_extensions_watcher import _get_file_handler_class
def _noop_schedule_reload(_path: str) -> None:
"""Stand in for a real debounce timer in unit tests."""
def make_handler() -> Any:
handler_class = _get_file_handler_class()
handler = handler_class()
# Avoid spinning up real debounce timers in unit tests.
handler._schedule_reload = _noop_schedule_reload
return handler
def test_delete_evicts_hash_entry() -> None:
handler = make_handler()
handler._file_hashes["/ext/dist/old-chunk.abc123.js"] = "digest"
handler.on_any_event(FileDeletedEvent("/ext/dist/old-chunk.abc123.js"))
assert "/ext/dist/old-chunk.abc123.js" not in handler._file_hashes
def test_delete_of_untracked_path_is_a_noop() -> None:
handler = make_handler()
handler.on_any_event(FileDeletedEvent("/ext/dist/never-seen.js"))
assert handler._file_hashes == {}
def test_move_out_of_dist_evicts_source_hash_entry() -> None:
handler = make_handler()
handler._file_hashes["/ext/dist/chunk.js"] = "digest"
handler.on_any_event(FileMovedEvent("/ext/dist/chunk.js", "/ext/tmp/chunk.js"))
assert "/ext/dist/chunk.js" not in handler._file_hashes

View File

@@ -1186,12 +1186,7 @@ def test_configuration_schema_enrichment_error_fallback(
assert response.status_code == 200
assert response.json["result"] == {"type": "object"}
# The warning is a stable, user-friendly message; the exception detail
# only goes to the server log.
assert response.json["warning"] == (
"Could not load metadata for this configuration; "
"showing the default form. See the server logs for details."
)
assert response.json["warning"] == "connection failed"
assert mock_cls.get_configuration_schema.call_count == 2