Compare commits

...

6 Commits

Author SHA1 Message Date
dependabot[bot]
623348f190 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.64.0 to 8.65.0 in /superset-frontend in the typescript-eslint group across 1 directory (#42512)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 10:29:58 -07:00
Amin Ghadersohi
5aaf9d337b fix(jinja): handle non-JSON body with JSON content-type in get_form_data (#42196) 2026-07-28 09:18:08 -07:00
JUST.in DO IT
181f5bc9c1 fix(mcp): capture created chart/dashboard id in generate_chart/generate_dashboard audit logs (#42367) 2026-07-28 09:16:58 -07:00
Đỗ Trọng Hải
e426fb7bc6 chore(ci): correct GH event type for opening new issue and PR in supersetbot orglabel job (#42424)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-28 09:16:41 -07:00
Evan Rusackas
b5c8d4a5df fix(mixed-timeseries): stop duplicating first metric in multi-metric + group-by series names (#40146)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-28 09:15:26 -07:00
dependabot[bot]
236c9a52fd chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.63.0 to 8.65.0 in /superset-websocket (#42515)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 08:06:12 -07:00
11 changed files with 1045 additions and 463 deletions

View File

@@ -2,10 +2,10 @@ name: supersetbot orglabel based on author
on:
issues:
types: [created, edited]
types: [opened, edited]
pull_request:
types: [created, edited]
types: [opened, edited]
jobs:
superbot-orglabel:
@@ -30,4 +30,4 @@ jobs:
# Label the issue with the appropriate org using supersetbot
# - this requires for the author to be publicly associated with their org
# - and for the org to be listed in `supersetbot/src/metadata.js`
supersetbot orglabel --issue ${{ github.event.number }} --repo ${{ github.repository }} || true
supersetbot orglabel --issue ${{ github.event.number || github.event.issue.number }} --repo ${{ github.repository }} || true

View File

@@ -215,7 +215,7 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
@@ -12776,17 +12776,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/type-utils": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/type-utils": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -12799,7 +12799,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.64.0",
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -12839,69 +12839,6 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
@@ -12924,29 +12861,15 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0"
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -12974,15 +12897,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -12998,116 +12921,10 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.64.0",
"@typescript-eslint/types": "^8.64.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.64.0",
"@typescript-eslint/tsconfig-utils": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13146,38 +12963,6 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -13201,19 +12986,6 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -13231,16 +13003,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0"
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -13254,120 +13026,14 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.64.0",
"@typescript-eslint/types": "^8.64.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.64.0",
"@typescript-eslint/tsconfig-utils": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/utils/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {

View File

@@ -300,7 +300,7 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",

View File

@@ -457,13 +457,23 @@ export default function transformProps(
const seriesName = inverted[entryName] || entryName;
const colorScaleKey = getOriginalSeries(seriesName, array);
const labelMapValues = rawLabelMap?.[seriesName];
let displayName: string;
if (groupby.length > 0) {
// When we have groupby, format as "metric, dimension"
// When we have groupby, format as "metric, dimension". Each series
// belongs to the metric recorded in its label-map tuple
// ([metric, ...dimensions]) — always using the first metric would
// prepend it to every other metric's series (#37921). Tuples without
// a metric part fall back to the first metric as before.
const metricDisplayName =
labelMapValues && labelMapValues.length > 1
? getMetricDisplayName(labelMapValues[0], verboseMap)
: MetricDisplayNameA;
const metricPart: string = showQueryIdentifiers
? `${MetricDisplayNameA} (Query A)`
: MetricDisplayNameA;
? `${metricDisplayName} (Query A)`
: metricDisplayName;
displayName = entryName.includes(metricPart)
? entryName
: `${metricPart}, ${entryName}`;
@@ -471,8 +481,6 @@ export default function transformProps(
// When no groupby, format as just the entry name with optional query identifier
displayName = showQueryIdentifiers ? `${entryName} (Query A)` : entryName;
}
const labelMapValues = rawLabelMap?.[seriesName];
if (labelMapValues) {
displayLabelMap[displayName] = labelMapValues;
}
@@ -536,13 +544,23 @@ export default function transformProps(
const seriesEntry = inverted[entryName] || entryName;
const colorScaleKey = getOriginalSeries(seriesEntry, array);
const labelMapValuesB = rawLabelMapB?.[seriesEntry];
let displayName: string;
if (groupbyB.length > 0) {
// When we have groupby, format as "metric, dimension"
// When we have groupby, format as "metric, dimension". Each series
// belongs to the metric recorded in its label-map tuple
// ([metric, ...dimensions]) — always using the first metric would
// prepend it to every other metric's series (#37921). Tuples without
// a metric part fall back to the first metric as before.
const metricDisplayName =
labelMapValuesB && labelMapValuesB.length > 1
? getMetricDisplayName(labelMapValuesB[0], verboseMap)
: MetricDisplayNameB;
const metricPart: string = showQueryIdentifiers
? `${MetricDisplayNameB} (Query B)`
: MetricDisplayNameB;
? `${metricDisplayName} (Query B)`
: metricDisplayName;
displayName = entryName.includes(metricPart)
? entryName
: `${metricPart}, ${entryName}`;
@@ -550,8 +568,6 @@ export default function transformProps(
// When no groupby, format as just the entry name with optional query identifier
displayName = showQueryIdentifiers ? `${entryName} (Query B)` : entryName;
}
const labelMapValuesB = rawLabelMapB?.[seriesEntry];
if (labelMapValuesB) {
displayLabelMapB[displayName] = labelMapValuesB;
}

View File

@@ -1161,3 +1161,83 @@ test('x-axis dedup keeps the forced min label when the endpoints format identica
expect(formatter(min)).toBe('May');
});
test('regression #37921: multi-metric Query A with groupby does not duplicate first metric in series names', () => {
// Regression test for https://github.com/apache/superset/issues/37921
// ("Residual" follow-up to #37055).
//
// When Query A has multiple metrics + at least one Group By dimension,
// the display-name builder in transformProps.ts used to prepend the FIRST
// metric's display name to every series that didn't literally contain it:
// name: `${MetricDisplayNameA}, ${entryName}`
// For series belonging to the *second* metric, this produced a
// cross-contaminated label like `score_one, score_two, A` — the
// user-visible "first metric duplicated" symptom in the legend / tooltip.
// The fix derives each series' metric from its label-map tuple instead.
const multiMetricRows = [
{
'score_one, A': 1,
'score_one, B': 2,
'score_two, A': 3,
'score_two, B': 4,
ds: 599616000000,
},
{
'score_one, A': 5,
'score_one, B': 6,
'score_two, A': 7,
'score_two, B': 8,
ds: 599916000000,
},
];
const multiMetricLabelMap = {
ds: ['ds'],
'score_one, A': ['score_one', 'A'],
'score_one, B': ['score_one', 'B'],
'score_two, A': ['score_two', 'A'],
'score_two, B': ['score_two', 'B'],
};
const queryAData = createTestQueryData(multiMetricRows, {
label_map: multiMetricLabelMap,
});
// Query B keeps the existing single-metric shape — the bug is on
// Query A's path so we just need a valid Query B alongside.
const queryBData = createTestQueryData(defaultQueryRows, {
label_map: defaultLabelMap,
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [queryAData, queryBData],
formData: {
...formData,
metrics: ['score_one', 'score_two'],
groupby: ['category'],
},
queriesData: [queryAData, queryBData],
});
const transformed = transformProps(chartProps);
const queryASeriesNames = (transformed.echartOptions.series as any[])
.map((s: any) => String(s.name))
.filter((n: string) => n.includes('score_'));
// Each (metric, dim_value) combo from Query A should appear exactly once
// with the *correct* metric prefix — not the first-metric-prepended-to-
// everything-else form. Comparing the sorted array (rather than using
// separate toContain assertions) also catches a regression that emits
// duplicate series for the same name.
expect([...queryASeriesNames].sort()).toEqual(
['score_one, A', 'score_one, B', 'score_two, A', 'score_two, B'].sort(),
);
// And explicitly: no series name should contain *both* metric names —
// that's the smoking gun for the duplication bug.
for (const name of queryASeriesNames) {
expect(name).not.toMatch(/score_one,\s+score_two/);
}
});

View File

@@ -24,8 +24,8 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.1.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.6.0",
@@ -402,9 +402,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -422,9 +419,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -442,9 +436,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -462,9 +453,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -482,9 +470,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -502,9 +487,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -769,6 +751,160 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
@@ -794,6 +930,136 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
@@ -876,6 +1142,160 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
@@ -2075,9 +2495,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2099,9 +2516,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2123,9 +2537,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2147,9 +2558,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -2884,6 +3292,85 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/type-utils": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.64.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",

View File

@@ -33,8 +33,8 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.1.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.6.0",

View File

@@ -261,6 +261,42 @@ class LoggingMiddleware(Middleware):
dataset_id = params.get("dataset_id")
return agent_id, user_id, dashboard_id, slice_id, dataset_id, params
def _extract_output_ids(self, result: ToolResult) -> tuple[int | None, int | None]:
"""Extract dashboard/chart IDs created by the tool from its response.
Create-style tools (generate_chart, generate_dashboard) don't take
chart_id/dashboard_id as input, so _extract_context_info never sees
them and every retry logs slice_id/dashboard_id=None even on the
attempt that actually persisted the object. Look at the response
body instead, since that's the only place the new ID appears.
Supports both flat ("chart_id"/"dashboard_id") and nested
("chart"/"dashboard" objects with an "id" field) response shapes.
"""
from superset.utils.json import loads as json_loads
try:
data = json_loads(result.content[0].text)
except (AttributeError, IndexError, ValueError, TypeError):
return None, None
if not isinstance(data, dict):
return None, None
slice_id = None
chart = data.get("chart")
if isinstance(chart, dict):
slice_id = chart.get("id")
if slice_id is None:
slice_id = data.get("chart_id")
dashboard_id = None
dashboard = data.get("dashboard")
if isinstance(dashboard, dict):
dashboard_id = dashboard.get("id")
if dashboard_id is None:
dashboard_id = data.get("dashboard_id")
return dashboard_id, slice_id
@staticmethod
def _resolve_tool_name(tool_name: str | None, params: Any) -> str | None:
"""Resolve the underlying tool name from call_tool proxy arguments.
@@ -283,6 +319,129 @@ class LoggingMiddleware(Middleware):
return params["name"]
return None
def _backfill_output_ids(
self,
success: bool,
result: Any,
dashboard_id: int | None,
slice_id: int | None,
) -> tuple[int | None, int | None]:
"""Fill in missing ids from a create tool's response on success.
Create-style tools (generate_chart, generate_dashboard) don't take
the new object's ID as input, so it's missing from params. On a
successful call, pull it from the response instead so retried
creates are distinguishable.
"""
if not success or not isinstance(result, ToolResult):
return dashboard_id, slice_id
output_dashboard_id, output_slice_id = self._extract_output_ids(result)
if dashboard_id is None:
dashboard_id = output_dashboard_id
if slice_id is None:
slice_id = output_slice_id
return dashboard_id, slice_id
@staticmethod
def _build_call_tool_payload(
*,
mcp_call_id: str,
tool_name: str | None,
agent_id: str | None,
params: Any,
method: str,
dashboard_id: int | None,
slice_id: int | None,
dataset_id: int | None,
success: bool,
mcp_tool: str | None,
error_type: str | None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"mcp_call_id": mcp_call_id,
"tool": tool_name,
"agent_id": agent_id,
"params": _sanitize_params(params),
"method": method,
"dashboard_id": dashboard_id,
"slice_id": slice_id,
"dataset_id": dataset_id,
"success": success,
}
if mcp_tool is not None:
payload["mcp_tool"] = mcp_tool
if error_type is not None:
payload["error_type"] = error_type
return payload
def _log_call_tool_result(
self,
*,
context: MiddlewareContext,
tool_name: str | None,
mcp_tool: str | None,
mcp_call_id: str,
agent_id: str | None,
user_id: int | None,
dashboard_id: int | None,
slice_id: int | None,
dataset_id: int | None,
params: Any,
success: bool,
error_type: str | None,
result: Any,
start_time: float,
) -> None:
duration_ms = int((time.time() - start_time) * 1000)
dashboard_id, slice_id = self._backfill_output_ids(
success, result, dashboard_id, slice_id
)
payload = self._build_call_tool_payload(
mcp_call_id=mcp_call_id,
tool_name=tool_name,
agent_id=agent_id,
params=params,
method=context.method,
dashboard_id=dashboard_id,
slice_id=slice_id,
dataset_id=dataset_id,
success=success,
mcp_tool=mcp_tool,
error_type=error_type,
)
if has_app_context():
event_logger.log(
user_id=user_id,
action="mcp_tool_call",
dashboard_id=dashboard_id,
duration_ms=duration_ms,
slice_id=slice_id,
referrer=None,
curated_payload=payload,
)
extra_parts = []
if mcp_tool is not None:
extra_parts.append(f"mcp_tool={mcp_tool}")
if error_type is not None:
extra_parts.append(f"error_type={error_type}")
extra = (", " + ", ".join(extra_parts)) if extra_parts else ""
logger.info(
"MCP tool call: tool=%s, agent_id=%s, user_id=%s, method=%s, "
"dashboard_id=%s, slice_id=%s, dataset_id=%s, duration_ms=%s, "
"success=%s, mcp_call_id=%s%s",
tool_name,
agent_id,
user_id,
context.method,
dashboard_id,
slice_id,
dataset_id,
duration_ms,
success,
mcp_call_id,
extra,
)
async def on_call_tool(
self,
context: MiddlewareContext,
@@ -300,6 +459,7 @@ class LoggingMiddleware(Middleware):
start_time = time.time()
success = False
error_type: str | None = None
result: Any = None
try:
result = await call_next(context)
success = not self._is_error_response(result)
@@ -316,53 +476,21 @@ class LoggingMiddleware(Middleware):
success = False
raise
finally:
duration_ms = int((time.time() - start_time) * 1000)
payload: dict[str, Any] = {
"mcp_call_id": mcp_call_id,
"tool": tool_name,
"agent_id": agent_id,
"params": _sanitize_params(params),
"method": context.method,
"dashboard_id": dashboard_id,
"slice_id": slice_id,
"dataset_id": dataset_id,
"success": success,
}
if mcp_tool is not None:
payload["mcp_tool"] = mcp_tool
if error_type is not None:
payload["error_type"] = error_type
if has_app_context():
event_logger.log(
user_id=user_id,
action="mcp_tool_call",
dashboard_id=dashboard_id,
duration_ms=duration_ms,
slice_id=slice_id,
referrer=None,
curated_payload=payload,
)
extra_parts = []
if mcp_tool is not None:
extra_parts.append(f"mcp_tool={mcp_tool}")
if error_type is not None:
extra_parts.append(f"error_type={error_type}")
extra = (", " + ", ".join(extra_parts)) if extra_parts else ""
logger.info(
"MCP tool call: tool=%s, agent_id=%s, user_id=%s, method=%s, "
"dashboard_id=%s, slice_id=%s, dataset_id=%s, duration_ms=%s, "
"success=%s, mcp_call_id=%s%s",
tool_name,
agent_id,
user_id,
context.method,
dashboard_id,
slice_id,
dataset_id,
duration_ms,
success,
mcp_call_id,
extra,
self._log_call_tool_result(
context=context,
tool_name=tool_name,
mcp_tool=mcp_tool,
mcp_call_id=mcp_call_id,
agent_id=agent_id,
user_id=user_id,
dashboard_id=dashboard_id,
slice_id=slice_id,
dataset_id=dataset_id,
params=params,
success=success,
error_type=error_type,
result=result,
start_time=start_time,
)
async def on_message(

View File

@@ -35,6 +35,7 @@ from flask_appbuilder.security.sqla import models as ab_models
from flask_appbuilder.security.sqla.models import User
from flask_babel import _
from sqlalchemy.exc import NoResultFound
from werkzeug.exceptions import BadRequest
from superset import appbuilder, dataframe, db, result_set, viz
from superset.common.db_query_status import QueryStatus
@@ -197,6 +198,27 @@ def loads_request_json(request_json_data: str) -> dict[Any, Any]:
return parsed if isinstance(parsed, dict) else {}
def get_request_json_body() -> dict[Any, Any]:
"""Parse the request body as JSON, coercing failures to ``{}``.
``request.is_json`` only inspects the Content-Type header, not whether the
body is actually parseable JSON. Callers reaching ``get_form_data`` from a
non-HTTP-chart-data context (e.g. an MCP tool call rendering
``filter_values()``) can have a request context whose Content-Type claims
JSON but whose body isn't a JSON chart-data payload, which makes Werkzeug
raise ``BadRequest`` from ``request.get_json()``. A well-formed but
non-object JSON body (e.g. ``null``, a scalar, or an array) is coerced to
``{}`` too, since callers treat the result as a mapping.
"""
if not request.is_json:
return {}
try:
data = request.get_json(cache=True)
except BadRequest:
return {}
return data if isinstance(data, dict) else {}
#: Parameter names `url_for` interprets itself rather than appending to the
#: query string. Request-supplied query keys matching these must never be
#: forwarded as `url_for` kwargs.
@@ -345,7 +367,7 @@ def get_form_data(
form_data: dict[str, Any] = initial_form_data or {}
if has_request_context():
json_data = request.get_json(cache=True) if request.is_json else {}
json_data = get_request_json_body()
# chart data API requests are JSON
first_query = (

View File

@@ -216,6 +216,102 @@ class TestLoggingMiddlewareOnCallTool:
assert call_kwargs["slice_id"] == 20
assert call_kwargs["curated_payload"]["dataset_id"] == 30
@patch("superset.mcp_service.middleware.event_logger")
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
@pytest.mark.asyncio
async def test_on_call_tool_extracts_chart_id_from_response(
self, mock_get_user_id, mock_event_logger
) -> None:
"""generate_chart takes no chart_id as input, so on a successful
create the new chart's ID must be pulled from the response body
instead -- otherwise every retry logs slice_id=None and a
successful attempt can't be told apart from the failed ones.
"""
middleware = LoggingMiddleware()
ctx = _make_context(name="generate_chart", params={"dataset_id": 5})
response_text = (
'{"success": true, "chart": {"id": 123, "slice_name": "My Chart"}}'
)
original_result = ToolResult(
content=[mt.TextContent(type="text", text=response_text)]
)
call_next = AsyncMock(return_value=original_result)
await middleware.on_call_tool(ctx, call_next)
call_kwargs = mock_event_logger.log.call_args[1]
assert call_kwargs["slice_id"] == 123
assert call_kwargs["curated_payload"]["slice_id"] == 123
assert call_kwargs["curated_payload"]["success"] is True
@patch("superset.mcp_service.middleware.event_logger")
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
@pytest.mark.asyncio
async def test_on_call_tool_extracts_dashboard_id_from_response(
self, mock_get_user_id, mock_event_logger
) -> None:
"""generate_dashboard likewise creates an ID that only appears in
the response, not the input params."""
middleware = LoggingMiddleware()
ctx = _make_context(name="generate_dashboard", params={"chart_ids": [1, 2]})
response_text = '{"success": true, "dashboard": {"id": 456}}'
original_result = ToolResult(
content=[mt.TextContent(type="text", text=response_text)]
)
call_next = AsyncMock(return_value=original_result)
await middleware.on_call_tool(ctx, call_next)
call_kwargs = mock_event_logger.log.call_args[1]
assert call_kwargs["dashboard_id"] == 456
assert call_kwargs["curated_payload"]["dashboard_id"] == 456
@patch("superset.mcp_service.middleware.event_logger")
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
@pytest.mark.asyncio
async def test_on_call_tool_does_not_extract_id_on_failed_response(
self, mock_get_user_id, mock_event_logger
) -> None:
"""A failed create (error schema response, no exception raised)
must not report a chart_id -- nothing was actually persisted."""
middleware = LoggingMiddleware()
ctx = _make_context(name="generate_chart", params={"dataset_id": 5})
response_text = (
'{"success": false, "chart": null, '
'"error": {"error_type": "validation_error"}}'
)
original_result = ToolResult(
content=[mt.TextContent(type="text", text=response_text)]
)
call_next = AsyncMock(return_value=original_result)
await middleware.on_call_tool(ctx, call_next)
call_kwargs = mock_event_logger.log.call_args[1]
assert call_kwargs["curated_payload"]["success"] is False
assert call_kwargs["slice_id"] is None
@patch("superset.mcp_service.middleware.event_logger")
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
@pytest.mark.asyncio
async def test_on_call_tool_prefers_input_slice_id_over_response(
self, mock_get_user_id, mock_event_logger
) -> None:
"""When chart_id is already known from input params (e.g.
update_chart), the response body must not override it."""
middleware = LoggingMiddleware()
ctx = _make_context(name="update_chart", params={"chart_id": 111})
response_text = '{"success": true, "chart": {"id": 999}}'
original_result = ToolResult(
content=[mt.TextContent(type="text", text=response_text)]
)
call_next = AsyncMock(return_value=original_result)
await middleware.on_call_tool(ctx, call_next)
call_kwargs = mock_event_logger.log.call_args[1]
assert call_kwargs["slice_id"] == 111
class TestLoggingMiddlewareOnMessage:
"""Tests for LoggingMiddleware.on_message()."""
@@ -468,6 +564,40 @@ class TestIsErrorResponse:
assert call_kwargs["curated_payload"]["tool"] == "get_chart_info"
class TestExtractOutputIds:
"""Tests for LoggingMiddleware._extract_output_ids()."""
def test_returns_none_for_non_json_body(self) -> None:
"""A malformed/non-JSON response body must not raise -- the
defensive try/except should fall back to (None, None)."""
middleware = LoggingMiddleware()
result = ToolResult(
content=[mt.TextContent(type="text", text="not valid json {{{")]
)
assert middleware._extract_output_ids(result) == (None, None)
def test_returns_none_for_empty_content(self) -> None:
"""A ToolResult with no content items must not raise."""
middleware = LoggingMiddleware()
assert middleware._extract_output_ids(ToolResult(content=[])) == (
None,
None,
)
def test_returns_none_for_non_dict_json_body(self) -> None:
"""A JSON body that parses but isn't an object (e.g. a bare
list) must not raise and must yield no IDs."""
middleware = LoggingMiddleware()
result = ToolResult(content=[mt.TextContent(type="text", text="[1, 2, 3]")])
assert middleware._extract_output_ids(result) == (None, None)
def test_extracts_both_ids_from_flat_response(self) -> None:
middleware = LoggingMiddleware()
response_text = '{"chart_id": 123, "dashboard_id": 456}'
result = ToolResult(content=[mt.TextContent(type="text", text=response_text)])
assert middleware._extract_output_ids(result) == (456, 123)
class TestMiddlewareChainOrder:
"""Test that the middleware order from server.py logs failures correctly.

View File

@@ -0,0 +1,53 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for superset.views.utils module"""
from flask import current_app
from superset.views.utils import get_form_data
def test_get_form_data_handles_non_json_body_with_json_content_type() -> None:
"""get_form_data returns gracefully when Content-Type claims JSON but the
body isn't parseable JSON, instead of letting Werkzeug's BadRequest escape.
This is the shape of the request context an MCP tool call runs in when a
chart/dataset SQL template calls the ``filter_values()`` Jinja macro: the
Content-Type header says ``application/json`` but the body is not a JSON
chart-data payload.
"""
with current_app.test_request_context(
data="not-json-at-all", content_type="application/json"
):
form_data, slc = get_form_data()
assert form_data == {}
assert slc is None
def test_get_form_data_handles_non_dict_json_body() -> None:
"""get_form_data coerces a well-formed but non-object JSON body to {}.
``request.get_json()`` happily returns a scalar or list for valid JSON
that isn't a JSON object (e.g. ``null`` or ``42``). Downstream code treats
the parsed body as a mapping, so a non-dict result must not leak through.
"""
with current_app.test_request_context(data="42", content_type="application/json"):
form_data, slc = get_form_data()
assert form_data == {}
assert slc is None