Compare commits

..

1 Commits

Author SHA1 Message Date
Evan Rusackas
1a16b2acec fix(semantic-layers): show a friendly warning when schema enrichment falls back
Enrichment failures (connection refused, driver quirks, etc.) were
toasted to the user verbatim, which reads as raw internals and varies
by driver. Replace it with a stable, translatable message pointing at
the server logs, where the full exception is already recorded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:42:56 -07:00
21 changed files with 3632 additions and 3570 deletions

1
.gitignore vendored
View File

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

View File

@@ -297,7 +297,7 @@ pre-commit run eslint # Frontend linting
## Platform-Specific Instructions
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor

View File

@@ -34,7 +34,6 @@ 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,9 +98,7 @@ 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 \
--extra-files "/app/superset/extensions/.reload_trigger" \
--exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*:*/superset/__init__.py"
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
;;
app-gunicorn)
echo "Starting web app..."

View File

@@ -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.2",
"google-cloud-bigquery>=3.42.1",
]
clickhouse = ["clickhouse-connect>=1.4.2, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
@@ -143,7 +143,7 @@ databricks = [
"databricks-sqlalchemy==1.0.5",
]
datafusion = ["flightsql-dbapi>=0.2.0, <0.3"]
db2 = ["ibm-db-sa<=0.4.4, >=0.4.4"]
db2 = ["ibm-db-sa>0.3.8, <=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>=4.0.2, <5"]
oracle = ["oracledb>=2.0.0, <5"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.61.0, <2"]

View File

@@ -341,7 +341,7 @@ geopy==2.4.1
# apache-superset
gevent==26.4.0
# via apache-superset
google-api-core==2.33.0
google-api-core==2.23.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.2
google-cloud-bigquery==3.42.1
# via
# apache-superset
# pandas-gbq

View File

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

View File

@@ -14,7 +14,7 @@
"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
under the License.
-->
# Change Log

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

View File

@@ -355,35 +355,6 @@ 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,26 +618,7 @@ const ResultSet = ({
}
if (query.state === QueryState.Failed) {
// 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;
});
const errors = [...(query.extra?.errors || []), ...(query.errors || [])];
return (
<ResultlessStyles>

View File

@@ -119,10 +119,9 @@ 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 ??
null,
datasource.tableSelector?.schema ||
datasource.databaseSelector?.schema ||
datasource.schema,
description: datasource.description,
main_dttm_col: datasource.main_dttm_col,
currency_code_column: datasource.currency_code_column ?? null,

View File

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

View File

@@ -23,11 +23,9 @@ 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>/<path:file>, where the file portion may
# contain nested segments (worker / WASM / chunk subfolders).
# Matches only the static asset endpoint: /api/v1/extensions/<publisher>/<name>/<file>
# Does not match the list (/), get (/<publisher>/<name>), or info (/_info) endpoints.
_ASSET_PATH_RE: re.Pattern[str] = re.compile(r"^/api/v1/extensions/[^/]+/[^/]+/.+$")
_ASSET_PATH_RE = re.compile(r"^/api/v1/extensions/[^/]+/[^/]+/[^/]+$")
class ExtensionCacheMiddleware:

View File

@@ -21,6 +21,7 @@ from __future__ import annotations
import logging
import os
import threading
import time
from pathlib import Path
from typing import Any
@@ -28,207 +29,37 @@ 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: # noqa: C901
def _get_file_handler_class() -> Any:
"""Get the file handler class, importing watchdog only when needed."""
try:
import hashlib
from watchdog.events import (
FileCreatedEvent,
FileDeletedEvent,
FileModifiedEvent,
FileMovedEvent,
FileSystemEventHandler,
)
from watchdog.events import FileSystemEventHandler
class LocalExtensionFileHandler(FileSystemEventHandler):
"""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 ─────────────────────────────────────────────────
"""Custom file system event handler for LOCAL_EXTENSIONS directories."""
def on_any_event(self, event: Any) -> None:
"""Handle file system events in the watched directories."""
"""Handle any file system event in the watched directories."""
if event.is_directory:
return
# 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)
# 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:
return
# Only react to true write events; skip access / close / open etc.
if not isinstance(
event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)
):
return
logger.info(
"File change detected in LOCAL_EXTENSIONS: %s", event.src_path
)
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)
# 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()))
return LocalExtensionFileHandler
except ImportError:
@@ -299,23 +130,11 @@ 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,17 +35,12 @@ from superset.utils.core import check_is_safe_zip
logger = logging.getLogger(__name__)
# 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/(?!.*\.\.)(.+)$")
FRONTEND_REGEX = 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.Pattern[str] = re.compile(r"^backend/src/(?!.*\.\.)(.+)$")
BACKEND_REGEX = re.compile(r"^backend/src/(?!.*\.\.)(.+)$")
class InMemoryLoader(importlib.abc.Loader):

View File

@@ -614,8 +614,15 @@ class SemanticLayerRestApi(BaseSupersetApi):
warning: str | None = None
try:
schema = cls.get_configuration_schema(parsed_config)
except Exception as ex: # pylint: disable=broad-except
warning = str(ex)
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."
)
)
logger.exception(
"Error enriching semantic layer configuration schema for type %s",
sl_type,

View File

@@ -64,21 +64,6 @@ 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

@@ -1,59 +0,0 @@
# 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,7 +1186,12 @@ def test_configuration_schema_enrichment_error_fallback(
assert response.status_code == 200
assert response.json["result"] == {"type": "object"}
assert response.json["warning"] == "connection failed"
# 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 mock_cls.get_configuration_schema.call_count == 2