mirror of
https://github.com/apache/superset.git
synced 2026-09-11 17:54:25 +00:00
Compare commits
541
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb5bd03598 | ||
|
|
d7941ccfec | ||
|
|
d79eb5842a | ||
|
|
970b5bcf75 | ||
|
|
7c4f87615b | ||
|
|
f0d521dfc2 | ||
|
|
39f12786a2 | ||
|
|
012bf52c8c | ||
|
|
0d50fd676b | ||
|
|
acdf70176a | ||
|
|
dae79a6cba | ||
|
|
362e5bf45e | ||
|
|
b1b6a057d8 | ||
|
|
9b52110ab1 | ||
|
|
fc84d5d959 | ||
|
|
d62f1546aa | ||
|
|
b6ac1ef63c | ||
|
|
59bc895f3f | ||
|
|
bc2ffc66e5 | ||
|
|
e10918307c | ||
|
|
68ee776ad6 | ||
|
|
0d681338aa | ||
|
|
6c88fcacfa | ||
|
|
e16656c6cf | ||
|
|
07c8e7f303 | ||
|
|
b2468d3752 | ||
|
|
7934665ac1 | ||
|
|
9366868f8f | ||
|
|
ae61000a12 | ||
|
|
7174695be7 | ||
|
|
4ee3a0fc07 | ||
|
|
f6c5219e89 | ||
|
|
72d39bea85 | ||
|
|
e9030b7fac | ||
|
|
f672b143db | ||
|
|
9f42ccecec | ||
|
|
5af17c7976 | ||
|
|
18d89f25ce | ||
|
|
73c4240ba4 | ||
|
|
1903b919d6 | ||
|
|
44177b4e35 | ||
|
|
e1ed5003a8 | ||
|
|
e6853894ab | ||
|
|
05fc5bb424 | ||
|
|
c373498543 | ||
|
|
fb3e129d62 | ||
|
|
003c232192 | ||
|
|
a51bbd46dc | ||
|
|
e77cfc93ed | ||
|
|
ef290b28e2 | ||
|
|
dd18b2eb54 | ||
|
|
d7a8c1934a | ||
|
|
af7a62ab3f | ||
|
|
69d2da9c61 | ||
|
|
67bc910eb5 | ||
|
|
a89f9bcb98 | ||
|
|
b302071723 | ||
|
|
e45330c2fd | ||
|
|
bd8d3ffb2d | ||
|
|
5e3a7ba106 | ||
|
|
c1a3de719a | ||
|
|
33deb028a6 | ||
|
|
919daabe54 | ||
|
|
0f2769ca3e | ||
|
|
a4a67296af | ||
|
|
b4000a025d | ||
|
|
a9761932bc | ||
|
|
29806780dc | ||
|
|
191337e08d | ||
|
|
a222dab781 | ||
|
|
230b25dd72 | ||
|
|
151d7d76da | ||
|
|
4f19bc4c5f | ||
|
|
11607dde04 | ||
|
|
e1bdb94efc | ||
|
|
6535fdd556 | ||
|
|
5fb89b865d | ||
|
|
6948e73ec7 | ||
|
|
c4cf03f899 | ||
|
|
d3de16c5f5 | ||
|
|
78fb09695b | ||
|
|
06818008c2 | ||
|
|
52ba4fd0cb | ||
|
|
4f2fa41f4e | ||
|
|
bf7ec853fa | ||
|
|
9fe3f634ec | ||
|
|
899e9294b2 | ||
|
|
dc9b459b27 | ||
|
|
7d3881f1da | ||
|
|
e5f9a6bf4b | ||
|
|
5cff657812 | ||
|
|
16387b0815 | ||
|
|
0857611a4e | ||
|
|
51ea2c297d | ||
|
|
fbd062165e | ||
|
|
55625c911f | ||
|
|
fca64de8e9 | ||
|
|
03725d1aaa | ||
|
|
a9487cbc84 | ||
|
|
ce5b2aa424 | ||
|
|
e535dce030 | ||
|
|
a7310b1fce | ||
|
|
cd6ce881a5 | ||
|
|
f0ef9f5e9c | ||
|
|
4ae16cb140 | ||
|
|
3d85e8e23b | ||
|
|
690a411cf3 | ||
|
|
be680408c9 | ||
|
|
4bdc8d4c68 | ||
|
|
db7a2bd682 | ||
|
|
2e0d482ccf | ||
|
|
e5b3a9c25d | ||
|
|
c289731212 | ||
|
|
f850c6b1b1 | ||
|
|
8ce234371b | ||
|
|
e5820b6b2b | ||
|
|
69f062b804 | ||
|
|
735dd5dbae | ||
|
|
cd7dddb5a1 | ||
|
|
7c76fd3d81 | ||
|
|
0b419a07f5 | ||
|
|
0b51e9cd5e | ||
|
|
e7b9fb277e | ||
|
|
838ee870d0 | ||
|
|
84af6c9f29 | ||
|
|
05227e8a80 | ||
|
|
76a209663d | ||
|
|
61c45e3dd8 | ||
|
|
e11a50bedf | ||
|
|
f4a6ea0fde | ||
|
|
e542e9f840 | ||
|
|
e0dcb2908d | ||
|
|
eaccb2e471 | ||
|
|
c1a1f2e7e7 | ||
|
|
45d5501aa7 | ||
|
|
388596e4fe | ||
|
|
b11d4f3ef0 | ||
|
|
998b9e387b | ||
|
|
b3e88db87e | ||
|
|
8471e82342 | ||
|
|
c3a0f2749b | ||
|
|
c2d96e0dce | ||
|
|
44e77fdf2b | ||
|
|
18d6feb499 | ||
|
|
0d91f5e982 | ||
|
|
5661fc9128 | ||
|
|
a6156676c8 | ||
|
|
b2bd2329bc | ||
|
|
724f1484b9 | ||
|
|
84f7b4a973 | ||
|
|
ddcb9be9a7 | ||
|
|
8d9b5bd479 | ||
|
|
4b88fc57b4 | ||
|
|
b76080e291 | ||
|
|
411f769896 | ||
|
|
ffcc6e8b63 | ||
|
|
86575e129b | ||
|
|
3e25f02da9 | ||
|
|
002d8ad1e4 | ||
|
|
6287a07912 | ||
|
|
fa97d0357f | ||
|
|
f836c3eccd | ||
|
|
499e27ea54 | ||
|
|
c2a35e2eea | ||
|
|
5138aa2c11 | ||
|
|
66a9e2e16e | ||
|
|
0f417f0040 | ||
|
|
1462ac9282 | ||
|
|
da371217ef | ||
|
|
c971ea3ec6 | ||
|
|
de98fdc37b | ||
|
|
fa1f12a0b5 | ||
|
|
de40b58e10 | ||
|
|
eea3557f61 | ||
|
|
7a243d329e | ||
|
|
98146251c4 | ||
|
|
0aa8cace1b | ||
|
|
450701ecec | ||
|
|
e9911fbac4 | ||
|
|
69c8eef78e | ||
|
|
2ff50667e7 | ||
|
|
f1cf274751 | ||
|
|
b65396ccd4 | ||
|
|
1ad76e847e | ||
|
|
4583ef93a4 | ||
|
|
f632d2474b | ||
|
|
b1d69f5b39 | ||
|
|
aba7e6dae4 | ||
|
|
8bcc90c766 | ||
|
|
e39dd1afce | ||
|
|
680cef0ee0 | ||
|
|
e17cf3c808 | ||
|
|
f49310b8ff | ||
|
|
c7955a38ef | ||
|
|
68067d7f44 | ||
|
|
5815665cc6 | ||
|
|
6649f35a0d | ||
|
|
5263abdc60 | ||
|
|
c49641538d | ||
|
|
d915e4f3ff | ||
|
|
bad5a35fce | ||
|
|
1bde6f3bfd | ||
|
|
4e0890ee1f | ||
|
|
d63308ca37 | ||
|
|
63cceb6a79 | ||
|
|
b8b2bdedf9 | ||
|
|
d5017e60c3 | ||
|
|
2e80f2a473 | ||
|
|
4c2dd63464 | ||
|
|
62302ad8c3 | ||
|
|
ed659958f3 | ||
|
|
36de05fe36 | ||
|
|
a64609f4f3 | ||
|
|
140f0001f2 | ||
|
|
587fe4af63 | ||
|
|
3a3a6536b7 | ||
|
|
4f695e1b4d | ||
|
|
6ba9096870 | ||
|
|
5106afb07f | ||
|
|
2bd4131636 | ||
|
|
7e452df1cc | ||
|
|
a626d06415 | ||
|
|
d159edc9a6 | ||
|
|
96fa2cbd2b | ||
|
|
9750881193 | ||
|
|
3db92021c7 | ||
|
|
5ccfc530b2 | ||
|
|
5f9fc31ae2 | ||
|
|
8e811de564 | ||
|
|
027de6339b | ||
|
|
bf9aff19b5 | ||
|
|
b05764d070 | ||
|
|
7be2acb2f3 | ||
|
|
83ad1eca26 | ||
|
|
92747246fc | ||
|
|
7380a59ab8 | ||
|
|
e56f8cc4fb | ||
|
|
7c79b9ab61 | ||
|
|
a62be684a0 | ||
|
|
a3dfbd7bff | ||
|
|
12eb40db01 | ||
|
|
d796543f5a | ||
|
|
e5ae626433 | ||
|
|
8195574345 | ||
|
|
6b029997d9 | ||
|
|
7a64483e6b | ||
|
|
e424b55036 | ||
|
|
613e6d6cde | ||
|
|
b3a402d936 | ||
|
|
c7d175b842 | ||
|
|
851bbeea48 | ||
|
|
c5bce756f0 | ||
|
|
3239f058c8 | ||
|
|
7e0c634c3a | ||
|
|
a9ced5c881 | ||
|
|
ace5f9d8c2 | ||
|
|
0452d1515a | ||
|
|
0330fdeb00 | ||
|
|
f2ff24d811 | ||
|
|
c51132f824 | ||
|
|
b4cb815ebf | ||
|
|
08d1ddd9fb | ||
|
|
23ac4cb3a4 | ||
|
|
5662ecab15 | ||
|
|
9e27d682f6 | ||
|
|
f0fcdcc76a | ||
|
|
135e0f8099 | ||
|
|
25eea295f6 | ||
|
|
c372f5980c | ||
|
|
3802acb1e0 | ||
|
|
bdb0030cf8 | ||
|
|
87f0540acd | ||
|
|
985d7b6a79 | ||
|
|
59f92f979a | ||
|
|
5cc286e383 | ||
|
|
26f4a5acad | ||
|
|
fdd08d3b70 | ||
|
|
1aac6c9474 | ||
|
|
7acb0c6d05 | ||
|
|
00eb86d03f | ||
|
|
1d0e836a29 | ||
|
|
ec6640b188 | ||
|
|
ff3b8d8398 | ||
|
|
022342839a | ||
|
|
38f0dc74f7 | ||
|
|
0bae05d4a9 | ||
|
|
1bb41a6e60 | ||
|
|
4423134739 | ||
|
|
190f1a59c5 | ||
|
|
5f99d613a0 | ||
|
|
6adc816805 | ||
|
|
aa97679327 | ||
|
|
94d8735d4b | ||
|
|
64c8d652e1 | ||
|
|
d30c5b4eee | ||
|
|
8ed75787cb | ||
|
|
4ee391e0d7 | ||
|
|
a67ca052d6 | ||
|
|
6b6e3803d1 | ||
|
|
51ec61c675 | ||
|
|
424f99efdf | ||
|
|
070be3de8b | ||
|
|
bd98269628 | ||
|
|
7ce371080c | ||
|
|
872632aca0 | ||
|
|
64bd03bd70 | ||
|
|
1e2d0faa55 | ||
|
|
8559786cc2 | ||
|
|
d4d22909cb | ||
|
|
4fae5758d5 | ||
|
|
f85efe6139 | ||
|
|
6ea9f2ade9 | ||
|
|
4036b784ed | ||
|
|
08a4ad662a | ||
|
|
e4021fb6e7 | ||
|
|
53b1d1097c | ||
|
|
55aa36fef8 | ||
|
|
3abcfb797a | ||
|
|
a741ddc03c | ||
|
|
7d26e33346 | ||
|
|
f6cd8066ab | ||
|
|
daefedebcd | ||
|
|
c37a3ec292 | ||
|
|
4245720851 | ||
|
|
f0b20dc445 | ||
|
|
e6f1209318 | ||
|
|
944944c49e | ||
|
|
ecbf396d4a | ||
|
|
b1474aaa60 | ||
|
|
b49e899974 | ||
|
|
11f2140c37 | ||
|
|
f1cd1ae710 | ||
|
|
e0a0a22542 | ||
|
|
2c9cf0bd55 | ||
|
|
38fdfb4ca2 | ||
|
|
15bab227bb | ||
|
|
d331a043a3 | ||
|
|
41d401a879 | ||
|
|
89f7e5e7ba | ||
|
|
aa1a69555b | ||
|
|
d1903afc69 | ||
|
|
dbc25dc555 | ||
|
|
a5d2324e21 | ||
|
|
38e82e4084 | ||
|
|
6bcc8bf2b2 | ||
|
|
f832f9b0d5 | ||
|
|
fc705d94e3 | ||
|
|
65eae027fa | ||
|
|
ac96f46c76 | ||
|
|
5c782397bb | ||
|
|
40387d5daa | ||
|
|
7f3351011d | ||
|
|
d6a6b6db14 | ||
|
|
388a1fd0be | ||
|
|
c2c929bf94 | ||
|
|
41473a520e | ||
|
|
50a5bb0671 | ||
|
|
20d0cfd156 | ||
|
|
5ad91fbb09 | ||
|
|
6229c99050 | ||
|
|
7e69d5d839 | ||
|
|
8700ec4e6d | ||
|
|
8cbf5fb8df | ||
|
|
9c288d66b5 | ||
|
|
8983edea66 | ||
|
|
95820fb9e6 | ||
|
|
6dc3d7ad9f | ||
|
|
cfa1aba1e0 | ||
|
|
43816d7528 | ||
|
|
6dd82afb0b | ||
|
|
e045f49787 | ||
|
|
38d3a39c06 | ||
|
|
23a5e95884 | ||
|
|
16f5a2a41a | ||
|
|
04e07acf98 | ||
|
|
3506773f51 | ||
|
|
d32e975eb9 | ||
|
|
21fb5a27e9 | ||
|
|
403f4ad78c | ||
|
|
ba5820b088 | ||
|
|
a93e319716 | ||
|
|
12aca72074 | ||
|
|
3fb903fdc6 | ||
|
|
4b26f8c712 | ||
|
|
37c4a36fdb | ||
|
|
811dcb3715 | ||
|
|
ccaac306e5 | ||
|
|
c596df9294 | ||
|
|
6852349d24 | ||
|
|
7c9d75b69e | ||
|
|
42201a98a1 | ||
|
|
09594b32f9 | ||
|
|
e2bb20121e | ||
|
|
56ebfb7848 | ||
|
|
5d9f53ff0c | ||
|
|
89d1b80ce7 | ||
|
|
962abf6904 | ||
|
|
ed3c5280a9 | ||
|
|
7222327992 | ||
|
|
e0b524fff2 | ||
|
|
e67bc5bee5 | ||
|
|
86a260e39b | ||
|
|
fdcb942f3c | ||
|
|
7a5c07b99c | ||
|
|
6d93eeb533 | ||
|
|
44179199ba | ||
|
|
100ad7d9ee | ||
|
|
c96c817ef5 | ||
|
|
519a64da82 | ||
|
|
24be9cd515 | ||
|
|
1987e816a5 | ||
|
|
0f4aa1ceea | ||
|
|
601fb45142 | ||
|
|
c9ebb13fa1 | ||
|
|
618113079f | ||
|
|
cc34d19d24 | ||
|
|
02ffb52f4a | ||
|
|
361afff798 | ||
|
|
2a6b0215f0 | ||
|
|
c1c296233f | ||
|
|
e05fdd8acd | ||
|
|
83823911b5 | ||
|
|
7004369c68 | ||
|
|
f5d7ce0f86 | ||
|
|
32eb8c8263 | ||
|
|
44c2c765ae | ||
|
|
0d5721910e | ||
|
|
28d67d59cd | ||
|
|
1d72480c17 | ||
|
|
1af5da6aad | ||
|
|
ea1c6ee30f | ||
|
|
97ea479cdc | ||
|
|
e088979fbe | ||
|
|
5e5c05362c | ||
|
|
c2a21915ff | ||
|
|
cbb2b2f3c2 | ||
|
|
82a74c88aa | ||
|
|
6b9dd23e3a | ||
|
|
b754f2d173 | ||
|
|
ee233d16d6 | ||
|
|
65f13f773e | ||
|
|
d4646d43a7 | ||
|
|
6465450b64 | ||
|
|
01aa4d3281 | ||
|
|
211f29b723 | ||
|
|
d6bfc98a61 | ||
|
|
5457c2da67 | ||
|
|
14b1b456e1 | ||
|
|
972e15e601 | ||
|
|
03de7e1ec6 | ||
|
|
3edf75123a | ||
|
|
fd1c423826 | ||
|
|
a314e5b35e | ||
|
|
e02ca8871d | ||
|
|
834d2abe70 | ||
|
|
91986fff02 | ||
|
|
05b9970aa6 | ||
|
|
6f301707f9 | ||
|
|
5865176f36 | ||
|
|
461037f645 | ||
|
|
c980f39aab | ||
|
|
a854fa60a2 | ||
|
|
1c8224f4c6 | ||
|
|
ca403dc45d | ||
|
|
96705c156a | ||
|
|
7909095ff3 | ||
|
|
aa5adb0fce | ||
|
|
dcb414aa06 | ||
|
|
afe093f1ca | ||
|
|
cc066b3576 | ||
|
|
39cd1cdd43 | ||
|
|
176bf00c16 | ||
|
|
68e38c8893 | ||
|
|
48220fb33f | ||
|
|
ed622e254a | ||
|
|
6e7d6a85b4 | ||
|
|
e8061a9c2b | ||
|
|
97a66f7a64 | ||
|
|
b6c3b3ef46 | ||
|
|
f4a57a13bc | ||
|
|
242636b36b | ||
|
|
ba7d7dcec0 | ||
|
|
ba7271b4d8 | ||
|
|
ca2d26a1e2 | ||
|
|
f6106cd26f | ||
|
|
1867336907 | ||
|
|
f5383263bc | ||
|
|
d5cf77cd60 | ||
|
|
f458e2d484 | ||
|
|
af5e05db2e | ||
|
|
32a64d02c7 | ||
|
|
9516d1a306 | ||
|
|
d91b96814e | ||
|
|
56d6bb1913 | ||
|
|
fc156d0014 | ||
|
|
0b8df8d3f2 | ||
|
|
83955e87ac | ||
|
|
4a9db243a1 | ||
|
|
d4f1f8db00 | ||
|
|
95f61bd223 | ||
|
|
7f476a79b3 | ||
|
|
65e21cf13c | ||
|
|
7943af359c | ||
|
|
09e9c6a522 | ||
|
|
a9def2fc15 | ||
|
|
27197faba9 | ||
|
|
ffe60bd960 | ||
|
|
d752be5f74 | ||
|
|
3056c41507 | ||
|
|
d42e9c4d1b | ||
|
|
5912941942 | ||
|
|
9b8106b382 | ||
|
|
9215eb5e45 | ||
|
|
fe7f220c21 | ||
|
|
3bb9704cd5 | ||
|
|
eb77452857 | ||
|
|
6d7cfac8b2 | ||
|
|
31754a39c9 | ||
|
|
bde48e563e | ||
|
|
0cfd760a36 | ||
|
|
13fe88000a | ||
|
|
cc8ad23d6f | ||
|
|
5c2cbb58bc | ||
|
|
6342c4f338 | ||
|
|
5fa70bdbd8 | ||
|
|
2a876e8b86 | ||
|
|
0533ca9941 | ||
|
|
5f20d2e15a | ||
|
|
6d1d5d64d1 | ||
|
|
06d6b513cd | ||
|
|
afa51125de | ||
|
|
26c07b1ffb | ||
|
|
9ecca47e69 | ||
|
|
6c1df93215 | ||
|
|
06fd0658ae | ||
|
|
a17f38a4e2 | ||
|
|
6ef4794778 | ||
|
|
4cd3ce164d | ||
|
|
8e3e57c1c8 | ||
|
|
61fbfda501 | ||
|
|
9017b9a74f |
@@ -24,7 +24,9 @@ notifications:
|
||||
discussions: notifications@superset.apache.org
|
||||
|
||||
github:
|
||||
del_branch_on_merge: true
|
||||
pull_requests:
|
||||
del_branch_on_merge: true
|
||||
allow_update_branch: true
|
||||
description: "Apache Superset is a Data Visualization and Data Exploration Platform"
|
||||
homepage: https://superset.apache.org/
|
||||
labels:
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
|
||||
|
||||
# Notify PMC members of changes to CI-executed scripts (supply-chain risk:
|
||||
# scripts/ files run directly in CI workflows and can execute arbitrary code)
|
||||
|
||||
/scripts/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
|
||||
|
||||
# Notify PMC members of changes to required GitHub Actions
|
||||
|
||||
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @Antonio-RiveroMartnez
|
||||
|
||||
+33
-4
@@ -18,10 +18,32 @@ e-mail address [security@superset.apache.org](mailto:security@superset.apache.or
|
||||
More details can be found on the ASF website at
|
||||
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability)
|
||||
|
||||
We kindly ask you to include the following information in your report:
|
||||
- Apache Superset version that you are using
|
||||
- A sanitized copy of your `superset_config.py` file or any config overrides
|
||||
- Detailed steps to reproduce the vulnerability
|
||||
**Submission Standards & AI Policy**
|
||||
|
||||
To ensure engineering focus remains on verified risks and to manage high reporting volumes, all reports must meet the following criteria:
|
||||
- Plain Text Format: In accordance with Apache guidelines, please provide all details in plain text within the email body. Avoid sending PDFs, Word documents, or password-protected archives.
|
||||
- Mandatory AI Disclosure: If you utilized Large Language Models (LLMs) or AI tools to identify a flaw or assist in writing a report, you must disclose this in your submission so our triage team can contextualize the findings.
|
||||
- Human-Verified PoC: All submissions must include a manual, step-by-step Proof of Concept (PoC) performed on a supported release. Raw AI outputs, hypothetical chat transcripts, or unverified scanner logs will be closed as Invalid.
|
||||
|
||||
We kindly ask you to include the following information in your report to assist our developers in triaging and remediating issues efficiently:
|
||||
- Version/Commit: The specific version of Apache Superset or the Git commit hash you are using.
|
||||
- Configuration: A sanitized copy of your `superset_config.py` file or any config overrides.
|
||||
- Environment: Your deployment method (e.g., Docker Compose, Helm, or source) and relevant OS/Browser details.
|
||||
- Impacted Component: Identification of the affected area (e.g., Python backend, React frontend, or a specific database connector).
|
||||
- Expected vs. Actual Behavior: A clear description of the intended system behavior versus the observed vulnerability.
|
||||
- Detailed Reproduction Steps: Clear, manual steps to reproduce the vulnerability.
|
||||
|
||||
**Out of Scope Vulnerabilities**
|
||||
|
||||
To prioritize engineering efforts on genuine architectural risks, the following scenarios are explicitly out of scope and will not be issued a CVE:
|
||||
- Attacks requiring Admin privileges: (e.g., CSS injection, template manipulation, dashboard ownership overrides, or modifying global system settings). Per the CVE vulnerability definition in CNA Operational Rules 4.1, a qualifying vulnerability must allow violation of a security policy. The Admin role is a fully trusted operational boundary defined by Apache Superset's security policy; actions within this boundary do not violate that policy and are therefore considered intended capabilities 'by design,' not vulnerabilities.
|
||||
- Brute Force and Rate Limiting: Reports targeting a lack of resource exhaustion protections, generic rate-limiting, or volumetric Denial of Service (DoS) attempts.
|
||||
- Theoretical attack vectors: Issues without a demonstrable, reproducible exploit path.
|
||||
- Non-Exploitable Findings: Missing security headers, generic banner disclosures, or descriptive error messages that do not lead to a direct, documented exploit.
|
||||
|
||||
**Outcome of Reports**
|
||||
|
||||
Reports that are deemed out-of-scope for a CVE but represent valid security best practices or hardening opportunities may be converted into public GitHub issues. This allows the community to contribute to the general hardening of the platform even when a specific vulnerability threshold is not met.
|
||||
|
||||
Note that Apache Superset is not responsible for any third-party dependencies that may
|
||||
have security issues. Any vulnerabilities found in third-party dependencies should be
|
||||
@@ -29,6 +51,13 @@ reported to the maintainers of those projects. Results from security scans of Ap
|
||||
Superset dependencies found on its official Docker image can be remediated at release time
|
||||
by extending the image itself.
|
||||
|
||||
**Vulnerability Aggregation & CVE Attribution**
|
||||
|
||||
In accordance with MITRE CNA Operational Rules (4.1.10, 4.1.11, and 4.2.13), Apache Superset issues CVEs based on the underlying architectural root cause rather than the number of affected endpoints or exploit payloads.
|
||||
- Aggregation: If multiple exploit vectors stem from the same programmatic failure or shared vulnerable code, they must be aggregated into a single, comprehensive report.
|
||||
- Independent Fixes: Separate CVEs will only be assigned if the vulnerabilities reside in decoupled architectural modules and can be fixed independently of one another.
|
||||
Reports that fail to aggregate related findings will be merged during triage to ensure an accurate and defensible CVE record.
|
||||
|
||||
**Your responsible disclosure and collaboration are invaluable.**
|
||||
|
||||
## Extra Information
|
||||
|
||||
@@ -26,16 +26,16 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ inputs.build == 'true' }}
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ inputs.build == 'true' }}
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Try to login to DockerHub
|
||||
if: ${{ inputs.login-to-dockerhub == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ inputs.dockerhub-user }}
|
||||
password: ${{ inputs.dockerhub-token }}
|
||||
|
||||
@@ -10,7 +10,7 @@ runs:
|
||||
steps:
|
||||
|
||||
- name: Setup Node Env
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -21,7 +21,7 @@ runs:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
if: ${{ inputs.from-npm == 'false' }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: apache-superset/supersetbot
|
||||
path: supersetbot
|
||||
|
||||
@@ -4,6 +4,10 @@ updates:
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
ignore:
|
||||
# Ignore temporarily as release schedule is too mentally taxing for dep-handling maintainers
|
||||
# Additionally, very few PRs are reviewed by this action.
|
||||
- dependency-name: anthropics/claude-code-action
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
- any-glob-to-any-file:
|
||||
- 'superset/migrations/**'
|
||||
|
||||
"risk:ci-script":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'scripts/**'
|
||||
|
||||
############################################
|
||||
# Dependencies
|
||||
############################################
|
||||
|
||||
@@ -51,27 +51,31 @@ jobs:
|
||||
- name: supersetbot bump-python -p "${{ github.event.inputs.package }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_PACKAGE: ${{ github.event.inputs.package }}
|
||||
INPUT_GROUP: ${{ github.event.inputs.group }}
|
||||
INPUT_EXTRA_FLAGS: ${{ github.event.inputs.extra-flags }}
|
||||
INPUT_LIMIT: ${{ github.event.inputs.limit }}
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
|
||||
PACKAGE_OPT=""
|
||||
if [ -n "${{ github.event.inputs.package }}" ]; then
|
||||
PACKAGE_OPT="-p ${{ github.event.inputs.package }}"
|
||||
if [ -n "${INPUT_PACKAGE}" ]; then
|
||||
PACKAGE_OPT="-p ${INPUT_PACKAGE}"
|
||||
fi
|
||||
|
||||
GROUP_OPT=""
|
||||
if [ -n "${{ github.event.inputs.group }}" ]; then
|
||||
GROUP_OPT="-g ${{ github.event.inputs.group }}"
|
||||
if [ -n "${INPUT_GROUP}" ]; then
|
||||
GROUP_OPT="-g ${INPUT_GROUP}"
|
||||
fi
|
||||
|
||||
EXTRA_FLAGS="${{ github.event.inputs.extra-flags }}"
|
||||
EXTRA_FLAGS="${INPUT_EXTRA_FLAGS}"
|
||||
|
||||
supersetbot bump-python \
|
||||
--verbose \
|
||||
--use-current-repo \
|
||||
--include-subpackages \
|
||||
--limit ${{ github.event.inputs.limit }} \
|
||||
--limit ${INPUT_LIMIT} \
|
||||
$PACKAGE_OPT \
|
||||
$GROUP_OPT \
|
||||
$EXTRA_FLAGS
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Check and notify
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment access denied
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@28f83620103c48a57093dcc2837eec89e036bb9f # beta
|
||||
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
|
||||
@@ -14,6 +14,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
setup_matrix:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
@@ -39,6 +40,7 @@ jobs:
|
||||
IMAGE_TAG: apache/superset:GHA-${{ matrix.build_preset }}-${{ github.run_id }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
@@ -89,11 +91,7 @@ jobs:
|
||||
# in the context of push (using multi-platform build), we need to pull the image locally
|
||||
- name: Docker pull
|
||||
if: github.event_name == 'push' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker)
|
||||
run: |
|
||||
for i in 1 2 3; do
|
||||
docker pull $IMAGE_TAG && break
|
||||
[ $i -lt 3 ] && sleep 30
|
||||
done
|
||||
run: docker pull $IMAGE_TAG
|
||||
|
||||
- name: Print docker stats
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
|
||||
|
||||
@@ -16,10 +16,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.NPM_TOKEN != '') || '' }}" ]; then
|
||||
if [ -n "${NPM_TOKEN}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
NPM_TOKEN: ${{ (secrets.NPM_TOKEN != '') || '' }}
|
||||
build:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
@@ -29,7 +31,7 @@ jobs:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
@@ -20,10 +20,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}" ]; then
|
||||
if [ -n "${AWS_ACCESS_KEY_ID}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}
|
||||
ephemeral-env-cleanup:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -56,7 +58,7 @@ jobs:
|
||||
- name: Login to Amazon ECR
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2
|
||||
|
||||
- name: Delete ECR image tag
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
@@ -69,7 +71,7 @@ jobs:
|
||||
|
||||
- name: Comment (success)
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
id: eval-label
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
LABEL_NAME="${{ github.event.inputs.label_name }}"
|
||||
LABEL_NAME="${INPUT_LABEL_NAME}"
|
||||
else
|
||||
LABEL_NAME="${{ github.event.label.name }}"
|
||||
fi
|
||||
@@ -60,10 +60,12 @@ jobs:
|
||||
echo "result=noop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
env:
|
||||
INPUT_LABEL_NAME: ${{ github.event.inputs.label_name }}
|
||||
- name: Get event SHA
|
||||
id: get-sha
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -94,7 +96,7 @@ jobs:
|
||||
core.setOutput("sha", prSha);
|
||||
|
||||
- name: Looking for feature flags in PR description
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
id: eval-feature-flags
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
return results;
|
||||
|
||||
- name: Reply with confirmation comment
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -197,7 +199,7 @@ jobs:
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@c962da2960ed15f492addc26fffa274485265950 # v2
|
||||
uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2
|
||||
|
||||
- name: Load, tag and push image to ECR
|
||||
id: push-image
|
||||
@@ -233,7 +235,7 @@ jobs:
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@c962da2960ed15f492addc26fffa274485265950 # v2
|
||||
uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2
|
||||
|
||||
- name: Check target image exists in ECR
|
||||
id: check-image
|
||||
@@ -248,7 +250,7 @@ jobs:
|
||||
|
||||
- name: Fail on missing container image
|
||||
if: steps.check-image.outcome == 'failure'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
@@ -276,7 +278,9 @@ jobs:
|
||||
- name: Describe ECS service
|
||||
id: describe-services
|
||||
run: |
|
||||
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
|
||||
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${INPUT_ISSUE_NUMBER}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
- name: Create ECS service
|
||||
id: create-service
|
||||
if: steps.describe-services.outputs.active != 'true'
|
||||
@@ -296,7 +300,7 @@ jobs:
|
||||
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
id: deploy-task
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@cbf54ec46642b86ff78c2f5793da6746954cf8ff # v2
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service
|
||||
@@ -307,7 +311,9 @@ jobs:
|
||||
- name: List tasks
|
||||
id: list-tasks
|
||||
run: |
|
||||
echo "task=$(aws ecs list-tasks --cluster superset-ci --service-name pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
|
||||
echo "task=$(aws ecs list-tasks --cluster superset-ci --service-name pr-${INPUT_ISSUE_NUMBER}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
- name: Get network interface
|
||||
id: get-eni
|
||||
run: |
|
||||
@@ -318,7 +324,7 @@ jobs:
|
||||
echo "ip=$(aws ec2 describe-network-interfaces --network-interface-ids ${{ steps.get-eni.outputs.eni }} | jq -r '.NetworkInterfaces | first | .Association.PublicIp')" >> $GITHUB_OUTPUT
|
||||
- name: Comment (success)
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
@@ -331,7 +337,7 @@ jobs:
|
||||
});
|
||||
- name: Comment (failure)
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
|
||||
@@ -16,10 +16,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.FOSSA_API_KEY != '' ) || '' }}" ]; then
|
||||
if [ -n "${FOSSA_API_KEY}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
FOSSA_API_KEY: ${{ (secrets.FOSSA_API_KEY != '' ) || '' }}
|
||||
license_check:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for 'hold' label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
|
||||
@@ -28,20 +28,21 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
|
||||
- name: Install helm-docs
|
||||
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
|
||||
|
||||
- name: Enable brew and helm-docs
|
||||
# Add brew to the path - see https://github.com/actions/runner-images/issues/6283
|
||||
run: |
|
||||
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||
echo "HOMEBREW_PREFIX=$HOMEBREW_PREFIX" >>"${GITHUB_ENV}"
|
||||
echo "HOMEBREW_CELLAR=$HOMEBREW_CELLAR" >>"${GITHUB_ENV}"
|
||||
echo "HOMEBREW_REPOSITORY=$HOMEBREW_REPOSITORY" >>"${GITHUB_ENV}"
|
||||
brew install norwoodj/tap/helm-docs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -56,18 +57,24 @@ jobs:
|
||||
yarn install --immutable
|
||||
|
||||
- name: Cache pre-commit environments
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
restore-keys: |
|
||||
pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
uses: ./.github/actions/file-changes-action
|
||||
with:
|
||||
output: ' '
|
||||
|
||||
- name: pre-commit
|
||||
run: |
|
||||
set +e # Don't exit immediately on failure
|
||||
export SKIP=eslint-frontend,type-checking-frontend
|
||||
pre-commit run --all-files
|
||||
export SKIP=type-checking-frontend
|
||||
pre-commit run --files ${{ steps.changed_files.outputs.files }}
|
||||
PRE_COMMIT_EXIT_CODE=$?
|
||||
git diff --quiet --exit-code
|
||||
GIT_DIFF_EXIT_CODE=$?
|
||||
|
||||
@@ -16,10 +16,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.NPM_TOKEN != '' && secrets.GH_PERSONAL_ACCESS_TOKEN != '') || '' }}" ]; then
|
||||
if [ -n "${NPM_TOKEN}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
NPM_TOKEN: ${{ (secrets.NPM_TOKEN != '' && secrets.GH_PERSONAL_ACCESS_TOKEN != '') || '' }}
|
||||
build:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
@@ -42,13 +44,13 @@ jobs:
|
||||
|
||||
- name: Install Node.js
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -62,7 +64,7 @@ jobs:
|
||||
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
id: npm-cache # use this to check for `cache-hit` (`steps.npm-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
steps:
|
||||
- name: Security Check - Authorize Maintainers Only
|
||||
id: auth
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -102,10 +102,12 @@ jobs:
|
||||
- name: Install Superset Showtime
|
||||
if: steps.auth.outputs.authorized == 'true'
|
||||
run: |
|
||||
echo "::notice::Maintainer ${{ github.actor }} triggered deploy for PR ${{ github.event.pull_request.number || github.event.inputs.pr_number }}"
|
||||
echo "::notice::Maintainer ${{ github.actor }} triggered deploy for PR ${PULL_REQUEST_NUMBER}"
|
||||
pip install --upgrade superset-showtime
|
||||
showtime version
|
||||
|
||||
env:
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
- name: Check what actions are needed
|
||||
if: steps.auth.outputs.authorized == 'true'
|
||||
id: check
|
||||
@@ -113,12 +115,14 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }}
|
||||
INPUT_SHA: ${{ github.event.inputs.sha }}
|
||||
run: |
|
||||
# Bulletproof PR number extraction
|
||||
if [[ -n "${{ github.event.pull_request.number }}" ]]; then
|
||||
PR_NUM="${{ github.event.pull_request.number }}"
|
||||
elif [[ -n "${{ github.event.inputs.pr_number }}" ]]; then
|
||||
PR_NUM="${{ github.event.inputs.pr_number }}"
|
||||
elif [[ -n "${INPUT_PR_NUMBER}" ]]; then
|
||||
PR_NUM="${INPUT_PR_NUMBER}"
|
||||
else
|
||||
echo "❌ No PR number found in event or inputs"
|
||||
exit 1
|
||||
@@ -127,8 +131,8 @@ jobs:
|
||||
echo "Using PR number: $PR_NUM"
|
||||
|
||||
# Run sync check-only with optional SHA override
|
||||
if [[ -n "${{ github.event.inputs.sha }}" ]]; then
|
||||
OUTPUT=$(python -m showtime sync $PR_NUM --check-only --sha "${{ github.event.inputs.sha }}")
|
||||
if [[ -n "${INPUT_SHA}" ]]; then
|
||||
OUTPUT=$(python -m showtime sync $PR_NUM --check-only --sha "${INPUT_SHA}")
|
||||
else
|
||||
OUTPUT=$(python -m showtime sync $PR_NUM --check-only)
|
||||
fi
|
||||
|
||||
@@ -27,10 +27,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}" ]; then
|
||||
if [ -n "${SUPERSET_SITE_BUILD}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
|
||||
build-deploy:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
@@ -44,15 +46,15 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
- name: Install Graphviz
|
||||
run: sudo apt-get install -y graphviz
|
||||
- name: Compute Entity Relationship diagram (ERD)
|
||||
@@ -68,7 +70,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -77,7 +79,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
|
||||
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
@@ -72,9 +72,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
@@ -104,20 +104,20 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: database-diagnostics
|
||||
path: docs/src/data/
|
||||
if_no_artifact_found: "warning"
|
||||
if_no_artifact_found: 'warning'
|
||||
- name: Use fresh diagnostics
|
||||
run: |
|
||||
if [ -f "src/data/databases-diagnostics.json" ]; then
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
@@ -259,7 +259,7 @@ jobs:
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.check.outputs.superset-extensions-cli
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: superset-extensions-cli
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Upload HTML coverage report
|
||||
if: steps.check.outputs.superset-extensions-cli
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: superset-extensions-cli-coverage-html
|
||||
path: htmlcov/
|
||||
|
||||
@@ -54,14 +54,14 @@ jobs:
|
||||
- name: Save Docker Image as Artifact
|
||||
if: steps.check.outputs.frontend
|
||||
run: |
|
||||
docker save $TAG | gzip > docker-image.tar.gz
|
||||
docker save $TAG | zstd -3 --threads=0 > docker-image.tar.zst
|
||||
|
||||
- name: Upload Docker Image Artifact
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: docker-image
|
||||
path: docker-image.tar.gz
|
||||
path: docker-image.tar.zst
|
||||
|
||||
sharded-jest-tests:
|
||||
needs: frontend-build
|
||||
@@ -78,7 +78,8 @@ jobs:
|
||||
name: docker-image
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < docker-image.tar.gz
|
||||
run: |
|
||||
zstd -d < docker-image.tar.zst | docker load
|
||||
|
||||
- name: npm run test with coverage
|
||||
run: |
|
||||
@@ -90,7 +91,7 @@ jobs:
|
||||
"npm run test -- --coverage --shard=${{ matrix.shard }}/8 --coverageReporters=json"
|
||||
|
||||
- name: Upload Coverage Artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: coverage-artifacts-${{ matrix.shard }}
|
||||
path: superset-frontend/coverage
|
||||
@@ -127,7 +128,7 @@ jobs:
|
||||
run: npx nyc merge coverage/ merged-output/coverage-summary.json
|
||||
|
||||
- name: Upload Code Coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: javascript
|
||||
use_oidc: true
|
||||
@@ -148,7 +149,7 @@ jobs:
|
||||
|
||||
- name: Load Docker Image
|
||||
run: |
|
||||
docker load < docker-image.tar.gz
|
||||
zstd -d < docker-image.tar.zst | docker load
|
||||
|
||||
- name: lint
|
||||
run: |
|
||||
@@ -171,7 +172,8 @@ jobs:
|
||||
name: docker-image
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < docker-image.tar.gz
|
||||
run: |
|
||||
zstd -d < docker-image.tar.zst | docker load
|
||||
|
||||
- name: Build Plugins Packages
|
||||
run: |
|
||||
@@ -189,7 +191,8 @@ jobs:
|
||||
name: docker-image
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < docker-image.tar.gz
|
||||
run: |
|
||||
zstd -d < docker-image.tar.zst | docker load
|
||||
|
||||
- name: Build Storybook and Run Tests
|
||||
run: |
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: v3.16.4
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: v3.5.4
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
CR_RELEASE_NAME_TEMPLATE: "superset-helm-chart-{{ .Version }}"
|
||||
|
||||
- name: Open Pull Request
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ env.branch_name }}';
|
||||
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -16,6 +16,8 @@ concurrency:
|
||||
jobs:
|
||||
test-mysql:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -68,11 +70,12 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: python,mysql
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
- name: Generate database diagnostics for docs
|
||||
if: steps.check.outputs.python
|
||||
env:
|
||||
@@ -98,13 +101,15 @@ jobs:
|
||||
"
|
||||
- name: Upload database diagnostics artifact
|
||||
if: steps.check.outputs.python
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: database-diagnostics
|
||||
path: databases-diagnostics.json
|
||||
retention-days: 7
|
||||
test-postgres:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["current", "previous", "next"]
|
||||
@@ -159,14 +164,17 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: python,postgres
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
test-sqlite:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -211,8 +219,9 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: python,sqlite
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
@@ -17,6 +17,8 @@ concurrency:
|
||||
jobs:
|
||||
test-postgres-presto:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -77,14 +79,17 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: python,presto
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
test-postgres-hive:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -145,8 +150,9 @@ jobs:
|
||||
pip install -e .[hive]
|
||||
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: python,hive
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
@@ -17,6 +17,8 @@ concurrency:
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["previous", "current", "next"]
|
||||
@@ -53,8 +55,9 @@ jobs:
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
with:
|
||||
flags: python,unit
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- name: Quickly add thumbs up!
|
||||
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, '@supersetbot')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
|
||||
|
||||
@@ -31,10 +31,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.DOCKERHUB_USER != '' && secrets.DOCKERHUB_TOKEN != '') || '' }}" ]; then
|
||||
if [ -n "${DOCKERHUB_USER}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
DOCKERHUB_USER: ${{ (secrets.DOCKERHUB_USER != '' && secrets.DOCKERHUB_TOKEN != '') || '' }}
|
||||
docker-release:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
@@ -60,7 +62,7 @@ jobs:
|
||||
build: "true"
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -72,17 +74,20 @@ jobs:
|
||||
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_RELEASE: ${{ github.event.inputs.release }}
|
||||
INPUT_FORCE_LATEST: ${{ github.event.inputs.force-latest }}
|
||||
INPUT_GIT_REF: ${{ github.event.inputs.git-ref }}
|
||||
run: |
|
||||
RELEASE="${{ github.event.release.tag_name }}"
|
||||
FORCE_LATEST=""
|
||||
EVENT="${{github.event_name}}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# in the case of a manually-triggered run, read release from input
|
||||
RELEASE="${{ github.event.inputs.release }}"
|
||||
if [ "${{ github.event.inputs.force-latest }}" = "true" ]; then
|
||||
RELEASE="${INPUT_RELEASE}"
|
||||
if [ "${INPUT_FORCE_LATEST}" = "true" ]; then
|
||||
FORCE_LATEST="--force-latest"
|
||||
fi
|
||||
git checkout "${{ github.event.inputs.git-ref }}"
|
||||
git checkout "${INPUT_GIT_REF}"
|
||||
EVENT="release"
|
||||
fi
|
||||
|
||||
@@ -112,7 +117,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -122,6 +127,7 @@ jobs:
|
||||
- name: Label the PRs with the right release-related labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_RELEASE: ${{ github.event.inputs.release }}
|
||||
run: |
|
||||
export GITHUB_ACTOR=""
|
||||
git fetch --all --tags
|
||||
@@ -129,6 +135,6 @@ jobs:
|
||||
RELEASE="${{ github.event.release.tag_name }}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# in the case of a manually-triggered run, read release from input
|
||||
RELEASE="${{ github.event.inputs.release }}"
|
||||
RELEASE="${INPUT_RELEASE}"
|
||||
fi
|
||||
supersetbot release-label $RELEASE
|
||||
|
||||
@@ -19,10 +19,12 @@ jobs:
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.GSHEET_KEY != '' ) || '' }}" ]; then
|
||||
if [ -n "${GSHEET_KEY}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
env:
|
||||
GSHEET_KEY: ${{ (secrets.GSHEET_KEY != '' ) || '' }}
|
||||
process-and-upload:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ rat-results.txt
|
||||
superset/app/
|
||||
superset-websocket/config.json
|
||||
.direnv
|
||||
*.log
|
||||
|
||||
# Node.js, webpack artifacts, storybook
|
||||
*.entry.js
|
||||
|
||||
@@ -53,7 +53,7 @@ extension-pkg-whitelist=pyarrow
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=all
|
||||
enable=disallowed-sql-import,consider-using-transaction
|
||||
enable=json-import,disallowed-sql-import,consider-using-transaction
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
@@ -50,4 +50,3 @@ under the License.
|
||||
- [4.1.4](./CHANGELOG/4.1.4.md)
|
||||
- [5.0.0](./CHANGELOG/5.0.0.md)
|
||||
- [6.0.0](./CHANGELOG/6.0.0.md)
|
||||
- [6.1.0](./CHANGELOG/6.1.0.md)
|
||||
|
||||
-1564
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -458,7 +458,7 @@ cd ../
|
||||
sed -i '' "s/version_string = .*/version_string = \"$SUPERSET_VERSION\"/" setup.py
|
||||
|
||||
# build the python distribution
|
||||
python setup.py sdist
|
||||
python -m build
|
||||
```
|
||||
|
||||
Publish to PyPI
|
||||
|
||||
@@ -287,6 +287,11 @@ categories:
|
||||
url: https://www.gfk.com/home
|
||||
contributors: ["@mherr"]
|
||||
|
||||
- name: Hifadih Business & Technology
|
||||
url: https://hifadih.net/en
|
||||
logo: hifadih.png
|
||||
contributors: ["@saintLaurent00"]
|
||||
|
||||
# Logo approved by @anmol-hpe on behalf of HPE
|
||||
- name: HPE
|
||||
url: https://www.hpe.com/in/en/home.html
|
||||
|
||||
+81
-93
@@ -24,42 +24,68 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Row-level security now filters table reads a same-named CTE used to hide
|
||||
### Granular Export Controls
|
||||
|
||||
`extract_tables_from_statement()` decided whether a reference was a CTE by matching its
|
||||
bare name against the enclosing scope's CTE names; it now resolves the name through
|
||||
`Scope.cte_sources`. Three kinds of real table read whose bare name collided with a CTE's
|
||||
were mistaken for the CTE and dropped from a statement's tables, so they were neither
|
||||
RLS-filtered nor access-checked: a schema- or catalog-qualified reference, a non-recursive
|
||||
CTE's own name inside its body, and a forward reference to a later `WITH` item.
|
||||
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
|
||||
|
||||
```sql
|
||||
WITH orders AS (SELECT 1 AS d) SELECT * FROM (SELECT * FROM public.orders) AS z
|
||||
WITH orders AS (SELECT * FROM orders) SELECT * FROM orders
|
||||
WITH q1 AS (SELECT key FROM q2), q2 AS (SELECT 1 AS key) SELECT * FROM q1
|
||||
```
|
||||
| Permission | Controls |
|
||||
|---|---|
|
||||
| `can_export_data` | CSV, Excel, JSON exports |
|
||||
| `can_export_image` | Screenshot/PDF exports |
|
||||
| `can_copy_clipboard` | Copy-to-clipboard operations |
|
||||
|
||||
Each read is now reported, so it is filtered when `RLS_IN_SQLLAB` is enabled, matched
|
||||
against `DISALLOWED_SQL_TABLES`, and requires dataset access under
|
||||
`raise_for_access(force_dataset_match=True)`. A query that previously ran, reading those
|
||||
rows unfiltered, may now be filtered or rejected. There is no opt-out — the previous
|
||||
behavior was a row-level-security bypass.
|
||||
When the feature flag is enabled, these permissions are enforced on both the frontend (disabled buttons with tooltips) and backend (403 responses from API endpoints). When disabled, legacy `can_csv` behavior is preserved.
|
||||
|
||||
### Table aliases keep their quoting through the row-level security rewrite
|
||||
**Migration behavior:** All three new permissions are granted to every role that currently has `can_csv`, preserving existing access. Admins can then selectively revoke individual export permissions from specific roles as needed.
|
||||
|
||||
Both RLS transformers took the table alias as a string with its quoting stripped and
|
||||
emitted it verbatim; they now carry the parsed identifier. Emitted SQL is unchanged for an
|
||||
unquoted identifier; a quoted one keeps its quoting, and a column-alias list
|
||||
(`FROM t AS x (c1, c2)`) survives the rewrite instead of being dropped. This repairs
|
||||
row-level security for any aliased table on Snowflake, and for at least one statement shape
|
||||
on MSSQL where the rewrite previously raised `AttributeError`.
|
||||
### Deck.gl MapBox viewport and opacity controls are functional
|
||||
|
||||
## 6.1.0
|
||||
The Deck.gl MapBox chart's **Opacity**, **Default longitude**, **Default latitude**, and **Zoom** controls were previously non-functional — changing them had no effect on the rendered map. These controls are now wired up correctly.
|
||||
|
||||
**Behavior change for existing charts:** Previously, the viewport controls had hard-coded default values (`-122.405293`, `37.772123`, zoom `11` — San Francisco) that were stored in each chart's `form_data` but never applied. The map always used `fitBounds` to center on the data. With this fix, those stored values are now respected, which means existing MapBox charts may open centered on the old default coordinates instead of fitting to data bounds.
|
||||
|
||||
**To restore fit-to-data behavior:** Open the chart in Explore, clear the **Default longitude**, **Default latitude**, and **Zoom** fields in the Viewport section, and re-save the chart.
|
||||
|
||||
### ClickHouse minimum driver version bump
|
||||
|
||||
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
|
||||
|
||||
### MCP Tool Observability
|
||||
|
||||
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
|
||||
|
||||
**Two-layer instrumentation:**
|
||||
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
|
||||
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
|
||||
|
||||
**Action naming convention:**
|
||||
- Tool-level logs: `mcp_tool_call` (via middleware)
|
||||
- Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`)
|
||||
|
||||
**Querying MCP logs:**
|
||||
```sql
|
||||
-- Top slowest MCP operations
|
||||
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
|
||||
FROM logs
|
||||
WHERE action LIKE 'mcp.%'
|
||||
GROUP BY action
|
||||
ORDER BY avg_ms DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- MCP tool success rate
|
||||
SELECT
|
||||
json_extract(curated_payload, '$.tool') as tool,
|
||||
COUNT(*) as total_calls,
|
||||
SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) as successful,
|
||||
ROUND(100.0 * SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
|
||||
FROM logs
|
||||
WHERE action = 'mcp_tool_call'
|
||||
GROUP BY tool
|
||||
ORDER BY total_calls DESC;
|
||||
```
|
||||
|
||||
**Security note:** Sensitive parameters (passwords, API keys, tokens) are automatically redacted in logs as `[REDACTED]`.
|
||||
|
||||
### Distributed Coordination Backend
|
||||
|
||||
A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-based backend for real-time coordination features in Superset. This backend enables:
|
||||
@@ -71,7 +97,6 @@ A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-b
|
||||
The distributed coordination is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
|
||||
|
||||
Example configuration in `superset_config.py`:
|
||||
|
||||
```python
|
||||
DISTRIBUTED_COORDINATION_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
@@ -86,11 +111,9 @@ See `superset/config.py` for complete configuration options.
|
||||
### WebSocket config for GAQ with Docker
|
||||
|
||||
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
|
||||
|
||||
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
|
||||
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
|
||||
|
||||
```yaml
|
||||
``` yaml
|
||||
services:
|
||||
superset-websocket:
|
||||
volumes:
|
||||
@@ -103,9 +126,7 @@ services:
|
||||
### Example Data Loading Improvements
|
||||
|
||||
#### New Directory Structure
|
||||
|
||||
Examples are now organized by name with data and configs co-located:
|
||||
|
||||
```
|
||||
superset/examples/
|
||||
├── _shared/ # Shared database & metadata configs
|
||||
@@ -117,12 +138,31 @@ superset/examples/
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Simplified Parquet-based Loading
|
||||
- Auto-discovery: create `superset/examples/my_dataset/data.parquet` to add a new example
|
||||
- Parquet is an Apache project format: compressed (~27% smaller), self-describing schema
|
||||
- YAML configs define datasets, charts, and dashboards declaratively
|
||||
- Removed Python-based data generation from individual example files
|
||||
|
||||
#### Test Data Reorganization
|
||||
- Moved `big_data.py` to `superset/cli/test_loaders.py` - better reflects its purpose as a test utility
|
||||
- Fixed inverted logic for `--load-test-data` flag (now correctly includes .test.yaml files when flag is set)
|
||||
- Clarified CLI flags:
|
||||
- `--force` / `-f`: Force reload even if tables exist
|
||||
- `--only-metadata` / `-m`: Create table metadata without loading data
|
||||
- `--load-test-data` / `-t`: Include test dashboards and .test.yaml configs
|
||||
- `--load-big-data` / `-b`: Generate synthetic stress-test data
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed numpy array serialization for PostgreSQL (converts complex types to JSON strings)
|
||||
- Fixed KeyError for `allow_csv_upload` field in database configs (now optional with default)
|
||||
- Fixed test data loading logic that was incorrectly filtering files
|
||||
|
||||
### MCP Service
|
||||
|
||||
The MCP (Model Context Protocol) service enables AI assistants and automation tools to interact programmatically with Superset.
|
||||
|
||||
#### New Features
|
||||
|
||||
- MCP service infrastructure with FastMCP framework
|
||||
- Tools for dashboards, charts, datasets, SQL Lab, and instance metadata
|
||||
- Optional dependency: install with `pip install apache-superset[fastmcp]`
|
||||
@@ -132,7 +172,6 @@ The MCP (Model Context Protocol) service enables AI assistants and automation to
|
||||
#### New Configuration Options
|
||||
|
||||
**Development** (single-user, local testing):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_DEV_USERNAME = "admin" # User for MCP authentication
|
||||
@@ -141,7 +180,6 @@ MCP_SERVICE_PORT = 5008
|
||||
```
|
||||
|
||||
**Production** (JWT-based, multi-user):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
@@ -187,14 +225,12 @@ superset mcp run --port 5008 --use-factory-config
|
||||
The MCP service runs as a **separate process** from the Superset web server.
|
||||
|
||||
**Important**:
|
||||
|
||||
- Requires same Python environment and configuration as Superset
|
||||
- Shares database connections with main Superset app
|
||||
- Can be scaled independently from web server
|
||||
- Requires `fastmcp` package (optional dependency)
|
||||
|
||||
**Installation**:
|
||||
|
||||
```bash
|
||||
# Install with MCP support
|
||||
pip install apache-superset[fastmcp]
|
||||
@@ -208,7 +244,6 @@ Use systemd, supervisord, or Kubernetes to manage the MCP service process.
|
||||
See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
|
||||
**Security**:
|
||||
|
||||
- Development: Uses `MCP_DEV_USERNAME` for single-user access
|
||||
- Production: **MUST** configure JWT authentication
|
||||
- See `superset/mcp_service/SECURITY.md` for details
|
||||
@@ -221,50 +256,14 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
- Developer Guide: `superset/mcp_service/CLAUDE.md`
|
||||
- Quick Start: `superset/mcp_service/README.md`
|
||||
|
||||
### MCP Tool Observability
|
||||
---
|
||||
|
||||
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
|
||||
|
||||
**Two-layer instrumentation:**
|
||||
|
||||
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
|
||||
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
|
||||
|
||||
**Action naming convention:**
|
||||
|
||||
- Tool-level logs: `mcp_tool_call` (via middleware)
|
||||
- Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`)
|
||||
|
||||
**Querying MCP logs:**
|
||||
|
||||
```sql
|
||||
-- Top slowest MCP operations
|
||||
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
|
||||
FROM logs
|
||||
WHERE action LIKE 'mcp.%'
|
||||
GROUP BY action
|
||||
ORDER BY avg_ms DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- MCP tool success rate
|
||||
SELECT
|
||||
json_extract(curated_payload, '$.tool') as tool,
|
||||
COUNT(*) as total_calls,
|
||||
SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) as successful,
|
||||
ROUND(100.0 * SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
|
||||
FROM logs
|
||||
WHERE action = 'mcp_tool_call'
|
||||
GROUP BY tool
|
||||
ORDER BY total_calls DESC;
|
||||
```
|
||||
|
||||
**Security note:** Sensitive parameters (passwords, API keys, tokens) are automatically redacted in logs as `[REDACTED]`.
|
||||
|
||||
### APP_NAME configuration
|
||||
- [35621](https://github.com/apache/superset/pull/35621): The default hash algorithm has changed from MD5 to SHA-256 for improved security and FedRAMP compliance. This affects cache keys for thumbnails, dashboard digests, chart digests, and filter option names. Existing cached data will be invalidated upon upgrade. To opt out of this change and maintain backward compatibility, set `HASH_ALGORITHM = "md5"` in your `superset_config.py`.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
|
||||
### Breaking Changes
|
||||
- [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set.
|
||||
- **Migration:**
|
||||
|
||||
```python
|
||||
# Before (Superset 5.x)
|
||||
APP_NAME = "My Custom App"
|
||||
@@ -283,22 +282,16 @@ ORDER BY total_calls DESC;
|
||||
APP_NAME = "My Custom App"
|
||||
# But you should migrate to THEME_DEFAULT.token.brandAppName
|
||||
```
|
||||
|
||||
- **Note:** For dark mode, set the same tokens in `THEME_DARK` configuration.
|
||||
|
||||
### CUSTOM_FONT_URLS configuration
|
||||
|
||||
- [36317](https://github.com/apache/superset/pull/36317): The `CUSTOM_FONT_URLS` configuration option has been removed. Use the new per-theme `fontUrls` token in `THEME_DEFAULT` or database-managed themes instead.
|
||||
- **Before:**
|
||||
|
||||
```python
|
||||
CUSTOM_FONT_URLS = [
|
||||
"https://fonts.example.com/myfont.css",
|
||||
]
|
||||
```
|
||||
|
||||
- **After:**
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
@@ -310,13 +303,7 @@ ORDER BY total_calls DESC;
|
||||
}
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
- [35621](https://github.com/apache/superset/pull/35621): The default hash algorithm has changed from MD5 to SHA-256 for improved security and FedRAMP compliance. This affects cache keys for thumbnails, dashboard digests, chart digests, and filter option names. Existing cached data will be invalidated upon upgrade. To opt out of this change and maintain backward compatibility, set `HASH_ALGORITHM = "md5"` in your `superset_config.py`.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
|
||||
## 6.0.0
|
||||
|
||||
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
|
||||
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
|
||||
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
|
||||
@@ -325,8 +312,8 @@ ORDER BY total_calls DESC;
|
||||
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
|
||||
- Custom colors are no longer supported to maintain consistency with Ant Design components
|
||||
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
|
||||
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
|
||||
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
|
||||
@@ -335,13 +322,13 @@ ORDER BY total_calls DESC;
|
||||
There's a migration added that can potentially affect a significant number of existing charts.
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
|
||||
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
- [32432](https://github.com/apache/superset/pull/32432) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
- [31976](https://github.com/apache/superset/pull/31976) Removed the `DISABLE_LEGACY_DATASOURCE_EDITOR` feature flag. The previous value of the feature flag was `True` and now the feature is permanently removed.
|
||||
- [31959](https://github.com/apache/superset/pull/32000) Removes CSV_UPLOAD_MAX_SIZE config, use your web server to control file upload size.
|
||||
- [32000](https://github.com/apache/superset/pull/32000) Removes CSV_UPLOAD_MAX_SIZE config, use your web server to control file upload size.
|
||||
- [31959](https://github.com/apache/superset/pull/31959) Removes the following endpoints from data uploads: `/api/v1/database/<id>/<file type>_upload` and `/api/v1/database/<file type>_metadata`, in favour of new one (Details on the PR). And simplifies permissions.
|
||||
- [31844](https://github.com/apache/superset/pull/31844) The `ALERT_REPORTS_EXECUTE_AS` and `THUMBNAILS_EXECUTE_AS` config parameters have been renamed to `ALERT_REPORTS_EXECUTORS` and `THUMBNAILS_EXECUTORS` respectively. A new config flag `CACHE_WARMUP_EXECUTORS` has also been introduced to be able to control which user is used to execute cache warmup tasks. Finally, the config flag `THUMBNAILS_SELENIUM_USER` has been removed. To use a fixed executor for async tasks, use the new `FixedExecutor` class. See the config and docs for more info on setting up different executor profiles.
|
||||
- [31894](https://github.com/apache/superset/pull/31894) Domain sharding is deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration will be removed in the next major version (6.0)
|
||||
@@ -716,6 +703,7 @@ ORDER BY total_calls DESC;
|
||||
|
||||
- [11509](https://github.com/apache/superset/pull/12491): Dataset metadata updates check user ownership, only owners or an Admin are allowed.
|
||||
- Security simplification (SIP-19), the following permission domains were simplified:
|
||||
|
||||
- [12072](https://github.com/apache/superset/pull/12072): `Query` with `can_read`, `can_write`
|
||||
- [12036](https://github.com/apache/superset/pull/12036): `Database` with `can_read`, `can_write`.
|
||||
- [12012](https://github.com/apache/superset/pull/12036): `Dashboard` with `can_read`, `can_write`.
|
||||
|
||||
@@ -115,6 +115,10 @@ services:
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
POSTGRES_DB: superset_light
|
||||
EXAMPLES_HOST: db-light
|
||||
EXAMPLES_DB: superset_light
|
||||
EXAMPLES_USER: superset
|
||||
EXAMPLES_PASSWORD: superset
|
||||
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
|
||||
GITHUB_HEAD_REF: ${GITHUB_HEAD_REF:-}
|
||||
GITHUB_SHA: ${GITHUB_SHA:-}
|
||||
@@ -137,6 +141,10 @@ services:
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
POSTGRES_DB: superset_light
|
||||
EXAMPLES_HOST: db-light
|
||||
EXAMPLES_DB: superset_light
|
||||
EXAMPLES_USER: superset
|
||||
EXAMPLES_PASSWORD: superset
|
||||
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
|
||||
healthcheck:
|
||||
disable: true
|
||||
@@ -157,6 +165,7 @@ services:
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
|
||||
NPM_RUN_PRUNE: false
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"
|
||||
# configuring the dev-server to use the host.docker.internal to connect to the backend
|
||||
superset: "http://superset-light:8088"
|
||||
# Webpack dev server must bind to 0.0.0.0 to be accessible from outside the container
|
||||
|
||||
@@ -80,7 +80,7 @@ case "${1}" in
|
||||
;;
|
||||
app)
|
||||
echo "Starting web app (using development server)..."
|
||||
flask run -p $PORT --reload --debugger --without-threads --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*"
|
||||
flask run -p $PORT --reload --debugger --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
|
||||
;;
|
||||
app-gunicorn)
|
||||
echo "Starting web app..."
|
||||
|
||||
@@ -76,21 +76,6 @@ value defined in `DATA_CACHE_CONFIG`.
|
||||
Note, that by setting the cache timeout to `-1`, caching for charting data can be disabled, either
|
||||
per chart, dataset or database, or by default if set in `DATA_CACHE_CONFIG`.
|
||||
|
||||
## Limiting Cached Result Size
|
||||
|
||||
Very large chart or SQL query results can flood the cache backend (Redis/Memcached), evicting many
|
||||
smaller useful entries or exhausting memory. To cap the size of any single value written to the data
|
||||
cache, set `DATA_CACHE_MAX_VALUE_SIZE` (in bytes) in `superset_config.py`:
|
||||
|
||||
```python
|
||||
DATA_CACHE_MAX_VALUE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||
```
|
||||
|
||||
When a result's serialized size exceeds this threshold it is not written to the data cache — the
|
||||
chart still renders, but the next load re-queries the datasource instead of getting a cache hit. The
|
||||
`skip_cache_value_too_large` statsd metric is incremented each time this happens. Set to `None` (the
|
||||
default) to disable the check.
|
||||
|
||||
## SQL Lab Query Results
|
||||
|
||||
Caching for SQL Lab query results is used when async queries are enabled and is configured using
|
||||
@@ -153,33 +138,14 @@ THUMBNAIL_CACHE_CONFIG = init_thumbnail_cache
|
||||
```
|
||||
|
||||
Using the above example cache keys for dashboards will be `superset_thumb__dashboard__{ID}`. You can
|
||||
override the base URL for Selenium using:
|
||||
override the base URL for selenium using:
|
||||
|
||||
```
|
||||
WEBDRIVER_BASEURL = "https://superset.company.com"
|
||||
```
|
||||
|
||||
To control which user account is used for rendering thumbnails and warming up caches, configure
|
||||
`THUMBNAIL_EXECUTORS` and `CACHE_WARMUP_EXECUTORS`. Each accepts a list of executor types (which
|
||||
resolve to an owner, creator, modifier, or the currently-logged-in user) and/or a `FixedExecutor`
|
||||
pinned to a specific username. By default, thumbnails render as the current user
|
||||
(`ExecutorType.CURRENT_USER`) and cache warmup runs as the chart/dashboard owner
|
||||
(`ExecutorType.OWNER`).
|
||||
|
||||
To force both to run as a dedicated service account (`admin` in this example):
|
||||
|
||||
```python
|
||||
from superset.tasks.types import ExecutorType, FixedExecutor
|
||||
|
||||
THUMBNAIL_EXECUTORS = [FixedExecutor("admin")]
|
||||
CACHE_WARMUP_EXECUTORS = [FixedExecutor("admin")]
|
||||
```
|
||||
|
||||
Use a dedicated read-only service account here rather than a personal admin account, so that
|
||||
thumbnail rendering and cache warmup tasks don't fail if a specific user's credentials change.
|
||||
|
||||
Additional Selenium WebDriver configuration can be set using `WEBDRIVER_CONFIGURATION`. You can
|
||||
implement a custom function to authenticate Selenium. The default function uses the `flask-login`
|
||||
Additional selenium web drive configuration can be set using `WEBDRIVER_CONFIGURATION`. You can
|
||||
implement a custom function to authenticate selenium. The default function uses the `flask-login`
|
||||
session cookie. Here's an example of a custom function signature:
|
||||
|
||||
```python
|
||||
@@ -193,20 +159,6 @@ Then on configuration:
|
||||
WEBDRIVER_AUTH_FUNC = auth_driver
|
||||
```
|
||||
|
||||
## ETag Support for Thumbnails
|
||||
|
||||
Thumbnail and screenshot endpoints return `ETag` response headers based on the cached content digest. Clients can use conditional requests to avoid downloading unchanged images:
|
||||
|
||||
```
|
||||
GET /api/v1/chart/42/thumbnail/
|
||||
If-None-Match: "abc123..."
|
||||
|
||||
→ 304 Not Modified (if unchanged)
|
||||
→ 200 OK (with new image if changed)
|
||||
```
|
||||
|
||||
This is particularly useful for embedded dashboards and external integrations that periodically poll for updated screenshots — unchanged thumbnails return immediately with no payload.
|
||||
|
||||
## Distributed Coordination Backend
|
||||
|
||||
Superset supports an optional distributed coordination (`DISTRIBUTED_COORDINATION_CONFIG`) for
|
||||
|
||||
@@ -109,6 +109,14 @@ SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
|
||||
You can generate a strong secure key with `openssl rand -base64 42`.
|
||||
|
||||
Alternatively, you can set the secret key using `SUPERSET_SECRET_KEY` environment variable:
|
||||
|
||||
On a Unix-based system, such as Linux or macOS, you can do so by running the following command in your terminal:
|
||||
|
||||
```bash
|
||||
export SUPERSET_SECRET_KEY=$(openssl rand -base64 42)
|
||||
```
|
||||
|
||||
:::caution Use a strong secret key
|
||||
This key will be used for securely signing session cookies and encrypting sensitive information stored in Superset's application metadata database.
|
||||
Your deployment must use a complex, unique key.
|
||||
@@ -364,26 +372,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
|
||||
]
|
||||
```
|
||||
|
||||
### PKCE Support
|
||||
|
||||
For public OAuth2 clients that cannot securely store a client secret, enable Proof Key for Code Exchange (PKCE) by adding `code_challenge_method` to the `remote_app` configuration:
|
||||
|
||||
```python
|
||||
OAUTH_PROVIDERS = [
|
||||
{
|
||||
'name': 'myProvider',
|
||||
'remote_app': {
|
||||
'client_id': 'myClientId',
|
||||
'client_secret': 'mySecret', # may be empty for pure public clients
|
||||
'code_challenge_method': 'S256', # enables PKCE
|
||||
'server_metadata_url': 'https://myAuthorizationServer/.well-known/openid-configuration'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
PKCE (`S256`) is recommended for all OAuth2 flows, even when a client secret is present, as it protects against authorization code interception attacks.
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
FAB supports authenticating user credentials against an LDAP server.
|
||||
|
||||
@@ -30,10 +30,6 @@ Superset's ZIP-based import/export also covers **dashboards**, **charts**, and *
|
||||
| └── ... (more databases)
|
||||
```
|
||||
|
||||
:::note
|
||||
When you export a database connection, the `masked_encrypted_extra` field (used for sensitive connection parameters such as service account JSON, OAuth tokens, and other encrypted credentials) is included in the export. When importing on another instance, these values are decrypted and re-encrypted using the destination instance's `SECRET_KEY`. Ensure the receiving instance has a valid `SECRET_KEY` configured before importing.
|
||||
:::
|
||||
|
||||
## Exporting Datasources to YAML
|
||||
|
||||
You can print your current datasources to stdout by running:
|
||||
|
||||
@@ -501,8 +501,7 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_SERVICE_URL` | `None` | Public base URL for MCP-generated links (set this when behind a reverse proxy) |
|
||||
| `MCP_DEBUG` | `False` | Enable debug logging |
|
||||
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
|
||||
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
|
||||
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
|
||||
| `MCP_PARSE_REQUEST_ENABLED` | `True` | Pre-parse MCP tool inputs from JSON strings into objects. Set to `False` for clients (Claude Desktop, LangChain) that do not double-serialize arguments — this produces cleaner tool schemas for those clients |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -518,7 +517,6 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_REQUIRED_SCOPES` | `[]` | Required JWT scopes |
|
||||
| `MCP_JWT_DEBUG_ERRORS` | `False` | Log detailed JWT errors server-side (never exposed in HTTP responses per RFC 6750) |
|
||||
| `MCP_AUTH_FACTORY` | `None` | Custom auth provider factory `(flask_app) -> auth_provider`. Takes precedence over built-in JWT |
|
||||
| `MCP_USER_RESOLVER` | `None` | Custom function `(app, access_token) -> username` to extract a Superset username from a validated JWT token. When `None`, the default resolver checks `preferred_username`, `username`, `email`, and `sub` claims in that order. |
|
||||
|
||||
### Response Size Guard
|
||||
|
||||
@@ -529,7 +527,6 @@ MCP_RESPONSE_SIZE_CONFIG = {
|
||||
"enabled": True,
|
||||
"token_limit": 25000,
|
||||
"warn_threshold_pct": 80,
|
||||
"max_list_items": 100,
|
||||
"excluded_tools": [
|
||||
"health_check",
|
||||
"get_chart_preview",
|
||||
@@ -544,7 +541,6 @@ MCP_RESPONSE_SIZE_CONFIG = {
|
||||
| `enabled` | `True` | Enable response size checking |
|
||||
| `token_limit` | `25000` | Maximum estimated token count per response |
|
||||
| `warn_threshold_pct` | `80` | Warn when response exceeds this percentage of the limit |
|
||||
| `max_list_items` | `100` | Cap on list-field length (e.g. `charts`, `native_filters`) applied to the `get_*_info` tools before falling back to more aggressive truncation. Raised from a hardcoded 30 in earlier versions; the higher default only keeps more data before the same token-budget fallback kicks in, so it's not a breaking change, but tenants that tuned workflows around the old 30-item cap should lower this value explicitly. |
|
||||
| `excluded_tools` | See above | Tools exempt from size checking (e.g., tools that return URLs, not data) |
|
||||
|
||||
### Caching
|
||||
@@ -604,43 +600,6 @@ MCP_STORE_CONFIG = {
|
||||
| `event_store_max_events` | `100` | Maximum events retained per session |
|
||||
| `event_store_ttl` | `3600` | Event TTL in seconds |
|
||||
|
||||
### Tool Search
|
||||
|
||||
By default the MCP server exposes a lightweight tool-search interface instead of advertising every tool at once. This reduces the initial context sent to the LLM by ~70%, which lowers cost and latency. The AI client discovers tools on demand by calling `search_tools` and then invokes them via `call_tool`.
|
||||
|
||||
```python
|
||||
MCP_TOOL_SEARCH_CONFIG = {
|
||||
"enabled": True,
|
||||
"strategy": "bm25", # "bm25" (natural language) or "regex"
|
||||
"max_results": 5,
|
||||
"always_visible": [ # Tools always listed (pinned)
|
||||
"health_check",
|
||||
"get_instance_info",
|
||||
],
|
||||
"search_tool_name": "search_tools",
|
||||
"call_tool_name": "call_tool",
|
||||
"include_schemas": False, # False=summary mode (name + parameters_hint)
|
||||
"compact_schemas": True, # Strip $defs (only applies when include_schemas=True)
|
||||
"max_description_length": 300,
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `enabled` | `True` | Enable tool search. When `False`, all tools are listed upfront |
|
||||
| `strategy` | `"bm25"` | Search ranking algorithm. `"bm25"` supports natural language; `"regex"` supports pattern matching |
|
||||
| `max_results` | `5` | Maximum tools returned per search query |
|
||||
| `always_visible` | See above | Tools that always appear in `list_tools`, regardless of search |
|
||||
| `include_schemas` | `False` | When `False` (default, "summary mode"), search results omit `inputSchema` entirely and include a lightweight `parameters_hint` listing top-level parameter names. Set to `True` to include the full `inputSchema` in search results. Full schemas are always used when a tool is actually invoked via `call_tool`. |
|
||||
| `compact_schemas` | `True` | Strip `$defs` / `$ref` and replace with `{"type": "object"}` in search results to reduce token cost. Only takes effect when `include_schemas=True` — ignored in summary mode. |
|
||||
| `max_description_length` | `300` | Truncate tool descriptions in search results (0 = no truncation). Applies in both summary and full-schema modes. |
|
||||
|
||||
:::tip
|
||||
Set `enabled: False` to revert to the traditional "show all tools at once" behavior, which some clients or workflows may prefer.
|
||||
:::
|
||||
|
||||
Tool search reduces the initial token cost from ~15–20K tokens (full catalog) down to ~4–5K tokens (pinned tools + search interface) — roughly 85% savings at the start of each conversation.
|
||||
|
||||
### Session & CSRF
|
||||
|
||||
These values are flat-merged into the Flask app config used by the MCP server process:
|
||||
@@ -662,102 +621,6 @@ MCP_CSRF_CONFIG = {
|
||||
|
||||
---
|
||||
|
||||
## Access Control
|
||||
|
||||
### RBAC Enforcement
|
||||
|
||||
The MCP server respects Superset's full role-based access control (RBAC). Every authenticated user can only access the data and operations their Superset roles permit — the same rules that apply in the Superset UI apply through MCP.
|
||||
|
||||
Each tool declares one or more required FAB permissions. The table below maps tool groups to their permission requirements:
|
||||
|
||||
| Tool group | Required FAB permission |
|
||||
|------------|------------------------|
|
||||
| `list_charts`, `get_chart_info`, `get_chart_data`, `get_chart_preview`, `generate_chart`, `update_chart` | `can_read` on `Chart` (read), `can_write` on `Chart` (mutate) |
|
||||
| `list_dashboards`, `get_dashboard_info`, `generate_dashboard`, `add_chart_to_existing_dashboard` | `can_read` on `Dashboard` (read), `can_write` on `Dashboard` (mutate) |
|
||||
| `list_datasets`, `get_dataset_info`, `create_virtual_dataset` | `can_read` on `Dataset` (read), `can_write` on `Dataset` (mutate) |
|
||||
| `list_databases`, `get_database_info` | `can_read` on `Database` |
|
||||
| `execute_sql` | `can_execute_sql_query` on `SQLLab` |
|
||||
| `open_sql_lab_with_context` | `can_read` on `SQLLab` |
|
||||
| `save_sql_query` | `can_write` on `SavedQuery` |
|
||||
| `health_check` | None (public) |
|
||||
|
||||
To disable RBAC checking globally (for trusted-network deployments or testing), set:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_RBAC_ENABLED = False
|
||||
```
|
||||
|
||||
:::warning
|
||||
Disabling RBAC removes all permission checks from MCP tool calls. Only do this on isolated, internal deployments where all MCP users are trusted admins.
|
||||
:::
|
||||
|
||||
### Audit Log
|
||||
|
||||
All MCP tool calls are recorded in Superset's action log. You can view them at **Settings → Action Log** (admin only). Each log entry records:
|
||||
|
||||
- The tool name (e.g., `mcp.generate_chart.db_write`)
|
||||
- The authenticated user
|
||||
- A timestamp
|
||||
|
||||
This makes MCP activity fully auditable alongside regular Superset activity. The action log uses the same event logger as the rest of Superset, so existing log ingestion pipelines (e.g., sending logs to Elasticsearch or a SIEM) capture MCP events automatically.
|
||||
|
||||
### Middleware Pipeline
|
||||
|
||||
Every MCP request passes through a middleware stack before reaching the tool function. The default stack (assembled in `build_middleware_list()` in `server.py`) is:
|
||||
|
||||
| Middleware | Purpose | Default |
|
||||
|------------|---------|---------|
|
||||
| `StructuredContentStripperMiddleware` | Strips `structuredContent` from responses for Claude.ai bridge compatibility | Enabled |
|
||||
| `LoggingMiddleware` | Logs each tool call with user, parameters, and duration | Enabled |
|
||||
| `GlobalErrorHandlerMiddleware` | Catches unhandled exceptions and sanitizes sensitive data before it reaches the client | Enabled |
|
||||
| `ResponseSizeGuardMiddleware` | Estimates token count, warns at 80% of limit, blocks at limit | Enabled (configurable via `MCP_RESPONSE_SIZE_CONFIG`) |
|
||||
| `ResponseCachingMiddleware` | Caches read-heavy tool responses (in-memory or Redis) | Disabled (enable via `MCP_CACHE_CONFIG`) |
|
||||
|
||||
Additional middleware classes (`RateLimitMiddleware`, `FieldPermissionsMiddleware`, `PrivateToolMiddleware`) are implemented in `superset/mcp_service/middleware.py` but are not added to the default pipeline. They are available for operators who want to layer them in via a custom startup path.
|
||||
|
||||
### Error Sanitization
|
||||
|
||||
The `GlobalErrorHandlerMiddleware` automatically redacts sensitive information from all error messages before they reach the LLM client. The following are replaced with generic messages:
|
||||
|
||||
- **Database connection strings** — replaced with a generic connection error message
|
||||
- **API keys and tokens** — redacted from error traces
|
||||
- **File system paths** — stripped to prevent information disclosure
|
||||
- **IP addresses** — removed from error context
|
||||
|
||||
This ensures that a misconfigured database connection or an unexpected exception never leaks credentials or internal topology to the LLM or its users. All regex patterns used for redaction are bounded to prevent ReDoS attacks.
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
Each MCP server process maintains its own SQLAlchemy connection pool to the database. For multi-worker deployments, total open connections = **workers × pool size**.
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
SQLALCHEMY_POOL_SIZE = 5
|
||||
SQLALCHEMY_MAX_OVERFLOW = 10
|
||||
SQLALCHEMY_POOL_TIMEOUT = 30
|
||||
SQLALCHEMY_POOL_RECYCLE = 3600 # Recycle connections after 1 hour
|
||||
```
|
||||
|
||||
For a 3-pod Kubernetes deployment with the defaults above, expect up to 3 × (5 + 10) = 45 connections. Size your database's `max_connections` accordingly.
|
||||
|
||||
### Response Caching
|
||||
|
||||
Enable response caching for read-heavy workloads (dashboards/datasets that don't change frequently). With the in-memory backend (default when `MCP_STORE_CONFIG` is disabled), caching is per-process. Use Redis-backed caching for consistent cache hits across multiple pods:
|
||||
|
||||
```python
|
||||
MCP_CACHE_CONFIG = {"enabled": True, "call_tool_ttl": 3600}
|
||||
MCP_STORE_CONFIG = {"enabled": True, "CACHE_REDIS_URL": "redis://redis:6379/0"}
|
||||
```
|
||||
|
||||
Mutating tools (`generate_chart`, `update_chart`, `execute_sql`, `generate_dashboard`) are always excluded from caching regardless of this setting.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server won't start
|
||||
@@ -828,32 +691,6 @@ while True:
|
||||
page += 1
|
||||
```
|
||||
|
||||
## Disabling built-in tools
|
||||
|
||||
If you have deployed a custom tool via a Superset extension that supersedes one of the built-in Superset tools, you can suppress the built-in version so AI clients only discover your replacement. Disabled tools are removed from the server at startup and are never advertised during tool discovery.
|
||||
|
||||
Set `MCP_DISABLED_TOOLS` in your `superset_config.py` to a set of tool names:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
|
||||
# Disable one tool
|
||||
MCP_DISABLED_TOOLS = {"execute_sql"}
|
||||
|
||||
# Disable multiple tools
|
||||
MCP_DISABLED_TOOLS = {"execute_sql", "health_check"}
|
||||
```
|
||||
|
||||
Tool names match the function name used in the `@tool` decorator (e.g., `execute_sql`, `list_charts`, `health_check`). Extension-prefixed tools can also be disabled using their full prefixed name:
|
||||
|
||||
```python
|
||||
MCP_DISABLED_TOOLS = {"extensions.myorg.myextension.some_tool"}
|
||||
```
|
||||
|
||||
:::note
|
||||
Specifying a tool name that does not exist logs a warning at startup and is otherwise ignored — it will not prevent the server from starting.
|
||||
:::
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS
|
||||
|
||||
@@ -22,6 +22,15 @@ While powerful, this feature executes template code on the server. Within the Su
|
||||
|
||||
If you grant these permissions to untrusted users, this feature can be exploited as a **Server-Side Template Injection (SSTI)** vulnerability. Do not enable `ENABLE_TEMPLATE_PROCESSING` unless you fully understand and accept the associated security risks.
|
||||
|
||||
Additionally:
|
||||
|
||||
- The `url_param()` macro allows URL parameters to influence the rendered SQL. Always validate or restrict `url_param()` values in your templates rather than interpolating them directly.
|
||||
- `filter.get('val')` returns raw filter values without escaping. Use the safe helpers described below (`|where_in`, `| replace("'", "''")`) rather than concatenating values directly into SQL strings.
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
`ENABLE_TEMPLATE_PROCESSING` defaults to `False`. Only enable it if your deployment requires Jinja templates and all users with dataset/chart edit access are administrators or fully trusted internal users.
|
||||
:::
|
||||
|
||||
When templating is enabled, python code can be embedded in virtual datasets and
|
||||
@@ -324,6 +333,16 @@ cache hit in the future and Superset can retrieve cached data.
|
||||
The `{{ url_param('custom_variable') }}` macro lets you define arbitrary URL
|
||||
parameters and reference them in your SQL code.
|
||||
|
||||
:::warning
|
||||
Always treat `url_param()` values as untrusted input. Escaping behaviour varies by context and configuration, so do not rely on it. Restrict values to an explicit allowlist before using them in SQL:
|
||||
|
||||
```sql
|
||||
{% set cc = url_param('countrycode') %}
|
||||
{% if cc not in ('US', 'ES', 'FR') %}{% set cc = 'US' %}{% endif %}
|
||||
WHERE country_code = '{{ cc }}'
|
||||
```
|
||||
:::
|
||||
|
||||
Here's a concrete example:
|
||||
|
||||
- You write the following query in SQL Lab:
|
||||
@@ -398,6 +417,16 @@ This is useful if:
|
||||
- You want to handle generating custom SQL conditions for a filter
|
||||
- You want to have the ability to filter inside the main query for speed purposes
|
||||
|
||||
:::warning
|
||||
`filter.get('val')` returns the raw filter value without escaping. For multi-value filters, use the `|where_in` Jinja filter, which handles quoting safely. For single-value operators like `LIKE`, escape single quotes before interpolating:
|
||||
|
||||
```sql
|
||||
{%- if filter.get('op') == 'LIKE' -%}
|
||||
AND full_name LIKE '{{ filter.get('val') | replace("'", "''") }}'
|
||||
{%- endif -%}
|
||||
```
|
||||
:::
|
||||
|
||||
Here's a concrete example:
|
||||
|
||||
```sql
|
||||
@@ -424,7 +453,7 @@ Here's a concrete example:
|
||||
|
||||
{%- if filter.get('op') == 'LIKE' -%}
|
||||
AND
|
||||
full_name LIKE {{ "'" + filter.get('val') + "'" }}
|
||||
full_name LIKE '{{ filter.get('val') | replace("'", "''") }}'
|
||||
{%- endif -%}
|
||||
|
||||
{%- endfor -%}
|
||||
|
||||
@@ -84,35 +84,6 @@ THEME_DARK = {
|
||||
# - OS preference detection is automatically enabled
|
||||
```
|
||||
|
||||
### App Branding
|
||||
|
||||
The application name shown in the browser title bar and navigation can be
|
||||
set via the `brandAppName` theme token:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"brandAppName": "Acme Analytics",
|
||||
# ... other tokens
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or in the theme CRUD UI JSON editor:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"brandAppName": "Acme Analytics"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The existing `APP_NAME` Python config key continues to work for backward compatibility.
|
||||
`brandAppName` takes precedence when both are set, and allows different themes to carry different brand names.
|
||||
Email and alert/report notification subjects are driven by backend settings such as
|
||||
`EMAIL_REPORTS_SUBJECT_PREFIX` and `APP_NAME`, not by this theme token.
|
||||
|
||||
### Migration from Configuration to UI
|
||||
|
||||
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
|
||||
@@ -341,25 +312,11 @@ Available chart types for `echartsOptionsOverridesByChartType`:
|
||||
- `echarts_heatmap` - Heatmaps
|
||||
- `echarts_mixed_timeseries` - Mixed time series
|
||||
|
||||
### Array Property Overrides
|
||||
|
||||
Array properties (such as color palettes) are fully supported in overrides. Arrays are **replaced entirely** rather than merged, so specify the complete array:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": { ... },
|
||||
"echartsOptionsOverrides": {
|
||||
# Replace the default color palette for all ECharts visualizations
|
||||
"color": ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Start with global overrides** for consistent styling across all charts
|
||||
2. **Use chart-specific overrides** for unique requirements per visualization type
|
||||
3. **Test thoroughly** as overrides use deep merge for objects, but arrays are completely replaced — always specify the full array value
|
||||
3. **Test thoroughly** as overrides use deep merge - nested objects are combined, but arrays are completely replaced
|
||||
4. **Document your overrides** to help team members understand custom styling
|
||||
5. **Consider performance** - complex overrides may impact chart rendering speed
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ A table with the permissions for these roles can be found at [/RESOURCES/STANDAR
|
||||
Admins have all possible rights, including granting or revoking rights from other
|
||||
users and altering other people’s slices and dashboards.
|
||||
|
||||
>#### Threat Model and Privilege Boundaries: The Admin Role
|
||||
>
|
||||
>Apache Superset is built with a granular permission model where users assigned the Admin role are considered fully trusted. Admins possess complete control over the application's configuration, UI rendering, and access controls.
|
||||
>
|
||||
>Consequently, actions performed by an Admin that alter the application's behavior or presentation—such as injecting custom CSS, modifying Jinja templates, or altering security flags—are intended administrative capabilities by design.
|
||||
>
|
||||
>In accordance with MITRE CNA Rule 4.1, a vulnerability must represent a violation of an explicit security policy. Because the Admin role is defined as a trusted operational boundary, actions executed with Admin privileges do not cross a security perimeter. Therefore, exploit vectors that strictly require Admin access are not classified as security vulnerabilities and are ineligible for CVE assignment.
|
||||
|
||||
### Alpha
|
||||
|
||||
Alpha users have access to all data sources, but they cannot grant or revoke access
|
||||
@@ -44,15 +52,6 @@ only see the objects that they have access to.
|
||||
The **sql_lab** role grants access to SQL Lab. Note that while **Admin** users have access
|
||||
to all databases by default, both **Alpha** and **Gamma** users need to be given access on a per database basis.
|
||||
|
||||
Beyond the base `sql_lab` role, two additional SQL Lab permissions must be explicitly granted for users who need these capabilities:
|
||||
|
||||
| Permission | Feature |
|
||||
|------------|---------|
|
||||
| `can_estimate_query_cost` on `SQLLab` | Estimate query cost before running |
|
||||
| `can_format_sql` on `SQLLab` | Format SQL using the database's dialect |
|
||||
|
||||
Grant these in **Security → List Roles** by adding the permissions to the relevant role.
|
||||
|
||||
### Public
|
||||
|
||||
The **Public** role is the most restrictive built-in role, designed specifically for anonymous/unauthenticated
|
||||
@@ -183,8 +182,6 @@ However, it is crucial to understand the following:
|
||||
|
||||
By combining Superset's configurable safeguards with strong database-level security practices, you can achieve a more robust and layered security posture.
|
||||
|
||||
**Dataset Sample Access**: The `get_samples()` endpoint now enforces datasource-level access control. Users can only fetch sample rows from datasets they have been explicitly granted access to — the same permission check applied when running chart queries. This closes a prior gap where unauthenticated or under-privileged access could retrieve sample data.
|
||||
|
||||
### REST API for user & role management
|
||||
|
||||
Flask-AppBuilder supports a REST API for user CRUD,
|
||||
|
||||
@@ -412,7 +412,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security roles](/developer-docs/api/get-security-roles) | `/api/v1/security/roles/` |
|
||||
| `POST` | [Create security roles](/developer-docs/api/create-security-roles) | `/api/v1/security/roles/` |
|
||||
| `GET` | [Get security roles info](/developer-docs/api/get-security-roles-info) | `/api/v1/security/roles/_info` |
|
||||
| `GET` | [Get security roles info](/developer-docs/api/get-security-roles-info) | `/api/v1/security/roles/_info` |
|
||||
| `DELETE` | [Delete security roles by pk](/developer-docs/api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `GET` | [Get security roles by pk](/developer-docs/api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `PUT` | [Update security roles by pk](/developer-docs/api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
@@ -430,7 +430,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security users](/developer-docs/api/get-security-users) | `/api/v1/security/users/` |
|
||||
| `POST` | [Create security users](/developer-docs/api/create-security-users) | `/api/v1/security/users/` |
|
||||
| `GET` | [Get security users info](/developer-docs/api/get-security-users-info) | `/api/v1/security/users/_info` |
|
||||
| `GET` | [Get security users info](/developer-docs/api/get-security-users-info) | `/api/v1/security/users/_info` |
|
||||
| `DELETE` | [Delete security users by pk](/developer-docs/api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `GET` | [Get security users by pk](/developer-docs/api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `PUT` | [Update security users by pk](/developer-docs/api/update-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
@@ -443,7 +443,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security permissions](/developer-docs/api/get-security-permissions) | `/api/v1/security/permissions/` |
|
||||
| `GET` | [Get security permissions info](/developer-docs/api/get-security-permissions-info) | `/api/v1/security/permissions/_info` |
|
||||
| `GET` | [Get security permissions info](/developer-docs/api/get-security-permissions-info) | `/api/v1/security/permissions/_info` |
|
||||
| `GET` | [Get security permissions by pk](/developer-docs/api/get-security-permissions-by-pk) | `/api/v1/security/permissions/{pk}` |
|
||||
|
||||
</details>
|
||||
@@ -455,7 +455,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security resources](/developer-docs/api/get-security-resources) | `/api/v1/security/resources/` |
|
||||
| `POST` | [Create security resources](/developer-docs/api/create-security-resources) | `/api/v1/security/resources/` |
|
||||
| `GET` | [Get security resources info](/developer-docs/api/get-security-resources-info) | `/api/v1/security/resources/_info` |
|
||||
| `GET` | [Get security resources info](/developer-docs/api/get-security-resources-info) | `/api/v1/security/resources/_info` |
|
||||
| `DELETE` | [Delete security resources by pk](/developer-docs/api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `GET` | [Get security resources by pk](/developer-docs/api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `PUT` | [Update security resources by pk](/developer-docs/api/update-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
@@ -469,7 +469,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security permissions resources](/developer-docs/api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `POST` | [Create security permissions resources](/developer-docs/api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `GET` | [Get security permissions resources info](/developer-docs/api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` |
|
||||
| `GET` | [Get security permissions resources info](/developer-docs/api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` |
|
||||
| `DELETE` | [Delete security permissions resources by pk](/developer-docs/api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `GET` | [Get security permissions resources by pk](/developer-docs/api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `PUT` | [Update security permissions resources by pk](/developer-docs/api/update-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
@@ -578,7 +578,29 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get api by version openapi](/developer-docs/api/get-api-by-version-openapi) | `/api/{version}/_openapi` |
|
||||
| `GET` | [Get api by version openapi](/developer-docs/api/get-api-by-version-openapi) | `/api/{version}/_openapi` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Themes</strong> (14 endpoints) — Manage UI themes for customizing Superset's appearance.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete themes](/developer-docs/api/bulk-delete-themes) | `/api/v1/theme/` |
|
||||
| `GET` | [Get a list of themes](/developer-docs/api/get-a-list-of-themes) | `/api/v1/theme/` |
|
||||
| `POST` | [Create a theme](/developer-docs/api/create-a-theme) | `/api/v1/theme/` |
|
||||
| `GET` | [Get metadata information about this API resource (theme-info)](/developer-docs/api/get-metadata-information-about-this-api-resource-theme-info) | `/api/v1/theme/_info` |
|
||||
| `DELETE` | [Delete a theme](/developer-docs/api/delete-a-theme) | `/api/v1/theme/{pk}` |
|
||||
| `GET` | [Get a theme](/developer-docs/api/get-a-theme) | `/api/v1/theme/{pk}` |
|
||||
| `PUT` | [Update a theme](/developer-docs/api/update-a-theme) | `/api/v1/theme/{pk}` |
|
||||
| `PUT` | [Set a theme as the system dark theme](/developer-docs/api/set-a-theme-as-the-system-dark-theme) | `/api/v1/theme/{pk}/set_system_dark` |
|
||||
| `PUT` | [Set a theme as the system default theme](/developer-docs/api/set-a-theme-as-the-system-default-theme) | `/api/v1/theme/{pk}/set_system_default` |
|
||||
| `GET` | [Download multiple themes as YAML files](/developer-docs/api/download-multiple-themes-as-yaml-files) | `/api/v1/theme/export/` |
|
||||
| `POST` | [Import themes from a ZIP file](/developer-docs/api/import-themes-from-a-zip-file) | `/api/v1/theme/import/` |
|
||||
| `GET` | [Get related fields data (theme-related-column-name)](/developer-docs/api/get-related-fields-data-theme-related-column-name) | `/api/v1/theme/related/{column_name}` |
|
||||
| `DELETE` | [Clear the system dark theme](/developer-docs/api/clear-the-system-dark-theme) | `/api/v1/theme/unset_system_dark` |
|
||||
| `DELETE` | [Clear the system default theme](/developer-docs/api/clear-the-system-default-theme) | `/api/v1/theme/unset_system_default` |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in or
|
||||
|
||||
First, be sure you are using the following versions of Node.js and npm:
|
||||
|
||||
- `Node.js`: Version 22 (LTS)
|
||||
- `Node.js`: Version 20
|
||||
- `npm`: Version 10
|
||||
|
||||
We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage your node environment:
|
||||
|
||||
@@ -34,14 +34,15 @@ Frontend contribution types allow extensions to extend Superset's user interface
|
||||
|
||||
Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
|
||||
|
||||
```typescript
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { views } from '@apache-superset/core';
|
||||
import MyPanel from './MyPanel';
|
||||
|
||||
views.registerView(
|
||||
{ id: 'my-extension.main', name: 'My Panel Name' },
|
||||
'sqllab.panels',
|
||||
MyPanel,
|
||||
() => <MyPanel />,
|
||||
);
|
||||
```
|
||||
|
||||
@@ -111,24 +112,6 @@ editors.registerEditor(
|
||||
|
||||
See [Editors Extension Point](./extension-points/editors) for implementation details.
|
||||
|
||||
### Chat
|
||||
|
||||
Extensions can add a chat interface to Superset by registering a trigger component and a panel component. The host owns the layout, open/close state, and display mode — the extension only provides the UI. The panel can be displayed as a floating overlay or docked as a resizable sidebar beside the page content, and the user's preference is persisted across reloads.
|
||||
|
||||
```tsx
|
||||
import { chat } from '@apache-superset/core';
|
||||
import ChatTrigger from './ChatTrigger';
|
||||
import ChatPanel from './ChatPanel';
|
||||
|
||||
chat.registerChat(
|
||||
{ id: 'my-org.my-chat', name: 'My Chat' },
|
||||
ChatTrigger,
|
||||
ChatPanel,
|
||||
);
|
||||
```
|
||||
|
||||
See [Chat](./extension-points/chat.md) for implementation details.
|
||||
|
||||
## Backend
|
||||
|
||||
Backend contribution types allow extensions to extend Superset's server-side capabilities. Backend contributions are registered at startup via classes and functions imported from the auto-discovered `entrypoint.py` file.
|
||||
|
||||
@@ -39,7 +39,11 @@ superset-extensions bundle: Packages the extension into a .supx file.
|
||||
|
||||
superset-extensions dev: Automatically rebuilds the extension as files change.
|
||||
|
||||
superset-extensions validate: Validates the extension structure and metadata.
|
||||
superset-extensions validate: Validates the extension structure and metadata consistency.
|
||||
|
||||
superset-extensions update: Updates derived and generated files in the extension project.
|
||||
Use --version [<version>] to update the version (prompts if no value given).
|
||||
Use --license [<license>] to update the license (prompts if no value given).
|
||||
```
|
||||
|
||||
When creating a new extension with `superset-extensions init`, the CLI generates a standardized folder structure:
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
---
|
||||
title: Chat
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Chat Contributions
|
||||
|
||||
Extensions can add a chat interface to Superset by registering a trigger and a panel. The host owns the layout, open/close state, and display mode — the extension only needs to provide the UI components.
|
||||
|
||||
## Overview
|
||||
|
||||
A chat registration consists of two React components:
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **Trigger** | Always-visible entry point (e.g., a floating button). Rendered in the bottom-right corner in floating mode, or as a fixed overlay in panel mode. |
|
||||
| **Panel** | The chat UI itself (message list, input, etc.). Mounted by the host in the active display mode. |
|
||||
|
||||
## Display Modes
|
||||
|
||||
The host supports two display modes, switchable by the user or the extension at runtime:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `floating` | Panel floats above page content, anchored to the bottom-right corner. |
|
||||
| `panel` | Panel is docked to the right side of the application as a resizable sidebar, sitting beside the page content. |
|
||||
|
||||
The user's last selected mode and open/closed state are persisted across page reloads.
|
||||
|
||||
## Registering a Chat
|
||||
|
||||
Call `chat.registerChat` from your extension's entry point with a descriptor, a trigger factory, and a panel factory:
|
||||
|
||||
```tsx
|
||||
import { chat } from '@apache-superset/core';
|
||||
import ChatTrigger from './ChatTrigger';
|
||||
import ChatPanel from './ChatPanel';
|
||||
|
||||
chat.registerChat(
|
||||
{ id: 'my-org.my-chat', name: 'My Chat' },
|
||||
ChatTrigger,
|
||||
ChatPanel,
|
||||
);
|
||||
```
|
||||
|
||||
Only one chat registration is active at a time. If a second extension calls `registerChat`, it replaces the first and a warning is logged.
|
||||
|
||||
## Opening and Closing the Chat
|
||||
|
||||
The trigger component is responsible for toggling the panel. Use `chat.isOpen()`, `chat.open()`, and `chat.close()` to control visibility:
|
||||
|
||||
```tsx
|
||||
import { chat } from '@apache-superset/core';
|
||||
|
||||
export default function ChatTrigger() {
|
||||
return (
|
||||
<button onClick={() => (chat.isOpen() ? chat.close() : chat.open())}>
|
||||
💬
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can also subscribe to open/close events from any component:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
const { dispose } = chat.onDidOpen(() => console.log('chat opened'));
|
||||
return dispose;
|
||||
}, []);
|
||||
```
|
||||
|
||||
## Changing the Display Mode
|
||||
|
||||
Call `chat.setDisplayMode` to switch between `'floating'` and `'panel'` modes. In your panel component, subscribe to `onDidChangeDisplayMode` to react to changes (including those triggered by the user):
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { chat } from '@apache-superset/core';
|
||||
|
||||
export default function ChatPanel() {
|
||||
const [mode, setMode] = useState(chat.getDisplayMode());
|
||||
|
||||
useEffect(() => {
|
||||
const { dispose } = chat.onDidChangeDisplayMode(m => setMode(m));
|
||||
return dispose;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ height: mode === 'panel' ? '100%' : '80vh' }}>
|
||||
<button onClick={() => chat.setDisplayMode(mode === 'panel' ? 'floating' : 'panel')}>
|
||||
{mode === 'panel' ? 'Float' : 'Dock'}
|
||||
</button>
|
||||
{/* message list and input */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Chat API Reference
|
||||
|
||||
All methods are available on the `chat` namespace from `@apache-superset/core`:
|
||||
|
||||
| Method / Event | Description |
|
||||
|----------------|-------------|
|
||||
| `registerChat(descriptor, trigger, panel)` | Register a chat extension. Returns a `Disposable` to unregister. |
|
||||
| `open()` | Open the chat panel. No-op if already open or no registration. |
|
||||
| `close()` | Close the chat panel. |
|
||||
| `isOpen()` | Returns `true` if the panel is currently open. |
|
||||
| `getDisplayMode()` | Returns the current display mode (`'floating'` or `'panel'`). |
|
||||
| `setDisplayMode(mode)` | Switch between `'floating'` and `'panel'` mode. |
|
||||
| `onDidOpen(listener)` | Subscribe to panel open events. Returns a `Disposable`. |
|
||||
| `onDidClose(listener)` | Subscribe to panel close events. Returns a `Disposable`. |
|
||||
| `onDidChangeDisplayMode(listener)` | Subscribe to display mode changes. Returns a `Disposable`. |
|
||||
| `onDidRegisterChat(listener)` | Subscribe to registration events. |
|
||||
| `onDidUnregisterChat(listener)` | Subscribe to unregistration events. |
|
||||
| `onDidResizePanel(listener)` | Subscribe to panel resize events (panel mode only). Not all hosts provide a resizer — do not rely on this firing. Returns a `Disposable`. |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Contribution Types](../contribution-types.md)** — Explore other contribution types
|
||||
- **[Development](../development.md)** — Set up your development environment
|
||||
@@ -47,8 +47,6 @@ module.exports = {
|
||||
collapsed: true,
|
||||
items: [
|
||||
'extensions/extension-points/sqllab',
|
||||
'extensions/extension-points/editors',
|
||||
'extensions/extension-points/chat',
|
||||
],
|
||||
},
|
||||
'extensions/development',
|
||||
|
||||
@@ -63,6 +63,109 @@ pytest tests/unit_tests/
|
||||
pytest tests/integration_tests/
|
||||
```
|
||||
|
||||
## Testing Alerts & Reports with Celery and MailHog
|
||||
|
||||
The Alerts & Reports feature relies on Celery for task scheduling and execution. To test it locally, you need Redis (message broker), Celery Beat (scheduler), a Celery Worker (executor), and an SMTP server to receive email notifications.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Redis running on `localhost:6379`
|
||||
- [MailHog](https://github.com/mailhog/MailHog) installed (a local SMTP server with a web UI for viewing caught emails)
|
||||
|
||||
### superset_config.py
|
||||
|
||||
Your `CeleryConfig` **must** include `beat_schedule`. When you define a custom `CeleryConfig` class in `superset_config.py`, it replaces the default entirely. If you omit `beat_schedule`, Celery Beat will start but never schedule any report tasks.
|
||||
|
||||
```python
|
||||
from celery.schedules import crontab
|
||||
from superset.tasks.types import ExecutorType
|
||||
|
||||
REDIS_HOST = "localhost"
|
||||
REDIS_PORT = "6379"
|
||||
|
||||
class CeleryConfig:
|
||||
broker_url = f"redis://{REDIS_HOST}:{REDIS_PORT}/0"
|
||||
result_backend = f"redis://{REDIS_HOST}:{REDIS_PORT}/0"
|
||||
broker_connection_retry_on_startup = True
|
||||
imports = (
|
||||
"superset.sql_lab",
|
||||
"superset.tasks.scheduler",
|
||||
"superset.tasks.thumbnails",
|
||||
"superset.tasks.cache",
|
||||
)
|
||||
worker_prefetch_multiplier = 10
|
||||
task_acks_late = True
|
||||
beat_schedule = {
|
||||
"reports.scheduler": {
|
||||
"task": "reports.scheduler",
|
||||
"schedule": crontab(minute="*", hour="*"),
|
||||
},
|
||||
"reports.prune_log": {
|
||||
"task": "reports.prune_log",
|
||||
"schedule": crontab(minute=0, hour=0),
|
||||
},
|
||||
}
|
||||
|
||||
CELERY_CONFIG = CeleryConfig
|
||||
|
||||
# SMTP settings pointing to MailHog
|
||||
SMTP_HOST = "localhost"
|
||||
SMTP_PORT = 1025
|
||||
SMTP_STARTTLS = False
|
||||
SMTP_SSL = False
|
||||
SMTP_USER = ""
|
||||
SMTP_PASSWORD = ""
|
||||
SMTP_MAIL_FROM = "superset@localhost"
|
||||
|
||||
# Must match where your frontend is running
|
||||
WEBDRIVER_BASEURL = "http://localhost:9000/"
|
||||
|
||||
ALERT_REPORTS_EXECUTE_AS = [ExecutorType.OWNER]
|
||||
|
||||
FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True,
|
||||
# Recommended for better screenshot support (WebGL/DeckGL charts)
|
||||
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS": True,
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
Do not include `"superset.tasks.async_queries"` in `CeleryConfig.imports` unless you need Global Async Queries. That module accesses `current_app.config` at import time and will crash the worker with a "Working outside of application context" error.
|
||||
:::
|
||||
|
||||
### Starting the Services
|
||||
|
||||
Start MailHog, then Celery Beat and Worker in separate terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1 - MailHog (SMTP on :1025, Web UI on :8025)
|
||||
MailHog
|
||||
|
||||
# Terminal 2 - Celery Beat (scheduler)
|
||||
celery --app=superset.tasks.celery_app:app beat --loglevel=info
|
||||
|
||||
# Terminal 3 - Celery Worker (executor)
|
||||
celery --app=superset.tasks.celery_app:app worker --concurrency=1 --loglevel=info
|
||||
```
|
||||
|
||||
Use `--concurrency=1` to limit resource usage on your dev machine.
|
||||
|
||||
### Verifying the Setup
|
||||
|
||||
1. **Beat** should log `Scheduler: Sending due task reports.scheduler (reports.scheduler)` once per minute
|
||||
2. **Worker** should log `Scheduling alert <name> eta: <timestamp>` for each active report
|
||||
3. Create a test report in **Settings > Alerts & Reports** with a `* * * * *` cron schedule
|
||||
4. Check **http://localhost:8025** (MailHog web UI) for the email within 1-2 minutes
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| Beat shows no output | Ensure `beat_schedule` is defined in your `CeleryConfig` and `--loglevel=info` is set |
|
||||
| "Report Schedule is still working, refusing to re-compute" | Previous executions are stuck. Reset with: `UPDATE report_schedule SET last_state = 'Not triggered' WHERE id = <id>;` |
|
||||
| Task backlog overwhelming the worker | Flush Redis: `redis-cli FLUSHDB`, then restart Beat and Worker |
|
||||
| Screenshot timeout | Ensure your frontend dev server is running and `WEBDRIVER_BASEURL` matches its URL |
|
||||
|
||||
---
|
||||
|
||||
*This documentation is under active development. Check back soon for updates!*
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ or a view.
|
||||
When working with tables, the solution would be to create a table that contains all the fields
|
||||
needed for your analysis, most likely through some scheduled batch process.
|
||||
|
||||
A view is a simple logical layer that abstracts an arbitrary SQL queries as a virtual table. This can
|
||||
A view is a simple logical layer that abstracts an arbitrary SQL query as a virtual table. This can
|
||||
allow you to join and union multiple tables and to apply some transformation using arbitrary SQL
|
||||
expressions. The limitation there is your database performance, as Superset effectively will run a
|
||||
query on top of your query (view). A good practice may be to limit yourself to joining your main
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Granular Export Controls
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Granular Export Controls
|
||||
|
||||
Superset provides granular, permission-based controls for data export, image export, and clipboard operations. These replace the legacy `can_csv` permission with three fine-grained permissions that can be assigned independently to roles.
|
||||
|
||||
## Feature Flag
|
||||
|
||||
Granular export controls are gated behind the `GRANULAR_EXPORT_CONTROLS` feature flag. When the flag is disabled, the legacy `can_csv` permission behavior is preserved.
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"GRANULAR_EXPORT_CONTROLS": True,
|
||||
}
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Resource | Controls |
|
||||
| -------------------- | ---------- | ---------------------------------------------------------------------- |
|
||||
| `can_export_data` | `Superset` | CSV, Excel, and JSON data exports from charts, dashboards, and SQL Lab |
|
||||
| `can_export_image` | `Superset` | Screenshot (JPEG/PNG) and PDF exports from charts and dashboards |
|
||||
| `can_copy_clipboard` | `Superset` | Copy-to-clipboard operations in SQL Lab and the Explore data pane |
|
||||
|
||||
## Default Role Assignments
|
||||
|
||||
The migration grants all three new permissions (`can_export_data`, `can_export_image`, `can_copy_clipboard`) to every role that currently has `can_csv`. This preserves existing behavior — no role loses access during the upgrade.
|
||||
|
||||
After the migration, admins can selectively revoke individual export permissions from any role to restrict access. For example, to prevent Gamma users from exporting data or images while still allowing clipboard operations, revoke `can_export_data` and `can_export_image` from the Gamma role.
|
||||
|
||||
## Configuration Steps
|
||||
|
||||
1. **Enable the feature flag** in `superset_config.py`:
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"GRANULAR_EXPORT_CONTROLS": True,
|
||||
}
|
||||
```
|
||||
|
||||
2. **Run the database migration** to register the new permissions:
|
||||
|
||||
```bash
|
||||
superset db upgrade
|
||||
```
|
||||
|
||||
3. **Initialize permissions** so roles are populated:
|
||||
|
||||
```bash
|
||||
superset init
|
||||
```
|
||||
|
||||
4. **Verify role assignments** in **Settings > List Roles**. Confirm that each role has the expected permissions from the table above.
|
||||
|
||||
5. **Customize as needed**: Grant or revoke individual export permissions on any role through the role editor.
|
||||
|
||||
## User Experience
|
||||
|
||||
When a user lacks a required export permission:
|
||||
|
||||
- **Menu items** (CSV, Excel, JSON, screenshot) appear **disabled** with an info tooltip icon explaining the restriction
|
||||
- **Buttons** (SQL Lab download, clipboard copy) appear **disabled** with a tooltip on hover
|
||||
- **API endpoints** return **403 Forbidden** when the corresponding permission is missing
|
||||
|
||||
## API Enforcement
|
||||
|
||||
The following API endpoints enforce granular export permissions when the feature flag is enabled:
|
||||
|
||||
| Endpoint | Required Permission |
|
||||
| --------------------------------------------------------- | ------------------- |
|
||||
| `GET /api/v1/chart/{id}/data/` (CSV/Excel format) | `can_export_data` |
|
||||
| `GET /api/v1/chart/{id}/cache_screenshot/` | `can_export_image` |
|
||||
| `POST /api/v1/dashboard/{id}/cache_dashboard_screenshot/` | `can_export_image` |
|
||||
| `GET /api/v1/sqllab/export/{client_id}/` | `can_export_data` |
|
||||
| `POST /api/v1/sqllab/export_streaming/` | `can_export_data` |
|
||||
@@ -256,34 +256,6 @@ For example, when running the local development build, the following will disabl
|
||||
Top Nav and remove the Filter Bar:
|
||||
`http://localhost:8088/superset/dashboard/my-dashboard/?standalone=1&show_filters=0`
|
||||
|
||||
### Table Chart Features
|
||||
|
||||
The **Table** chart type has several advanced capabilities worth knowing:
|
||||
|
||||
#### Conditional Formatting
|
||||
|
||||
Conditional formatting rules highlight cells based on their values. Rules can be applied to:
|
||||
- **Numeric columns** — color cells above/below a threshold, or use a gradient across a range
|
||||
- **String columns** — highlight cells matching specific text values or patterns
|
||||
- **Boolean columns** — color cells that are `true` or `false`, or `null`/`not null`
|
||||
|
||||
Each rule has a **"Use gradient"** toggle: enabled applies a varying opacity (lighter = further from threshold), disabled applies a solid fill at full opacity regardless of value.
|
||||
|
||||
#### HTML Rendering in Table Cells
|
||||
|
||||
Table chart cells can render raw HTML, enabling rich formatting such as hyperlinks, colored badges, and icons directly in the data. Enable this per-column in the chart's **Column Configuration** panel by toggling **Render HTML**.
|
||||
|
||||
:::caution
|
||||
Only enable HTML rendering for columns sourced from data you control. Rendering untrusted HTML can expose users to cross-site scripting (XSS) risks.
|
||||
:::
|
||||
|
||||
#### Column Header Tooltips
|
||||
|
||||
Column headers display a tooltip with the column's **Description** from the dataset editor when the user hovers over them. Keep dataset column descriptions up to date to improve chart discoverability.
|
||||
|
||||
#### Display Controls
|
||||
|
||||
In dashboard view mode (without entering Edit mode), charts with configurable display options expose a **Display Controls** panel accessible from the chart's context menu. This surfaces controls such as Time Grain, Time Column, and layer visibility for applicable chart types — making it easy to adjust a chart's view without going to Explore.
|
||||
### AG Grid Interactive Table
|
||||
|
||||
The **AG Grid Interactive Table** chart type is Superset's fully-featured data grid, suitable for large paginated datasets where the standard Table chart is not enough.
|
||||
@@ -342,26 +314,6 @@ ECharts option overrides bypass Superset's validation layer. Invalid option keys
|
||||
|
||||
When the **Search Box** is visible in a Table chart, the **Download** action exports only the rows currently visible after the search filter is applied — not the full underlying dataset. This matches the visual output and is intentional. To export the full dataset regardless of search state, use the **Download as CSV** option from the chart's three-dot menu in the dashboard or from the Explore chart toolbar before applying a search filter.
|
||||
|
||||
### Sharing a Specific Tab
|
||||
|
||||
When a dashboard has tabs, each tab gets its own shareable URL. Navigate to the tab you want to share and copy the URL from your browser's address bar — the tab anchor is encoded in the URL so that anyone opening the link lands directly on that tab.
|
||||
|
||||
### Auto-Refresh
|
||||
|
||||
Dashboards can be configured to refresh automatically at a fixed interval without user interaction. Open a dashboard, click the **⋮** (more options) menu in the top-right, and select **Set auto-refresh interval**. Choose an interval (e.g., every 10 seconds, 1 minute, or 10 minutes). The setting is per-session and resets when you close the tab.
|
||||
|
||||
:::note
|
||||
Auto-refresh triggers a full data reload for all charts on the dashboard. For dashboards with expensive queries, choose longer intervals to avoid overloading your database.
|
||||
:::
|
||||
|
||||
### Last Queried Timestamp
|
||||
|
||||
Charts can display a "Last queried at" timestamp showing when the chart data was last fetched. This is useful on auto-refreshing dashboards to confirm data freshness. Enable it in **Dashboard Properties → Styling → Show last queried time**.
|
||||
|
||||
### Saving a Chart to a Specific Tab
|
||||
|
||||
When saving or adding a chart to a dashboard from Explore, you can select which tab it should land on using the tab tree-select dropdown in the "Add to dashboard" modal.
|
||||
|
||||
:::resources
|
||||
- [Dashboard Customization](https://docs.preset.io/docs/dashboard-customization) - Advanced dashboard styling and layout options
|
||||
- [Blog: BI Dashboard Best Practices](https://preset.io/blog/bi-dashboard-best-practices/)
|
||||
|
||||
@@ -33,29 +33,6 @@ SQL templating must be enabled by your administrator via the `ENABLE_TEMPLATE_PR
|
||||
For advanced configuration options, see the [SQL Templating Configuration Guide](/admin-docs/configuration/sql-templating).
|
||||
:::
|
||||
|
||||
## Using Jinja in Calculated Columns
|
||||
|
||||
Jinja template macros are available in calculated column expressions in the dataset editor — not just in SQL Lab queries and virtual datasets. This allows column expressions to reference the current user or dynamic context.
|
||||
|
||||
**Example: User-scoped calculated column**
|
||||
|
||||
```sql
|
||||
CASE WHEN sales_rep = '{{ current_username() }}' THEN amount ELSE 0 END
|
||||
```
|
||||
|
||||
**Example: Conditional display based on role**
|
||||
|
||||
Because `current_user_roles()` returns a Python list, test role membership with a Jinja
|
||||
conditional at template time rather than matching against the list's string representation:
|
||||
|
||||
```sql
|
||||
{% if 'Finance' in current_user_roles() %}revenue{% else %}NULL{% endif %} AS finance_revenue
|
||||
```
|
||||
|
||||
:::note
|
||||
The `ENABLE_TEMPLATE_PROCESSING` feature flag must be enabled by your administrator for Jinja in calculated columns to work.
|
||||
:::
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Jinja templates use double curly braces `{{ }}` for expressions and `{% %}` for logic blocks.
|
||||
@@ -266,7 +243,6 @@ Using `remove_filter=True` applies the filter in the inner query for better perf
|
||||
- Use `|tojson` to serialize arrays as JSON strings
|
||||
- Test queries with explicit parameter values before relying on filter context
|
||||
- For complex templating needs, ask your administrator about custom Jinja macros
|
||||
- **Format SQL is Jinja-aware**: The "Format SQL" button in SQL Lab correctly preserves `{{ }}` and `{% %}` template syntax and applies your selected database's SQL dialect when formatting.
|
||||
|
||||
:::resources
|
||||
- [Admin Guide: SQL Templating Configuration](/admin-docs/configuration/sql-templating)
|
||||
|
||||
@@ -55,10 +55,9 @@ Ask your AI assistant to browse what's available in your Superset instance:
|
||||
|
||||
Describe the visualization you want and AI creates it for you:
|
||||
|
||||
- **Preview-first workflow** -- by default AI generates an Explore link so you can review the chart before it is saved. Say "save it" to commit permanently
|
||||
- **Create charts from natural language** -- describe what you want to see and AI picks the right chart type, metrics, and dimensions
|
||||
- **Preview before saving** -- `generate_chart` defaults to `save_chart=False`, showing the chart in Explore before it's committed. Ask AI to save once you're satisfied.
|
||||
- **Modify existing charts** -- `update_chart` also supports preview mode so you can review changes before saving (update filters, change chart types, add metrics)
|
||||
- **Modify existing charts** -- `update_chart` also supports preview mode so you can review changes before saving
|
||||
- **Get Explore links** -- open any chart in Superset's Explore view for further refinement
|
||||
|
||||
**Example prompts:**
|
||||
@@ -66,16 +65,6 @@ Describe the visualization you want and AI creates it for you:
|
||||
> "Update chart 42 to use a line chart instead"
|
||||
> "Give me a link to explore this chart further"
|
||||
|
||||
:::tip Preview-first workflow
|
||||
Charts are **not saved by default**. The workflow is intentionally iterative:
|
||||
|
||||
1. **Explore** — AI generates an Explore link so you can see the chart before it exists in Superset
|
||||
2. **Iterate** — ask the AI to adjust the chart; changes are previewed without touching the database
|
||||
3. **Save** — when you're happy, say "save it" and the chart is permanently stored
|
||||
|
||||
To skip the preview and save immediately, include "and save it" in your prompt.
|
||||
:::
|
||||
|
||||
### Create Dashboards
|
||||
|
||||
Build dashboards from a collection of charts:
|
||||
@@ -87,40 +76,16 @@ Build dashboards from a collection of charts:
|
||||
> "Create a dashboard called 'Q4 Sales Overview' with charts 10, 15, and 22"
|
||||
> "Add the revenue trend chart to the executive dashboard"
|
||||
|
||||
### Browse Databases
|
||||
|
||||
Discover what database connections are configured in your Superset instance:
|
||||
|
||||
- **List databases** -- see all database connections you have access to
|
||||
- **Get database details** -- name, backend type (PostgreSQL, Snowflake, etc.), and connection status
|
||||
|
||||
**Example prompts:**
|
||||
> "What databases are connected to Superset?"
|
||||
> "Show me details about the data warehouse connection"
|
||||
|
||||
### Create Virtual Datasets
|
||||
|
||||
Build ad-hoc SQL datasets that can be used as the basis for charts:
|
||||
|
||||
- **Create virtual datasets** -- write a SQL query and save it as a reusable dataset
|
||||
- **Use immediately in charts** -- the returned dataset ID can be passed directly to chart creation
|
||||
|
||||
**Example prompts:**
|
||||
> "Create a dataset from: SELECT region, SUM(revenue) as total_revenue FROM orders GROUP BY region"
|
||||
> "Make a virtual dataset called 'monthly_signups' from the users table filtered to last 12 months"
|
||||
|
||||
### Run SQL Queries
|
||||
|
||||
Execute SQL directly through your AI assistant:
|
||||
|
||||
- **Run queries** -- execute SQL with full Superset RBAC enforcement (you can only query data your roles allow)
|
||||
- **Open SQL Lab** -- get a link to SQL Lab pre-populated with a query, ready to run and explore
|
||||
- **Save queries** -- save a SQL query to SQL Lab's Saved Queries for later reuse
|
||||
|
||||
**Example prompts:**
|
||||
> "Run this query: SELECT region, SUM(revenue) FROM sales GROUP BY region"
|
||||
> "Open SQL Lab with a query to show the top 10 customers by order count"
|
||||
> "Save this query as 'Weekly Revenue Report'"
|
||||
|
||||
### Analyze Chart Data
|
||||
|
||||
|
||||
+33
-32
@@ -40,13 +40,13 @@
|
||||
"version:remove:components": "node scripts/manage-versions.mjs remove components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/faster": "^3.9.2",
|
||||
"@docusaurus/plugin-client-redirects": "3.9.2",
|
||||
"@docusaurus/preset-classic": "3.9.2",
|
||||
"@docusaurus/theme-live-codeblock": "^3.9.2",
|
||||
"@docusaurus/theme-mermaid": "^3.9.2",
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@docusaurus/core": "^3.10.0",
|
||||
"@docusaurus/faster": "^3.10.0",
|
||||
"@docusaurus/plugin-client-redirects": "^3.10.0",
|
||||
"@docusaurus/preset-classic": "3.10.0",
|
||||
"@docusaurus/theme-live-codeblock": "^3.10.0",
|
||||
"@docusaurus/theme-mermaid": "^3.10.0",
|
||||
"@emotion/core": "^11.0.0",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
@@ -55,24 +55,24 @@
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^8.6.17",
|
||||
"@storybook/addon-docs": "^8.6.18",
|
||||
"@storybook/blocks": "^8.6.15",
|
||||
"@storybook/channels": "^8.6.17",
|
||||
"@storybook/client-logger": "^8.6.17",
|
||||
"@storybook/components": "^8.6.17",
|
||||
"@storybook/core": "^8.6.17",
|
||||
"@storybook/core-events": "^8.6.17",
|
||||
"@storybook/channels": "^8.6.18",
|
||||
"@storybook/client-logger": "^8.6.18",
|
||||
"@storybook/components": "^8.6.18",
|
||||
"@storybook/core": "^8.6.18",
|
||||
"@storybook/core-events": "^8.6.18",
|
||||
"@storybook/csf": "^0.1.13",
|
||||
"@storybook/docs-tools": "^8.6.17",
|
||||
"@storybook/preview-api": "^8.6.17",
|
||||
"@storybook/docs-tools": "^8.6.18",
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.17",
|
||||
"antd": "^6.3.1",
|
||||
"baseline-browser-mapping": "^2.10.0",
|
||||
"caniuse-lite": "^1.0.30001775",
|
||||
"docusaurus-plugin-openapi-docs": "^4.6.0",
|
||||
"docusaurus-theme-openapi-docs": "^4.6.0",
|
||||
"@swc/core": "^1.15.30",
|
||||
"antd": "^6.3.6",
|
||||
"baseline-browser-mapping": "^2.10.21",
|
||||
"caniuse-lite": "^1.0.30001790",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.1",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
@@ -85,29 +85,29 @@
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.17",
|
||||
"swagger-ui-react": "^5.32.0",
|
||||
"storybook": "^8.6.18",
|
||||
"swagger-ui-react": "^5.32.4",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.9.1",
|
||||
"@docusaurus/tsconfig": "^3.9.2",
|
||||
"@docusaurus/module-type-aliases": "^3.10.0",
|
||||
"@docusaurus/tsconfig": "^3.10.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"webpack": "^5.105.4"
|
||||
"globals": "^17.5.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"webpack": "^5.106.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -124,7 +124,8 @@
|
||||
"resolutions": {
|
||||
"react-redux": "^9.2.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"baseline-browser-mapping": "^2.9.19"
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"webpackbar": "^7.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
@@ -129,6 +129,30 @@ def add_missing_schemas(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]
|
||||
}
|
||||
fixed.append("DashboardColorsConfigUpdateSchema")
|
||||
|
||||
# DashboardChartCustomizationsConfigUpdateSchema (dashboards/schemas.py)
|
||||
if "DashboardChartCustomizationsConfigUpdateSchema" not in schemas:
|
||||
schemas["DashboardChartCustomizationsConfigUpdateSchema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of deleted chart customization IDs.",
|
||||
},
|
||||
"modified": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "List of modified chart customizations.",
|
||||
},
|
||||
"reordered": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of chart customization IDs in new order.",
|
||||
},
|
||||
},
|
||||
}
|
||||
fixed.append("DashboardChartCustomizationsConfigUpdateSchema")
|
||||
|
||||
# FormatQueryPayloadSchema - based on superset/sqllab/schemas.py
|
||||
if "FormatQueryPayloadSchema" not in schemas:
|
||||
schemas["FormatQueryPayloadSchema"] = {
|
||||
@@ -295,6 +319,7 @@ TAG_DESCRIPTIONS = {
|
||||
"Security Roles": "Manage security roles and their permissions.",
|
||||
"Security Users": "Manage user accounts.",
|
||||
"Tags": "Organize assets with tags.",
|
||||
"Themes": "Manage UI themes for customizing Superset's appearance.",
|
||||
"User": "User profile and preferences.",
|
||||
}
|
||||
|
||||
|
||||
Vendored
+12
@@ -51,6 +51,18 @@
|
||||
"lifecycle": "development",
|
||||
"description": "Enable Superset extensions for custom functionality without modifying core"
|
||||
},
|
||||
{
|
||||
"name": "FAB_API_KEY_ENABLED",
|
||||
"default": false,
|
||||
"lifecycle": "development",
|
||||
"description": "Enable API key authentication via FAB SecurityManager When enabled, users can create/manage API keys in the User Info page"
|
||||
},
|
||||
{
|
||||
"name": "GRANULAR_EXPORT_CONTROLS",
|
||||
"default": false,
|
||||
"lifecycle": "development",
|
||||
"description": "Enable granular export controls (can_export_data, can_export_image, can_copy_clipboard) instead of the single can_csv permission"
|
||||
},
|
||||
{
|
||||
"name": "MATRIXIFY",
|
||||
"default": false,
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Vendored
+4884
-907
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"strict": false,
|
||||
|
||||
@@ -109,6 +109,14 @@ SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
|
||||
You can generate a strong secure key with `openssl rand -base64 42`.
|
||||
|
||||
Alternatively, you can set the secret key using `SUPERSET_SECRET_KEY` environment variable:
|
||||
|
||||
On a Unix-based system, such as Linux or macOS, you can do so by running the following command in your terminal:
|
||||
|
||||
```bash
|
||||
export SUPERSET_SECRET_KEY=$(openssl rand -base64 42)
|
||||
```
|
||||
|
||||
:::caution Use a strong secret key
|
||||
This key will be used for securely signing session cookies and encrypting sensitive information stored in Superset's application metadata database.
|
||||
Your deployment must use a complex, unique key.
|
||||
|
||||
@@ -35,7 +35,7 @@ or a view.
|
||||
When working with tables, the solution would be to create a table that contains all the fields
|
||||
needed for your analysis, most likely through some scheduled batch process.
|
||||
|
||||
A view is a simple logical layer that abstracts an arbitrary SQL queries as a virtual table. This can
|
||||
A view is a simple logical layer that abstracts an arbitrary SQL query as a virtual table. This can
|
||||
allow you to join and union multiple tables and to apply some transformation using arbitrary SQL
|
||||
expressions. The limitation there is your database performance, as Superset effectively will run a
|
||||
query on top of your query (view). A good practice may be to limit yourself to joining your main
|
||||
|
||||
+1136
-1398
File diff suppressed because it is too large
Load Diff
Generated
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "superset-release",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
+5
-3
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <47.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <4.0.0",
|
||||
"flask-appbuilder>=5.0.2,<6",
|
||||
"flask-appbuilder>=5.2.1, <6.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -89,7 +89,7 @@ dependencies = [
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"pygeohash",
|
||||
"pyarrow>=16.1.0, <19", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyarrow>=16.1.0, <21", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyyaml>=6.0.0, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=5.0.0, <6.0",
|
||||
@@ -183,6 +183,7 @@ risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.3, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
|
||||
sqlite = ["syntaqlite>=0.1.0"]
|
||||
spark = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
@@ -226,6 +227,7 @@ development = [
|
||||
"ruff",
|
||||
"sqloxide",
|
||||
"statsd",
|
||||
"syntaqlite>=0.1.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -238,7 +240,7 @@ combine_as_imports = true
|
||||
include_trailing_comma = true
|
||||
line_length = 88
|
||||
known_first_party = "superset, apache-superset-core, apache-superset-extensions-cli"
|
||||
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, marshmallow-union, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, pgsanity, polyline, prison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, selenium, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
|
||||
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, marshmallow-union, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, pgsanity, polyline, prison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, selenium, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, syntaqlite, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
|
||||
multi_line_output = 3
|
||||
order_by_type = false
|
||||
|
||||
|
||||
@@ -25,6 +25,16 @@ filelock>=3.20.3,<4.0.0
|
||||
# Security: decompression bomb fix (required by aiohttp 3.13.3)
|
||||
brotli>=1.2.0,<2.0.0
|
||||
numexpr>=2.9.0
|
||||
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
|
||||
cryptography>=46.0.7,<47.0.0
|
||||
# Security: Snyk - XSS vulnerability in Mako templates
|
||||
mako>=1.3.11,<2.0.0
|
||||
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
|
||||
pyarrow>=20.0.0,<21.0.0
|
||||
# Security: CVE-2026-27459 - pyopenssl certificate validation
|
||||
pyopenssl>=26.0.0,<27.0.0
|
||||
# Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File
|
||||
requests>=2.33.0,<3.0.0
|
||||
|
||||
# 5.0.0 has a sensitive deprecation used in other libs
|
||||
# -> https://github.com/aio-libs/async-timeout/blob/master/CHANGES.rst#500-2024-10-31
|
||||
|
||||
+20
-13
@@ -86,8 +86,9 @@ cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==46.0.5
|
||||
cryptography==46.0.7
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
# paramiko
|
||||
# pyopenssl
|
||||
@@ -120,7 +121,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==5.0.2
|
||||
flask-appbuilder==5.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
@@ -205,11 +206,12 @@ kombu==5.5.3
|
||||
# via celery
|
||||
limits==5.1.0
|
||||
# via flask-limiter
|
||||
mako==1.3.10
|
||||
mako==1.3.11
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
markdown==3.8
|
||||
markdown==3.8.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
@@ -279,7 +281,7 @@ parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
pillow==11.3.0
|
||||
pillow==12.1.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
@@ -291,9 +293,11 @@ prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
prompt-toolkit==3.0.51
|
||||
# via click-repl
|
||||
pyarrow==16.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
pyasn1==0.6.2
|
||||
pyarrow==20.0.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
pyasn1==0.6.3
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# rsa
|
||||
@@ -309,9 +313,9 @@ pydantic-core==2.33.2
|
||||
# via pydantic
|
||||
pygeohash==3.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
pygments==2.19.1
|
||||
pygments==2.20.0
|
||||
# via rich
|
||||
pyjwt==2.10.1
|
||||
pyjwt==2.12.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
@@ -319,8 +323,10 @@ pyjwt==2.10.1
|
||||
# redis
|
||||
pynacl==1.6.2
|
||||
# via paramiko
|
||||
pyopenssl==25.3.0
|
||||
# via shillelagh
|
||||
pyopenssl==26.0.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# shillelagh
|
||||
pyparsing==3.2.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
pysocks==1.7.1
|
||||
@@ -353,8 +359,9 @@ referencing==0.36.2
|
||||
# via
|
||||
# jsonschema
|
||||
# jsonschema-specifications
|
||||
requests==2.32.4
|
||||
requests==2.33.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# requests-cache
|
||||
# shillelagh
|
||||
requests-cache==1.2.1
|
||||
|
||||
@@ -178,7 +178,7 @@ croniter==6.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
cryptography==46.0.5
|
||||
cryptography==46.0.7
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -259,7 +259,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==5.0.2
|
||||
flask-appbuilder==5.2.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -503,12 +503,12 @@ limits==5.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-limiter
|
||||
mako==1.3.10
|
||||
mako==1.3.11
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
markdown==3.8
|
||||
markdown==3.8.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -655,7 +655,7 @@ pgsanity==0.2.9
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
pillow==11.3.0
|
||||
pillow==12.1.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -710,13 +710,13 @@ psycopg2-binary==2.9.9
|
||||
# via apache-superset
|
||||
py-key-value-aio==0.4.4
|
||||
# via fastmcp
|
||||
pyarrow==16.1.0
|
||||
pyarrow==20.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# db-dtypes
|
||||
# pandas-gbq
|
||||
pyasn1==0.6.2
|
||||
pyasn1==0.6.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pyasn1-modules
|
||||
@@ -756,7 +756,7 @@ pygeohash==3.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
pygments==2.19.1
|
||||
pygments==2.20.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# rich
|
||||
@@ -764,7 +764,7 @@ pyhive==0.7.0
|
||||
# via apache-superset
|
||||
pyinstrument==4.4.0
|
||||
# via apache-superset
|
||||
pyjwt==2.10.1
|
||||
pyjwt==2.12.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -778,7 +778,7 @@ pynacl==1.6.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# paramiko
|
||||
pyopenssl==25.3.0
|
||||
pyopenssl==26.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# shillelagh
|
||||
@@ -865,7 +865,7 @@ referencing==0.36.2
|
||||
# jsonschema
|
||||
# jsonschema-path
|
||||
# jsonschema-specifications
|
||||
requests==2.32.4
|
||||
requests==2.33.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# docker
|
||||
@@ -992,10 +992,14 @@ starlette==0.49.1
|
||||
# via mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
syntaqlite==0.1.0
|
||||
# via apache-superset
|
||||
tabulate==0.9.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
tomli-w==1.2.0
|
||||
# via apache-superset-extensions-cli
|
||||
tomlkit==0.13.3
|
||||
# via pylint
|
||||
tqdm==4.67.1
|
||||
|
||||
+1
-11
@@ -45,17 +45,7 @@ if [ ${#js_ts_files[@]} -gt 0 ]; then
|
||||
# Skip custom OXC build in pre-commit for speed
|
||||
export SKIP_CUSTOM_OXC=true
|
||||
# Use quiet mode in pre-commit to reduce noise (only show errors)
|
||||
# Capture output so we can treat "No files found" (all files ignored by
|
||||
# ignorePatterns) as success rather than a false-positive failure.
|
||||
output=$(npx oxlint --config oxlint.json --fix --quiet "${js_ts_files[@]}" 2>&1) || {
|
||||
if echo "$output" | grep -q "No files found"; then
|
||||
echo "No files to lint after applying ignore patterns"
|
||||
exit 0
|
||||
fi
|
||||
echo "$output" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -n "$output" ] && echo "$output"
|
||||
npx oxlint --config oxlint.json --fix --quiet "${js_ts_files[@]}"
|
||||
else
|
||||
echo "No JavaScript/TypeScript files to lint"
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-core"
|
||||
version = "0.1.0rc3"
|
||||
version = "0.1.0rc2"
|
||||
description = "Core Python package for building Apache Superset backend extensions and integrations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
Generated
+12
-76
@@ -7025,9 +7025,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -7121,15 +7121,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -7306,26 +7297,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
@@ -7355,15 +7326,6 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shallow-clone": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||
@@ -7590,15 +7552,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
|
||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
|
||||
"integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -13159,9 +13120,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
@@ -13220,15 +13181,6 @@
|
||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||
"dev": true
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -13359,12 +13311,6 @@
|
||||
"integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==",
|
||||
"dev": true
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
@@ -13383,15 +13329,6 @@
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"shallow-clone": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||
@@ -13563,15 +13500,14 @@
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
|
||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
|
||||
"integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ export type EmbedDashboardParams = {
|
||||
iframeTitle?: string;
|
||||
/** additional iframe sandbox attributes ex (allow-top-navigation, allow-popups-to-escape-sandbox) **/
|
||||
iframeSandboxExtras?: string[];
|
||||
/** Additional Permissions Policy features for the iframe's `allow` attribute (e.g., ['camera', 'microphone']). `fullscreen` and `clipboard-write` are granted by default. **/
|
||||
/** iframe allow attribute for Permissions Policy (e.g., ['clipboard-write', 'fullscreen']) **/
|
||||
iframeAllowExtras?: string[];
|
||||
/** force a specific refererPolicy to be used in the iframe request **/
|
||||
referrerPolicy?: ReferrerPolicy;
|
||||
@@ -233,14 +233,9 @@ export async function embedDashboard({
|
||||
iframe.src = `${supersetDomain}/embedded/${id}${urlParamsString}`;
|
||||
iframe.title = iframeTitle;
|
||||
iframe.style.background = 'transparent';
|
||||
// Permissions Policy features the embedded dashboard relies on. Modern
|
||||
// browsers gate these APIs on the iframe's `allow` attribute regardless
|
||||
// of sandbox flags, so we include them by default. Host apps can extend
|
||||
// the list via `iframeAllowExtras`.
|
||||
const allowFeatures = Array.from(
|
||||
new Set(['fullscreen', 'clipboard-write', ...iframeAllowExtras]),
|
||||
);
|
||||
iframe.setAttribute('allow', allowFeatures.join('; '));
|
||||
if (iframeAllowExtras.length > 0) {
|
||||
iframe.setAttribute('allow', iframeAllowExtras.join('; '));
|
||||
}
|
||||
//@ts-ignore
|
||||
mountPoint.replaceChildren(iframe);
|
||||
log('placed the iframe');
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-extensions-cli"
|
||||
version = "0.1.0rc3"
|
||||
version = "0.1.0rc2"
|
||||
description = "Official command-line interface for building, bundling, and managing Apache Superset extensions"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -49,6 +49,7 @@ dependencies = [
|
||||
"jinja2>=3.1.6",
|
||||
"semver>=3.0.4",
|
||||
"tomli>=2.2.1; python_version < '3.11'",
|
||||
"tomli-w>=1.2.0",
|
||||
"watchdog>=6.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ from superset_extensions_cli.utils import (
|
||||
validate_display_name,
|
||||
validate_publisher,
|
||||
validate_technical_name,
|
||||
write_json,
|
||||
write_toml,
|
||||
)
|
||||
|
||||
REMOTE_ENTRY_REGEX = re.compile(r"^remoteEntry\..+\.js$")
|
||||
@@ -292,6 +294,7 @@ def app() -> None:
|
||||
|
||||
@app.command()
|
||||
def validate() -> None:
|
||||
"""Validate the extension structure and metadata consistency."""
|
||||
validate_npm()
|
||||
|
||||
cwd = Path.cwd()
|
||||
@@ -372,12 +375,167 @@ def validate() -> None:
|
||||
click.secho(" Convention requires: frontend/src/index.tsx", fg="yellow")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate version and license consistency across extension.json, frontend, and backend
|
||||
mismatches: list[str] = []
|
||||
frontend_pkg_path = cwd / "frontend" / "package.json"
|
||||
frontend_pkg = None
|
||||
if frontend_pkg_path.is_file():
|
||||
frontend_pkg = read_json(frontend_pkg_path)
|
||||
if frontend_pkg:
|
||||
if frontend_pkg.get("version") != extension.version:
|
||||
mismatches.append(
|
||||
f" frontend/package.json version: {frontend_pkg.get('version')} "
|
||||
f"(expected {extension.version})"
|
||||
)
|
||||
if extension.license and frontend_pkg.get("license") != extension.license:
|
||||
mismatches.append(
|
||||
f" frontend/package.json license: {frontend_pkg.get('license')} "
|
||||
f"(expected {extension.license})"
|
||||
)
|
||||
|
||||
backend_pyproject_path = cwd / "backend" / "pyproject.toml"
|
||||
if backend_pyproject_path.is_file():
|
||||
backend_pyproject = read_toml(backend_pyproject_path)
|
||||
if backend_pyproject:
|
||||
project = backend_pyproject.get("project", {})
|
||||
if project.get("version") != extension.version:
|
||||
mismatches.append(
|
||||
f" backend/pyproject.toml version: {project.get('version')} "
|
||||
f"(expected {extension.version})"
|
||||
)
|
||||
if extension.license and project.get("license") != extension.license:
|
||||
mismatches.append(
|
||||
f" backend/pyproject.toml license: {project.get('license')} "
|
||||
f"(expected {extension.license})"
|
||||
)
|
||||
|
||||
if mismatches:
|
||||
click.secho("❌ Metadata mismatch detected:", err=True, fg="red")
|
||||
for mismatch in mismatches:
|
||||
click.secho(mismatch, err=True, fg="red")
|
||||
click.secho(
|
||||
"Run `superset-extensions update` to sync from extension.json.",
|
||||
fg="yellow",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
click.secho("✅ Validation successful", fg="green")
|
||||
|
||||
|
||||
@app.command()
|
||||
@click.option(
|
||||
"--version",
|
||||
"version_opt",
|
||||
is_flag=False,
|
||||
flag_value="__prompt__",
|
||||
default=None,
|
||||
help="Set a new version. Prompts for value if none given.",
|
||||
)
|
||||
@click.option(
|
||||
"--license",
|
||||
"license_opt",
|
||||
is_flag=False,
|
||||
flag_value="__prompt__",
|
||||
default=None,
|
||||
help="Set a new license. Prompts for value if none given.",
|
||||
)
|
||||
def update(version_opt: str | None, license_opt: str | None) -> None:
|
||||
"""Update derived and generated files in the extension project."""
|
||||
cwd = Path.cwd()
|
||||
|
||||
extension_json_path = cwd / "extension.json"
|
||||
extension_data = read_json(extension_json_path)
|
||||
if not extension_data:
|
||||
click.secho("❌ extension.json not found.", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
extension = ExtensionConfig.model_validate(extension_data)
|
||||
except Exception as e:
|
||||
click.secho(f"❌ Invalid extension.json: {e}", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
# Resolve version: prompt if flag used without value
|
||||
if version_opt == "__prompt__":
|
||||
version_opt = click.prompt("Version", default=extension.version)
|
||||
target_version = (
|
||||
version_opt
|
||||
if version_opt and version_opt != extension.version
|
||||
else extension.version
|
||||
)
|
||||
|
||||
# Resolve license: prompt if flag used without value
|
||||
if license_opt == "__prompt__":
|
||||
license_opt = click.prompt("License", default=extension.license or "")
|
||||
target_license = (
|
||||
license_opt
|
||||
if license_opt and license_opt != extension.license
|
||||
else extension.license
|
||||
)
|
||||
|
||||
updated: list[str] = []
|
||||
|
||||
# Update extension.json if version or license changed
|
||||
ext_changed = False
|
||||
if version_opt and version_opt != extension.version:
|
||||
extension_data["version"] = target_version
|
||||
ext_changed = True
|
||||
if license_opt and license_opt != extension.license:
|
||||
extension_data["license"] = target_license
|
||||
ext_changed = True
|
||||
if ext_changed:
|
||||
try:
|
||||
ExtensionConfig.model_validate(extension_data)
|
||||
except Exception as e:
|
||||
click.secho(f"❌ Invalid value: {e}", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
write_json(extension_json_path, extension_data)
|
||||
updated.append("extension.json")
|
||||
|
||||
# Update frontend/package.json
|
||||
frontend_pkg_path = cwd / "frontend" / "package.json"
|
||||
if frontend_pkg_path.is_file():
|
||||
frontend_pkg = read_json(frontend_pkg_path)
|
||||
if frontend_pkg:
|
||||
pkg_changed = False
|
||||
if frontend_pkg.get("version") != target_version:
|
||||
frontend_pkg["version"] = target_version
|
||||
pkg_changed = True
|
||||
if target_license and frontend_pkg.get("license") != target_license:
|
||||
frontend_pkg["license"] = target_license
|
||||
pkg_changed = True
|
||||
if pkg_changed:
|
||||
write_json(frontend_pkg_path, frontend_pkg)
|
||||
updated.append("frontend/package.json")
|
||||
|
||||
# Update backend/pyproject.toml
|
||||
backend_pyproject_path = cwd / "backend" / "pyproject.toml"
|
||||
if backend_pyproject_path.is_file():
|
||||
backend_pyproject = read_toml(backend_pyproject_path)
|
||||
if backend_pyproject:
|
||||
project = backend_pyproject.setdefault("project", {})
|
||||
toml_changed = False
|
||||
if project.get("version") != target_version:
|
||||
project["version"] = target_version
|
||||
toml_changed = True
|
||||
if target_license and project.get("license") != target_license:
|
||||
project["license"] = target_license
|
||||
toml_changed = True
|
||||
if toml_changed:
|
||||
write_toml(backend_pyproject_path, backend_pyproject)
|
||||
updated.append("backend/pyproject.toml")
|
||||
|
||||
if updated:
|
||||
for path in updated:
|
||||
click.secho(f"✅ Updated {path}", fg="green")
|
||||
else:
|
||||
click.secho("✅ All files already up to date.", fg="green")
|
||||
|
||||
|
||||
@app.command()
|
||||
@click.pass_context
|
||||
def build(ctx: click.Context) -> None:
|
||||
"""Build extension assets."""
|
||||
ctx.invoke(validate)
|
||||
cwd = Path.cwd()
|
||||
frontend_dir = cwd / "frontend"
|
||||
@@ -413,6 +571,7 @@ def build(ctx: click.Context) -> None:
|
||||
)
|
||||
@click.pass_context
|
||||
def bundle(ctx: click.Context, output: Path | None) -> None:
|
||||
"""Package the extension into a .supx file."""
|
||||
ctx.invoke(build)
|
||||
|
||||
cwd = Path.cwd()
|
||||
@@ -453,6 +612,7 @@ def bundle(ctx: click.Context, output: Path | None) -> None:
|
||||
@app.command()
|
||||
@click.pass_context
|
||||
def dev(ctx: click.Context) -> None:
|
||||
"""Automatically rebuild the extension as files change."""
|
||||
cwd = Path.cwd()
|
||||
frontend_dir = cwd / "frontend"
|
||||
backend_dir = cwd / "backend"
|
||||
@@ -647,6 +807,7 @@ def init(
|
||||
frontend_opt: bool | None,
|
||||
backend_opt: bool | None,
|
||||
) -> None:
|
||||
"""Scaffold a new extension project."""
|
||||
# Get extension names with graceful validation
|
||||
names = prompt_for_extension_info(display_name_opt, publisher_opt, name_opt)
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tomli_w
|
||||
|
||||
from superset_core.extensions.constants import (
|
||||
DISPLAY_NAME_PATTERN,
|
||||
PUBLISHER_PATTERN,
|
||||
@@ -109,6 +111,14 @@ def read_json(path: Path) -> dict[str, Any] | None:
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def write_json(path: Path, data: dict[str, Any]) -> None:
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
|
||||
|
||||
def write_toml(path: Path, data: dict[str, Any]) -> None:
|
||||
path.write_text(tomli_w.dumps(data))
|
||||
|
||||
|
||||
def _normalize_for_identifiers(name: str) -> str:
|
||||
"""
|
||||
Normalize display name to clean lowercase words.
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import tomli_w
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
@@ -138,3 +140,69 @@ def extension_setup_for_bundling():
|
||||
(backend_dir / "__init__.py").write_text("# init")
|
||||
|
||||
return _setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extension_with_versions():
|
||||
"""Create an extension directory structure with configurable versions and licenses."""
|
||||
|
||||
def _create(
|
||||
base_path: Path,
|
||||
ext_version: str = "1.0.0",
|
||||
frontend_version: str | None = None,
|
||||
backend_version: str | None = None,
|
||||
ext_license: str | None = "Apache-2.0",
|
||||
frontend_license: str | None = None,
|
||||
backend_license: str | None = None,
|
||||
) -> None:
|
||||
extension_json = {
|
||||
"publisher": "test-org",
|
||||
"name": "test-extension",
|
||||
"displayName": "Test Extension",
|
||||
"version": ext_version,
|
||||
"permissions": [],
|
||||
}
|
||||
if ext_license is not None:
|
||||
extension_json["license"] = ext_license
|
||||
(base_path / "extension.json").write_text(json.dumps(extension_json))
|
||||
|
||||
if frontend_version is not None:
|
||||
frontend_dir = base_path / "frontend"
|
||||
frontend_dir.mkdir(exist_ok=True)
|
||||
(frontend_dir / "src").mkdir(exist_ok=True)
|
||||
(frontend_dir / "src" / "index.tsx").write_text("// entry")
|
||||
pkg = {
|
||||
"name": "@test-org/test-extension",
|
||||
"version": frontend_version,
|
||||
}
|
||||
if frontend_license is not None:
|
||||
pkg["license"] = frontend_license
|
||||
elif ext_license is not None:
|
||||
pkg["license"] = ext_license
|
||||
(frontend_dir / "package.json").write_text(json.dumps(pkg, indent=2))
|
||||
|
||||
if backend_version is not None:
|
||||
backend_dir = base_path / "backend"
|
||||
backend_dir.mkdir(exist_ok=True)
|
||||
src_dir = backend_dir / "src" / "test_org" / "test_extension"
|
||||
src_dir.mkdir(parents=True, exist_ok=True)
|
||||
(src_dir / "entrypoint.py").write_text("# entry")
|
||||
project = {
|
||||
"name": "test-org-test-extension",
|
||||
"version": backend_version,
|
||||
}
|
||||
if backend_license is not None:
|
||||
project["license"] = backend_license
|
||||
elif ext_license is not None:
|
||||
project["license"] = ext_license
|
||||
pyproject = {
|
||||
"project": project,
|
||||
"tool": {
|
||||
"apache_superset_extensions": {
|
||||
"build": {"include": ["src/**/*.py"]}
|
||||
}
|
||||
},
|
||||
}
|
||||
(backend_dir / "pyproject.toml").write_text(tomli_w.dumps(pyproject))
|
||||
|
||||
return _create
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_build_command_success_flow(
|
||||
# Setup mocks
|
||||
mock_rebuild_frontend.return_value = "remoteEntry.abc123.js"
|
||||
mock_read_toml.return_value = {
|
||||
"project": {"name": "test"},
|
||||
"project": {"name": "test", "version": "1.0.0"},
|
||||
"tool": {
|
||||
"apache_superset_extensions": {
|
||||
"build": {"include": ["src/test_org/test_extension/**/*.py"]}
|
||||
@@ -162,7 +162,7 @@ def test_build_command_handles_frontend_build_failure(
|
||||
# Setup mocks
|
||||
mock_rebuild_frontend.return_value = None # Indicates failure
|
||||
mock_read_toml.return_value = {
|
||||
"project": {"name": "test"},
|
||||
"project": {"name": "test", "version": "1.0.0"},
|
||||
"tool": {
|
||||
"apache_superset_extensions": {
|
||||
"build": {"include": ["src/test_org/test_extension/**/*.py"]}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# 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 __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from superset_extensions_cli.cli import app
|
||||
from superset_extensions_cli.utils import read_json, read_toml
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_syncs_versions(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test update syncs frontend and backend versions from extension.json."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="2.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
)
|
||||
|
||||
result = cli_runner.invoke(app, ["update"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Updated frontend/package.json" in result.output
|
||||
assert "Updated backend/pyproject.toml" in result.output
|
||||
|
||||
frontend_pkg = read_json(isolated_filesystem / "frontend" / "package.json")
|
||||
assert frontend_pkg["version"] == "2.0.0"
|
||||
|
||||
backend_pyproject = read_toml(isolated_filesystem / "backend" / "pyproject.toml")
|
||||
assert backend_pyproject["project"]["version"] == "2.0.0"
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_noop_when_all_match(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test update reports no changes when everything already matches."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
)
|
||||
|
||||
result = cli_runner.invoke(app, ["update"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "All files already up to date" in result.output
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_fails_without_extension_json(cli_runner, isolated_filesystem):
|
||||
"""Test update fails when extension.json is missing."""
|
||||
result = cli_runner.invoke(app, ["update"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "extension.json not found" in result.output
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_with_version_flag(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test --version updates extension.json first, then syncs all files."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
)
|
||||
|
||||
result = cli_runner.invoke(app, ["update", "--version", "3.0.0"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Updated extension.json" in result.output
|
||||
assert "Updated frontend/package.json" in result.output
|
||||
assert "Updated backend/pyproject.toml" in result.output
|
||||
|
||||
ext = read_json(isolated_filesystem / "extension.json")
|
||||
assert ext["version"] == "3.0.0"
|
||||
|
||||
frontend_pkg = read_json(isolated_filesystem / "frontend" / "package.json")
|
||||
assert frontend_pkg["version"] == "3.0.0"
|
||||
|
||||
backend_pyproject = read_toml(isolated_filesystem / "backend" / "pyproject.toml")
|
||||
assert backend_pyproject["project"]["version"] == "3.0.0"
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_with_license_flag(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test --license updates license across all files."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
ext_license="Apache-2.0",
|
||||
)
|
||||
|
||||
result = cli_runner.invoke(app, ["update", "--license", "MIT"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Updated extension.json" in result.output
|
||||
assert "Updated frontend/package.json" in result.output
|
||||
assert "Updated backend/pyproject.toml" in result.output
|
||||
|
||||
ext = read_json(isolated_filesystem / "extension.json")
|
||||
assert ext["license"] == "MIT"
|
||||
|
||||
frontend_pkg = read_json(isolated_filesystem / "frontend" / "package.json")
|
||||
assert frontend_pkg["license"] == "MIT"
|
||||
|
||||
backend_pyproject = read_toml(isolated_filesystem / "backend" / "pyproject.toml")
|
||||
assert backend_pyproject["project"]["license"] == "MIT"
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_version_prompt_default(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test --version without value prompts with current version as default."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
)
|
||||
|
||||
# Hit enter to accept default — nothing should change
|
||||
result = cli_runner.invoke(app, ["update", "--version"], input="\n")
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "All files already up to date" in result.output
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_update_rejects_invalid_version(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test --version with an invalid semver string exits with error."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
)
|
||||
|
||||
result = cli_runner.invoke(app, ["update", "--version", "not-a-version"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Invalid value" in result.output
|
||||
|
||||
# Verify extension.json was not modified
|
||||
ext = read_json(isolated_filesystem / "extension.json")
|
||||
assert ext["version"] == "1.0.0"
|
||||
@@ -207,3 +207,66 @@ def test_validate_npm_with_empty_version_output_raises_error(mock_run, mock_whic
|
||||
# semver.compare will raise ValueError for empty version
|
||||
with pytest.raises(ValueError):
|
||||
validate_npm()
|
||||
|
||||
|
||||
# Version Consistency Tests
|
||||
@pytest.mark.cli
|
||||
def test_validate_fails_on_version_mismatch(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test validate fails when frontend/backend versions differ from extension.json."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="2.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
)
|
||||
|
||||
with patch("superset_extensions_cli.cli.validate_npm"):
|
||||
result = cli_runner.invoke(app, ["validate"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Metadata mismatch" in result.output
|
||||
assert "superset-extensions update" in result.output
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_validate_passes_with_matching_versions(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test validate passes when all versions match extension.json."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
)
|
||||
|
||||
with patch("superset_extensions_cli.cli.validate_npm"):
|
||||
result = cli_runner.invoke(app, ["validate"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Validation successful" in result.output
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
def test_validate_fails_on_license_mismatch(
|
||||
cli_runner, isolated_filesystem, extension_with_versions
|
||||
):
|
||||
"""Test validate fails when frontend/backend licenses differ from extension.json."""
|
||||
extension_with_versions(
|
||||
isolated_filesystem,
|
||||
ext_version="1.0.0",
|
||||
frontend_version="1.0.0",
|
||||
backend_version="1.0.0",
|
||||
ext_license="Apache-2.0",
|
||||
frontend_license="MIT",
|
||||
backend_license="MIT",
|
||||
)
|
||||
|
||||
with patch("superset_extensions_cli.cli.validate_npm"):
|
||||
result = cli_runner.invoke(app, ["validate"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Metadata mismatch" in result.output
|
||||
assert "license" in result.output
|
||||
|
||||
@@ -20,7 +20,7 @@ from __future__ import annotations
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from superset_extensions_cli.utils import read_json, read_toml
|
||||
from superset_extensions_cli.utils import read_json, read_toml, write_json, write_toml
|
||||
|
||||
|
||||
# Read JSON Tests
|
||||
@@ -269,3 +269,32 @@ def test_read_toml_with_permission_denied(isolated_filesystem):
|
||||
toml_file.chmod(0o644)
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
|
||||
|
||||
# Write JSON Tests
|
||||
@pytest.mark.unit
|
||||
def test_write_json_round_trip(isolated_filesystem):
|
||||
"""Test write_json then read_json round-trip preserves content."""
|
||||
data = {"name": "test-extension", "version": "2.0.0", "nested": {"key": "value"}}
|
||||
json_file = isolated_filesystem / "output.json"
|
||||
|
||||
write_json(json_file, data)
|
||||
result = read_json(json_file)
|
||||
|
||||
assert result == data
|
||||
|
||||
|
||||
# Write TOML Tests
|
||||
@pytest.mark.unit
|
||||
def test_write_toml_round_trip(isolated_filesystem):
|
||||
"""Test write_toml then read_toml round-trip preserves content."""
|
||||
data = {
|
||||
"project": {"name": "test-package", "version": "1.0.0"},
|
||||
"tool": {"apache_superset_extensions": {"build": {"include": ["src/**/*.py"]}}},
|
||||
}
|
||||
toml_file = isolated_filesystem / "output.toml"
|
||||
|
||||
write_toml(toml_file, data)
|
||||
result = read_toml(toml_file)
|
||||
|
||||
assert result == data
|
||||
|
||||
@@ -127,7 +127,6 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
'import',
|
||||
'file-progress',
|
||||
'lodash',
|
||||
'theme-colors',
|
||||
'icons',
|
||||
|
||||
@@ -1,171 +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.
|
||||
*/
|
||||
import { CHART_LIST } from 'cypress/utils/urls';
|
||||
import { setGridMode, toggleBulkSelect } from 'cypress/utils';
|
||||
import {
|
||||
setFilter,
|
||||
interceptBulkDelete,
|
||||
interceptUpdate,
|
||||
interceptDelete,
|
||||
interceptFiltering,
|
||||
interceptFavoriteStatus,
|
||||
} from '../explore/utils';
|
||||
|
||||
function orderAlphabetical() {
|
||||
setFilter('Sort', 'Alphabetical');
|
||||
}
|
||||
|
||||
function openProperties() {
|
||||
cy.get('[aria-label="more"]').eq(0).click();
|
||||
cy.getBySel('chart-list-edit-option').click();
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
cy.get('[aria-label="more"]').eq(0).click();
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
cy.getBySel('delete-modal-input').type('DELETE');
|
||||
cy.getBySel('modal-confirm-button').click();
|
||||
}
|
||||
|
||||
function visitChartList() {
|
||||
interceptFiltering();
|
||||
interceptFavoriteStatus();
|
||||
cy.visit(CHART_LIST);
|
||||
cy.wait('@filtering');
|
||||
cy.wait('@favoriteStatus');
|
||||
}
|
||||
|
||||
describe('Charts list', () => {
|
||||
describe('common actions', () => {
|
||||
beforeEach(() => {
|
||||
visitChartList();
|
||||
});
|
||||
|
||||
it('should bulk delete correctly', () => {
|
||||
cy.createSampleCharts([0, 1, 2, 3]);
|
||||
|
||||
interceptBulkDelete();
|
||||
toggleBulkSelect();
|
||||
|
||||
// bulk deletes in card-view
|
||||
setGridMode('card');
|
||||
orderAlphabetical();
|
||||
|
||||
cy.getBySel('skeleton-card').should('not.exist');
|
||||
cy.getBySel('styled-card').contains('1 - Sample chart').click();
|
||||
cy.getBySel('styled-card').contains('2 - Sample chart').click();
|
||||
cy.getBySel('bulk-select-action').contains('Delete').click();
|
||||
confirmDelete();
|
||||
cy.wait('@bulkDelete');
|
||||
cy.getBySel('styled-card')
|
||||
.eq(1)
|
||||
.should('not.contain', '1 - Sample chart');
|
||||
cy.getBySel('styled-card')
|
||||
.eq(2)
|
||||
.should('not.contain', '2 - Sample chart');
|
||||
|
||||
// bulk deletes in list-view
|
||||
setGridMode('list');
|
||||
cy.get('.loading').should('not.exist');
|
||||
cy.getBySel('table-row').contains('3 - Sample chart').should('exist');
|
||||
cy.getBySel('table-row').contains('4 - Sample chart').should('exist');
|
||||
cy.get('[data-test="table-row"] input[type="checkbox"]').eq(0).click();
|
||||
cy.get('[data-test="table-row"] input[type="checkbox"]').eq(1).click();
|
||||
cy.getBySel('bulk-select-action').eq(0).contains('Delete').click();
|
||||
confirmDelete();
|
||||
cy.wait('@bulkDelete');
|
||||
cy.get('.loading').should('exist');
|
||||
cy.get('.loading').should('not.exist');
|
||||
cy.getBySel('table-row').eq(0).should('not.contain', '3 - Sample chart');
|
||||
cy.getBySel('table-row').eq(1).should('not.contain', '4 - Sample chart');
|
||||
});
|
||||
|
||||
it('should delete correctly in card mode', () => {
|
||||
cy.createSampleCharts([0, 1]);
|
||||
interceptDelete();
|
||||
|
||||
// deletes in card-view
|
||||
setGridMode('card');
|
||||
orderAlphabetical();
|
||||
|
||||
cy.getBySel('styled-card').contains('1 - Sample chart');
|
||||
openMenu();
|
||||
cy.getBySel('chart-list-delete-option').click();
|
||||
confirmDelete();
|
||||
cy.wait('@delete');
|
||||
cy.getBySel('styled-card')
|
||||
.contains('1 - Sample chart')
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('should delete correctly in list mode', () => {
|
||||
cy.createSampleCharts([2, 3]);
|
||||
interceptDelete();
|
||||
cy.getBySel('sort-header').contains('Name').click();
|
||||
|
||||
// Modal closes immediately without this
|
||||
cy.wait(2000);
|
||||
|
||||
cy.getBySel('table-row').eq(0).contains('3 - Sample chart');
|
||||
cy.getBySel('delete').eq(0).click();
|
||||
confirmDelete();
|
||||
cy.wait('@delete');
|
||||
cy.get('.loading').should('exist');
|
||||
cy.get('.loading').should('not.exist');
|
||||
cy.getBySel('table-row').eq(0).should('not.contain', '3 - Sample chart');
|
||||
});
|
||||
|
||||
it('should edit correctly', () => {
|
||||
cy.createSampleCharts([0]);
|
||||
interceptUpdate();
|
||||
|
||||
// edits in card-view
|
||||
setGridMode('card');
|
||||
orderAlphabetical();
|
||||
cy.getBySel('skeleton-card').should('not.exist');
|
||||
cy.getBySel('styled-card').eq(0).contains('1 - Sample chart');
|
||||
|
||||
// change title
|
||||
openProperties();
|
||||
cy.getBySel('properties-modal-name-input').type(' | EDITED');
|
||||
cy.get('button:contains("Save")').click();
|
||||
cy.wait('@update');
|
||||
cy.getBySel('styled-card').eq(0).contains('1 - Sample chart | EDITED');
|
||||
|
||||
// edits in list-view
|
||||
setGridMode('list');
|
||||
// Wait for list view to fully render after mode change
|
||||
cy.get('.loading').should('not.exist');
|
||||
cy.getBySel('table-row').should('be.visible');
|
||||
// Target the specific row by chart title to avoid flakiness from row ordering
|
||||
cy.getBySel('table-row')
|
||||
.contains('1 - Sample chart | EDITED')
|
||||
.parents('[data-test="table-row"]')
|
||||
.find('[data-test="edit-alt"]')
|
||||
.click();
|
||||
cy.getBySel('properties-modal-name-input').clear();
|
||||
cy.getBySel('properties-modal-name-input').type('1 - Sample chart');
|
||||
cy.get('button:contains("Save")').click();
|
||||
cy.wait('@update');
|
||||
cy.getBySel('table-row').contains('1 - Sample chart').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,42 +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.
|
||||
*/
|
||||
|
||||
import { DATASET_LIST_PATH } from 'cypress/utils/urls';
|
||||
|
||||
describe('Dataset list', () => {
|
||||
before(() => {
|
||||
cy.visit(DATASET_LIST_PATH);
|
||||
});
|
||||
|
||||
xit('should open Explore on dataset name click', () => {
|
||||
cy.intercept('**/api/v1/explore/**').as('explore');
|
||||
cy.get('[data-test="listview-table"] [data-test="internal-link"]')
|
||||
.contains('birth_names')
|
||||
.click();
|
||||
cy.wait('@explore');
|
||||
cy.get('[data-test="datasource-control"] .title-select').contains(
|
||||
'birth_names',
|
||||
);
|
||||
cy.get('.metric-option-label').first().contains('COUNT(*)');
|
||||
cy.get('.column-option-label').first().contains('ds');
|
||||
cy.get('[data-test="fast-viz-switcher"] > div:not([role="button"]')
|
||||
.contains('Table')
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
@@ -23,18 +23,6 @@ export function interceptFiltering() {
|
||||
cy.intercept('GET', `**/api/v1/chart/?q=*`).as('filtering');
|
||||
}
|
||||
|
||||
export function interceptBulkDelete() {
|
||||
cy.intercept('DELETE', `**/api/v1/chart/?q=*`).as('bulkDelete');
|
||||
}
|
||||
|
||||
export function interceptDelete() {
|
||||
cy.intercept('DELETE', `**/api/v1/chart/*`).as('delete');
|
||||
}
|
||||
|
||||
export function interceptFavoriteStatus() {
|
||||
cy.intercept('GET', '**/api/v1/chart/favorite_status/*').as('favoriteStatus');
|
||||
}
|
||||
|
||||
export function interceptUpdate() {
|
||||
cy.intercept('PUT', `**/api/v1/chart/*`).as('update');
|
||||
}
|
||||
@@ -43,32 +31,13 @@ export const interceptV1ChartData = (alias = 'v1Data') => {
|
||||
cy.intercept('**/api/v1/chart/data*').as(alias);
|
||||
};
|
||||
|
||||
export function interceptExploreJson(alias = 'getJson') {
|
||||
cy.intercept('POST', `**/superset/explore_json/**`).as(alias);
|
||||
}
|
||||
|
||||
export const interceptFormDataKey = () => {
|
||||
cy.intercept('POST', '**/api/v1/explore/form_data').as('formDataKey');
|
||||
};
|
||||
|
||||
export function interceptExploreGet() {
|
||||
function interceptExploreGet() {
|
||||
cy.intercept({
|
||||
method: 'GET',
|
||||
url: /.*\/api\/v1\/explore\/\?(form_data_key|dashboard_page_id|slice_id)=.*/,
|
||||
}).as('getExplore');
|
||||
}
|
||||
|
||||
export function setFilter(filter: string, option: string) {
|
||||
interceptFiltering();
|
||||
|
||||
cy.get(`[aria-label^="${filter}"]`).first().click();
|
||||
cy.get(`.ant-select-item-option[title="${option}"]`).first().click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.wait('@filtering');
|
||||
}
|
||||
|
||||
export function saveChartToDashboard(chartName: string, dashboardName: string) {
|
||||
interceptDashboardGet();
|
||||
interceptUpdate();
|
||||
|
||||
@@ -1,73 +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.
|
||||
*/
|
||||
import { selectResultsTab } from './sqllab.helper';
|
||||
|
||||
describe.skip('SqlLab datasource panel', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
// TODO the test below is flaky, and has been disabled for the time being
|
||||
// (notice the `it.skip`)
|
||||
it('creates a table preview when a database, schema, and table are selected', () => {
|
||||
cy.intercept('**/superset/table/**').as('tableMetadata');
|
||||
|
||||
// it should have dropdowns to select database, schema, and table
|
||||
cy.get('.sql-toolbar .Select').should('have.length', 3);
|
||||
|
||||
cy.get('.sql-toolbar .table-schema').should('not.exist');
|
||||
cy.get('[data-test="filterable-table-container"]').should('not.exist');
|
||||
|
||||
cy.get('.sql-toolbar .Select')
|
||||
.eq(0) // database select
|
||||
.within(() => {
|
||||
// note: we have to set force: true because the input is invisible / cypress throws
|
||||
cy.get('input').type('main{enter}', { force: true });
|
||||
});
|
||||
|
||||
cy.get('.sql-toolbar .Select')
|
||||
.eq(1) // schema select
|
||||
.within(() => {
|
||||
cy.get('input').type('main{enter}', { force: true });
|
||||
});
|
||||
|
||||
cy.get('.sql-toolbar .Select')
|
||||
.eq(2) // table select
|
||||
.within(() => {
|
||||
cy.get('input').type('birth_names{enter}', { force: true });
|
||||
});
|
||||
|
||||
cy.wait('@tableMetadata');
|
||||
|
||||
cy.get('.sql-toolbar .table-schema').should('have.length', 1);
|
||||
selectResultsTab().should('have.length', 1);
|
||||
|
||||
// add another table and check for added schema + preview
|
||||
cy.get('.sql-toolbar .Select')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.get('input').type('logs{enter}', { force: true });
|
||||
});
|
||||
|
||||
cy.wait('@tableMetadata');
|
||||
|
||||
cy.get('.sql-toolbar .table-schema').should('have.length', 2);
|
||||
selectResultsTab().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
@@ -1,192 +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.
|
||||
*/
|
||||
import { nanoid } from 'nanoid';
|
||||
import { selectResultsTab, assertSQLLabResultsAreEqual } from './sqllab.helper';
|
||||
|
||||
function parseClockStr(node: JQuery) {
|
||||
return Number.parseFloat(node.text().replace(/:/g, ''));
|
||||
}
|
||||
|
||||
describe('SqlLab query panel', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
it.skip('supports entering and running a query', () => {
|
||||
// row limit has to be < ~10 for us to be able to determine how many rows
|
||||
// are fetched below (because React _Virtualized_ does not render all rows)
|
||||
let clockTime = 0;
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '**/api/v1/sqllab/execute/',
|
||||
}).as('mockSQLResponse');
|
||||
|
||||
cy.get('.TableSelector .Select:eq(0)').click();
|
||||
cy.get('.TableSelector .Select:eq(0) input[type=text]').focus();
|
||||
cy.focused().type('{enter}');
|
||||
|
||||
cy.get('#brace-editor textarea').focus();
|
||||
cy.focused().clear();
|
||||
cy.focused().type(`{selectall}{backspace}SELECT 1`);
|
||||
|
||||
cy.get('#js-sql-toolbar button:eq(0)').eq(0).click();
|
||||
|
||||
// wait for 300 milliseconds
|
||||
cy.wait(300);
|
||||
|
||||
// started timer
|
||||
cy.get('.sql-toolbar .label-success').then(node => {
|
||||
clockTime = parseClockStr(node);
|
||||
// should be longer than 0.2s
|
||||
expect(clockTime).greaterThan(0.2);
|
||||
});
|
||||
|
||||
cy.wait('@mockSQLResponse');
|
||||
|
||||
// timer is increasing
|
||||
cy.get('.sql-toolbar .label-success').then(node => {
|
||||
const newClockTime = parseClockStr(node);
|
||||
expect(newClockTime).greaterThan(0.9);
|
||||
clockTime = newClockTime;
|
||||
});
|
||||
|
||||
// rerun the query
|
||||
cy.get('#js-sql-toolbar button:eq(0)').eq(0).click();
|
||||
|
||||
// should restart the timer
|
||||
cy.get('.sql-toolbar .label-success').contains('00:00:00');
|
||||
cy.wait('@mockSQLResponse');
|
||||
cy.get('.sql-toolbar .label-success').then(node => {
|
||||
expect(parseClockStr(node)).greaterThan(0.9);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('successfully saves a query', () => {
|
||||
cy.intercept('**/api/v1/database/**/tables/**').as('getTables');
|
||||
|
||||
const query =
|
||||
'SELECT ds, gender, name, num FROM main.birth_names ORDER BY name LIMIT 3';
|
||||
const savedQueryTitle = `CYPRESS TEST QUERY ${nanoid()}`;
|
||||
|
||||
// we will assert that the results of the query we save, and the saved query are the same
|
||||
let initialResultsTable: HTMLElement | null = null;
|
||||
let savedQueryResultsTable = null;
|
||||
|
||||
cy.get('#brace-editor textarea').clear({ force: true });
|
||||
cy.get('#brace-editor textarea').type(`{selectall}{backspace}${query}`, {
|
||||
force: true,
|
||||
});
|
||||
cy.get('#brace-editor textarea').focus(); // focus => blur is required for updating the query that is to be saved
|
||||
cy.focused().blur();
|
||||
|
||||
// ctrl + r also runs query
|
||||
cy.get('#brace-editor textarea').type('{ctrl}r', { force: true });
|
||||
|
||||
cy.wait('@sqlLabQuery');
|
||||
|
||||
// Save results to check against below
|
||||
selectResultsTab().then(resultsA => {
|
||||
initialResultsTable = resultsA[0];
|
||||
});
|
||||
|
||||
cy.get('#js-sql-toolbar button')
|
||||
.eq(1) // save query
|
||||
.click();
|
||||
|
||||
// Enter name + save into modal
|
||||
cy.get('.modal-sm input').clear({ force: true });
|
||||
cy.get('.modal-sm input').type(`{selectall}{backspace}${savedQueryTitle}`, {
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get('.modal-sm .modal-body button')
|
||||
.eq(0) // save
|
||||
.click();
|
||||
|
||||
// first row contains most recent link, follow back to SqlLab
|
||||
cy.get('table tr:first-child a[href*="savedQueryId"').click();
|
||||
|
||||
// will timeout without explicitly waiting here
|
||||
cy.wait(['@getSavedQuery', '@getTables']);
|
||||
|
||||
// run the saved query
|
||||
cy.get('#js-sql-toolbar button')
|
||||
.eq(0) // run query
|
||||
.click();
|
||||
|
||||
cy.wait('@sqlLabQuery');
|
||||
|
||||
// assert the results of the saved query match the initial results
|
||||
selectResultsTab().then(resultsB => {
|
||||
savedQueryResultsTable = resultsB[0];
|
||||
|
||||
assertSQLLabResultsAreEqual(initialResultsTable, savedQueryResultsTable);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('Create a chart from a query', () => {
|
||||
cy.intercept('**/api/v1/sqllab/execute/').as('queryFinished');
|
||||
cy.intercept('**/api/v1/explore/**').as('explore');
|
||||
cy.intercept('**/api/v1/chart/**').as('chart');
|
||||
cy.intercept('**/tabstateview/**').as('tabstateview');
|
||||
|
||||
// cypress doesn't handle opening a new tab, override window.open to open in the same tab
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'open', url => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
win.location.href = url;
|
||||
});
|
||||
});
|
||||
cy.wait('@tabstateview');
|
||||
|
||||
const query = 'SELECT gender, name FROM birth_names';
|
||||
|
||||
cy.get('.ace_text-input').focus();
|
||||
cy.focused().clear({ force: true });
|
||||
cy.focused().type(`{selectall}{backspace}${query}`, { force: true });
|
||||
cy.get('.sql-toolbar button').contains('Run').click();
|
||||
cy.wait('@queryFinished');
|
||||
|
||||
cy.get(
|
||||
'.SouthPane .ant-tabs-content > .ant-tabs-tabpane-active > div button:first',
|
||||
{ timeout: 10000 },
|
||||
).click();
|
||||
|
||||
cy.wait('@explore');
|
||||
cy.get('[data-test="datasource-control"] .title-select').contains(query);
|
||||
cy.get('.column-option-label').first().contains('gender');
|
||||
cy.get('.column-option-label').last().contains('name');
|
||||
|
||||
cy.get(
|
||||
'[data-test="all_columns"] [data-test="dnd-labels-container"] > div:first-child',
|
||||
).contains('gender');
|
||||
cy.get(
|
||||
'[data-test="all_columns"] [data-test="dnd-labels-container"] > div:nth-child(2)',
|
||||
).contains('name');
|
||||
|
||||
cy.wait('@chart');
|
||||
cy.get('[data-test="slice-container"] table > thead th')
|
||||
.first()
|
||||
.contains('gender');
|
||||
cy.get('[data-test="slice-container"] table > thead th')
|
||||
.last()
|
||||
.contains('name');
|
||||
});
|
||||
});
|
||||
@@ -1,41 +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.
|
||||
*/
|
||||
export const selectResultsTab = () =>
|
||||
cy.get('.SouthPane .ReactVirtualized__Table', { timeout: 10000 });
|
||||
|
||||
// this function asserts that the result set for two SQL lab table results are equal
|
||||
export const assertSQLLabResultsAreEqual = (resultsA, resultsB) => {
|
||||
const [headerA, bodyWrapperA] = resultsA.childNodes;
|
||||
const bodyA = bodyWrapperA.childNodes[0];
|
||||
|
||||
const [headerB, bodyWrapperB] = resultsB.childNodes;
|
||||
const bodyB = bodyWrapperB.childNodes[0];
|
||||
|
||||
expect(headerA.childNodes.length).to.equal(headerB.childNodes.length);
|
||||
expect(bodyA.childNodes.length).to.equal(bodyB.childNodes.length);
|
||||
|
||||
bodyA.childNodes.forEach((rowA, rowIndex) => {
|
||||
const rowB = bodyB.childNodes[rowIndex];
|
||||
|
||||
rowA.childNodes.forEach((cellA, columnIndex) => {
|
||||
const cellB = rowB.childNodes[columnIndex];
|
||||
expect(cellA.innerText).to.equal(cellB.innerText);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,113 +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.
|
||||
*/
|
||||
describe('SqlLab query tabs', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
const tablistSelector = '[data-test="sql-editor-tabs"] > [role="tablist"]';
|
||||
const tabSelector = `${tablistSelector} [role="tab"]:not([type="button"])`;
|
||||
|
||||
it('allows you to create and close a tab', () => {
|
||||
cy.get(tabSelector).then(tabs => {
|
||||
const initialTabCount = tabs.length;
|
||||
const initialUntitledCount = Math.max(
|
||||
0,
|
||||
...tabs
|
||||
.map(
|
||||
(i, tabItem) =>
|
||||
Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]) ||
|
||||
0,
|
||||
)
|
||||
.toArray(),
|
||||
);
|
||||
|
||||
// add two new tabs
|
||||
cy.get('[data-test="add-tab-icon"]:visible:last').click({ force: true });
|
||||
cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 1}`);
|
||||
cy.get(tabSelector).should('have.length', initialTabCount + 1);
|
||||
|
||||
cy.get('[data-test="add-tab-icon"]:visible:last').click({ force: true });
|
||||
cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 2}`);
|
||||
cy.get(tabSelector).should('have.length', initialTabCount + 2);
|
||||
|
||||
// close the tabs
|
||||
cy.get(`${tabSelector}:last [data-test="dropdown-trigger"]`).click({
|
||||
force: true,
|
||||
});
|
||||
cy.get('[data-test="close-tab-menu-option"]').click();
|
||||
cy.get(tabSelector).should('have.length', initialTabCount + 1);
|
||||
cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 1}`);
|
||||
|
||||
cy.get(`${tablistSelector} [aria-label="remove"]:last`).click();
|
||||
cy.get(tabSelector).should('have.length', initialTabCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens a new tab by a button and a shortcut', () => {
|
||||
const editorContent = '.ace_editor .ace_content';
|
||||
const editorInput = '.ace_editor textarea';
|
||||
const queryLimitSelector = '#js-sql-toolbar .limitDropdown';
|
||||
cy.get(tabSelector).then(tabs => {
|
||||
const initialTabCount = tabs.length;
|
||||
const initialUntitledCount = Math.max(
|
||||
0,
|
||||
...tabs
|
||||
.map(
|
||||
(i, tabItem) =>
|
||||
Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]) ||
|
||||
0,
|
||||
)
|
||||
.toArray(),
|
||||
);
|
||||
|
||||
// configure some editor settings
|
||||
cy.get(editorInput).type('some random query string', { force: true });
|
||||
cy.get(queryLimitSelector).parent().click({ force: true });
|
||||
cy.get('.ant-dropdown-menu')
|
||||
.last()
|
||||
.find('.ant-dropdown-menu-item')
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
// open a new tab by a button
|
||||
cy.get('[data-test="add-tab-icon"]:visible:last').click({ force: true });
|
||||
cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 1}`);
|
||||
cy.get(tabSelector).should('have.length', initialTabCount + 1);
|
||||
cy.get(editorContent).contains('SELECT ...');
|
||||
cy.get(queryLimitSelector).contains('10');
|
||||
|
||||
// close the tab
|
||||
cy.get(`${tabSelector}:last [data-test="dropdown-trigger"]`).click({
|
||||
force: true,
|
||||
});
|
||||
cy.get(`${tablistSelector} [aria-label="remove"]:last`).click({
|
||||
force: true,
|
||||
});
|
||||
cy.get(tabSelector).should('have.length', initialTabCount);
|
||||
|
||||
// open a new tab by a shortcut
|
||||
cy.get('body').type('{ctrl}t');
|
||||
cy.get(tabSelector).should('have.length', initialTabCount + 1);
|
||||
cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 1}`);
|
||||
cy.get(editorContent).contains('SELECT ...');
|
||||
cy.get(queryLimitSelector).contains('10');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,28 +25,6 @@ export interface ChartSpec {
|
||||
viz: string;
|
||||
}
|
||||
|
||||
const viewTypeIcons = {
|
||||
card: 'appstore',
|
||||
list: 'unordered-list',
|
||||
};
|
||||
|
||||
export function setGridMode(type: 'card' | 'list') {
|
||||
const icon = viewTypeIcons[type];
|
||||
cy.get(`[aria-label="${icon}"]`).click();
|
||||
}
|
||||
|
||||
export function toggleBulkSelect() {
|
||||
cy.getBySel('bulk-select').click();
|
||||
}
|
||||
|
||||
export function clearAllInputs() {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('.ant-select-clear').length) {
|
||||
cy.get('.ant-select-clear').click({ multiple: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const toSlicelike = ($chart: JQuery<HTMLElement>): Slice => {
|
||||
const chartId = $chart.attr('data-test-chart-id');
|
||||
const vizType = $chart.attr('data-test-viz-type');
|
||||
|
||||
@@ -25,8 +25,3 @@ export const SUPPORTED_CHARTS_DASHBOARD =
|
||||
'/superset/dashboard/supported_charts_dash/';
|
||||
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
|
||||
export const DATABASE_LIST = '/databaseview/list';
|
||||
export const DATASET_LIST_PATH = 'tablemodelview/list';
|
||||
export const ALERT_LIST = '/alert/list/';
|
||||
export const REPORT_LIST = '/report/list/';
|
||||
export const LOGIN = '/login/';
|
||||
export const REGISTER = '/register/';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user