mirror of
https://github.com/apache/superset.git
synced 2026-06-09 17:49:26 +00:00
Compare commits
360 Commits
enxdev/fix
...
enxdev/cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94e0071883 | ||
|
|
2f71771b56 | ||
|
|
d7ddf2023d | ||
|
|
c58408d76c | ||
|
|
1188cfef1d | ||
|
|
fb0e7fecaf | ||
|
|
3afbb48188 | ||
|
|
837f41986d | ||
|
|
8eda626466 | ||
|
|
fe9818226d | ||
|
|
1e8438a478 | ||
|
|
8fdabc44f5 | ||
|
|
e9e9245112 | ||
|
|
580be2cf32 | ||
|
|
911bb9dcda | ||
|
|
380e70060b | ||
|
|
507cf93687 | ||
|
|
ba6e9cc90f | ||
|
|
228ac0d568 | ||
|
|
c6ecaf9642 | ||
|
|
534d2191ff | ||
|
|
709fd52b0b | ||
|
|
c5d795c1f1 | ||
|
|
983f2818b0 | ||
|
|
b4eda37fbf | ||
|
|
a5fe47ee71 | ||
|
|
dc423b22b3 | ||
|
|
7c7ab88a60 | ||
|
|
21189ae130 | ||
|
|
06f95f5362 | ||
|
|
5da63d716b | ||
|
|
9bb700ff0d | ||
|
|
c0a12f4cfb | ||
|
|
138e405cb6 | ||
|
|
849f297e9d | ||
|
|
9da4536354 | ||
|
|
2463eb65b1 | ||
|
|
d3f07a7ba5 | ||
|
|
6348aa1917 | ||
|
|
ef7379c47e | ||
|
|
84aaaaa6b0 | ||
|
|
b85a2cdab1 | ||
|
|
381b99ae84 | ||
|
|
6b0d747939 | ||
|
|
151df43d9d | ||
|
|
3d7021fdf9 | ||
|
|
2babb48081 | ||
|
|
4715cfd372 | ||
|
|
5a6306983e | ||
|
|
7f452e4096 | ||
|
|
7eaaffde89 | ||
|
|
0984839788 | ||
|
|
863e93539a | ||
|
|
81bc3088e2 | ||
|
|
19d01521bf | ||
|
|
1623ceda73 | ||
|
|
e956f82224 | ||
|
|
2aca35cb68 | ||
|
|
44777cc110 | ||
|
|
20024ce3af | ||
|
|
b069b6caf6 | ||
|
|
70ee6e21eb | ||
|
|
550c80f640 | ||
|
|
108e40cbb6 | ||
|
|
8119204857 | ||
|
|
645aa3b1df | ||
|
|
55bb75efe6 | ||
|
|
601f9c2b8c | ||
|
|
fa42b13eb8 | ||
|
|
aa4092ba68 | ||
|
|
45a616439b | ||
|
|
98c096df05 | ||
|
|
42367afb25 | ||
|
|
875673f670 | ||
|
|
79c74af2e9 | ||
|
|
7406098708 | ||
|
|
ccce0cab18 | ||
|
|
94c1a1b1f2 | ||
|
|
04939c94cc | ||
|
|
937eff6d52 | ||
|
|
f5f4a41598 | ||
|
|
639866625d | ||
|
|
7d323dc0ae | ||
|
|
0d1b702ce8 | ||
|
|
ddeec68c88 | ||
|
|
0ad09d5cd0 | ||
|
|
6662529306 | ||
|
|
09cd2c26cd | ||
|
|
cbd731e661 | ||
|
|
3f94c9db2d | ||
|
|
80a3df3550 | ||
|
|
6f97d9817e | ||
|
|
7d69f76127 | ||
|
|
9a31362fa5 | ||
|
|
cd5bdf11ac | ||
|
|
75d94ff466 | ||
|
|
c505c70c52 | ||
|
|
23d18743bd | ||
|
|
ddb09f468d | ||
|
|
8dcc7e7eec | ||
|
|
ff5e43c8a0 | ||
|
|
bdb081329f | ||
|
|
aa547da960 | ||
|
|
966c243db6 | ||
|
|
696705794b | ||
|
|
41572dbf9d | ||
|
|
5ba60d51fd | ||
|
|
cf5307d0c6 | ||
|
|
9d1bc6b2cc | ||
|
|
6a125bf774 | ||
|
|
43fde2fb07 | ||
|
|
2be2246a00 | ||
|
|
80a5f6b787 | ||
|
|
c373da1bb9 | ||
|
|
80ea36c852 | ||
|
|
6ea4e22785 | ||
|
|
fcb1e299ac | ||
|
|
f4dfb7f026 | ||
|
|
001834470b | ||
|
|
e5c7200551 | ||
|
|
cb2a56d16e | ||
|
|
e5ff6de790 | ||
|
|
accc94da51 | ||
|
|
c914df5a67 | ||
|
|
e3ba85b1a5 | ||
|
|
b8a2f925ee | ||
|
|
77c2bed5f7 | ||
|
|
56fd991efd | ||
|
|
61b32d1b7d | ||
|
|
3191b0fdcd | ||
|
|
cf08a5ebf7 | ||
|
|
f7f50a7977 | ||
|
|
725f5ed2a9 | ||
|
|
faa76f6741 | ||
|
|
8e4a460cc7 | ||
|
|
b9dc9d722e | ||
|
|
fa41769a08 | ||
|
|
df21fe6571 | ||
|
|
12bef03f4a | ||
|
|
0b9764aed5 | ||
|
|
ac522ded1c | ||
|
|
c54990c861 | ||
|
|
3bbb35e8a3 | ||
|
|
a2a369cb5c | ||
|
|
9af6746dbe | ||
|
|
6abee0289b | ||
|
|
8c62f533d7 | ||
|
|
17d1a45bc9 | ||
|
|
6eaee211aa | ||
|
|
3e589436fa | ||
|
|
a9df2c7e5e | ||
|
|
8508af3201 | ||
|
|
49f3dbba73 | ||
|
|
616c243278 | ||
|
|
00dd31494d | ||
|
|
b97d3ef520 | ||
|
|
4d2b10d916 | ||
|
|
86fa5bb46f | ||
|
|
19c2b67d09 | ||
|
|
d2d46169bf | ||
|
|
1b8099811b | ||
|
|
242c27a974 | ||
|
|
24422c8311 | ||
|
|
1632b235ae | ||
|
|
093b43c7a5 | ||
|
|
4996d7c277 | ||
|
|
d26a7aac3d | ||
|
|
699e741c69 | ||
|
|
fc0245bdb0 | ||
|
|
7275116f4c | ||
|
|
88abd41c8b | ||
|
|
ddb647cd3a | ||
|
|
aba6ea536c | ||
|
|
ca8855dc03 | ||
|
|
052e567f77 | ||
|
|
e2ed989639 | ||
|
|
2abbb64e6b | ||
|
|
c6faa50338 | ||
|
|
817a35f445 | ||
|
|
a6d2c95480 | ||
|
|
c29591b3b1 | ||
|
|
365914f1c7 | ||
|
|
41da35e9db | ||
|
|
861e668f74 | ||
|
|
41059c68bb | ||
|
|
94092d2f72 | ||
|
|
986148d924 | ||
|
|
f04221a06c | ||
|
|
70aa96458a | ||
|
|
8beea84952 | ||
|
|
3f0fbbaac9 | ||
|
|
ce602fc5a8 | ||
|
|
8731974e5c | ||
|
|
a06eb8fc78 | ||
|
|
aa8b474c58 | ||
|
|
efdfefeea2 | ||
|
|
f77fa3ae39 | ||
|
|
bffc3fc58f | ||
|
|
2b8e31bf68 | ||
|
|
74d1c83ec5 | ||
|
|
1523d797ca | ||
|
|
97be689b5c | ||
|
|
dab628c13a | ||
|
|
e1bc8e7ae8 | ||
|
|
5312d0adf8 | ||
|
|
041ecbc248 | ||
|
|
a33fcb0edd | ||
|
|
316dd3db22 | ||
|
|
d52e28c564 | ||
|
|
a2eda11a81 | ||
|
|
63249c8c97 | ||
|
|
acc80242f0 | ||
|
|
a621520387 | ||
|
|
9a79588d35 | ||
|
|
7e8b8e25a5 | ||
|
|
ab5ea1f7d3 | ||
|
|
a340293fef | ||
|
|
469979714f | ||
|
|
585745695c | ||
|
|
c7bbfff475 | ||
|
|
8e47eb1cc1 | ||
|
|
afc4f3c9b3 | ||
|
|
35125d8521 | ||
|
|
ab66f0066a | ||
|
|
dfa4dbb96c | ||
|
|
f10a296ed0 | ||
|
|
f17e4de9cd | ||
|
|
87be424f9c | ||
|
|
4d95a8d034 | ||
|
|
2d6e68b5f2 | ||
|
|
2e7bec3646 | ||
|
|
f4787a4f25 | ||
|
|
fa4e571db5 | ||
|
|
838ee27c29 | ||
|
|
7f54b0b13d | ||
|
|
f165c3fa78 | ||
|
|
8c6271e9ff | ||
|
|
44a8d9d469 | ||
|
|
d350792d43 | ||
|
|
c9136af8b6 | ||
|
|
f0838353a5 | ||
|
|
16b56873b0 | ||
|
|
62b4ee3d9e | ||
|
|
43c3c06035 | ||
|
|
40de44f6de | ||
|
|
b8ea4448d6 | ||
|
|
8d8eeb3505 | ||
|
|
a69bbcb044 | ||
|
|
f614863ed7 | ||
|
|
53e2793bc3 | ||
|
|
e73d2d0bf6 | ||
|
|
ae5823fb9c | ||
|
|
63e3a18e8f | ||
|
|
661ff31c6d | ||
|
|
ead21d9789 | ||
|
|
c3bda6baea | ||
|
|
61cb0aeae7 | ||
|
|
cf9ee99b5a | ||
|
|
e1948c87c6 | ||
|
|
63ae80ab62 | ||
|
|
8f88bbcc79 | ||
|
|
b2320820b4 | ||
|
|
8853ab5c75 | ||
|
|
b0da0cf202 | ||
|
|
96b96ad7d4 | ||
|
|
f7e1f96894 | ||
|
|
ec09cec6bd | ||
|
|
f663f47628 | ||
|
|
8a0026b173 | ||
|
|
f037449b75 | ||
|
|
e68251fa70 | ||
|
|
0dc58d1042 | ||
|
|
c73106b7a2 | ||
|
|
9441240e5c | ||
|
|
08164e33bb | ||
|
|
894058fe3d | ||
|
|
6bd1b46216 | ||
|
|
ef4514f5ab | ||
|
|
e041f25385 | ||
|
|
d744f5715c | ||
|
|
fb60662353 | ||
|
|
207a7bf7f9 | ||
|
|
09a94fa26b | ||
|
|
7e088792b9 | ||
|
|
b6f545e61e | ||
|
|
952a6f3a23 | ||
|
|
8b551d3f74 | ||
|
|
709ef9b615 | ||
|
|
e9d46d843f | ||
|
|
9cc2deb903 | ||
|
|
03d25277ba | ||
|
|
bbe2f207d2 | ||
|
|
c381677dfd | ||
|
|
09572cd5ef | ||
|
|
33585b0480 | ||
|
|
b64561f3a3 | ||
|
|
fe484f6bb2 | ||
|
|
8caa74354f | ||
|
|
9c90a6854c | ||
|
|
2fef4e41f2 | ||
|
|
965ec47296 | ||
|
|
b21450681d | ||
|
|
5003ee1499 | ||
|
|
816794b198 | ||
|
|
841871f1e7 | ||
|
|
55203bbc74 | ||
|
|
2fa3bbd91c | ||
|
|
168b49bf34 | ||
|
|
838ac8f553 | ||
|
|
42668cf634 | ||
|
|
8d985d223b | ||
|
|
e57387098b | ||
|
|
af6ac4d09c | ||
|
|
f8e13770fc | ||
|
|
5cdd542ae5 | ||
|
|
e40648dfcb | ||
|
|
c728b4a11f | ||
|
|
0febe32dc9 | ||
|
|
2a0ebd7055 | ||
|
|
6e23e4541d | ||
|
|
5af8fe77fa | ||
|
|
3599c78a03 | ||
|
|
d97b5d6509 | ||
|
|
869ab37f59 | ||
|
|
3b4892c48c | ||
|
|
91d96419fe | ||
|
|
42149f6a78 | ||
|
|
c945ef6763 | ||
|
|
21059b54f0 | ||
|
|
8ab4695ba3 | ||
|
|
b0d26196fc | ||
|
|
df8222ffcd | ||
|
|
64f0e88de7 | ||
|
|
f4af6a2caf | ||
|
|
31087177ab | ||
|
|
8e98ca6569 | ||
|
|
f7f6c29adf | ||
|
|
a94edfe418 | ||
|
|
5549100601 | ||
|
|
558ff4452b | ||
|
|
5966bb1c1e | ||
|
|
ac035083d7 | ||
|
|
e25d708197 | ||
|
|
48cb3f5885 | ||
|
|
dcef6f8a41 | ||
|
|
f09fd63495 | ||
|
|
bc26006a43 | ||
|
|
8b483f320e | ||
|
|
89c2a47433 | ||
|
|
b8b91574e0 | ||
|
|
5526464def | ||
|
|
73f66e4c14 | ||
|
|
f187a8e1c4 | ||
|
|
4c3f65ef0b | ||
|
|
53d8e5bdfa | ||
|
|
2f95d288dd | ||
|
|
2f5fcc21f9 | ||
|
|
d1d07112aa | ||
|
|
e3711bec39 | ||
|
|
ce9cab098f |
14
.asf.yaml
14
.asf.yaml
@@ -77,23 +77,17 @@ github:
|
||||
# combination here.
|
||||
contexts:
|
||||
- lint-check
|
||||
- cypress-matrix (0, chrome)
|
||||
- cypress-matrix (1, chrome)
|
||||
- cypress-matrix (2, chrome)
|
||||
- cypress-matrix (3, chrome)
|
||||
- cypress-matrix (4, chrome)
|
||||
- cypress-matrix (5, chrome)
|
||||
- cypress-matrix-required
|
||||
- dependency-review
|
||||
- frontend-build
|
||||
- playwright-tests (chromium)
|
||||
- playwright-tests-required
|
||||
- pre-commit (current)
|
||||
- pre-commit (previous)
|
||||
- test-mysql
|
||||
- test-postgres (current)
|
||||
- test-postgres-required
|
||||
- test-postgres-hive
|
||||
- test-postgres-presto
|
||||
- test-sqlite
|
||||
- unit-tests (current)
|
||||
- unit-tests-required
|
||||
|
||||
required_pull_request_reviews:
|
||||
dismiss_stale_reviews: false
|
||||
|
||||
67
.github/SECURITY.md
vendored
67
.github/SECURITY.md
vendored
@@ -1,67 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
This is a project of the [Apache Software Foundation](https://apache.org) and follows the
|
||||
ASF [vulnerability handling process](https://apache.org/security/#vulnerability-handling).
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
**⚠️ Please do not file GitHub issues for security vulnerabilities as they are public! ⚠️**
|
||||
|
||||
|
||||
Apache Software Foundation takes a rigorous standpoint in annihilating the security issues
|
||||
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
|
||||
pertaining to its features and functionality.
|
||||
If you have any concern or believe you have found a vulnerability in Apache Superset,
|
||||
please get in touch with the Apache Superset Security Team privately at
|
||||
e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
|
||||
|
||||
More details can be found on the ASF website at
|
||||
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-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
|
||||
reported to the maintainers of those projects. Results from security scans of Apache
|
||||
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
|
||||
|
||||
- [Apache Superset documentation](https://superset.apache.org/docs/security)
|
||||
- [Common Vulnerabilities and Exposures by release](https://superset.apache.org/docs/security/cves)
|
||||
- [How Security Vulnerabilities are Reported & Handled in Apache Superset (Blog)](https://preset.io/blog/how-security-vulnerabilities-are-reported-and-handled-in-apache-superset/)
|
||||
35
.github/actions/setup-backend/action.yml
vendored
35
.github/actions/setup-backend/action.yml
vendored
@@ -24,32 +24,41 @@ runs:
|
||||
- name: Interpret Python Version
|
||||
id: set-python-version
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_PYTHON_VERSION: ${{ inputs.python-version }}
|
||||
run: |
|
||||
if [ "${{ inputs.python-version }}" = "current" ]; then
|
||||
echo "PYTHON_VERSION=3.11" >> $GITHUB_ENV
|
||||
elif [ "${{ inputs.python-version }}" = "next" ]; then
|
||||
if [ "$INPUT_PYTHON_VERSION" = "current" ]; then
|
||||
RESOLVED_VERSION="3.11"
|
||||
elif [ "$INPUT_PYTHON_VERSION" = "next" ]; then
|
||||
# currently disabled in GHA matrixes because of library compatibility issues
|
||||
echo "PYTHON_VERSION=3.12" >> $GITHUB_ENV
|
||||
elif [ "${{ inputs.python-version }}" = "previous" ]; then
|
||||
echo "PYTHON_VERSION=3.10" >> $GITHUB_ENV
|
||||
RESOLVED_VERSION="3.12"
|
||||
elif [ "$INPUT_PYTHON_VERSION" = "previous" ]; then
|
||||
RESOLVED_VERSION="3.10"
|
||||
elif printf '%s' "$INPUT_PYTHON_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
|
||||
RESOLVED_VERSION="$INPUT_PYTHON_VERSION"
|
||||
else
|
||||
echo "PYTHON_VERSION=${{ inputs.python-version }}" >> $GITHUB_ENV
|
||||
echo "Invalid python-version: '$INPUT_PYTHON_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
python-version: ${{ steps.set-python-version.outputs.python-version }}
|
||||
cache: ${{ inputs.cache }}
|
||||
- name: Install dependencies
|
||||
env:
|
||||
INPUT_INSTALL_SUPERSET: ${{ inputs.install-superset }}
|
||||
INPUT_REQUIREMENTS_TYPE: ${{ inputs.requirements-type }}
|
||||
run: |
|
||||
if [ "${{ inputs.install-superset }}" = "true" ]; then
|
||||
if [ "$INPUT_INSTALL_SUPERSET" = "true" ]; then
|
||||
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev
|
||||
|
||||
pip install --upgrade pip setuptools wheel uv
|
||||
|
||||
if [ "${{ inputs.requirements-type }}" = "dev" ]; then
|
||||
if [ "$INPUT_REQUIREMENTS_TYPE" = "dev" ]; then
|
||||
uv pip install --system -r requirements/development.txt
|
||||
elif [ "${{ inputs.requirements-type }}" = "base" ]; then
|
||||
elif [ "$INPUT_REQUIREMENTS_TYPE" = "base" ]; then
|
||||
uv pip install --system -r requirements/base.txt
|
||||
fi
|
||||
|
||||
|
||||
15
.github/actions/setup-docker/action.yml
vendored
15
.github/actions/setup-docker/action.yml
vendored
@@ -26,16 +26,25 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ inputs.build == 'true' }}
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
with:
|
||||
# Pin the binfmt image to a specific QEMU release. The default
|
||||
# (`tonistiigi/binfmt:latest`) is a moving target, and drift across
|
||||
# QEMU's x86_64→aarch64 translator has been the proximate cause of
|
||||
# intermittent `exit code: 132` (SIGILL) failures during the arm64
|
||||
# leg of the multi-platform docker build — newer Node native modules
|
||||
# emit instructions QEMU's user-mode emulation occasionally drops on
|
||||
# the floor. Pinning a known-good release stabilises that path.
|
||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ inputs.build == 'true' }}
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Try to login to DockerHub
|
||||
if: ${{ inputs.login-to-dockerhub == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ inputs.dockerhub-user }}
|
||||
password: ${{ inputs.dockerhub-token }}
|
||||
|
||||
5
.github/actions/setup-supersetbot/action.yml
vendored
5
.github/actions/setup-supersetbot/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
steps:
|
||||
|
||||
- name: Setup Node Env
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -21,8 +21,9 @@ runs:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
if: ${{ inputs.from-npm == 'false' }}
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: apache-superset/supersetbot
|
||||
path: supersetbot
|
||||
|
||||
|
||||
61
.github/dependabot.yml
vendored
61
.github/dependabot.yml
vendored
@@ -1,7 +1,6 @@
|
||||
version: 2
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
ignore:
|
||||
@@ -10,6 +9,8 @@ updates:
|
||||
- dependency-name: anthropics/claude-code-action
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
@@ -57,6 +58,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 30
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
@@ -72,6 +75,8 @@ updates:
|
||||
labels:
|
||||
- pip
|
||||
- dependabot
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: ".github/actions"
|
||||
@@ -79,6 +84,8 @@ updates:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs/"
|
||||
@@ -102,6 +109,8 @@ updates:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-websocket/"
|
||||
@@ -111,6 +120,8 @@ updates:
|
||||
- npm
|
||||
- dependabot
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-websocket/utils/client-ws-app/"
|
||||
@@ -121,6 +132,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Now for all of our plugins and packages!
|
||||
|
||||
@@ -133,6 +146,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
|
||||
@@ -143,6 +158,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
|
||||
@@ -153,6 +170,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
|
||||
@@ -166,6 +185,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
|
||||
@@ -176,6 +197,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
|
||||
@@ -186,6 +209,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
|
||||
@@ -196,6 +221,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
|
||||
@@ -206,6 +233,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-table/"
|
||||
@@ -219,6 +248,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
|
||||
@@ -229,6 +260,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
|
||||
@@ -239,6 +272,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
|
||||
@@ -249,6 +284,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
|
||||
@@ -259,6 +296,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
|
||||
@@ -269,6 +308,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
|
||||
@@ -279,6 +320,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
|
||||
@@ -289,6 +332,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
|
||||
@@ -299,6 +344,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
|
||||
@@ -309,6 +356,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
|
||||
@@ -323,6 +372,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/generator-superset/"
|
||||
@@ -333,6 +384,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
|
||||
@@ -343,6 +396,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-core/"
|
||||
@@ -358,6 +413,8 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-switchboard/"
|
||||
@@ -368,3 +425,5 @@ updates:
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
143
.github/workflows/bashlib.sh
vendored
143
.github/workflows/bashlib.sh
vendored
@@ -20,10 +20,6 @@ set -e
|
||||
GITHUB_WORKSPACE=${GITHUB_WORKSPACE:-.}
|
||||
ASSETS_MANIFEST="$GITHUB_WORKSPACE/superset/static/assets/manifest.json"
|
||||
|
||||
# Rounded job start time, used to create a unique Cypress build id for
|
||||
# parallelization so we can manually rerun a job after 20 minutes
|
||||
NONCE=$(echo "$(date "+%Y%m%d%H%M") - ($(date +%M)%20)" | bc)
|
||||
|
||||
# Echo only when not in parallel mode
|
||||
say() {
|
||||
if [[ $(echo "$INPUT_PARALLEL" | tr '[:lower:]' '[:upper:]') != 'TRUE' ]]; then
|
||||
@@ -59,6 +55,15 @@ build-assets() {
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
build-embedded-sdk() {
|
||||
cd "$GITHUB_WORKSPACE/superset-embedded-sdk"
|
||||
|
||||
say "::group::Build embedded SDK bundle for E2E tests"
|
||||
npm ci
|
||||
npm run build
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
build-instrumented-assets() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend"
|
||||
|
||||
@@ -175,10 +180,13 @@ cypress-run-all() {
|
||||
local APP_ROOT=$2
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
|
||||
|
||||
# Start Flask and run it in background
|
||||
# --no-debugger means disable the interactive debugger on the 500 page
|
||||
# so errors can print to stderr.
|
||||
local flasklog="${HOME}/flask.log"
|
||||
# Start the Superset backend via gunicorn (not `flask run`). The Flask
|
||||
# development server is single-threaded and has no crash-recovery, so
|
||||
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
|
||||
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
|
||||
# / `Missing CSRF token` cascades. Gunicorn gives us multiple workers,
|
||||
# a request timeout, and worker-recycling under load.
|
||||
local serverlog="${HOME}/superset-cypress.log"
|
||||
local port=8081
|
||||
CYPRESS_BASE_URL="http://localhost:${port}"
|
||||
if [ -n "$APP_ROOT" ]; then
|
||||
@@ -187,8 +195,58 @@ cypress-run-all() {
|
||||
fi
|
||||
export CYPRESS_BASE_URL
|
||||
|
||||
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
|
||||
local flaskProcessId=$!
|
||||
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
|
||||
# gthread threads) to keep parity with production. Multi-worker
|
||||
# configurations expose timing-sensitive races in the SQL Lab → Explore
|
||||
# navigation flow under E2E. We diverge from the entrypoint on:
|
||||
# --timeout 120: heavy dashboard import/export specs exceed the 60s
|
||||
# default
|
||||
# --max-requests / --max-requests-jitter: recycle the worker under
|
||||
# test load to avoid leaks accumulating across the run
|
||||
# superset.app:create_app(): explicit factory so we don't depend on
|
||||
# FLASK_APP being exported
|
||||
nohup gunicorn \
|
||||
--bind "127.0.0.1:$port" \
|
||||
--workers 1 \
|
||||
--worker-class gthread \
|
||||
--threads 20 \
|
||||
--timeout 120 \
|
||||
--max-requests 500 \
|
||||
--max-requests-jitter 50 \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
"superset.app:create_app()" \
|
||||
>"$serverlog" 2>&1 </dev/null &
|
||||
local serverPid=$!
|
||||
|
||||
# Ensure the backend is cleaned up and its log is emitted even when the
|
||||
# test runner fails under `set -e`.
|
||||
trap '
|
||||
echo "::group::gunicorn log for Cypress run"
|
||||
cat "'"$serverlog"'" || true
|
||||
echo "::endgroup::"
|
||||
kill '"$serverPid"' 2>/dev/null || true
|
||||
' EXIT
|
||||
|
||||
# Wait for the backend to be ready before launching Cypress; otherwise
|
||||
# the first spec can race the server bind and see connection errors.
|
||||
local timeout=60
|
||||
say "Waiting for gunicorn server to start on port $port..."
|
||||
while [ $timeout -gt 0 ]; do
|
||||
if curl -f "http://localhost:${port}${APP_ROOT}/health" >/dev/null 2>&1; then
|
||||
say "gunicorn server is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "::error::gunicorn server failed to start within 60 seconds"
|
||||
echo "::group::Server startup log"
|
||||
cat "$serverlog"
|
||||
echo "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
|
||||
USE_DASHBOARD_FLAG=''
|
||||
if [ "$USE_DASHBOARD" = "true" ]; then
|
||||
@@ -200,13 +258,6 @@ cypress-run-all() {
|
||||
# memoryMonitorPid=$!
|
||||
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID --group $PARALLEL_ID --retries 5 $USE_DASHBOARD_FLAG
|
||||
# kill $memoryMonitorPid
|
||||
|
||||
# After job is done, print out Flask log for debugging
|
||||
echo "::group::Flask log for default run"
|
||||
cat "$flasklog"
|
||||
echo "::endgroup::"
|
||||
# make sure the program exits
|
||||
kill $flaskProcessId
|
||||
}
|
||||
|
||||
playwright-install() {
|
||||
@@ -224,29 +275,55 @@ playwright-run() {
|
||||
local APP_ROOT=$1
|
||||
local TEST_PATH=$2
|
||||
|
||||
# Start Flask from the project root (same as Cypress)
|
||||
# Start the Superset backend via gunicorn from the project root.
|
||||
# See cypress-run-all() above for the rationale — the Flask dev server
|
||||
# cannot survive the dashboard import/export tests under load.
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
local flasklog="${HOME}/flask-playwright.log"
|
||||
local serverlog="${HOME}/superset-playwright.log"
|
||||
local port=8081
|
||||
PLAYWRIGHT_BASE_URL="http://localhost:${port}"
|
||||
# Use 127.0.0.1 explicitly: `flask run` binds IPv4 only, and Node's DNS
|
||||
# resolution for `localhost` can return `::1` first (IPv6), which then
|
||||
# refuses against the IPv4 listener and surfaces as
|
||||
# `connect ECONNREFUSED ::1:<port>` in API helpers driven from Node
|
||||
# (e.g., the embedded test app's exposed token fetcher).
|
||||
PLAYWRIGHT_BASE_URL="http://127.0.0.1:${port}"
|
||||
if [ -n "$APP_ROOT" ]; then
|
||||
export SUPERSET_APP_ROOT=$APP_ROOT
|
||||
PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL}${APP_ROOT}/
|
||||
fi
|
||||
export PLAYWRIGHT_BASE_URL
|
||||
|
||||
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
|
||||
local flaskProcessId=$!
|
||||
# See cypress-run-all() above for the args rationale (1 worker × 20
|
||||
# gthread threads matching docker/entrypoints/run-server.sh, plus a
|
||||
# 120s timeout and request-recycling for heavy E2E load).
|
||||
nohup gunicorn \
|
||||
--bind "127.0.0.1:$port" \
|
||||
--workers 1 \
|
||||
--worker-class gthread \
|
||||
--threads 20 \
|
||||
--timeout 120 \
|
||||
--max-requests 500 \
|
||||
--max-requests-jitter 50 \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
"superset.app:create_app()" \
|
||||
>"$serverlog" 2>&1 </dev/null &
|
||||
local serverPid=$!
|
||||
|
||||
# Ensure cleanup on exit
|
||||
trap "kill $flaskProcessId 2>/dev/null || true" EXIT
|
||||
# Ensure cleanup on exit (and emit the server log on failure)
|
||||
trap '
|
||||
echo "::group::gunicorn log for Playwright run"
|
||||
cat "'"$serverlog"'" || true
|
||||
echo "::endgroup::"
|
||||
kill '"$serverPid"' 2>/dev/null || true
|
||||
' EXIT
|
||||
|
||||
# Wait for server to be ready with health check
|
||||
local timeout=60
|
||||
say "Waiting for Flask server to start on port $port..."
|
||||
say "Waiting for gunicorn server to start on port $port..."
|
||||
while [ $timeout -gt 0 ]; do
|
||||
if curl -f ${PLAYWRIGHT_BASE_URL}/health >/dev/null 2>&1; then
|
||||
say "Flask server is ready"
|
||||
say "gunicorn server is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
@@ -254,9 +331,9 @@ playwright-run() {
|
||||
done
|
||||
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "::error::Flask server failed to start within 60 seconds"
|
||||
echo "::group::Flask startup log"
|
||||
cat "$flasklog"
|
||||
echo "::error::gunicorn server failed to start within 60 seconds"
|
||||
echo "::group::Server startup log"
|
||||
cat "$serverlog"
|
||||
echo "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
@@ -271,7 +348,6 @@ playwright-run() {
|
||||
if ! find "playwright/tests/${TEST_PATH}" -name "*.spec.ts" -type f 2>/dev/null | grep -q .; then
|
||||
echo "No test files found in ${TEST_PATH} - skipping test run"
|
||||
say "::endgroup::"
|
||||
kill $flaskProcessId
|
||||
return 0
|
||||
fi
|
||||
echo "Running tests: ${TEST_PATH}"
|
||||
@@ -288,13 +364,6 @@ playwright-run() {
|
||||
fi
|
||||
say "::endgroup::"
|
||||
|
||||
# After job is done, print out Flask log for debugging
|
||||
echo "::group::Flask log for Playwright run"
|
||||
cat "$flasklog"
|
||||
echo "::endgroup::"
|
||||
# make sure the program exits
|
||||
kill $flaskProcessId
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
|
||||
43
.github/workflows/cancel_duplicates.yml
vendored
43
.github/workflows/cancel_duplicates.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Cancel Duplicates
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Miscellaneous"
|
||||
types:
|
||||
- requested
|
||||
|
||||
jobs:
|
||||
cancel-duplicate-runs:
|
||||
name: Cancel duplicate workflow runs
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check number of queued tasks
|
||||
id: check_queued
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
get_count() {
|
||||
echo $(curl -s -H "Authorization: token $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/$GITHUB_REPO/actions/runs?status=$1" | \
|
||||
jq ".total_count")
|
||||
}
|
||||
count=$(( `get_count queued` + `get_count in_progress` ))
|
||||
echo "Found $count unfinished jobs."
|
||||
echo "count=$count" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
if: steps.check_queued.outputs.count >= 20
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Cancel duplicate workflow runs
|
||||
if: steps.check_queued.outputs.count >= 20
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
pip install click requests typing_extensions python-dateutil
|
||||
python ./scripts/cancel_github_workflows.py
|
||||
13
.github/workflows/check-python-deps.yml
vendored
13
.github/workflows/check-python-deps.yml
vendored
@@ -38,6 +38,19 @@ jobs:
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
|
||||
# Authenticate the Docker daemon so the python:slim pull in
|
||||
# uv-pip-compile.sh uses our (much higher) authenticated rate limit
|
||||
# instead of the shared-runner anonymous one. Best-effort: on fork PRs the
|
||||
# secrets are unavailable, so this no-ops and the pull falls back to
|
||||
# anonymous (covered by the retry loop in the script).
|
||||
- name: Login to Docker Hub
|
||||
if: steps.check.outputs.python
|
||||
continue-on-error: true
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Run uv
|
||||
if: steps.check.outputs.python
|
||||
run: ./scripts/uv-pip-compile.sh
|
||||
|
||||
@@ -26,6 +26,8 @@ jobs:
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check and notify
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
|
||||
4
.github/workflows/claude.yml
vendored
4
.github/workflows/claude.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
if: |
|
||||
@@ -75,6 +78,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
|
||||
39
.github/workflows/codeql-analysis.yml
vendored
39
.github/workflows/codeql-analysis.yml
vendored
@@ -15,9 +15,35 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
analyze:
|
||||
name: Analyze
|
||||
needs: changes
|
||||
# Skip on PRs that touch neither code group (e.g. docs-only) so the
|
||||
# analysis runners don't spin up. push/schedule runs always proceed:
|
||||
# the change-detector returns "all changed" for non-PR events.
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -31,17 +57,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
persist-credentials: false
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -52,7 +74,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -28,6 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
continue-on-error: true
|
||||
@@ -50,6 +52,8 @@ jobs:
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
|
||||
71
.github/workflows/docker.yml
vendored
71
.github/workflows/docker.yml
vendored
@@ -19,8 +19,30 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
docker: ${{ steps.check.outputs.docker }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
setup_matrix:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
matrix_config: ${{ steps.set_matrix.outputs.matrix_config }}
|
||||
steps:
|
||||
@@ -32,8 +54,13 @@ jobs:
|
||||
|
||||
docker-build:
|
||||
name: docker-build
|
||||
needs: setup_matrix
|
||||
needs: [setup_matrix, changes]
|
||||
if: >-
|
||||
needs.changes.outputs.python == 'true' ||
|
||||
needs.changes.outputs.frontend == 'true' ||
|
||||
needs.changes.outputs.docker == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
build_preset: ${{fromJson(needs.setup_matrix.outputs.matrix_config)}}
|
||||
@@ -50,14 +77,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Docker Environment
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
|
||||
uses: ./.github/actions/setup-docker
|
||||
with:
|
||||
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
|
||||
@@ -65,28 +85,27 @@ jobs:
|
||||
build: "true"
|
||||
|
||||
- name: Setup supersetbot
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
|
||||
- name: Build Docker Image
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_PRESET: ${{ matrix.build_preset }}
|
||||
run: |
|
||||
# Single platform builds in pull_request context to speed things up
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
|
||||
PLATFORM_ARG="--platform linux/arm64 --platform linux/amd64"
|
||||
# can only --load images in single-platform builds
|
||||
PUSH_OR_LOAD="--push"
|
||||
elif [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
||||
PLATFORM_ARG="--platform linux/amd64"
|
||||
PUSH_OR_LOAD="--load"
|
||||
fi
|
||||
|
||||
supersetbot docker \
|
||||
$PUSH_OR_LOAD \
|
||||
--preset ${{ matrix.build_preset }} \
|
||||
--preset "$BUILD_PRESET" \
|
||||
--context "$EVENT" \
|
||||
--context-ref "$RELEASE" $FORCE_LATEST \
|
||||
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
|
||||
@@ -94,11 +113,14 @@ 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: docker pull $IMAGE_TAG
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
for i in 1 2 3; do
|
||||
docker pull $IMAGE_TAG && break
|
||||
[ $i -lt 3 ] && sleep 30
|
||||
done
|
||||
|
||||
- name: Print docker stats
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
|
||||
run: |
|
||||
echo "SHA: ${{ github.sha }}"
|
||||
echo "IMAGE: $IMAGE_TAG"
|
||||
@@ -106,10 +128,12 @@ jobs:
|
||||
docker history $IMAGE_TAG
|
||||
|
||||
- name: docker-compose sanity check
|
||||
if: (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'dev'
|
||||
if: matrix.build_preset == 'dev'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_PRESET: ${{ matrix.build_preset }}
|
||||
run: |
|
||||
export SUPERSET_BUILD_TARGET=${{ matrix.build_preset }}
|
||||
export SUPERSET_BUILD_TARGET=$BUILD_PRESET
|
||||
# This should reuse the CACHED image built in the previous steps
|
||||
docker compose build superset-init --build-arg DEV_MODE=false --build-arg INCLUDE_CHROMIUM=false
|
||||
docker compose up superset-init --exit-code-from superset-init
|
||||
@@ -117,20 +141,16 @@ jobs:
|
||||
docker-compose-image-tag:
|
||||
# Run this job only on pushes to master (not for PRs)
|
||||
# goal is to check that building the latest image works, not required for all PR pushes
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: changes
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.changes.outputs.docker == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Docker Environment
|
||||
if: steps.check.outputs.docker
|
||||
uses: ./.github/actions/setup-docker
|
||||
with:
|
||||
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
|
||||
@@ -138,7 +158,6 @@ jobs:
|
||||
build: "false"
|
||||
install-docker-compose: "true"
|
||||
- name: docker-compose sanity check
|
||||
if: steps.check.outputs.docker
|
||||
shell: bash
|
||||
run: |
|
||||
docker compose -f docker-compose-image-tag.yml up superset-init --exit-code-from superset-init
|
||||
|
||||
2
.github/workflows/embedded-sdk-release.yml
vendored
2
.github/workflows/embedded-sdk-release.yml
vendored
@@ -34,6 +34,8 @@ jobs:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
|
||||
2
.github/workflows/embedded-sdk-test.yml
vendored
2
.github/workflows/embedded-sdk-test.yml
vendored
@@ -22,6 +22,8 @@ jobs:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
|
||||
83
.github/workflows/ephemeral-env-pr-close.yml
vendored
83
.github/workflows/ephemeral-env-pr-close.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: Cleanup ephemeral envs (PR close) [DEPRECATED]
|
||||
|
||||
# ⚠️ DEPRECATION NOTICE ⚠️
|
||||
# This workflow is deprecated and will be removed in a future version.
|
||||
# The new Superset Showtime workflow handles cleanup automatically.
|
||||
# See .github/workflows/showtime.yml and showtime-cleanup.yml for replacements.
|
||||
# Migration guide: https://github.com/mistercrunch/superset-showtime
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
- name: "Check for secrets"
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
name: Cleanup ephemeral envs
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
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 }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Describe ECS service
|
||||
id: describe-services
|
||||
run: |
|
||||
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Delete ECS service
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
id: delete-service
|
||||
run: |
|
||||
aws ecs delete-service \
|
||||
--cluster superset-ci \
|
||||
--service pr-${{ github.event.number }}-service \
|
||||
--force
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
|
||||
|
||||
- name: Delete ECR image tag
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
id: delete-image-tag
|
||||
run: |
|
||||
aws ecr batch-delete-image \
|
||||
--registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
|
||||
--repository-name superset-ci \
|
||||
--image-ids imageTag=pr-${{ github.event.number }}
|
||||
|
||||
- name: Comment (success)
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: ${{ github.event.number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '⚠️ **DEPRECATED WORKFLOW** - Ephemeral environment shutdown and build artifacts deleted. Please migrate to the new Superset Showtime system for future PRs.'
|
||||
})
|
||||
350
.github/workflows/ephemeral-env.yml
vendored
350
.github/workflows/ephemeral-env.yml
vendored
@@ -1,350 +0,0 @@
|
||||
name: Ephemeral env workflow [DEPRECATED]
|
||||
|
||||
# ⚠️ DEPRECATION NOTICE ⚠️
|
||||
# This workflow is deprecated and will be removed in a future version.
|
||||
# Please use the new Superset Showtime workflow instead:
|
||||
# - Use label "🎪 trigger-start" instead of "testenv-up"
|
||||
# - Showtime provides better reliability and easier management
|
||||
# - See .github/workflows/showtime.yml for the replacement
|
||||
# - Migration guide: https://github.com/mistercrunch/superset-showtime
|
||||
|
||||
# Example manual trigger:
|
||||
# gh workflow run ephemeral-env.yml --ref fix_ephemerals --field label_name="testenv-up" --field issue_number=666
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
label_name:
|
||||
description: 'Label name to simulate label-based /testenv trigger'
|
||||
required: true
|
||||
default: 'testenv-up'
|
||||
issue_number:
|
||||
description: 'Issue or PR number'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
ephemeral-env-label:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-label
|
||||
cancel-in-progress: true
|
||||
name: Evaluate ephemeral env label trigger
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
slash-command: ${{ steps.eval-label.outputs.result }}
|
||||
feature-flags: ${{ steps.eval-feature-flags.outputs.result }}
|
||||
sha: ${{ steps.get-sha.outputs.sha }}
|
||||
env:
|
||||
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Check for the "testenv-up" label
|
||||
id: eval-label
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
LABEL_NAME="${INPUT_LABEL_NAME}"
|
||||
else
|
||||
LABEL_NAME="${{ github.event.label.name }}"
|
||||
fi
|
||||
|
||||
echo "Evaluating label: $LABEL_NAME"
|
||||
|
||||
if [[ "$LABEL_NAME" == "testenv-up" ]]; then
|
||||
echo "result=up" >> $GITHUB_OUTPUT
|
||||
else
|
||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
let prSha;
|
||||
|
||||
// If event is workflow_dispatch, use the issue_number from inputs
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
const prNumber = "${{ github.event.inputs.issue_number }}";
|
||||
if (!prNumber) {
|
||||
console.log("No PR number found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch PR details using the provided issue_number
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
prSha = pr.head.sha;
|
||||
} else {
|
||||
// If it's not workflow_dispatch, use the PR head sha from the event
|
||||
prSha = context.payload.pull_request.head.sha;
|
||||
}
|
||||
|
||||
console.log(`PR SHA: ${prSha}`);
|
||||
core.setOutput("sha", prSha);
|
||||
|
||||
- name: Looking for feature flags in PR description
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
id: eval-feature-flags
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
script: |
|
||||
const description = context.payload.pull_request
|
||||
? context.payload.pull_request.body || ''
|
||||
: context.payload.inputs.pr_description || '';
|
||||
|
||||
const pattern = /FEATURE_(\w+)=(\w+)/g;
|
||||
let results = [];
|
||||
[...description.matchAll(pattern)].forEach(match => {
|
||||
const config = {
|
||||
name: `SUPERSET_FEATURE_${match[1]}`,
|
||||
value: match[2],
|
||||
};
|
||||
results.push(config);
|
||||
});
|
||||
|
||||
return results;
|
||||
|
||||
- name: Reply with confirmation comment
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const action = '${{ steps.eval-label.outputs.result }}';
|
||||
const user = context.actor;
|
||||
const runId = context.runId;
|
||||
const workflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
|
||||
|
||||
const issueNumber = context.payload.pull_request
|
||||
? context.payload.pull_request.number
|
||||
: context.payload.inputs.issue_number;
|
||||
|
||||
if (!issueNumber) {
|
||||
throw new Error("Issue number is not available.");
|
||||
}
|
||||
|
||||
const body = `⚠️ **DEPRECATED WORKFLOW** ⚠️\n\n@${user} This workflow is deprecated! Please use the new **Superset Showtime** system instead:\n\n` +
|
||||
`- Replace "testenv-up" label with "🎪 trigger-start"\n` +
|
||||
`- Better reliability and easier management\n` +
|
||||
`- See https://github.com/mistercrunch/superset-showtime for details\n\n` +
|
||||
`Processing your ephemeral environment request [here](${workflowUrl}). Action: **${action}**.` +
|
||||
` More information on [how to use or configure ephemeral environments]` +
|
||||
`(https://superset.apache.org/docs/contributing/howtos/#github-ephemeral-environments)`;
|
||||
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
|
||||
ephemeral-docker-build:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-build
|
||||
cancel-in-progress: true
|
||||
needs: ephemeral-env-label
|
||||
if: needs.ephemeral-env-label.outputs.slash-command == 'up'
|
||||
name: ephemeral-docker-build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ needs.ephemeral-env-label.outputs.sha }} : ${{steps.get-sha.outputs.sha}} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ needs.ephemeral-env-label.outputs.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Docker Environment
|
||||
uses: ./.github/actions/setup-docker
|
||||
with:
|
||||
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
build: "true"
|
||||
install-docker-compose: "false"
|
||||
|
||||
- name: Setup supersetbot
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
|
||||
- name: Build ephemeral env image
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
supersetbot docker \
|
||||
--push \
|
||||
--load \
|
||||
--preset ci \
|
||||
--platform linux/amd64 \
|
||||
--context-ref "$RELEASE" \
|
||||
--extra-flags "--build-arg INCLUDE_CHROMIUM=false"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
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 }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
|
||||
|
||||
- name: Load, tag and push image to ECR
|
||||
id: push-image
|
||||
env:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: superset-ci
|
||||
IMAGE_TAG: apache/superset:${{ needs.ephemeral-env-label.outputs.sha }}-ci
|
||||
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-$PR_NUMBER-ci
|
||||
docker push -a $ECR_REGISTRY/$ECR_REPOSITORY
|
||||
|
||||
ephemeral-env-up:
|
||||
needs: [ephemeral-env-label, ephemeral-docker-build]
|
||||
if: needs.ephemeral-env-label.outputs.slash-command == 'up'
|
||||
name: Spin up an ephemeral environment
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
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 }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
|
||||
|
||||
- name: Check target image exists in ECR
|
||||
id: check-image
|
||||
continue-on-error: true
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
aws ecr describe-images \
|
||||
--registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
|
||||
--repository-name superset-ci \
|
||||
--image-ids imageTag=pr-$PR_NUMBER-ci
|
||||
|
||||
- name: Fail on missing container image
|
||||
if: steps.check-image.outcome == 'failure'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
const errMsg = '@${{ github.event.comment.user.login }} Container image not yet published for this PR. Please try again when build is complete.';
|
||||
github.rest.issues.createComment({
|
||||
issue_number: ${{ github.event.inputs.issue_number || github.event.pull_request.number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: errMsg
|
||||
});
|
||||
core.setFailed(errMsg);
|
||||
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@6853cfae8c3a7d978fbf68b5a55453395541dfbb # v1
|
||||
with:
|
||||
task-definition: .github/workflows/ecs-task-definition.json
|
||||
container-name: superset-ci
|
||||
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-ci
|
||||
|
||||
- name: Update env vars in the Amazon ECS task definition
|
||||
run: |
|
||||
cat <<< "$(jq '.containerDefinitions[0].environment += ${{ needs.ephemeral-env-label.outputs.feature-flags }}' < ${{ steps.task-def.outputs.task-definition }})" > ${{ steps.task-def.outputs.task-definition }}
|
||||
|
||||
- name: Describe ECS service
|
||||
id: describe-services
|
||||
run: |
|
||||
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'
|
||||
env:
|
||||
ECR_SUBNETS: subnet-0e15a5034b4121710,subnet-0e8efef4a72224974
|
||||
ECR_SECURITY_GROUP: sg-092ff3a6ae0574d91
|
||||
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
aws ecs create-service \
|
||||
--cluster superset-ci \
|
||||
--service-name pr-$PR_NUMBER-service \
|
||||
--task-definition superset-ci \
|
||||
--launch-type FARGATE \
|
||||
--desired-count 1 \
|
||||
--platform-version LATEST \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[$ECR_SUBNETS],securityGroups=[$ECR_SECURITY_GROUP],assignPublicIp=ENABLED}" \
|
||||
--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@a310a830f5c14e583e35d84e4e1ec7dd177c3c9c # v2
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service
|
||||
cluster: superset-ci
|
||||
wait-for-service-stability: true
|
||||
wait-for-minutes: 10
|
||||
|
||||
- name: List tasks
|
||||
id: list-tasks
|
||||
run: |
|
||||
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: |
|
||||
echo "eni=$(aws ecs describe-tasks --cluster superset-ci --tasks ${{ steps.list-tasks.outputs.task }} | jq '.tasks[0].attachments[0].details | map(select(.name=="networkInterfaceId"))[0].value')" >> $GITHUB_OUTPUT
|
||||
- name: Get public IP
|
||||
id: get-ip
|
||||
run: |
|
||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issue_number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `@${{ github.actor }} Ephemeral environment spinning up at http://${{ steps.get-ip.outputs.ip }}:8080. Credentials are 'admin'/'admin'. Please allow several minutes for bootstrapping and startup.`
|
||||
});
|
||||
- name: Comment (failure)
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issue_number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '@${{ github.event.inputs.user_login || github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
|
||||
})
|
||||
22
.github/workflows/github-action-validator.yml
vendored
22
.github/workflows/github-action-validator.yml
vendored
@@ -6,21 +6,34 @@ on:
|
||||
- "master"
|
||||
- "[0-9].[0-9]*"
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
validate-all-ghas:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
# Required for the zizmor action to upload its SARIF results to
|
||||
# GitHub code scanning (advanced-security is enabled by default).
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -29,3 +42,6 @@ jobs:
|
||||
|
||||
- name: Run Script
|
||||
run: bash .github/workflows/github-action-validator.sh
|
||||
|
||||
- name: Check for security issues on GHA workflows
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
|
||||
7
.github/workflows/labeler.yml
vendored
7
.github/workflows/labeler.yml
vendored
@@ -2,6 +2,11 @@ name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
@@ -9,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
|
||||
4
.github/workflows/latest-release-tag.yml
vendored
4
.github/workflows/latest-release-tag.yml
vendored
@@ -19,8 +19,10 @@ jobs:
|
||||
|
||||
- name: Check for latest tag
|
||||
id: latest-tag
|
||||
env:
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
source ./scripts/tag_latest_release.sh $(echo ${{ github.event.release.tag_name }}) --dry-run
|
||||
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
|
||||
7
.github/workflows/no-hold-label.yml
vendored
7
.github/workflows/no-hold-label.yml
vendored
@@ -7,10 +7,13 @@ on:
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
# Let each label event run to completion. Cancelling in-progress runs leaves
|
||||
# CANCELLED entries in the PR's check-suite rollup, which poisons GitHub's
|
||||
# `status:success` search filter even though all real CI passed. The job is
|
||||
# a tiny no-op github-script call, so the wasted compute is negligible.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check-hold-label:
|
||||
|
||||
5
.github/workflows/pr-lint.yml
vendored
5
.github/workflows/pr-lint.yml
vendored
@@ -8,6 +8,11 @@ on:
|
||||
# Possible values: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-check:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
12
.github/workflows/pre-commit.yml
vendored
12
.github/workflows/pre-commit.yml
vendored
@@ -19,9 +19,13 @@ concurrency:
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["current", "previous", "next"]
|
||||
# Run the full version spread on push (master/release) and nightly,
|
||||
# but only the current version on PRs — lint/format/type results
|
||||
# rarely differ across patch versions, so 3x per PR is wasteful.
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
@@ -45,6 +49,8 @@ jobs:
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
|
||||
- name: Install Frontend Dependencies
|
||||
run: |
|
||||
@@ -71,10 +77,12 @@ jobs:
|
||||
output: ' '
|
||||
|
||||
- name: pre-commit
|
||||
env:
|
||||
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
|
||||
run: |
|
||||
set +e # Don't exit immediately on failure
|
||||
export SKIP=type-checking-frontend
|
||||
pre-commit run --files ${{ steps.changed_files.outputs.files }}
|
||||
pre-commit run --files $CHANGED_FILES
|
||||
PRE_COMMIT_EXIT_CODE=$?
|
||||
git diff --quiet --exit-code
|
||||
GIT_DIFF_EXIT_CODE=$?
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
- "master"
|
||||
- "[0-9].[0-9]*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -27,9 +30,12 @@ jobs:
|
||||
if: needs.config.outputs.has-secrets
|
||||
name: Bump version and publish package(s)
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
# pulls all commits (needed for lerna / semantic release to correctly version)
|
||||
fetch-depth: 0
|
||||
- name: Get tags and filter trigger tags
|
||||
|
||||
9
.github/workflows/showtime-trigger.yml
vendored
9
.github/workflows/showtime-trigger.yml
vendored
@@ -2,6 +2,7 @@ name: 🎪 Superset Showtime
|
||||
|
||||
# Ultra-simple: just sync on any PR state change
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; this workflow does not check out or execute PR-provided code
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, synchronize, closed]
|
||||
|
||||
@@ -102,7 +103,7 @@ jobs:
|
||||
- name: Install Superset Showtime
|
||||
if: steps.auth.outputs.authorized == 'true'
|
||||
run: |
|
||||
echo "::notice::Maintainer ${{ github.actor }} triggered deploy for PR ${PULL_REQUEST_NUMBER}"
|
||||
echo "::notice::Maintainer $GITHUB_ACTOR triggered deploy for PR ${PULL_REQUEST_NUMBER}"
|
||||
pip install --upgrade superset-showtime
|
||||
showtime version
|
||||
|
||||
@@ -173,9 +174,11 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
CHECK_PR_NUMBER: ${{ steps.check.outputs.pr_number }}
|
||||
CHECK_TARGET_SHA: ${{ steps.check.outputs.target_sha }}
|
||||
run: |
|
||||
PR_NUM="${{ steps.check.outputs.pr_number }}"
|
||||
TARGET_SHA="${{ steps.check.outputs.target_sha }}"
|
||||
PR_NUM="$CHECK_PR_NUMBER"
|
||||
TARGET_SHA="$CHECK_TARGET_SHA"
|
||||
if [[ -n "$TARGET_SHA" ]]; then
|
||||
python -m showtime sync $PR_NUM --sha "$TARGET_SHA"
|
||||
else
|
||||
|
||||
12
.github/workflows/superset-docs-deploy.yml
vendored
12
.github/workflows/superset-docs-deploy.yml
vendored
@@ -2,6 +2,7 @@ name: Docs Deployment
|
||||
|
||||
on:
|
||||
# Deploy after integration tests complete on master
|
||||
# zizmor: ignore[dangerous-triggers] - runs in base-branch context after a trusted upstream workflow; scoped to master
|
||||
workflow_run:
|
||||
workflows: ["Python-Integration"]
|
||||
types: [completed]
|
||||
@@ -27,6 +28,9 @@ concurrency:
|
||||
group: docs-deploy-asf-site
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -45,7 +49,13 @@ jobs:
|
||||
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
|
||||
build-deploy:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
# For workflow_run triggers, only deploy when the triggering run originated
|
||||
# from this repository (not a fork), ensuring the checked-out code and any
|
||||
# local actions executed with deploy credentials are trusted.
|
||||
if: >-
|
||||
needs.config.outputs.has-secrets &&
|
||||
(github.event_name != 'workflow_run' ||
|
||||
github.event.workflow_run.head_repository.full_name == github.repository)
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
|
||||
9
.github/workflows/superset-docs-verify.yml
vendored
9
.github/workflows/superset-docs-verify.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
- "superset/db_engine_specs/**"
|
||||
- ".github/workflows/superset-docs-verify.yml"
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes artifacts from the trusted upstream workflow
|
||||
workflow_run:
|
||||
workflows: ["Python-Integration"]
|
||||
types: [completed]
|
||||
@@ -16,6 +17,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.head_sha || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
linkinator:
|
||||
# See docs here: https://github.com/marketplace/actions/linkinator
|
||||
@@ -25,6 +29,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
# 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
|
||||
@@ -97,7 +103,8 @@ jobs:
|
||||
# Only runs if integration tests succeeded
|
||||
if: >
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository
|
||||
name: Build (after integration tests)
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
|
||||
139
.github/workflows/superset-e2e.yml
vendored
139
.github/workflows/superset-e2e.yml
vendored
@@ -27,9 +27,32 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
cypress-matrix:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
# Somehow one test flakes on 24.04 for unknown reasons, this is the only GHA left on 22.04
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
@@ -40,9 +63,14 @@ jobs:
|
||||
# https://github.com/cypress-io/github-action/issues/48
|
||||
fail-fast: false
|
||||
matrix:
|
||||
parallel_id: [0, 1, 2, 3, 4, 5]
|
||||
parallel_id: [0, 1]
|
||||
browser: ["chrome"]
|
||||
app_root: ["", "/app/prefix"]
|
||||
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
|
||||
# The /app/prefix variant (push events only) is smoke-tested on a single
|
||||
# shard rather than the full matrix, so exclude it from the other shards.
|
||||
exclude:
|
||||
- parallel_id: 1
|
||||
app_root: "/app/prefix"
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -89,51 +117,40 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
submodules: recursive
|
||||
# -------------------------------------------------------
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
- name: Setup postgres
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Import test data
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
- name: Install npm dependencies
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Install cypress
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: cypress-install
|
||||
- name: Run Cypress
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ matrix.browser }}
|
||||
PARALLEL_ID: ${{ matrix.parallel_id }}
|
||||
PARALLELISM: 6
|
||||
PARALLELISM: 2
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
with:
|
||||
@@ -141,8 +158,9 @@ jobs:
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
env:
|
||||
APP_ROOT: ${{ matrix.app_root }}
|
||||
run: |
|
||||
APP_ROOT="${{ matrix.app_root }}"
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Artifacts
|
||||
@@ -153,7 +171,10 @@ jobs:
|
||||
name: cypress-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}-${{ matrix.parallel_id }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
playwright-tests:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
@@ -161,7 +182,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["chromium"]
|
||||
app_root: ["", "/app/prefix"]
|
||||
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -206,46 +227,39 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
submodules: recursive
|
||||
# -------------------------------------------------------
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
- name: Setup postgres
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Import test data
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
- name: Install npm dependencies
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Build embedded SDK
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-embedded-sdk
|
||||
- name: Install Playwright
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: playwright-install
|
||||
- name: Run Playwright (Required Tests)
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
@@ -254,8 +268,9 @@ jobs:
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
env:
|
||||
APP_ROOT: ${{ matrix.app_root }}
|
||||
run: |
|
||||
APP_ROOT="${{ matrix.app_root }}"
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
@@ -266,3 +281,63 @@ jobs:
|
||||
${{ github.workspace }}/superset-frontend/playwright-results/
|
||||
${{ github.workspace }}/superset-frontend/test-results/
|
||||
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
# Stable required-status-check anchors. cypress-matrix and playwright-tests
|
||||
# are matrix jobs gated on change detection (python || frontend). On a PR
|
||||
# that touches neither — e.g. a docs-only PR — they are skipped at the job
|
||||
# level, which happens before matrix expansion, so the per-combination
|
||||
# contexts (`cypress-matrix (0, chrome)`, `playwright-tests (chromium)`) are
|
||||
# never produced and branch protection waits on them forever. These
|
||||
# always-running jobs report a single stable context that passes when the
|
||||
# underlying matrix job succeeded or was skipped, and fails only on a real
|
||||
# failure. Require these in .asf.yaml instead of the matrix-expanded names.
|
||||
#
|
||||
# A matrix job reads as "skipped" in two distinct cases, and only the first
|
||||
# is a legitimate pass: (a) change detection succeeded and gated the job off
|
||||
# (docs-only PR); (b) the `changes` job itself failed or was cancelled, in
|
||||
# which case GHA skips its dependents too. Accepting (b) would let a broken
|
||||
# change-detector report a false green, so each anchor first requires
|
||||
# `changes` to have succeeded before honouring a skip.
|
||||
cypress-matrix-required:
|
||||
needs: [changes, cypress-matrix]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Check cypress-matrix result
|
||||
env:
|
||||
CHANGES: ${{ needs.changes.result }}
|
||||
RESULT: ${{ needs.cypress-matrix.result }}
|
||||
run: |
|
||||
if [ "$CHANGES" != "success" ]; then
|
||||
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
|
||||
echo "cypress-matrix did not pass (result: $RESULT)"
|
||||
exit 1
|
||||
fi
|
||||
echo "cypress-matrix result: $RESULT (changes: $CHANGES)"
|
||||
|
||||
playwright-tests-required:
|
||||
needs: [changes, playwright-tests]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Check playwright-tests result
|
||||
env:
|
||||
CHANGES: ${{ needs.changes.result }}
|
||||
RESULT: ${{ needs.playwright-tests.result }}
|
||||
run: |
|
||||
if [ "$CHANGES" != "success" ]; then
|
||||
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
|
||||
echo "playwright-tests did not pass (result: $RESULT)"
|
||||
exit 1
|
||||
fi
|
||||
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
|
||||
|
||||
@@ -20,9 +20,12 @@ concurrency:
|
||||
jobs:
|
||||
test-superset-extensions-cli-package:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["previous", "current", "next"]
|
||||
# Full version spread on push (master/release) + nightly; current only
|
||||
# on PRs to cut runner cost (cross-version breaks are caught at merge).
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["previous", "current", "next"]') }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: superset-extensions-cli
|
||||
@@ -53,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.check.outputs.superset-extensions-cli
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: superset-extensions-cli
|
||||
|
||||
11
.github/workflows/superset-frontend.yml
vendored
11
.github/workflows/superset-frontend.yml
vendored
@@ -16,9 +16,13 @@ concurrency:
|
||||
env:
|
||||
TAG: apache/superset:GHA-${{ github.run_id }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
frontend-build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
should-run: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
@@ -71,6 +75,7 @@ jobs:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
@@ -100,6 +105,7 @@ jobs:
|
||||
needs: [sharded-jest-tests]
|
||||
if: needs.frontend-build.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
@@ -128,7 +134,7 @@ jobs:
|
||||
run: npx nyc merge coverage/ merged-output/coverage-summary.json
|
||||
|
||||
- name: Upload Code Coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: javascript
|
||||
use_oidc: true
|
||||
@@ -141,6 +147,7 @@ jobs:
|
||||
needs: frontend-build
|
||||
if: needs.frontend-build.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
@@ -165,6 +172,7 @@ jobs:
|
||||
needs: frontend-build
|
||||
if: needs.frontend-build.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
@@ -184,6 +192,7 @@ jobs:
|
||||
needs: frontend-build
|
||||
if: needs.frontend-build.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
|
||||
8
.github/workflows/superset-helm-release.yml
vendored
8
.github/workflows/superset-helm-release.yml
vendored
@@ -62,6 +62,8 @@ jobs:
|
||||
run: echo "branch_name=helm-publish-${GITHUB_SHA:0:7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Force recreate branch from gh-pages
|
||||
env:
|
||||
BRANCH_NAME: ${{ env.branch_name }}
|
||||
run: |
|
||||
# Ensure a clean working directory
|
||||
git reset --hard
|
||||
@@ -73,13 +75,13 @@ jobs:
|
||||
git fetch origin gh-pages
|
||||
|
||||
# Check out and reset the target branch based on gh-pages
|
||||
git checkout -B ${{ env.branch_name }} origin/gh-pages
|
||||
git checkout -B "$BRANCH_NAME" origin/gh-pages
|
||||
|
||||
# Remove submodules from the branch
|
||||
git submodule deinit -f --all
|
||||
|
||||
# Force push to the remote branch
|
||||
git push origin ${{ env.branch_name }} --force
|
||||
git push origin "$BRANCH_NAME" --force
|
||||
|
||||
# Return to the original branch
|
||||
git checkout local_gha_temp
|
||||
@@ -104,7 +106,7 @@ jobs:
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ env.branch_name }}';
|
||||
const branchName = process.env.BRANCH_NAME;
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
|
||||
|
||||
if (!branchName) {
|
||||
|
||||
56
.github/workflows/superset-playwright.yml
vendored
56
.github/workflows/superset-playwright.yml
vendored
@@ -23,10 +23,33 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# NOTE: Required Playwright tests are in superset-e2e.yml (E2E / playwright-tests)
|
||||
# This workflow contains only experimental tests that run in shadow mode
|
||||
playwright-tests-experimental:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -80,51 +103,58 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
submodules: recursive
|
||||
# -------------------------------------------------------
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
- name: Setup postgres
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Import test data
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
- name: Install npm dependencies
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Build embedded SDK
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-embedded-sdk
|
||||
- name: Install Playwright
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: playwright-install
|
||||
- name: Run Playwright (Experimental Tests)
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" experimental/
|
||||
- name: Run Playwright (Embedded Tests)
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
# Scope embedded-only env vars to this step. Setting them at the job
|
||||
# level enabled the EMBEDDED_SUPERSET feature flag inside Flask for
|
||||
# the preceding "Required Tests" and "Experimental Tests" steps too,
|
||||
# which loads extra handlers and destabilizes the werkzeug dev
|
||||
# server under the 2-worker Playwright load. Required Tests should
|
||||
# match master's Flask configuration.
|
||||
SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true"
|
||||
INCLUDE_EMBEDDED: "true"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" embedded
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
|
||||
@@ -14,8 +14,30 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-mysql:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test-mysql:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
@@ -27,6 +49,8 @@ jobs:
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
# Authenticated pulls use our higher Docker Hub rate limit. Empty on
|
||||
# fork PRs (secrets unavailable) -> runner falls back to anonymous.
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
@@ -47,37 +71,27 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python
|
||||
- name: Setup MySQL
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: setup-mysql
|
||||
- name: Start Celery worker
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python integration tests (MySQL)
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: python,mysql
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
- name: Generate database diagnostics for docs
|
||||
if: steps.check.outputs.python
|
||||
env:
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
SUPERSET__SQLALCHEMY_DATABASE_URI: |
|
||||
@@ -100,19 +114,23 @@ jobs:
|
||||
print(f'Generated diagnostics for {len(docs)} databases')
|
||||
"
|
||||
- name: Upload database diagnostics artifact
|
||||
if: steps.check.outputs.python
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: database-diagnostics
|
||||
path: databases-diagnostics.json
|
||||
retention-days: 7
|
||||
test-postgres:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["current", "previous", "next"]
|
||||
# Full version spread on push (master/release) + nightly; current only
|
||||
# on PRs to cut runner cost (cross-version breaks are caught at merge).
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
@@ -138,33 +156,24 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Setup Postgres
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: |
|
||||
setup-postgres
|
||||
- name: Start Celery worker
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python integration tests (PostgreSQL)
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: python,postgres
|
||||
verbose: true
|
||||
@@ -172,7 +181,10 @@ jobs:
|
||||
slug: apache/superset
|
||||
|
||||
test-sqlite:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
@@ -194,34 +206,47 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python
|
||||
- name: Install dependencies
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: |
|
||||
# sqlite needs this working directory
|
||||
mkdir ${{ github.workspace }}/.temp
|
||||
- name: Start Celery worker
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python integration tests (SQLite)
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: python,sqlite
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
# Stable required-status-check anchor for the matrix-based test-postgres job.
|
||||
# It is gated on change detection, so on non-Python PRs it is skipped and
|
||||
# never produces its `test-postgres (current)` context (a job-level skip
|
||||
# happens before matrix expansion). This always-running job reports a single
|
||||
# context branch protection can require: it passes when test-postgres
|
||||
# succeeded or was skipped, and fails only on a real failure.
|
||||
test-postgres-required:
|
||||
needs: [changes, test-postgres]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check test-postgres result
|
||||
env:
|
||||
RESULT: ${{ needs.test-postgres.result }}
|
||||
run: |
|
||||
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
|
||||
echo "test-postgres did not pass (result: $RESULT)"
|
||||
exit 1
|
||||
fi
|
||||
echo "test-postgres result: $RESULT"
|
||||
|
||||
@@ -15,8 +15,30 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-postgres-presto:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test-postgres-presto:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
@@ -54,32 +76,21 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python == 'true'
|
||||
- name: Setup Postgres
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: |
|
||||
echo "${{ steps.check.outputs.python }}"
|
||||
setup-postgres
|
||||
run: setup-postgres
|
||||
- name: Start Celery worker
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python unit tests (PostgreSQL)
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: python,presto
|
||||
verbose: true
|
||||
@@ -87,7 +98,10 @@ jobs:
|
||||
slug: apache/superset
|
||||
|
||||
test-postgres-hive:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
id-token: write
|
||||
env:
|
||||
@@ -117,40 +131,28 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create csv upload directory
|
||||
if: steps.check.outputs.python
|
||||
run: sudo mkdir -p /tmp/.superset/uploads
|
||||
- name: Give write access to the csv upload directory
|
||||
if: steps.check.outputs.python
|
||||
run: sudo chown -R $USER:$USER /tmp/.superset
|
||||
- name: Start hadoop and hive
|
||||
if: steps.check.outputs.python
|
||||
run: docker compose -f scripts/databases/hive/docker-compose.yml up -d
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python
|
||||
- name: Setup Postgres
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Start Celery worker
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python unit tests (PostgreSQL)
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
pip install -e .[hive]
|
||||
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: python,hive
|
||||
verbose: true
|
||||
|
||||
60
.github/workflows/superset-python-unittest.yml
vendored
60
.github/workflows/superset-python-unittest.yml
vendored
@@ -15,13 +15,37 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
unit-tests:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["previous", "current", "next"]
|
||||
# Full version spread on push (master/release) + nightly; current only
|
||||
# on PRs to cut runner cost (cross-version breaks are caught at merge).
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["previous", "current", "next"]') }}
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
steps:
|
||||
@@ -30,25 +54,17 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
if: steps.check.outputs.python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Python unit tests
|
||||
if: steps.check.outputs.python
|
||||
env:
|
||||
SUPERSET_TESTENV: true
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear --maxfail=50
|
||||
- name: Python 100% coverage unit tests
|
||||
if: steps.check.outputs.python
|
||||
env:
|
||||
SUPERSET_TESTENV: true
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
@@ -56,9 +72,31 @@ jobs:
|
||||
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
flags: python,unit
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
# Stable required-status-check anchor. `unit-tests` is a matrix job gated on
|
||||
# change detection, so on non-Python PRs it is skipped and never produces its
|
||||
# `unit-tests (current)` context (a job-level skip happens before matrix
|
||||
# expansion). This always-running job reports a single context that branch
|
||||
# protection can require: it passes when unit-tests succeeded or was skipped,
|
||||
# and fails only on a real failure.
|
||||
unit-tests-required:
|
||||
needs: [changes, unit-tests]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check unit-tests result
|
||||
env:
|
||||
RESULT: ${{ needs.unit-tests.result }}
|
||||
run: |
|
||||
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
|
||||
echo "unit-tests did not pass (result: $RESULT)"
|
||||
exit 1
|
||||
fi
|
||||
echo "unit-tests result: $RESULT"
|
||||
|
||||
88
.github/workflows/superset-translations-comment.yml
vendored
Normal file
88
.github/workflows/superset-translations-comment.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: Translation Regression Comment
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes the uploaded artifact; never checks out PR code (see note below)
|
||||
workflow_run:
|
||||
workflows: ["Translations"]
|
||||
types: [completed]
|
||||
|
||||
# This workflow posts a PR comment when the Translations workflow detects a
|
||||
# regression. It uses the workflow_run trigger so that it always runs in the
|
||||
# base-branch context and can safely be granted write permissions, even for
|
||||
# PRs from forks.
|
||||
#
|
||||
# IMPORTANT: This workflow must NEVER check out code from the PR branch.
|
||||
# All data comes from the artifact uploaded by the Translations workflow.
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
post-comment:
|
||||
runs-on: ubuntu-24.04
|
||||
# Only act when the Translations workflow failed (which means a regression
|
||||
# was detected — the workflow exits 1 on regression).
|
||||
if: github.event.workflow_run.conclusion == 'failure'
|
||||
|
||||
steps:
|
||||
- name: Download regression artifact
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: translation-regression
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: /tmp/translation-regression
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const prNumberFile = '/tmp/translation-regression/pr-number.txt';
|
||||
const reportFile = '/tmp/translation-regression/regression-report.md';
|
||||
|
||||
if (!fs.existsSync(prNumberFile) || !fs.existsSync(reportFile)) {
|
||||
console.log('Artifact files not found, skipping comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
const prNumber = parseInt(fs.readFileSync(prNumberFile, 'utf8').trim(), 10);
|
||||
if (!prNumber) {
|
||||
console.log('Could not parse PR number, skipping comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
const report = fs.readFileSync(reportFile, 'utf8');
|
||||
const marker = '<!-- translation-regression-bot -->';
|
||||
const body = `${marker}\n${report}`;
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
console.log(`Updated existing comment ${existing.id} on PR #${prNumber}`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
console.log(`Created new comment on PR #${prNumber}`);
|
||||
}
|
||||
90
.github/workflows/superset-translations.yml
vendored
90
.github/workflows/superset-translations.yml
vendored
@@ -20,6 +20,9 @@ concurrency:
|
||||
jobs:
|
||||
frontend-check-translations:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
@@ -38,6 +41,8 @@ jobs:
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'superset-frontend/package-lock.json'
|
||||
- name: Install dependencies
|
||||
if: steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
@@ -51,12 +56,16 @@ jobs:
|
||||
|
||||
babel-extract:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
@@ -64,12 +73,83 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Python
|
||||
if: steps.check.outputs.python
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
uses: ./.github/actions/setup-backend/
|
||||
|
||||
- name: Install msgcat
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install gettext tools
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
run: sudo apt-get update && sudo apt-get install -y gettext
|
||||
|
||||
- name: Test babel extraction
|
||||
if: steps.check.outputs.python
|
||||
# Fetch the base ref so we can compare PR-introduced regressions
|
||||
# against a fair baseline (also runs babel_update against the base
|
||||
# source) — this isolates the PR's contribution from any pre-existing
|
||||
# drift on the base branch.
|
||||
- name: Fetch base ref and create comparison worktree
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
env:
|
||||
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
run: |
|
||||
# For PRs use the base branch; for direct pushes compare against the previous commit.
|
||||
BASE_REF="$PR_BASE_REF"
|
||||
if [ -n "$BASE_REF" ]; then
|
||||
git fetch --depth=1 origin "$BASE_REF"
|
||||
else
|
||||
git fetch --depth=2 origin "$GITHUB_REF"
|
||||
fi
|
||||
git worktree add /tmp/base-worktree FETCH_HEAD
|
||||
|
||||
# Run babel_update against BASE source + BASE translations. Any drift
|
||||
# already present on the base branch (source strings that have changed
|
||||
# without .po updates) shows up here as fuzzies — and will also show
|
||||
# up in the PR run, so it cancels out in the comparison.
|
||||
- name: Baseline — run babel_update against BASE source
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
working-directory: /tmp/base-worktree
|
||||
run: ./scripts/translations/babel_update.sh
|
||||
|
||||
- name: Record baseline translation counts
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
run: |
|
||||
python scripts/translations/check_translation_regression.py \
|
||||
--count \
|
||||
--translations-dir /tmp/base-worktree/superset/translations \
|
||||
> /tmp/before.json
|
||||
|
||||
# Run babel_update against the PR source and PR translations. This keeps
|
||||
# committed .po fixes in play while the base babel_update above still
|
||||
# cancels out translation drift already present on the base branch.
|
||||
- name: Run babel_update against PR source
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
run: ./scripts/translations/babel_update.sh
|
||||
|
||||
- name: Check for translation regression
|
||||
id: regression
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
python scripts/translations/check_translation_regression.py \
|
||||
--compare /tmp/before.json \
|
||||
--report /tmp/regression-report.md
|
||||
|
||||
# Save the PR number so the comment workflow can post the report without
|
||||
# needing write permissions on this pull_request-triggered job.
|
||||
- name: Save PR number for comment workflow
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
steps.regression.outcome == 'failure'
|
||||
run: echo "${{ github.event.pull_request.number }}" > /tmp/pr-number.txt
|
||||
|
||||
- name: Upload regression artifact
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
steps.regression.outcome == 'failure'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: translation-regression
|
||||
path: |
|
||||
/tmp/regression-report.md
|
||||
/tmp/pr-number.txt
|
||||
|
||||
- name: Fail if regression detected
|
||||
if: steps.regression.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
1
.github/workflows/superset-websocket.yml
vendored
1
.github/workflows/superset-websocket.yml
vendored
@@ -22,6 +22,7 @@ concurrency:
|
||||
jobs:
|
||||
app-checks:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
17
.github/workflows/tag-release.yml
vendored
17
.github/workflows/tag-release.yml
vendored
@@ -21,6 +21,9 @@ on:
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -42,6 +45,8 @@ jobs:
|
||||
if: needs.config.outputs.has-secrets
|
||||
name: docker-release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
|
||||
@@ -51,6 +56,7 @@ jobs:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Docker Environment
|
||||
@@ -62,9 +68,11 @@ jobs:
|
||||
build: "true"
|
||||
|
||||
- name: Use Node.js 20
|
||||
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup supersetbot
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
@@ -77,8 +85,9 @@ jobs:
|
||||
INPUT_RELEASE: ${{ github.event.inputs.release }}
|
||||
INPUT_FORCE_LATEST: ${{ github.event.inputs.force-latest }}
|
||||
INPUT_GIT_REF: ${{ github.event.inputs.git-ref }}
|
||||
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
RELEASE="${{ github.event.release.tag_name }}"
|
||||
RELEASE="${GITHUB_EVENT_RELEASE_TAG_NAME}"
|
||||
FORCE_LATEST=""
|
||||
EVENT="${{github.event_name}}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
@@ -114,12 +123,15 @@ jobs:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20
|
||||
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup supersetbot
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
@@ -128,11 +140,12 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_RELEASE: ${{ github.event.inputs.release }}
|
||||
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
export GITHUB_ACTOR=""
|
||||
git fetch --all --tags
|
||||
git checkout master
|
||||
RELEASE="${{ github.event.release.tag_name }}"
|
||||
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="${INPUT_RELEASE}"
|
||||
|
||||
2
.github/workflows/tech-debt.yml
vendored
2
.github/workflows/tech-debt.yml
vendored
@@ -33,6 +33,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
|
||||
15
.github/workflows/welcome-new-users.yml
vendored
15
.github/workflows/welcome-new-users.yml
vendored
@@ -1,22 +1,29 @@
|
||||
name: Welcome New Contributor
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - posts a welcome comment only; does not check out or execute PR-provided code
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Welcome Message
|
||||
uses: actions/first-interaction@v3
|
||||
continue-on-error: true
|
||||
uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
pr-message: |-
|
||||
repo_token: ${{ github.token }}
|
||||
issue_message: |-
|
||||
Congrats on opening your first issue and thank you for contributing to Superset! :tada: :heart:
|
||||
|
||||
Please read our [New Contributor Welcome & Expectations](https://github.com/apache/superset/wiki/New-Contributor-Welcome-&-Expectations) guide.
|
||||
pr_message: |-
|
||||
Congrats on making your first PR and thank you for contributing to Superset! :tada: :heart:
|
||||
|
||||
Please read our [New Contributor Welcome & Expectations](https://github.com/apache/superset/wiki/New-Contributor-Welcome-&-Expectations) guide.
|
||||
|
||||
We hope to see you in our [Slack](https://apache-superset.slack.com/) community too! Not signed up? Use our [Slack App](http://bit.ly/join-superset-slack) to self-register.
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -115,6 +115,8 @@ release.json
|
||||
superset/translations/**/messages.json
|
||||
# these mo binary files are generated by `pybabel compile`
|
||||
superset/translations/**/messages.mo
|
||||
# cross-language index generated by scripts/translations/build_translation_index.py
|
||||
superset/translations/translation_index.json
|
||||
|
||||
docker/requirements-local.txt
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ repos:
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-added-large-files
|
||||
exclude: ^.*\.(geojson)$|^docs/static/img/screenshots/.*|^superset-frontend/CHANGELOG\.md$|^superset/examples/.*/data\.parquet$
|
||||
exclude: ^.*\.(geojson)$|^docs/static/img/screenshots/.*|^superset-frontend/CHANGELOG\.md$|^superset/examples/.*/data\.parquet$|^superset/translations/.*\.po$
|
||||
- id: check-yaml
|
||||
exclude: ^helm/superset/templates/
|
||||
- id: debug-statements
|
||||
@@ -158,3 +158,14 @@ repos:
|
||||
language: system
|
||||
files: ^superset/config\.py$
|
||||
pass_filenames: false
|
||||
- id: zizmor
|
||||
name: zizmor (GHA security audit)
|
||||
entry: zizmor
|
||||
language: python
|
||||
additional_dependencies: [zizmor==1.25.2]
|
||||
files: ^\.github/
|
||||
types: [yaml]
|
||||
pass_filenames: false
|
||||
# Advisory until pre-existing findings are resolved; remove
|
||||
# --no-exit-codes to make this hook blocking.
|
||||
args: [--no-exit-codes, .github/]
|
||||
|
||||
@@ -53,7 +53,7 @@ extension-pkg-whitelist=pyarrow
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=all
|
||||
enable=json-import,disallowed-sql-import,consider-using-transaction
|
||||
enable=disallowed-sql-import,consider-using-transaction
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
38
AGENTS.md
38
AGENTS.md
@@ -52,6 +52,43 @@ Common pre-commit failures:
|
||||
- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
|
||||
- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
|
||||
|
||||
## Security and Threat Model
|
||||
|
||||
Before evaluating any code path for security issues, read [`SECURITY.md`](SECURITY.md). It is the canonical, authoritative source for Apache Superset's security model and is referenced by both human reporters and automated scanners.
|
||||
|
||||
In short, the test for whether a finding is in scope is one question:
|
||||
|
||||
> *Does it let a principal perform an action the role and capability matrix in `SECURITY.md` does not entitle them to?*
|
||||
|
||||
If yes, it is in scope. If no, it is not.
|
||||
|
||||
The three trust boundaries are:
|
||||
|
||||
1. **The Admin role** is a fully trusted operational principal. Anything an Admin can do through documented configuration, API, or UI is an intended capability, not a vulnerability.
|
||||
2. **The operator** owns deployment-time decisions (secrets, network exposure, feature-flag selection, connector and codec choices, notification destinations, third-party plugins). Misconfiguration at this layer is a deployment defect, not a Superset vulnerability.
|
||||
3. **The codebase** is responsible for enforcing the role and capability matrix wherever it exposes functionality to a principal: API routes, command and DAO layers, UI handlers, background jobs, and any other entry point. A missing or incorrect enforcement check is in scope no matter where it lives.
|
||||
|
||||
The security model assumes that operator-controlled infrastructure, including the metadata database, cache backends, message brokers, secret stores, and deployment environment, remains within the operator's trust boundary. Vulnerabilities must demonstrate a security boundary violation by an attacker who does not already control those systems.
|
||||
|
||||
Route-level authorization in this codebase uses one of three Flask-AppBuilder decorators depending on the route type:
|
||||
|
||||
- `@protect()` for REST API routes (`ModelRestApi` / `BaseApi`)
|
||||
- `@has_access_api` for legacy view routes
|
||||
- `@has_access` for legacy HTML view routes
|
||||
|
||||
Object-level authorization via `security_manager.raise_for_access(...)` applies to data-bearing resources: dashboards, charts, datasets and datasources, queries, database and table access, and query contexts. Other resources (annotations, tags, CSS templates, reports, RLS rules, and similar) rely on the route-level decorator plus DAO `base_filters` for ownership scoping; the absence of `raise_for_access` on these resources is by design, not a finding. Code that omits the per-object gate on a route that returns or mutates a specific data-bearing object is in scope; code that follows the correct pattern for its resource class can still contain injection, SSRF, XSS, or other classes of finding unrelated to authorization, which are evaluated separately.
|
||||
|
||||
The full role and capability matrix, in-scope and out-of-scope class lists, and CVE aggregation rules are in [`SECURITY.md`](SECURITY.md). Defer to that document for any specifics.
|
||||
|
||||
**Requirements for findings filed by automated tooling**
|
||||
|
||||
Automated scanners (LLM-based code scanners, static analyzers, dependency tools) that file findings against this codebase must, in each finding, name:
|
||||
|
||||
1. The specific role and capability matrix row in [`SECURITY.md`](SECURITY.md) the finding believes is violated.
|
||||
2. The principal the finding assumes the attacker holds (Public, Gamma, sql_lab, Alpha, Admin, Embedded guest token, or a custom role with explicit capability grants).
|
||||
|
||||
Findings that cannot identify both should be filed as questions, not vulnerabilities. This requirement exists to ensure every reported issue is testable against the published security model and to keep speculative or pattern-match-only reports out of the triage queue.
|
||||
|
||||
## Key Directories
|
||||
|
||||
```
|
||||
@@ -128,6 +165,7 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
|
||||
## Architecture Patterns
|
||||
|
||||
### Security & Features
|
||||
- **Security model**: see the top-level [Security and Threat Model](#security-and-threat-model) section and [`SECURITY.md`](SECURITY.md)
|
||||
- **RBAC**: Role-based access via Flask-AppBuilder
|
||||
- **Feature flags**: Control feature rollouts
|
||||
- **Row-level security**: SQL-based data access control
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -55,6 +55,13 @@ WORKDIR /app/superset-frontend
|
||||
RUN mkdir -p /app/superset/static/assets \
|
||||
/app/superset/translations
|
||||
|
||||
# Harden `npm ci` against transient npm-registry network blips (e.g. ECONNRESET),
|
||||
# which otherwise fail the entire multi-platform image build with no retry.
|
||||
ENV npm_config_fetch_retries=5 \
|
||||
npm_config_fetch_retry_mintimeout=20000 \
|
||||
npm_config_fetch_retry_maxtimeout=120000 \
|
||||
npm_config_fetch_timeout=600000
|
||||
|
||||
# Mount package files and install dependencies if not in dev mode
|
||||
# NOTE: we mount packages and plugins as they are referenced in package.json as workspaces
|
||||
# ideally we'd COPY only their package.json. Here npm ci will be cached as long
|
||||
@@ -113,7 +120,7 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash
|
||||
# Some bash scripts needed throughout the layers
|
||||
COPY --chmod=755 docker/*.sh /app/docker/
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
# Using uv as it's faster/simpler than pip
|
||||
RUN uv venv /app/.venv
|
||||
@@ -202,6 +209,8 @@ RUN mkdir -p /app/data && chown -R superset:superset /app/data
|
||||
|
||||
# Copy compiled things from previous stages
|
||||
COPY --from=superset-node /app/superset/static/assets superset/static/assets
|
||||
# Copy service.worker.js optionall as it doesn't exist when DEV_MODE=true
|
||||
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
|
||||
|
||||
# TODO, when the next version comes out, use --exclude superset/translations
|
||||
COPY superset superset
|
||||
|
||||
@@ -189,6 +189,11 @@ Try out Superset's [quickstart](https://superset.apache.org/docs/quickstart/) gu
|
||||
- [Join our community's Slack](http://bit.ly/join-superset-slack)
|
||||
and please read our [Slack Community Guidelines](https://github.com/apache/superset/blob/master/CODE_OF_CONDUCT.md#slack-community-guidelines)
|
||||
- [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org). To join, simply send an email to [dev-subscribe@superset.apache.org](mailto:dev-subscribe@superset.apache.org)
|
||||
- Follow us on social media:
|
||||
[X](https://x.com/apachesuperset) |
|
||||
[LinkedIn](https://www.linkedin.com/company/apache-superset) |
|
||||
[Bluesky](https://bsky.app/profile/apachesuperset.bsky.social) |
|
||||
[Reddit](https://reddit.com/r/apache-superset)
|
||||
- If you want to help troubleshoot GitHub Issues involving the numerous database drivers that Superset supports, please consider adding your name and the databases you have access to on the [Superset Database Familiarity Rolodex](https://docs.google.com/spreadsheets/d/1U1qxiLvOX0kBTUGME1AHHi6Ywel6ECF8xk_Qy-V9R8c/edit#gid=0)
|
||||
- Join Superset's Town Hall and [Operational Model](https://preset.io/blog/the-superset-operational-model-wants-you/) recurring meetings. Meeting info is available on the [Superset Community Calendar](https://superset.apache.org/community)
|
||||
|
||||
|
||||
145
SECURITY.md
Normal file
145
SECURITY.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Security Policy
|
||||
|
||||
This is a project of the [Apache Software Foundation](https://apache.org) and follows the
|
||||
ASF [vulnerability handling process](https://apache.org/security/#vulnerability-handling).
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
**⚠️ Please do not file GitHub issues for security vulnerabilities as they are public! ⚠️**
|
||||
|
||||
|
||||
Apache Software Foundation takes a rigorous standpoint in resolving the security issues
|
||||
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
|
||||
pertaining to its features and functionality.
|
||||
If you have any concern or believe you have found a vulnerability in Apache Superset,
|
||||
please get in touch with the Apache Superset Security Team privately at
|
||||
e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
|
||||
|
||||
More details can be found on the ASF website at
|
||||
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-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.
|
||||
|
||||
## Security Model
|
||||
|
||||
This section defines what Apache Superset considers a security issue and what it does not. It is the canonical reference for reporters, the Apache Superset Security Team, and any automated tool (LLM-based scanner, static analyzer, dependency tool) that needs to constrain its hypotheses to behaviors that genuinely violate the project's security policy.
|
||||
|
||||
The model is intentionally written in terms of principals, trust boundaries, and capability surface rather than in terms of specific files, functions, or libraries. New code paths inherit the model automatically.
|
||||
|
||||
### Trust Boundaries
|
||||
|
||||
Apache Superset's threat model assumes three trust boundaries.
|
||||
|
||||
1. *The Admin role* is a fully trusted operational principal. Anything an Admin can do through the documented user interface, REST API, or configuration system is an intended capability, not a vulnerability, even if individually powerful or destructive. The Admin role is, by policy, equivalent to operating-system-level trust over the Apache Superset application. This is unavoidable rather than aspirational: an Admin can, for example, register new database connections of arbitrary type, execute arbitrary SQL through SQL Lab, render Jinja templates that resolve to SQL or rendered HTML, and override application configuration. Granting Admin is functionally equivalent to granting shell access on the host, which is the reasoning behind treating it as a trust boundary in the sense of MITRE CNA Operational Rules 4.1.
|
||||
|
||||
2. *The operator* is whoever deploys, configures, and runs Apache Superset. Behaviors that depend on deployment-time decisions are the operator's responsibility, not Apache Superset's. This includes the values of secrets, the network reachability of the application and its data sources, the choice of database connectors and cache backends, the selection of feature flags, the destinations of notifications, and the trust placed in third-party plugins. Defaults that fail closed are the responsibility of the Apache Superset codebase. Defaults that fail open must be accompanied by a documented hardening requirement; applying that hardening is the operator's responsibility, while shipping an undocumented or unflagged fail-open default is a codebase issue.
|
||||
|
||||
3. *The Apache Superset codebase* is responsible for enforcing the role and capability matrix below across its product surface. A failure to enforce, anywhere in that surface, is in scope. The codebase's commitments are limited to the role and capability matrix and to controls Apache Superset's own documentation (this file and the linked Security documentation) explicitly positions as security boundaries; configurable hardening that operators can layer on top is treated separately under *Vulnerability Scope* below.
|
||||
|
||||
### Roles and Capabilities
|
||||
|
||||
Apache Superset ships with the following first-class principals. Detailed permission definitions live in the [Security documentation](https://superset.apache.org/docs/security).
|
||||
|
||||
| Principal | Read data | Write objects | Execute SQL | Manage databases | Manage users, roles, RLS |
|
||||
|---|---|---|---|---|---|
|
||||
| Public (anonymous) | none by default | no | no | no | no |
|
||||
| Gamma | only granted datasets | own charts and dashboards on granted datasets | no by default (requires the `sql_lab` role) | no | no |
|
||||
| Alpha | all data sources | own charts, dashboards, and datasets | no by default (requires the `sql_lab` role) | data upload to existing databases only | no |
|
||||
| Admin | all | all | yes | yes | yes |
|
||||
| Embedded guest token | data sources reachable through the embedded dashboards the token authorizes | no | no | no | no |
|
||||
|
||||
The `sql_lab` role is *additive*: it grants the SQL Lab permission set on top of the base role above, and is the only path by which Gamma or Alpha gain SQL execution capability. Database access is still scoped per the base role's grants. Admin includes SQL Lab access by default.
|
||||
|
||||
Deployments may grant or revoke individual view-menu permissions, which shifts the boundary for that deployment but does not redefine the model. Any custom role created by an operator inherits the same principle: its capabilities are whatever the operator has explicitly granted it. The Public principal follows the same rule: operators may grant the Public role read access to specific datasets or dashboards (typically for anonymous reporting use cases), which shifts the boundary for that deployment without redefining the model.
|
||||
|
||||
### Vulnerability Scope
|
||||
|
||||
The test for whether a finding is in scope is a single question:
|
||||
|
||||
> *Does this finding let a principal perform an action the role and capability matrix above does not entitle them to?*
|
||||
|
||||
If yes, it is in scope. If no, it is out of scope. The lists below apply that test to the classes Apache Superset most commonly receives reports about; they are illustrative, not exhaustive.
|
||||
|
||||
*In Scope*
|
||||
|
||||
- A user, embedded guest, or anonymous visitor reads, modifies, or deletes data outside their granted set. Includes object-level access bypass on charts, dashboards, datasets, saved queries, tags, annotations, and similar per-object endpoints, and row-level-security rule bypass.
|
||||
- A user supplies input that the codebase should sanitize or parameterize but does not, causing arbitrary SQL, template code, or scripts to execute. Includes injection through Jinja templates, SQL-construction paths, and any field the codebase passes to a query engine or template engine.
|
||||
- A user bypasses authentication, fixates or reuses another user's session, or reaches an authenticated endpoint without logging in.
|
||||
- An embedded guest token authorizes actions outside the dashboard it was issued for, or can be forged, replayed, or escalated to a higher principal.
|
||||
- Apache Superset, acting on behalf of an unprivileged user, fetches an outbound URL the user controls in a feature where Apache Superset itself, not the operator, controls the outbound destination set (server-side request forgery).
|
||||
- An Apache Superset default fails open without an accompanying documented hardening requirement. The codebase is responsible for shipping fail-closed defaults or for documenting the hardening required when a default fails open; failures of that responsibility are in scope (see *Trust Boundaries*).
|
||||
- A user bypasses a control Apache Superset documents specifically as a security boundary. This includes row-level security, the access checks tied to the role and capability matrix above, and any feature whose documentation positions it as security-relevant. The codebase commits to enforcing those controls; bypasses are in scope regardless of which principal triggers them.
|
||||
- A user causes a script to execute in another user's browser through a field the codebase renders to that other user (cross-site scripting), or causes cross-origin leakage of authenticated session state or data.
|
||||
- A user reaches a route, page, or API endpoint that requires a role they do not have.
|
||||
|
||||
*Out of Scope*
|
||||
|
||||
- Any action an Admin role can perform through documented configuration, API, or UI. The Admin role is a trusted operational principal by policy. Per MITRE CNA Operational Rules 4.1, a qualifying vulnerability must violate a security policy; behavior within a documented trust boundary does not.
|
||||
- Deployment or operator decisions: the values of secrets and tokens, whether internal networks are reachable from the server, which database connectors or cache backends are enabled, which feature flags are set, where notifications are delivered, and which third-party plugins are loaded.
|
||||
- Compromise, modification, or malicious control of trusted backend infrastructure. Apache Superset assumes the integrity of its metastore, cache backends (for example Redis or Memcached), message brokers, secret stores, and other operator-managed infrastructure. Findings that require an attacker to read from, write to, or otherwise tamper with these systems, including injecting malicious state, serialized objects, cache entries, task metadata, configuration, or database records, are post-compromise scenarios and do not constitute vulnerabilities in Apache Superset itself. A finding remains in scope only if an unprivileged user can cause such modification through a vulnerability in Apache Superset.
|
||||
- The continued presence of expired key-value or metastore-cache entries that have not yet been deleted from the metadata database. Such entries are excluded from reads once expired, are purged opportunistically on write, and are removed in bulk by the scheduled `prune_key_value` maintenance task; their lingering until purged is an eventual-cleanup property, not a security boundary, and does not constitute a vulnerability.
|
||||
- How a downstream application (spreadsheet program, email client, browser handling user-downloaded files) interprets output Apache Superset produced for it.
|
||||
- Findings without a reproducible proof of concept against a supported release. The burden of demonstrating exploitability rests with the reporter; findings closed for lack of a proof of concept may be refiled if one is later produced.
|
||||
- Brute force, rate limiting, denial of service, or resource exhaustion that does not bypass a documented control.
|
||||
- Missing security headers, banner or version disclosure, user or object enumeration through error messages or timing, and similar low-impact information disclosure that does not enable a further concrete exploit.
|
||||
- Bypasses of configurable defense-in-depth hardening that Apache Superset does not document as a security boundary. Apache Superset is not a SQL or database firewall: operator-deployable filters such as SQL function or table denylists, URI restrictions on already-authorized database connectors, and similar belt-and-braces controls are provided to let operators layer hardening on top of the role and capability matrix, not as firewall-grade guarantees the codebase commits to. Findings against such hardening are improvements, not vulnerabilities, unless the documentation positions the specific control as security-relevant.
|
||||
- Hardening suggestions that improve defense in depth but do not violate the security model.
|
||||
|
||||
Findings in third-party dependencies fall into two cases. A finding in a transitive dependency, or in an operator-selected dependency that Apache Superset does not ship, is out of scope and should be reported to the dependency's maintainers. A finding caused by Apache Superset pinning a known-vulnerable version of a direct dependency it ships, or using a dependency in a way that creates a vulnerability the dependency itself does not have, remains in scope. Dependency findings in the official Apache Superset Docker image that fall into the first case can be remediated by extending the image at release time.
|
||||
|
||||
When uncertain whether a finding falls in scope, please file it through the reporting process above. The triage team will classify it and explain the reasoning if it is closed as out of scope.
|
||||
|
||||
**Outcome of Reports**
|
||||
|
||||
Reports that are deemed out of scope for a CVE but represent valid security best practices or hardening opportunities are typically converted into public GitHub issues, where the community can contribute fixes alongside the maintainers. The triage decision and reasoning are communicated back to the reporter in either case.
|
||||
|
||||
**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
|
||||
|
||||
- [Apache Superset documentation](https://superset.apache.org/docs/security)
|
||||
- [Common Vulnerabilities and Exposures by release](https://superset.apache.org/docs/security/cves)
|
||||
- [How Security Vulnerabilities are Reported & Handled in Apache Superset (Blog)](https://preset.io/blog/how-security-vulnerabilities-are-reported-and-handled-in-apache-superset/)
|
||||
|
||||
**For LLM agents and automated scanners**
|
||||
|
||||
This file is the canonical source of truth for what Apache Superset considers a security vulnerability. Automated tooling (LLM-based code scanners, static analyzers, dependency tools) should treat the Security Model section as authoritative when classifying findings. The repository's [AGENTS.md](AGENTS.md) file contains a short pointer to this document for agents that read AGENTS.md as their entry point.
|
||||
53
UPDATING.md
53
UPDATING.md
@@ -24,6 +24,59 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Duration formatter precision
|
||||
|
||||
The `DURATION` number formatter now uses `Intl.DurationFormat` for locale-aware output. By default, sub-second fields are omitted, so values that previously displayed fractional seconds with `pretty-ms`, such as `10500` milliseconds rendering as `10.5s`, now render as `10s`.
|
||||
|
||||
To preserve sub-second precision in custom duration formatters, enable `formatSubMilliseconds`.
|
||||
|
||||
### Cache warmup authenticates via SUPERSET_CACHE_WARMUP_USER
|
||||
|
||||
The `cache-warmup` Celery task now drives a real WebDriver session for reliable authentication and reads the user to authenticate as from the new `SUPERSET_CACHE_WARMUP_USER` config option. It no longer consults `CACHE_WARMUP_EXECUTORS` for the warmup path. `SUPERSET_CACHE_WARMUP_USER` defaults to `None`, so the task fails fast with a clear message until you set it. Operators who previously relied on `CACHE_WARMUP_EXECUTORS` for cache warmup must set `SUPERSET_CACHE_WARMUP_USER` to a dedicated least-privilege user with access to the dashboards they want warmed up before the next warmup run.
|
||||
|
||||
### YDB now uses a native sqlglot dialect
|
||||
|
||||
YDB SQL parsing now relies on the dedicated [`ydb-sqlglot-plugin`](https://pypi.org/project/ydb-sqlglot-plugin/) dialect, which registers itself with sqlglot automatically. YDB users must install this plugin (e.g., via `pip install "apache-superset[ydb]"`) to avoid a `ValueError` when Superset parses YDB queries.
|
||||
|
||||
### Embedded dashboards enforce configured Allowed Domains for postMessage
|
||||
|
||||
The embedded dashboard page now validates the origin of incoming `postMessage` events against the dashboard's configured **Allowed Domains**. The server-rendered embedded page exposes the configured domains in its bootstrap payload, and the frontend rejects message events whose origin is not in that list.
|
||||
|
||||
Enforcement only applies when the Allowed Domains list is non-empty. If the list is empty (the default), any origin is accepted, so there is no behavior change for embeds that did not configure Allowed Domains.
|
||||
|
||||
### Default guest/async JWT secrets are rejected at startup
|
||||
|
||||
Superset already refuses to start in production (non-debug, non-testing) when `SECRET_KEY` is left at its built-in default, and when `GUEST_TOKEN_JWT_SECRET` is left at its default while `EMBEDDED_SUPERSET` is enabled. This behavior is extended to `GLOBAL_ASYNC_QUERIES_JWT_SECRET`: if the `GLOBAL_ASYNC_QUERIES` feature flag is enabled and the secret is still the publicly known default (`test-secret-change-me`), Superset logs a clear error and refuses to start.
|
||||
|
||||
As with the existing `SECRET_KEY` check, this only fails in production. In debug mode, testing mode, or under the test runner, a warning is logged instead of exiting, so local development is unaffected.
|
||||
|
||||
To resolve the error, set a strong random value in `superset_config.py`:
|
||||
|
||||
```python
|
||||
GLOBAL_ASYNC_QUERIES_JWT_SECRET = "<output of: openssl rand -base64 42>"
|
||||
```
|
||||
|
||||
The check is only active when the relevant feature is enabled, so deployments that do not use global async queries (or embedding) are not affected.
|
||||
|
||||
### Dataset import validates catalog against the target connection
|
||||
|
||||
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
|
||||
|
||||
If you relied on importing datasets with a non-default catalog, enable "Allow changing catalogs" on the target connection, or set the dataset's catalog to the connection's default before importing.
|
||||
|
||||
### Extension supply-chain controls (denylist + version policy)
|
||||
|
||||
Two opt-in static gates control which extensions are allowed to load:
|
||||
|
||||
- `EXTENSION_DENYLIST` refuses extensions matching an id (every version) or `id@version` (a single version), e.g. `["compromised-extension", "other-ext@1.2.3"]`.
|
||||
- `EXTENSION_VERSION_POLICY` enforces a minimum version per extension id, e.g. `{"acme.widget": "1.2.0"}` (PEP 440 comparison); a release below the minimum is refused.
|
||||
|
||||
Both default to empty (no behavior change). They apply to both the `LOCAL_EXTENSIONS` and `EXTENSIONS_PATH` load paths.
|
||||
|
||||
### Dynamic Group By respects the sort toggle for display values
|
||||
|
||||
The Dynamic Group By chart customization now orders its display values according to the "Sort display control values" toggle: ascending (A–Z), descending (Z–A), or the dataset's source order when the toggle is unset. Previously the dropdown always sorted alphabetically. Existing dashboards where the toggle was never set will show options in source order instead of A–Z; open the customization and enable the toggle to restore alphabetical ordering.
|
||||
|
||||
### Granular Export Controls
|
||||
|
||||
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
|
||||
|
||||
@@ -61,6 +61,31 @@ services:
|
||||
volumes:
|
||||
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./docker/nginx/templates:/etc/nginx/templates:ro
|
||||
# Wait for the webpack dev server's manifest.json to be served before
|
||||
# starting nginx. This prevents 404s on static assets at startup. The
|
||||
# probe targets host.docker.internal so it works regardless of whether
|
||||
# the dev server runs in the superset-node container
|
||||
# (BUILD_SUPERSET_FRONTEND_IN_DOCKER=true, the default) or directly on
|
||||
# the host (BUILD_SUPERSET_FRONTEND_IN_DOCKER=false).
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
url="http://host.docker.internal:9000/static/assets/manifest.json"
|
||||
max_attempts=150 # ~5 minutes at 2s intervals
|
||||
echo "Waiting for webpack dev server at $url..."
|
||||
attempt=0
|
||||
until curl -sf --max-time 5 -o /dev/null "$url"; do
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
|
||||
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Webpack dev server is ready; starting nginx."
|
||||
exec nginx -g 'daemon off;'
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
|
||||
@@ -80,7 +80,23 @@ case "${1}" in
|
||||
;;
|
||||
app)
|
||||
echo "Starting web app (using development server)..."
|
||||
flask run -p $PORT --reload --debugger --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
|
||||
|
||||
# Environment-based debugger control for security
|
||||
# Only enable Werkzeug interactive debugger when explicitly requested
|
||||
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
|
||||
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
|
||||
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
|
||||
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
|
||||
export FLASK_DEBUG=1
|
||||
DEBUGGER_FLAG="--debugger"
|
||||
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
|
||||
else
|
||||
export FLASK_DEBUG=0
|
||||
DEBUGGER_FLAG="--no-debugger"
|
||||
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
|
||||
fi
|
||||
|
||||
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
|
||||
;;
|
||||
app-gunicorn)
|
||||
echo "Starting web app..."
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
# Configuration for docker-compose-light.yml - disables Redis and uses minimal services
|
||||
|
||||
# Import all settings from the main config first
|
||||
import os
|
||||
|
||||
from flask_caching.backends.filesystemcache import FileSystemCache
|
||||
|
||||
from superset_config import * # noqa: F403
|
||||
@@ -36,3 +38,32 @@ THUMBNAIL_CACHE_CONFIG = CACHE_CONFIG
|
||||
|
||||
# Disable Celery entirely for lightweight mode
|
||||
CELERY_CONFIG = None # type: ignore[assignment,misc]
|
||||
|
||||
# Honor SUPERSET_FEATURE_<NAME> env vars on top of any flags inherited from
|
||||
# superset_config. Lets local dev/e2e enable features (e.g. EMBEDDED_SUPERSET)
|
||||
# without editing shipped config files. Only the literal string "true"
|
||||
# (case-insensitive) is treated as enabled — "1"/"yes"/"on" are not, matching
|
||||
# the strict-string convention used elsewhere in Superset's env parsing.
|
||||
FEATURE_FLAGS = {
|
||||
**FEATURE_FLAGS, # noqa: F405
|
||||
**{
|
||||
name[len("SUPERSET_FEATURE_") :]: value.strip().lower() == "true"
|
||||
for name, value in os.environ.items()
|
||||
if name.startswith("SUPERSET_FEATURE_")
|
||||
},
|
||||
}
|
||||
|
||||
if os.environ.get("SUPERSET_FEATURE_EMBEDDED_SUPERSET", "").strip().lower() == "true":
|
||||
# Disable Talisman so /embedded/<uuid> doesn't return X-Frame-Options:SAMEORIGIN.
|
||||
# Without this, browsers refuse to render Superset inside an iframe from a
|
||||
# different origin (i.e. the embedded SDK use case). Production/CI configures
|
||||
# Talisman with explicit `frame-ancestors`; for the lightweight local stack we
|
||||
# just turn it off.
|
||||
TALISMAN_ENABLED = False
|
||||
|
||||
# Guest tokens (used by the embedded SDK) inherit the "Public" role's perms.
|
||||
# Out of the box Public has zero perms, so embedded dashboards immediately fail
|
||||
# their first call (`/api/v1/me/roles/`) with 403. Mirror Public to Gamma —
|
||||
# the standard read-only viewer role — so the embedded flow can authenticate
|
||||
# and load dashboard data in local dev.
|
||||
PUBLIC_ROLE_LIKE = "Gamma"
|
||||
|
||||
@@ -86,6 +86,39 @@ instead requires a cachelib object.
|
||||
|
||||
See [Async Queries via Celery](/admin-docs/configuration/async-queries-celery) for details.
|
||||
|
||||
## Celery beat
|
||||
|
||||
Superset has a Celery task that will periodically warm up the cache based on different strategies.
|
||||
To use it, add the following to your `superset_config.py`:
|
||||
|
||||
```python
|
||||
from celery.schedules import crontab
|
||||
from superset.config import CeleryConfig
|
||||
|
||||
# User that will be used to authenticate and render dashboards for cache warmup
|
||||
SUPERSET_CACHE_WARMUP_USER = "user_with_permission_to_dashboards"
|
||||
|
||||
# Extend the default CeleryConfig to add cache warmup schedule
|
||||
class CustomCeleryConfig(CeleryConfig):
|
||||
beat_schedule = {
|
||||
**CeleryConfig.beat_schedule,
|
||||
'cache-warmup-hourly': {
|
||||
'task': 'cache-warmup',
|
||||
'schedule': crontab(minute=0, hour='*'), # hourly
|
||||
'kwargs': {
|
||||
'strategy_name': 'top_n_dashboards',
|
||||
'top_n': 5,
|
||||
'since': '7 days ago',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
CELERY_CONFIG = CustomCeleryConfig
|
||||
```
|
||||
|
||||
This will cache the top 5 most popular dashboards every hour. For other
|
||||
strategies, check the `superset/tasks/cache.py` file.
|
||||
|
||||
## Caching Thumbnails
|
||||
|
||||
This is an optional feature that can be turned on by activating its [feature flag](/admin-docs/configuration/configuring-superset#feature-flags) on config:
|
||||
|
||||
@@ -157,8 +157,15 @@ superset load_examples
|
||||
superset init
|
||||
|
||||
# To start a development web server on port 8088, use -p to bind to another port
|
||||
superset run -p 8088 --with-threads --reload --debugger
|
||||
superset run -p 8088 --with-threads --reload
|
||||
|
||||
# For debugging with interactive console (⚠️ localhost only)
|
||||
# superset run -p 8088 --with-threads --reload --debugger
|
||||
```
|
||||
|
||||
:::warning Security Note
|
||||
The `--debugger` flag enables Werkzeug's interactive console at `/console`. Only use this for local development and never bind to `0.0.0.0` or expose the server to networks when debugging is enabled.
|
||||
:::
|
||||
|
||||
If everything worked, you should be able to navigate to `hostname:port` in your browser (e.g.
|
||||
locally by default at `localhost:8088`) and login using the username and password you created.
|
||||
|
||||
@@ -157,8 +157,15 @@ superset load_examples
|
||||
superset init
|
||||
|
||||
# To start a development web server on port 8088, use -p to bind to another port
|
||||
superset run -p 8088 --with-threads --reload --debugger
|
||||
superset run -p 8088 --with-threads --reload
|
||||
|
||||
# For debugging with interactive console (⚠️ localhost only)
|
||||
# superset run -p 8088 --with-threads --reload --debugger
|
||||
```
|
||||
|
||||
:::warning Security Note
|
||||
The `--debugger` flag enables Werkzeug's interactive console at `/console`. Only use this for local development and never bind to `0.0.0.0` or expose the server to networks when debugging is enabled.
|
||||
:::
|
||||
|
||||
If everything worked, you should be able to navigate to `hostname:port` in your browser (e.g.
|
||||
locally by default at `localhost:8088`) and login using the username and password you created.
|
||||
|
||||
@@ -88,7 +88,6 @@ using our `docker compose` constructs to support production-type use-cases. For
|
||||
environments, we recommend using [minikube](https://minikube.sigs.k8s.io/docs/start/) along
|
||||
our [installing on k8s](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
||||
documentation.
|
||||
configured to be secure.
|
||||
:::
|
||||
|
||||
### Supported environment variables
|
||||
@@ -103,6 +102,8 @@ Affecting the Docker build process:
|
||||
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
|
||||
- **SUPERSET_LOG_LEVEL (default=info)**: Can be set to debug, info, warning, error, critical
|
||||
for more verbose logging
|
||||
- **SUPERSET_DEBUG_ENABLED (default=false)**: Enable Werkzeug debugger with interactive console.
|
||||
Set to `true` for debugging: `SUPERSET_DEBUG_ENABLED=true docker compose up`
|
||||
|
||||
For more env vars that affect your configuration, see this
|
||||
[superset_config.py](https://github.com/apache/superset/blob/master/docker/pythonpath_dev/superset_config.py)
|
||||
|
||||
@@ -335,6 +335,92 @@ npm run build-translation
|
||||
pybabel compile -d superset/translations
|
||||
```
|
||||
|
||||
### Backfilling missing translations with AI
|
||||
|
||||
For languages with many untranslated strings, the repo includes a script that
|
||||
uses Claude AI to generate draft translations for any missing entries. All
|
||||
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
|
||||
comment so that human reviewers know they need to be checked before merging.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
```bash
|
||||
pip install -r superset/translations/requirements.txt
|
||||
```
|
||||
|
||||
Claude Code must be installed and authenticated (`claude --version` should
|
||||
work). The script calls `claude -p` internally — no separate API key is needed.
|
||||
|
||||
#### Step 1 — Build the translation index
|
||||
|
||||
The index captures every already-translated string in every language and
|
||||
serves as cross-language context for the AI. Rebuild it whenever `.po` files
|
||||
change significantly:
|
||||
|
||||
```bash
|
||||
python scripts/translations/build_translation_index.py
|
||||
# Writes: superset/translations/translation_index.json
|
||||
```
|
||||
|
||||
#### Step 2 — Preview with a dry run
|
||||
|
||||
Check what would be translated without writing anything:
|
||||
|
||||
```bash
|
||||
python scripts/translations/backfill_po.py --lang fr --limit 20 --dry-run
|
||||
```
|
||||
|
||||
Output shows each string, its translation, and a context tag:
|
||||
- No tag — 3+ reference languages available (high confidence)
|
||||
- `[ctx:N]` — only N other languages have this string (lower confidence)
|
||||
- `[ctx:0]` — no other language has this string yet; English alone used
|
||||
|
||||
#### Step 3 — Run the backfill
|
||||
|
||||
```bash
|
||||
python scripts/translations/backfill_po.py --lang fr
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--lang LANG` | required | ISO language code (`fr`, `de`, `ja`, …) |
|
||||
| `--batch-size N` | 50 | Strings per Claude request |
|
||||
| `--limit N` | unlimited | Stop after N entries |
|
||||
| `--min-context N` | 0 | Skip entries with fewer than N reference translations |
|
||||
| `--model MODEL` | `claude-sonnet-4-6` | Claude model to use |
|
||||
| `--dry-run` | off | Print without writing |
|
||||
| `--no-fuzzy` | off | Don't mark entries as fuzzy |
|
||||
|
||||
Use `--min-context 2` to skip strings that have fewer than 2 reference
|
||||
translations in other languages. Those strings are more likely to be ambiguous
|
||||
(short labels, UI fragments) where the correct meaning can't be inferred
|
||||
without additional context.
|
||||
|
||||
#### Step 4 — Review and commit
|
||||
|
||||
Open the target `.po` file and search for `fuzzy`. For each generated entry:
|
||||
|
||||
1. Verify the translation is correct for the UI context.
|
||||
2. Remove the `# Machine-translated via backfill_po.py` comment and the
|
||||
`#, fuzzy` flag line once you are satisfied.
|
||||
3. If the translation is wrong, correct the `msgstr` before removing the flag.
|
||||
4. Commit the `.po` file — do **not** commit `translation_index.json` (it is
|
||||
gitignored and regenerated locally).
|
||||
|
||||
#### Running via npm
|
||||
|
||||
From `superset-frontend/`:
|
||||
|
||||
```bash
|
||||
# Rebuild index
|
||||
npm run translations:build-index
|
||||
|
||||
# Backfill (pass arguments after --)
|
||||
npm run translations:backfill -- --lang fr --dry-run
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
### Python
|
||||
|
||||
@@ -917,6 +917,23 @@ const config: Config = {
|
||||
footer: {
|
||||
links: [],
|
||||
copyright: `
|
||||
<div class="footer__social-links">
|
||||
<a href="https://bit.ly/join-superset-slack" target="_blank" rel="noopener noreferrer" title="Join us on Slack" aria-label="Slack">
|
||||
<img src="/img/community/slack-symbol.svg" alt="Slack" />
|
||||
</a>
|
||||
<a href="https://x.com/apachesuperset" target="_blank" rel="noopener noreferrer" title="Follow us on X" aria-label="X">
|
||||
<img src="/img/community/x-symbol.svg" alt="X" />
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/apache-superset" target="_blank" rel="noopener noreferrer" title="Follow us on LinkedIn" aria-label="LinkedIn">
|
||||
<img src="/img/community/linkedin-symbol.svg" alt="LinkedIn" />
|
||||
</a>
|
||||
<a href="https://bsky.app/profile/apachesuperset.bsky.social" target="_blank" rel="noopener noreferrer" title="Follow us on Bluesky" aria-label="Bluesky">
|
||||
<img src="/img/community/bluesky-symbol.svg" alt="Bluesky" />
|
||||
</a>
|
||||
<a href="https://reddit.com/r/apache-superset" target="_blank" rel="noopener noreferrer" title="Follow us on Reddit" aria-label="Reddit">
|
||||
<img src="/img/community/reddit-symbol.svg" alt="Reddit" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer__ci-services">
|
||||
<span>CI powered by</span>
|
||||
<a href="https://www.netlify.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/netlify.png" alt="Netlify" title="Netlify - Deploy Previews" /></a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"copyright": {
|
||||
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a> | \n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a> | \n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a> | \n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a> | \n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a> | \n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
|
||||
"message": "\n <div class=\"footer__social-links\">\n <a href=\"https://bit.ly/join-superset-slack\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Join us on Slack\" aria-label=\"Slack\">\n <img src=\"/img/community/slack-symbol.svg\" alt=\"Slack\" />\n </a>\n <a href=\"https://x.com/apachesuperset\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on X\" aria-label=\"X\">\n <img src=\"/img/community/x-symbol.svg\" alt=\"X\" />\n </a>\n <a href=\"https://www.linkedin.com/company/apache-superset\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on LinkedIn\" aria-label=\"LinkedIn\">\n <img src=\"/img/community/linkedin-symbol.svg\" alt=\"LinkedIn\" />\n </a>\n <a href=\"https://bsky.app/profile/apachesuperset.bsky.social\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on Bluesky\" aria-label=\"Bluesky\">\n <img src=\"/img/community/bluesky-symbol.svg\" alt=\"Bluesky\" />\n </a>\n <a href=\"https://reddit.com/r/apache-superset\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on Reddit\" aria-label=\"Reddit\">\n <img src=\"/img/community/reddit-symbol.svg\" alt=\"Reddit\" />\n </a>\n </div>\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/admin-docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a> | \n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a> | \n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a> | \n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a> | \n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a> | \n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
|
||||
"description": "The footer copyright"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,17 @@
|
||||
command = "yarn install && yarn build"
|
||||
# Output directory (relative to base)
|
||||
publish = "build"
|
||||
# Skip builds when no docs changes (exit 0 = skip, exit 1 = build)
|
||||
# Checks for changes in docs/ and README.md (which gets pulled into docs)
|
||||
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../README.md"
|
||||
# Skip builds when no docs changes (exit 0 = skip, non-zero = build).
|
||||
# Checks for changes in docs/ and README.md (which gets pulled into docs).
|
||||
#
|
||||
# $CACHED_COMMIT_REF is the last *deployed* commit. On a PR's first build it
|
||||
# is empty, so the original `git diff` errored and Netlify fell back to
|
||||
# building -- which is why every PR built a docs preview once even with no
|
||||
# docs changes. When it is empty we instead diff the whole branch against its
|
||||
# merge-base with master, so non-docs PRs are skipped from the very first
|
||||
# build. Subsequent builds (and the master production build) keep the cheaper
|
||||
# incremental $CACHED_COMMIT_REF diff. Any failure exits non-zero -> build.
|
||||
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" "$COMMIT_REF" -- . ../README.md; else git fetch origin master --depth=100 >/dev/null 2>&1; git diff --quiet "$(git merge-base origin/master "$COMMIT_REF" 2>/dev/null || echo origin/master)" "$COMMIT_REF" -- . ../README.md; fi'
|
||||
|
||||
[build.environment]
|
||||
# Node version matching docs/.nvmrc
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"version:remove:components": "node scripts/manage-versions.mjs remove components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@ant-design/icons": "^6.2.5",
|
||||
"@docusaurus/core": "^3.10.1",
|
||||
"@docusaurus/faster": "^3.10.1",
|
||||
"@docusaurus/plugin-client-redirects": "^3.10.1",
|
||||
@@ -70,13 +70,13 @@
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.33",
|
||||
"@swc/core": "^1.15.40",
|
||||
"antd": "^6.4.3",
|
||||
"baseline-browser-mapping": "^2.10.31",
|
||||
"baseline-browser-mapping": "^2.10.33",
|
||||
"caniuse-lite": "^1.0.30001793",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"js-yaml": "^4.2.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
@@ -101,16 +101,16 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.4",
|
||||
"webpack": "^5.107.0"
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -128,7 +128,13 @@
|
||||
"react-redux": "^9.2.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"swagger-client": "3.37.3"
|
||||
"swagger-client": "3.37.3",
|
||||
"lodash": "4.18.1",
|
||||
"lodash-es": "4.18.1",
|
||||
"yaml": "1.10.3",
|
||||
"uuid": "11.1.1",
|
||||
"serialize-javascript": "7.0.5",
|
||||
"d3-color": "3.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
@@ -260,10 +260,45 @@ a > span > svg {
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
padding-top: 90px;
|
||||
padding-top: 130px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.footer__social-links {
|
||||
background-color: #173036;
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer__social-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.footer__social-links a:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.footer__social-links img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
/* The brand SVGs ship in their native colors (e.g. Slack's dark aubergine,
|
||||
X's near-black), which disappear on the dark footer. Render them all as
|
||||
uniform white silhouettes. The icons are single-path glyphs whose
|
||||
counters (the LinkedIn "in", Slack gaps, Reddit face) are transparent
|
||||
cut-outs, so they stay legible against the footer background. */
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.footer__ci-services {
|
||||
background-color: #0d3e49;
|
||||
color: #e1e1e1;
|
||||
@@ -309,6 +344,21 @@ a > span > svg {
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 996px) {
|
||||
.footer {
|
||||
padding-top: 120px;
|
||||
}
|
||||
|
||||
.footer__social-links {
|
||||
top: 44px;
|
||||
gap: 20px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.footer__social-links img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.footer__ci-services {
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
|
||||
21
docs/static/img/community/reddit-symbol.svg
vendored
Normal file
21
docs/static/img/community/reddit-symbol.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="#FF4500">
|
||||
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12c-.688 0-1.25.561-1.25 1.25 0 .687.562 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
21
docs/static/img/community/slack-symbol.svg
vendored
Normal file
21
docs/static/img/community/slack-symbol.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="#4A154B">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.124 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.52 2.521h-2.522V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.166 0a2.528 2.528 0 0 1 2.521 2.522v6.312zm-2.521 10.124a2.528 2.528 0 0 1 2.521 2.522A2.528 2.528 0 0 1 15.166 24a2.528 2.528 0 0 1-2.521-2.52v-2.522h2.521zm0-1.271a2.528 2.528 0 0 1-2.521-2.521 2.528 2.528 0 0 1 2.521-2.521h6.312A2.528 2.528 0 0 1 24 15.165a2.528 2.528 0 0 1-2.52 2.521h-6.313z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
432
docs/yarn.lock
432
docs/yarn.lock
@@ -212,14 +212,14 @@
|
||||
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
|
||||
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
|
||||
|
||||
"@ant-design/icons@^6.2.3":
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.3.tgz#66e1c7fdea009b9c3fab6964062bedc76f308ad8"
|
||||
integrity sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==
|
||||
"@ant-design/icons@^6.2.3", "@ant-design/icons@^6.2.5":
|
||||
version "6.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.5.tgz#31c142aa6ce5eaf99598aaead222f4c459693512"
|
||||
integrity sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.1"
|
||||
"@ant-design/icons-svg" "^4.4.2"
|
||||
"@rc-component/util" "^1.10.1"
|
||||
"@rc-component/util" "^1.11.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@ant-design/react-slick@~2.0.0":
|
||||
@@ -3021,10 +3021,10 @@
|
||||
os-homedir "^1.0.1"
|
||||
regexpu-core "^4.5.4"
|
||||
|
||||
"@pkgr/core@^0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz"
|
||||
integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
|
||||
"@pkgr/core@^0.3.6":
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.3.6.tgz#3569708bd4be4d8870ba32bf1c456dac81600d97"
|
||||
integrity sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==
|
||||
|
||||
"@pnpm/config.env-replace@^1.1.0":
|
||||
version "1.1.0"
|
||||
@@ -4033,86 +4033,86 @@
|
||||
dependencies:
|
||||
apg-lite "^1.0.4"
|
||||
|
||||
"@swc/core-darwin-arm64@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz#d84134fb80417d41128739f0b9014542e3ed9dd3"
|
||||
integrity sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==
|
||||
"@swc/core-darwin-arm64@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz#b05d715b04c4fd47baf59288233da85a683cc0bc"
|
||||
integrity sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==
|
||||
|
||||
"@swc/core-darwin-x64@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz#0badb9834071f1c6005986571d4a96359c1d7cd0"
|
||||
integrity sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==
|
||||
"@swc/core-darwin-x64@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz#3180daef5c1e47b435f8edd084509e0a5c0d883b"
|
||||
integrity sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz#b7577a825b59d98b6a9a5c991d842046efe1c34a"
|
||||
integrity sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==
|
||||
"@swc/core-linux-arm-gnueabihf@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz#18fcd3c70e48fdfae07c9f18751b1409ce1e5e84"
|
||||
integrity sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==
|
||||
|
||||
"@swc/core-linux-arm64-gnu@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz#304c48321494a18c67b2913c273b08674ee70d8c"
|
||||
integrity sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==
|
||||
"@swc/core-linux-arm64-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz#26304933922f2a8e3194770e404403fc25a19c89"
|
||||
integrity sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==
|
||||
|
||||
"@swc/core-linux-arm64-musl@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz#d116cbc04ccb4f4ee810da6bca79d4423605dbcd"
|
||||
integrity sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==
|
||||
"@swc/core-linux-arm64-musl@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz#3402dfba04ba7b8ea81f243e2f8fa2c336b54d03"
|
||||
integrity sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==
|
||||
|
||||
"@swc/core-linux-ppc64-gnu@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz#f5354dba36db9414305bab344c817d57b8b457c2"
|
||||
integrity sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==
|
||||
"@swc/core-linux-ppc64-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz#b3df9065cad352328c1eeef08a28fc9fe98785aa"
|
||||
integrity sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==
|
||||
|
||||
"@swc/core-linux-s390x-gnu@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz#016df9f4c9d7fd65b85ca9c558c5aec341f06da0"
|
||||
integrity sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==
|
||||
"@swc/core-linux-s390x-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz#58e5b601f641dde81b30626ef66a668701ec918f"
|
||||
integrity sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==
|
||||
|
||||
"@swc/core-linux-x64-gnu@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz#49f36558ede072e71999aa37f123367daed2a662"
|
||||
integrity sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==
|
||||
"@swc/core-linux-x64-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz#cf057dce0c148c53f2d30152baaf60ea29e5d59c"
|
||||
integrity sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==
|
||||
|
||||
"@swc/core-linux-x64-musl@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz#b096665f5cfeee2612325f301da5c1590b10d8f3"
|
||||
integrity sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==
|
||||
"@swc/core-linux-x64-musl@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz#21fb1a4d0193e9bbcd1469ecd36166d2e96e4006"
|
||||
integrity sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==
|
||||
|
||||
"@swc/core-win32-arm64-msvc@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz#f3101263a0dbaa173ec47638c9719d0b89838bd2"
|
||||
integrity sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==
|
||||
"@swc/core-win32-arm64-msvc@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz#1dba23b2b0db86b3d6d65da2abd627cc607a1fbc"
|
||||
integrity sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==
|
||||
|
||||
"@swc/core-win32-ia32-msvc@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz#eb981ef5613d42c9220559bdb0c8bc58cf6c3eb9"
|
||||
integrity sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==
|
||||
"@swc/core-win32-ia32-msvc@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz#b2da1e33165d469467b1046a2189db468da488eb"
|
||||
integrity sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==
|
||||
|
||||
"@swc/core-win32-x64-msvc@1.15.33":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz#a2fed9956933027ceb368857bac4bb4ee203d47c"
|
||||
integrity sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==
|
||||
"@swc/core-win32-x64-msvc@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz#3563f7e8ce8708f5fda43eb8e0956ef11e0da320"
|
||||
integrity sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==
|
||||
|
||||
"@swc/core@^1.15.33", "@swc/core@^1.7.39":
|
||||
version "1.15.33"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.33.tgz#2a6571c8aca961925f14beae52b3f43c18370fc6"
|
||||
integrity sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==
|
||||
"@swc/core@^1.15.40", "@swc/core@^1.7.39":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.40.tgz#941c949aa88c0d8d291f102f519f3c2c77701b90"
|
||||
integrity sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
"@swc/types" "^0.1.26"
|
||||
optionalDependencies:
|
||||
"@swc/core-darwin-arm64" "1.15.33"
|
||||
"@swc/core-darwin-x64" "1.15.33"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.15.33"
|
||||
"@swc/core-linux-arm64-gnu" "1.15.33"
|
||||
"@swc/core-linux-arm64-musl" "1.15.33"
|
||||
"@swc/core-linux-ppc64-gnu" "1.15.33"
|
||||
"@swc/core-linux-s390x-gnu" "1.15.33"
|
||||
"@swc/core-linux-x64-gnu" "1.15.33"
|
||||
"@swc/core-linux-x64-musl" "1.15.33"
|
||||
"@swc/core-win32-arm64-msvc" "1.15.33"
|
||||
"@swc/core-win32-ia32-msvc" "1.15.33"
|
||||
"@swc/core-win32-x64-msvc" "1.15.33"
|
||||
"@swc/core-darwin-arm64" "1.15.40"
|
||||
"@swc/core-darwin-x64" "1.15.40"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.15.40"
|
||||
"@swc/core-linux-arm64-gnu" "1.15.40"
|
||||
"@swc/core-linux-arm64-musl" "1.15.40"
|
||||
"@swc/core-linux-ppc64-gnu" "1.15.40"
|
||||
"@swc/core-linux-s390x-gnu" "1.15.40"
|
||||
"@swc/core-linux-x64-gnu" "1.15.40"
|
||||
"@swc/core-linux-x64-musl" "1.15.40"
|
||||
"@swc/core-win32-arm64-msvc" "1.15.40"
|
||||
"@swc/core-win32-ia32-msvc" "1.15.40"
|
||||
"@swc/core-win32-x64-msvc" "1.15.40"
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
@@ -4812,100 +4812,110 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.59.4", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz#c67bfee32caae9cb587dce1ac59c3bf43b659707"
|
||||
integrity sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==
|
||||
"@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
|
||||
integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.4"
|
||||
"@typescript-eslint/type-utils" "8.59.4"
|
||||
"@typescript-eslint/utils" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/type-utils" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.59.4", "@typescript-eslint/parser@^8.59.3":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.4.tgz#77d99e3b27663e7a22cf12c3fb769db509e5e93c"
|
||||
integrity sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==
|
||||
"@typescript-eslint/parser@8.60.0", "@typescript-eslint/parser@^8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.0.tgz#38d611b8e658cb10850d4975e8a175a222fbcd6a"
|
||||
integrity sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.59.4"
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz#5830535a0e7a3ae806e2669964f47a74c4bc6b0e"
|
||||
integrity sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==
|
||||
"@typescript-eslint/project-service@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
|
||||
integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.59.4"
|
||||
"@typescript-eslint/types" "^8.59.4"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.0"
|
||||
"@typescript-eslint/types" "^8.60.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz#507d1258c758147dac1adee9517a205a8ac1e046"
|
||||
integrity sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==
|
||||
"@typescript-eslint/scope-manager@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
|
||||
integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.59.4", "@typescript-eslint/tsconfig-utils@^8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz#218ba229d96dde35212e3a76a7d0a6bc831398be"
|
||||
integrity sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==
|
||||
"@typescript-eslint/tsconfig-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz#3af78c48956227a407dea9626b8db8ca53f130d2"
|
||||
integrity sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz#359fc53ba39a1f1860fddda40ebe5bfe0d87faed"
|
||||
integrity sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==
|
||||
"@typescript-eslint/tsconfig-utils@^8.60.0":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
|
||||
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
|
||||
|
||||
"@typescript-eslint/type-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
|
||||
integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/utils" "8.59.4"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.59.4", "@typescript-eslint/types@^8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.4.tgz#c29d5c21bfbaa8347ddc677d3ac1fcd2db0f848e"
|
||||
integrity sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==
|
||||
"@typescript-eslint/types@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.0.tgz#e77ad768e933263b1960b2fe79de75fe1cc6e7db"
|
||||
integrity sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz#d005e5e1fb425526f39685594bed34a04ad755ea"
|
||||
integrity sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==
|
||||
"@typescript-eslint/types@^8.60.0":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
|
||||
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz#c102196a44414481190041c99eea1d854e66001b"
|
||||
integrity sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.59.4"
|
||||
"@typescript-eslint/tsconfig-utils" "8.59.4"
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/project-service" "8.60.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.4.tgz#8ccd2b08aecc72c7efc0d7ac6695631d199d256e"
|
||||
integrity sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==
|
||||
"@typescript-eslint/utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
|
||||
integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.4"
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz#1ac23b747b011f5cbdb449da97769f6c5f3a9355"
|
||||
integrity sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==
|
||||
"@typescript-eslint/visitor-keys@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
|
||||
integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -5568,10 +5578,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.10.31, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.31"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz#9c6825f052601ce6974a90dd49683b1726887b0b"
|
||||
integrity sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==
|
||||
baseline-browser-mapping@^2.10.33, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.33"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz#27c299b096404978831958d429f48390424c4f9b"
|
||||
integrity sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -6538,14 +6548,9 @@ d3-chord@3:
|
||||
dependencies:
|
||||
d3-path "1 - 3"
|
||||
|
||||
"d3-color@1 - 2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz"
|
||||
integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
|
||||
|
||||
"d3-color@1 - 3", d3-color@3:
|
||||
"d3-color@1 - 2", "d3-color@1 - 3", d3-color@3, d3-color@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
||||
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
||||
|
||||
d3-contour@4:
|
||||
@@ -7253,10 +7258,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.21.4:
|
||||
version "5.21.5"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz#8f80167d009d8f01267ad61035e59fe5c94ac3a6"
|
||||
integrity sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==
|
||||
enhanced-resolve@^5.22.0:
|
||||
version "5.22.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz#c34bc3f414298496fc244b21bbe316440782da17"
|
||||
integrity sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.3.3"
|
||||
@@ -7517,13 +7522,13 @@ eslint-config-prettier@^10.1.8:
|
||||
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz"
|
||||
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
|
||||
|
||||
eslint-plugin-prettier@^5.5.5:
|
||||
version "5.5.5"
|
||||
resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz"
|
||||
integrity sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==
|
||||
eslint-plugin-prettier@^5.5.6:
|
||||
version "5.5.6"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz#363ebe4d769bce157ccdd8129ce3efd91dc62564"
|
||||
integrity sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.1"
|
||||
synckit "^0.11.12"
|
||||
synckit "^0.11.13"
|
||||
|
||||
eslint-plugin-react@^7.37.5:
|
||||
version "7.37.5"
|
||||
@@ -9336,7 +9341,7 @@ js-yaml@4.1.0:
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
js-yaml@=4.1.1, js-yaml@^4.1.0, js-yaml@^4.1.1:
|
||||
js-yaml@=4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
@@ -9351,6 +9356,13 @@ js-yaml@^3.13.1:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524"
|
||||
integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsdoc-type-pratt-parser@^4.0.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.8.0.tgz"
|
||||
@@ -9676,10 +9688,10 @@ locate-path@^7.1.0:
|
||||
dependencies:
|
||||
p-locate "^6.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
lodash-es@4.18.1, lodash-es@^4.17.21:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
|
||||
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
|
||||
|
||||
lodash.debounce@^4, lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
@@ -9701,12 +9713,7 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz"
|
||||
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
||||
|
||||
lodash@4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.18.1:
|
||||
lodash@4.17.21, lodash@4.18.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.18.1:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
|
||||
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
|
||||
@@ -12270,14 +12277,7 @@ pvutils@^1.1.3, pvutils@^1.1.5:
|
||||
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c"
|
||||
integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==
|
||||
|
||||
qs@^6.12.3:
|
||||
version "6.14.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c"
|
||||
integrity sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
qs@~6.15.1:
|
||||
qs@^6.12.3, qs@~6.15.1:
|
||||
version "6.15.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.2.tgz#fd55426d710403ddccc45e0f9eab16db7727ece9"
|
||||
integrity sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==
|
||||
@@ -13374,12 +13374,10 @@ serialize-error@^8.1.0:
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
serialize-javascript@7.0.5, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1"
|
||||
integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==
|
||||
|
||||
serve-handler@^6.1.7:
|
||||
version "6.1.7"
|
||||
@@ -14105,12 +14103,12 @@ swc-loader@^0.2.6, swc-loader@^0.2.7:
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
|
||||
synckit@^0.11.12:
|
||||
version "0.11.12"
|
||||
resolved "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz"
|
||||
integrity sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==
|
||||
synckit@^0.11.13:
|
||||
version "0.11.13"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.13.tgz#062a5ea57d81befc35892f8254de5c567e97c80a"
|
||||
integrity sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
"@pkgr/core" "^0.3.6"
|
||||
|
||||
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0, tapable@^2.3.3:
|
||||
version "2.3.3"
|
||||
@@ -14391,15 +14389,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.59.4:
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.4.tgz#834e3b53f4d1a764a985ceb8592c4a95d6a8da7c"
|
||||
integrity sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==
|
||||
typescript-eslint@^8.60.0:
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
|
||||
integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.59.4"
|
||||
"@typescript-eslint/parser" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/utils" "8.59.4"
|
||||
"@typescript-eslint/eslint-plugin" "8.60.0"
|
||||
"@typescript-eslint/parser" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
@@ -14733,15 +14731,10 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@8.3.2, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
"uuid@^11.1.0 || ^12 || ^13 || ^14.0.0":
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d"
|
||||
integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==
|
||||
uuid@11.1.1, uuid@8.3.2, "uuid@^11.1.0 || ^12 || ^13 || ^14.0.0", uuid@^8.3.2:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.1.tgz#f6d81d2e1c65d00762e5e29b16c5d2d995e208ad"
|
||||
integrity sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==
|
||||
|
||||
uvu@^0.5.0:
|
||||
version "0.5.6"
|
||||
@@ -14954,20 +14947,20 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
webpack-sources@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.4.1.tgz#009d110999ebd9fb3a6fa8d32eec6f84d940e65d"
|
||||
integrity sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==
|
||||
webpack-sources@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.0.tgz#87bf7f5801a4e985b1f1c92b64b9620a02f76d08"
|
||||
integrity sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==
|
||||
|
||||
webpack-virtual-modules@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
|
||||
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
|
||||
|
||||
webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.107.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.0.tgz#9e0d8d8baf24e76f058103f4f06ac6bb528b645a"
|
||||
integrity sha512-PSxeHk/dmLYZlnTU+vL1Gej6Evg5RNtl3flhxBresfznFnzxinHMzHKloHnywM/3ouQv7/AlZCswWDIkNSggUA==
|
||||
webpack@^5.107.2, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.107.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.2.tgz#dea14dcb177b46b29de15f952f7303691ee2b596"
|
||||
integrity sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.8"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
@@ -14978,7 +14971,7 @@ webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.28.1"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.21.4"
|
||||
enhanced-resolve "^5.22.0"
|
||||
es-module-lexer "^2.1.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
@@ -14991,7 +14984,7 @@ webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
tapable "^2.3.0"
|
||||
terser-webpack-plugin "^5.5.0"
|
||||
watchpack "^2.5.1"
|
||||
webpack-sources "^3.4.1"
|
||||
webpack-sources "^3.5.0"
|
||||
|
||||
webpackbar@^7.0.0:
|
||||
version "7.0.0"
|
||||
@@ -15146,9 +15139,9 @@ ws@^7.3.1:
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
ws@^8.18.0, ws@^8.2.3:
|
||||
version "8.18.3"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz"
|
||||
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
||||
version "8.20.1"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz"
|
||||
integrity sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==
|
||||
|
||||
wsl-utils@^0.1.0:
|
||||
version "0.1.0"
|
||||
@@ -15216,12 +15209,7 @@ yaml-ast-parser@0.0.43:
|
||||
resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz"
|
||||
integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
|
||||
|
||||
yaml@1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yaml@^1.10.0:
|
||||
yaml@1.10.2, yaml@1.10.3, yaml@^1.10.0:
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3"
|
||||
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
||||
|
||||
@@ -39,7 +39,7 @@ dependencies = [
|
||||
"apache-superset-core",
|
||||
"backoff>=1.8.0",
|
||||
"celery>=5.3.6, <6.0.0",
|
||||
"click>=8.0.3",
|
||||
"click>=8.4.0",
|
||||
"click-option-group",
|
||||
"colorama",
|
||||
"flask-cors>=6.0.0, <7.0",
|
||||
@@ -58,7 +58,7 @@ dependencies = [
|
||||
"flask-wtf>=1.1.0, <2.0",
|
||||
"geopy",
|
||||
"greenlet>=3.0.3, <=3.5.0",
|
||||
"gunicorn>=22.0.0; sys_platform != 'win32'",
|
||||
"gunicorn>=25.3.0, <26; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# holidays>=0.45 required for security fix
|
||||
"holidays>=0.45, <1",
|
||||
@@ -66,12 +66,12 @@ dependencies = [
|
||||
"isodate",
|
||||
"jsonpath-ng>=1.6.1, <2",
|
||||
"Mako>=1.2.2",
|
||||
"markdown>=3.0",
|
||||
"markdown>=3.10.2",
|
||||
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
|
||||
"marshmallow>=3.0, <4",
|
||||
"marshmallow-union>=0.1",
|
||||
"msgpack>=1.0.0, <1.2",
|
||||
"nh3>=0.2.11, <0.4",
|
||||
"nh3>=0.3.5, <0.4",
|
||||
"numpy>1.23.5, <2.3",
|
||||
"packaging",
|
||||
# --------------------------
|
||||
@@ -101,15 +101,15 @@ dependencies = [
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
|
||||
"sqlglot>=28.10.0, <29",
|
||||
"sqlglot>=30.8.0, <31",
|
||||
# newer pandas needs 0.9+
|
||||
"tabulate>=0.9.0, <1.0",
|
||||
"tabulate>=0.10.0, <1.0",
|
||||
"typing-extensions>=4, <5",
|
||||
"waitress; sys_platform == 'win32'",
|
||||
"watchdog>=6.0.0",
|
||||
"wtforms>=2.3.3, <4",
|
||||
"wtforms-json",
|
||||
"xlsxwriter>=3.0.7, <3.3",
|
||||
"xlsxwriter>=3.2.9, <3.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -123,7 +123,7 @@ bigquery = [
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
|
||||
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
|
||||
d1 = [
|
||||
"superset-engine-d1>=0.1.0",
|
||||
"sqlalchemy-d1>=0.1.0",
|
||||
@@ -135,11 +135,11 @@ databricks = [
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
]
|
||||
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
|
||||
denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
|
||||
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
|
||||
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
|
||||
drill = ["sqlalchemy-drill>=1.1.4, <2"]
|
||||
drill = ["sqlalchemy-drill>=1.1.10, <2"]
|
||||
druid = ["pydruid>=0.6.5,<0.7"]
|
||||
duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
|
||||
duckdb = ["duckdb>=1.5.2,<2", "duckdb-engine>=0.17.0"]
|
||||
dynamodb = ["pydynamodb>=0.4.2"]
|
||||
solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
|
||||
@@ -154,7 +154,7 @@ fastmcp = [
|
||||
]
|
||||
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=23.9.1"]
|
||||
gevent = ["gevent>=26.4.0"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
|
||||
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
|
||||
hive = [
|
||||
@@ -165,7 +165,7 @@ hive = [
|
||||
"thrift_sasl>=0.4.3, < 1.0.0",
|
||||
]
|
||||
impala = ["impyla>0.16.2, <0.23"]
|
||||
kusto = ["sqlalchemy-kusto>=3.0.0, <4"]
|
||||
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
|
||||
kylin = ["kylinpy>=2.8.1, <2.9"]
|
||||
mssql = ["pymssql>=2.2.8, <3"]
|
||||
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
|
||||
@@ -180,7 +180,7 @@ ocient = [
|
||||
oracle = ["cx-Oracle>8.0.0, <8.4"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.37.0, <2"]
|
||||
playwright = ["playwright>=1.60.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.328.0"]
|
||||
@@ -190,7 +190,7 @@ risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
|
||||
sqlite = ["syntaqlite>=0.1.0"]
|
||||
sqlite = ["syntaqlite>=0.1.0,<0.5.0"]
|
||||
spark = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
@@ -199,16 +199,16 @@ spark = [
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.7.21",
|
||||
"taos-ws-py>=0.3.8"
|
||||
"taos-ws-py>=0.6.9"
|
||||
]
|
||||
teradata = ["teradatasql>=16.20.0.23"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
vertica = ["sqlalchemy-vertica-python>= 0.5.9, < 0.7"]
|
||||
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
|
||||
netezza = ["nzalchemy>=11.0.2"]
|
||||
starrocks = ["starrocks>=1.0.0"]
|
||||
doris = ["pydoris>=1.0.0, <2.0.0"]
|
||||
oceanbase = ["oceanbase_py>=0.0.1"]
|
||||
ydb = ["ydb-sqlalchemy>=0.1.2"]
|
||||
oceanbase = ["oceanbase_py>=0.0.1.2"]
|
||||
ydb = ["ydb-sqlalchemy>=0.1.2", "ydb-sqlglot-plugin>=0.2.5"]
|
||||
development = [
|
||||
# no bounds for apache-superset-extensions-cli until a stable version
|
||||
"apache-superset-extensions-cli",
|
||||
@@ -220,21 +220,22 @@ development = [
|
||||
"openapi-spec-validator",
|
||||
"parameterized",
|
||||
"pip",
|
||||
"polib", # used by scripts/translations/ and their unit tests
|
||||
"pre-commit",
|
||||
"progress>=1.5,<2",
|
||||
"psutil",
|
||||
"pyfakefs",
|
||||
"pyinstrument>=4.0.2,<6",
|
||||
"pyinstrument>=5.1.2,<6",
|
||||
"pylint",
|
||||
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
|
||||
"pytest-asyncio",
|
||||
"pytest-cov",
|
||||
"pytest-mock",
|
||||
"python-ldap>=3.4.4",
|
||||
"python-ldap>=3.4.7",
|
||||
"ruff",
|
||||
"sqloxide",
|
||||
"statsd",
|
||||
"syntaqlite>=0.1.0",
|
||||
"syntaqlite>=0.4.2,<0.5.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -455,6 +456,7 @@ authorized_licenses = [
|
||||
"isc license (iscl)",
|
||||
"isc license",
|
||||
"mit",
|
||||
"mit and psf-2.0",
|
||||
"mit-cmu",
|
||||
"mozilla public license 2.0 (mpl 2.0)",
|
||||
"osi approved",
|
||||
|
||||
@@ -60,7 +60,7 @@ cffi==2.0.0
|
||||
# pynacl
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
click==8.2.1
|
||||
click==8.4.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# celery
|
||||
@@ -161,12 +161,12 @@ geopy==2.4.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
google-auth==2.43.0
|
||||
# via shillelagh
|
||||
greenlet==3.1.1
|
||||
greenlet==3.5.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
gunicorn==25.3.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
# via wsproto
|
||||
@@ -176,7 +176,7 @@ holidays==0.82
|
||||
# via apache-superset (pyproject.toml)
|
||||
humanize==4.12.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
idna==3.10
|
||||
idna==3.15
|
||||
# via
|
||||
# email-validator
|
||||
# requests
|
||||
@@ -208,12 +208,12 @@ kombu==5.5.3
|
||||
# via celery
|
||||
limits==5.1.0
|
||||
# via flask-limiter
|
||||
mako==1.3.11
|
||||
mako==1.3.12
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
markdown==3.8.1
|
||||
markdown==3.10.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
@@ -241,7 +241,7 @@ msgpack==1.0.8
|
||||
# via apache-superset (pyproject.toml)
|
||||
msgspec==0.19.0
|
||||
# via flask-session
|
||||
nh3==0.2.21
|
||||
nh3==0.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
numexpr==2.10.2
|
||||
# via -r requirements/base.in
|
||||
@@ -415,13 +415,13 @@ sqlalchemy-utils==0.42.0
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
# flask-appbuilder
|
||||
sqlglot==28.10.0
|
||||
sqlglot==30.8.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.9.0
|
||||
tabulate==0.10.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
trio==0.30.0
|
||||
# via
|
||||
@@ -451,7 +451,7 @@ tzdata==2025.2
|
||||
# pandas
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==2.6.3
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# requests
|
||||
@@ -490,7 +490,7 @@ wtforms-json==0.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
xlrd==2.0.1
|
||||
# via pandas
|
||||
xlsxwriter==3.0.9
|
||||
xlsxwriter==3.2.9
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# pandas
|
||||
|
||||
@@ -52,7 +52,7 @@ attrs==25.3.0
|
||||
# referencing
|
||||
# requests-cache
|
||||
# trio
|
||||
authlib==1.6.9
|
||||
authlib==1.6.12
|
||||
# via fastmcp
|
||||
babel==2.17.0
|
||||
# via
|
||||
@@ -130,7 +130,7 @@ charset-normalizer==3.4.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# requests
|
||||
click==8.2.1
|
||||
click==8.4.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -219,7 +219,7 @@ docstring-parser==0.17.0
|
||||
# via cyclopts
|
||||
docutils==0.22.2
|
||||
# via rich-rst
|
||||
duckdb==1.4.2
|
||||
duckdb==1.5.3
|
||||
# via
|
||||
# apache-superset
|
||||
# duckdb-engine
|
||||
@@ -317,7 +317,7 @@ flask-wtf==1.2.2
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
fonttools==4.55.0
|
||||
fonttools==4.60.2
|
||||
# via matplotlib
|
||||
freezegun==1.5.1
|
||||
# via apache-superset
|
||||
@@ -331,7 +331,7 @@ geopy==2.4.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
gevent==24.2.1
|
||||
gevent==26.4.0
|
||||
# via apache-superset
|
||||
google-api-core==2.23.0
|
||||
# via
|
||||
@@ -346,6 +346,7 @@ google-auth==2.43.0
|
||||
# google-api-core
|
||||
# google-auth-oauthlib
|
||||
# google-cloud-bigquery
|
||||
# google-cloud-bigquery-storage
|
||||
# google-cloud-core
|
||||
# pandas-gbq
|
||||
# pydata-google-auth
|
||||
@@ -360,7 +361,7 @@ google-cloud-bigquery==3.27.0
|
||||
# apache-superset
|
||||
# pandas-gbq
|
||||
# sqlalchemy-bigquery
|
||||
google-cloud-bigquery-storage==2.19.1
|
||||
google-cloud-bigquery-storage==2.26.0
|
||||
# via pandas-gbq
|
||||
google-cloud-core==2.4.1
|
||||
# via google-cloud-bigquery
|
||||
@@ -372,7 +373,7 @@ googleapis-common-protos==1.66.0
|
||||
# via
|
||||
# google-api-core
|
||||
# grpcio-status
|
||||
greenlet==3.1.1
|
||||
greenlet==3.5.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -388,7 +389,7 @@ grpcio==1.71.0
|
||||
# grpcio-status
|
||||
grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
gunicorn==23.0.0
|
||||
gunicorn==25.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -421,7 +422,7 @@ humanize==4.12.3
|
||||
# apache-superset
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
idna==3.10
|
||||
idna==3.15
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# anyio
|
||||
@@ -506,12 +507,12 @@ limits==5.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-limiter
|
||||
mako==1.3.11
|
||||
mako==1.3.12
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
markdown==3.8.1
|
||||
markdown==3.10.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -565,7 +566,7 @@ msgspec==0.19.0
|
||||
# flask-session
|
||||
mysqlclient==2.2.6
|
||||
# via apache-superset
|
||||
nh3==0.2.21
|
||||
nh3==0.3.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -677,6 +678,8 @@ ply==3.11
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# jsonpath-ng
|
||||
polib==1.2.0
|
||||
# via apache-superset
|
||||
polyline==2.0.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -699,7 +702,7 @@ proto-plus==1.25.0
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-bigquery-storage
|
||||
protobuf==4.25.8
|
||||
protobuf==5.29.6
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-bigquery-storage
|
||||
@@ -765,7 +768,7 @@ pygments==2.20.0
|
||||
# rich
|
||||
pyhive==0.7.0
|
||||
# via apache-superset
|
||||
pyinstrument==4.4.0
|
||||
pyinstrument==5.1.2
|
||||
# via apache-superset
|
||||
pyjwt==2.12.0
|
||||
# via
|
||||
@@ -835,9 +838,9 @@ python-dotenv==1.2.2
|
||||
# apache-superset
|
||||
# fastmcp
|
||||
# pydantic-settings
|
||||
python-ldap==3.4.4
|
||||
python-ldap==3.4.7
|
||||
# via apache-superset
|
||||
python-multipart==0.0.20
|
||||
python-multipart==0.0.29
|
||||
# via mcp
|
||||
pytz==2025.2
|
||||
# via
|
||||
@@ -985,7 +988,7 @@ sqlalchemy-utils==0.42.0
|
||||
# apache-superset
|
||||
# apache-superset-core
|
||||
# flask-appbuilder
|
||||
sqlglot==28.10.0
|
||||
sqlglot==30.8.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -1002,9 +1005,9 @@ starlette==0.49.1
|
||||
# via mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
syntaqlite==0.1.0
|
||||
syntaqlite==0.4.2
|
||||
# via apache-superset
|
||||
tabulate==0.9.0
|
||||
tabulate==0.10.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -1069,7 +1072,7 @@ url-normalize==2.2.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# requests-cache
|
||||
urllib3==2.6.3
|
||||
urllib3==2.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# botocore
|
||||
@@ -1087,7 +1090,7 @@ vine==5.1.0
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
virtualenv==20.29.2
|
||||
virtualenv==20.36.1
|
||||
# via pre-commit
|
||||
watchdog==6.0.0
|
||||
# via
|
||||
@@ -1137,7 +1140,7 @@ xlrd==2.0.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pandas
|
||||
xlsxwriter==3.0.9
|
||||
xlsxwriter==3.2.9
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -31,7 +31,9 @@ PATTERNS = {
|
||||
r"^superset/",
|
||||
r"^scripts/",
|
||||
r"^setup\.py",
|
||||
r"^pyproject\.toml$",
|
||||
r"^requirements/.+\.txt",
|
||||
r"^pyproject\.toml",
|
||||
r"^.pylintrc",
|
||||
],
|
||||
"frontend": [
|
||||
@@ -155,7 +157,7 @@ def main(event_type: str, sha: str, repo: str) -> None:
|
||||
|
||||
def get_git_sha() -> str:
|
||||
return os.getenv("GITHUB_SHA") or subprocess.check_output( # noqa: S603
|
||||
["git", "rev-parse", "HEAD"] # noqa: S607
|
||||
["git", "rev-parse", "HEAD"] # noqa: S603, S607
|
||||
).strip().decode("utf-8")
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ if [ ${#js_ts_files[@]} -gt 0 ]; then
|
||||
echo "$output" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -n "$output" ] && echo "$output"
|
||||
if [ -n "$output" ]; then echo "$output"; fi
|
||||
else
|
||||
echo "No JavaScript/TypeScript files to lint"
|
||||
fi
|
||||
|
||||
686
scripts/translations/backfill_po.py
Normal file
686
scripts/translations/backfill_po.py
Normal file
@@ -0,0 +1,686 @@
|
||||
# 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.
|
||||
"""Backfill missing translations in a .po file using Claude AI.
|
||||
|
||||
For each untranslated (empty msgstr) entry in the target language, the script
|
||||
sends the English source string along with all available translations in other
|
||||
languages to Claude as context, then writes the AI-generated translation back
|
||||
into the .po file marked as #, fuzzy for human review.
|
||||
|
||||
Usage:
|
||||
# Build the translation index first (one-time or when .po files change)
|
||||
python scripts/translations/build_translation_index.py
|
||||
|
||||
# Backfill French translations
|
||||
python scripts/translations/backfill_po.py --lang fr
|
||||
|
||||
# Dry run (print what would be translated without writing)
|
||||
python scripts/translations/backfill_po.py --lang de --dry-run
|
||||
|
||||
# Limit to 100 entries and use a specific model
|
||||
python scripts/translations/backfill_po.py --lang es --limit 100 \
|
||||
--model claude-opus-4-6
|
||||
|
||||
Options:
|
||||
--lang LANG ISO language code to backfill (required)
|
||||
--batch-size N Number of strings per Claude request (default: 50)
|
||||
--limit N Stop after translating N entries (default: unlimited)
|
||||
--min-context N Skip entries with fewer than N existing translations across
|
||||
reference languages (default: 0 — translate everything)
|
||||
--model MODEL Claude model ID (default: claude-sonnet-4-6)
|
||||
--index PATH Path to translation_index.json (default: auto-detect)
|
||||
--dry-run Print translations without writing to .po file
|
||||
--no-fuzzy Do not mark generated translations as fuzzy (default: mark fuzzy)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import polib # type: ignore[import-untyped]
|
||||
except ImportError:
|
||||
print("polib is required. Run: pip install polib", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
TRANSLATIONS_DIR = Path(__file__).parent.parent.parent / "superset" / "translations"
|
||||
DEFAULT_INDEX = TRANSLATIONS_DIR / "translation_index.json"
|
||||
DEFAULT_MODEL = "claude-sonnet-4-6"
|
||||
DEFAULT_BATCH_SIZE = 50
|
||||
|
||||
_ASF_LICENSE_HEADER = """\
|
||||
# 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.
|
||||
"""
|
||||
|
||||
# Language names for the prompt, keyed by ISO code
|
||||
LANGUAGE_NAMES: dict[str, str] = {
|
||||
"ar": "Arabic",
|
||||
"ca": "Catalan",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fa": "Persian (Farsi)",
|
||||
"fr": "French",
|
||||
"it": "Italian",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"mi": "Māori",
|
||||
"nl": "Dutch",
|
||||
"pl": "Polish",
|
||||
"pt": "Portuguese",
|
||||
"pt_BR": "Brazilian Portuguese",
|
||||
"ru": "Russian",
|
||||
"sk": "Slovak",
|
||||
"sl": "Slovenian",
|
||||
"tr": "Turkish",
|
||||
"uk": "Ukrainian",
|
||||
"zh": "Chinese (Simplified)",
|
||||
"zh_TW": "Chinese (Traditional)",
|
||||
}
|
||||
|
||||
|
||||
def _ensure_license_header(po_path: Path, *, dry_run: bool = False) -> None:
|
||||
"""Prepend the ASF license header to the .po file if it is missing."""
|
||||
content = po_path.read_text(encoding="utf-8")
|
||||
if "Licensed to the Apache Software Foundation" not in content:
|
||||
if dry_run:
|
||||
print(
|
||||
f"[dry-run] Would add ASF license header to {po_path}", file=sys.stderr
|
||||
)
|
||||
else:
|
||||
po_path.write_text(_ASF_LICENSE_HEADER + content, encoding="utf-8")
|
||||
print(f"Added ASF license header to {po_path}", file=sys.stderr)
|
||||
|
||||
|
||||
def _lang_name(code: str) -> str:
|
||||
"""Return a human-readable language name for an ISO language code."""
|
||||
return LANGUAGE_NAMES.get(code, code)
|
||||
|
||||
|
||||
def _plural_key(msgid: str, msgid_plural: str) -> str:
|
||||
"""Build the translation index key used for pluralized entries."""
|
||||
return f"{msgid}\x00{msgid_plural}"
|
||||
|
||||
|
||||
def _is_missing(entry: polib.POEntry) -> bool:
|
||||
"""Return True for entries that need a translation."""
|
||||
if entry.obsolete:
|
||||
return False
|
||||
if entry.msgid_plural:
|
||||
return not any(v for v in entry.msgstr_plural.values())
|
||||
return not entry.msgstr
|
||||
|
||||
|
||||
def _context_langs(
|
||||
item: dict[str, Any], index: dict[str, Any], target_lang: str
|
||||
) -> list[str]:
|
||||
"""Return sorted list of language codes that have translations for this entry."""
|
||||
key = item["index_key"]
|
||||
if key not in index:
|
||||
return []
|
||||
return sorted(
|
||||
lang for lang, val in index[key].items() if lang != target_lang and val
|
||||
)
|
||||
|
||||
|
||||
def _context_count(
|
||||
item: dict[str, Any], index: dict[str, Any], target_lang: str
|
||||
) -> int:
|
||||
"""Return the number of other-language translations available for this entry."""
|
||||
return len(_context_langs(item, index, target_lang))
|
||||
|
||||
|
||||
def _render_item(
|
||||
i: int,
|
||||
item: dict[str, Any],
|
||||
index: dict[str, Any],
|
||||
target_lang: str,
|
||||
reference_langs_sorted: list[str],
|
||||
) -> list[str]:
|
||||
"""Render one batch entry as prompt lines."""
|
||||
lines: list[str] = []
|
||||
ctx = _context_count(item, index, target_lang)
|
||||
if ctx == 0:
|
||||
lines.append(
|
||||
f"--- [{i}] (no reference translations — translate conservatively) ---"
|
||||
)
|
||||
else:
|
||||
plural = "s" if ctx != 1 else ""
|
||||
lines.append(f"--- [{i}] ({ctx} reference translation{plural}) ---")
|
||||
lines.append(f"English: {json.dumps(item['msgid'], ensure_ascii=False)}")
|
||||
if item.get("msgid_plural"):
|
||||
plural_json = json.dumps(item["msgid_plural"], ensure_ascii=False)
|
||||
lines.append(f"English plural: {plural_json}")
|
||||
key = item["index_key"]
|
||||
if key in index and reference_langs_sorted:
|
||||
for lang in reference_langs_sorted:
|
||||
val = index[key].get(lang)
|
||||
if val is None:
|
||||
continue
|
||||
if isinstance(val, dict):
|
||||
forms = "; ".join(
|
||||
f"[{k}] {json.dumps(v, ensure_ascii=False)}" for k, v in val.items()
|
||||
)
|
||||
lines.append(f"{_lang_name(lang)}: {forms}")
|
||||
else:
|
||||
lines.append(
|
||||
f"{_lang_name(lang)}: {json.dumps(val, ensure_ascii=False)}"
|
||||
)
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def build_prompt(
|
||||
target_lang: str,
|
||||
batch: list[dict[str, Any]],
|
||||
index: dict[str, Any],
|
||||
) -> str:
|
||||
"""Build the Claude prompt for a batch of entries."""
|
||||
lang_name = _lang_name(target_lang)
|
||||
|
||||
# Collect which other languages actually have translations for this batch
|
||||
reference_langs: set[str] = set()
|
||||
for item in batch:
|
||||
key = item["index_key"]
|
||||
if key in index:
|
||||
reference_langs.update(
|
||||
lang for lang, val in index[key].items() if lang != target_lang and val
|
||||
)
|
||||
reference_langs_sorted = sorted(reference_langs)
|
||||
|
||||
lines: list[str] = [
|
||||
"You are a professional translator specializing in software UI strings.",
|
||||
f"Translate the following English strings into {lang_name} ({target_lang}).",
|
||||
"",
|
||||
"Rules:",
|
||||
"- Preserve all format placeholders exactly: %(name)s, {name}, %s, %d, etc.",
|
||||
"- Preserve HTML tags if present.",
|
||||
"- Keep the same tone and register as the reference translations.",
|
||||
"- For plural forms, provide translations for all plural forms"
|
||||
" required by the language.",
|
||||
"- Return ONLY a JSON object mapping each numeric index (as a string)"
|
||||
" to its translation.",
|
||||
"- Do not add any explanation, preamble, or markdown fences.",
|
||||
"",
|
||||
"Important: Many strings are short fragments or single words that are"
|
||||
" ambiguous in English (e.g. 'Scale' could mean a measurement scale,"
|
||||
" to scale an image, or fish scales). Use the translations in other"
|
||||
" languages as your primary signal for which meaning is intended —"
|
||||
" they collectively disambiguate the intended sense. When no"
|
||||
" other-language translations are available for an entry, translate"
|
||||
" conservatively based on the most common meaning in a data"
|
||||
" visualization UI context.",
|
||||
"",
|
||||
]
|
||||
|
||||
if reference_langs_sorted:
|
||||
lines.append(
|
||||
f"Reference translations are provided per string where available "
|
||||
f"({', '.join(_lang_name(lc) for lc in reference_langs_sorted)})."
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
lines.append("Strings to translate:")
|
||||
lines.append("")
|
||||
|
||||
for i, item in enumerate(batch):
|
||||
lines.extend(_render_item(i, item, index, target_lang, reference_langs_sorted))
|
||||
|
||||
# Add guidance on plural form counts per language whenever ANY entry in
|
||||
# the batch is plural — batches mix singular and plural in .po order, so
|
||||
# gating on the first entry would silently drop the guidance whenever
|
||||
# the plural entries happen to land after a singular one.
|
||||
if any(item.get("msgid_plural") for item in batch):
|
||||
lines.append(
|
||||
"Note: provide ALL plural forms required by the target language "
|
||||
"(e.g. French needs 2, Russian needs 3, Arabic needs 6)."
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
lines.append(
|
||||
'Expected output format: {"0": "<translation>", "1": "<translation>", ...}'
|
||||
)
|
||||
lines.append("(keys are the numeric indices of the strings above)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def parse_response(text: str, batch_size: int) -> dict[int, str]:
|
||||
"""Parse the JSON object from Claude's response."""
|
||||
# Strip any accidental markdown fences
|
||||
text = re.sub(r"^```[^\n]*\n", "", text.strip())
|
||||
text = re.sub(r"\n```$", "", text)
|
||||
try:
|
||||
raw = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(
|
||||
f"Could not parse response as JSON: {exc}\n\nResponse:\n{text}"
|
||||
) from exc
|
||||
# _process_batches only catches ValueError/RuntimeError, so a non-object
|
||||
# response (list, scalar, null) must surface as ValueError rather than
|
||||
# bubbling up an AttributeError from .items() and aborting the whole run.
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError(
|
||||
f"Expected a JSON object mapping indices to translations, "
|
||||
f"got {type(raw).__name__}.\n\nResponse:\n{text}"
|
||||
)
|
||||
# Preserve dict/list values as JSON strings so plural responses (where
|
||||
# v is a dict of plural forms) can be re-parsed downstream by
|
||||
# _apply_translation's json.loads. str(v) on a dict produces Python
|
||||
# repr ({'0': 'x'}) which is not valid JSON.
|
||||
return {
|
||||
int(k): (
|
||||
json.dumps(v, ensure_ascii=False) if isinstance(v, (dict, list)) else str(v)
|
||||
)
|
||||
for k, v in raw.items()
|
||||
if str(k).isdigit()
|
||||
}
|
||||
|
||||
|
||||
def translate_batch(
|
||||
model: str,
|
||||
target_lang: str,
|
||||
batch: list[dict[str, Any]],
|
||||
index: dict[str, Any],
|
||||
) -> dict[int, str]:
|
||||
"""Send a batch of strings to Claude via `claude -p`.
|
||||
|
||||
Returns a dict mapping batch index to translated string.
|
||||
"""
|
||||
claude_bin = shutil.which("claude")
|
||||
if not claude_bin:
|
||||
raise RuntimeError(
|
||||
"claude CLI not found. Install Claude Code or add it to PATH."
|
||||
)
|
||||
prompt = build_prompt(target_lang, batch, index)
|
||||
# Pipe the prompt over stdin rather than passing it as argv: a single batch
|
||||
# with many reference languages can grow into the tens of KB and approach
|
||||
# ARG_MAX on some platforms.
|
||||
# claude_bin is resolved via shutil.which — not user-controlled input
|
||||
result = subprocess.run( # noqa: S603
|
||||
[claude_bin, "--model", model, "-p"],
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"claude exited with code {result.returncode}:\n{result.stderr}"
|
||||
)
|
||||
return parse_response(result.stdout.strip(), len(batch))
|
||||
|
||||
|
||||
def _apply_plural_translation(entry: polib.POEntry, translation: str) -> None:
|
||||
"""Distribute a model response across the entry's plural forms.
|
||||
|
||||
Model may return a JSON dict ({"0": "form0", "1": "form1"}), a JSON list
|
||||
(["form0", "form1"], also valid since plural forms are ordered), a JSON
|
||||
scalar (a single translation that fills every form), or a plain non-JSON
|
||||
string (older models that ignore the JSON instruction).
|
||||
"""
|
||||
try:
|
||||
plural_value = json.loads(translation)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
for k in entry.msgstr_plural:
|
||||
entry.msgstr_plural[k] = translation
|
||||
return
|
||||
|
||||
if isinstance(plural_value, dict):
|
||||
entry.msgstr_plural = {int(k): str(v) for k, v in plural_value.items()}
|
||||
return
|
||||
|
||||
if isinstance(plural_value, list) and plural_value:
|
||||
# Distribute list items across plural form indices in order; if the
|
||||
# model returned fewer forms than the language requires, repeat the
|
||||
# last form rather than leaving slots blank.
|
||||
forms = [str(v) for v in plural_value]
|
||||
for k in sorted(entry.msgstr_plural):
|
||||
entry.msgstr_plural[k] = forms[k] if k < len(forms) else forms[-1]
|
||||
return
|
||||
|
||||
# Scalar (or empty list) — broadcast to every form.
|
||||
fill = str(plural_value) if plural_value not in (None, []) else translation
|
||||
for k in entry.msgstr_plural:
|
||||
entry.msgstr_plural[k] = fill
|
||||
|
||||
|
||||
def _apply_translation(
|
||||
entry: polib.POEntry,
|
||||
translation: str,
|
||||
item: dict[str, Any],
|
||||
model: str,
|
||||
mark_fuzzy: bool,
|
||||
) -> None:
|
||||
"""Write a translation string into a POEntry and add attribution."""
|
||||
if entry.msgid_plural:
|
||||
_apply_plural_translation(entry, translation)
|
||||
else:
|
||||
entry.msgstr = translation
|
||||
|
||||
if mark_fuzzy and "fuzzy" not in entry.flags:
|
||||
entry.flags.append("fuzzy")
|
||||
|
||||
refs = item["context_langs"]
|
||||
refs_tag = f" [refs: {', '.join(refs)}]" if refs else " [no refs]"
|
||||
attribution = f"Machine-translated via backfill_po.py ({model}){refs_tag}"
|
||||
if entry.tcomment:
|
||||
if attribution not in entry.tcomment:
|
||||
entry.tcomment = f"{entry.tcomment}\n{attribution}"
|
||||
else:
|
||||
entry.tcomment = attribution
|
||||
|
||||
|
||||
def _build_batch_items(
|
||||
entries: list[polib.POEntry],
|
||||
index: dict[str, Any],
|
||||
lang: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert a list of POEntries into the dict format used by translate_batch."""
|
||||
items: list[dict[str, Any]] = []
|
||||
for entry in entries:
|
||||
if entry.msgid_plural:
|
||||
item: dict[str, Any] = {
|
||||
"msgid": entry.msgid,
|
||||
"msgid_plural": entry.msgid_plural,
|
||||
"index_key": _plural_key(entry.msgid, entry.msgid_plural),
|
||||
"is_plural": True,
|
||||
}
|
||||
else:
|
||||
item = {
|
||||
"msgid": entry.msgid,
|
||||
"index_key": entry.msgid,
|
||||
"is_plural": False,
|
||||
}
|
||||
item["context_langs"] = _context_langs(item, index, lang)
|
||||
item["context_count"] = len(item["context_langs"])
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def _process_batches(
|
||||
missing: list[polib.POEntry],
|
||||
index: dict[str, Any],
|
||||
lang: str,
|
||||
batch_size: int,
|
||||
model: str,
|
||||
dry_run: bool,
|
||||
mark_fuzzy: bool,
|
||||
cat: polib.POFile | None = None,
|
||||
po_path: Path | None = None,
|
||||
) -> tuple[int, int]:
|
||||
"""Translate missing entries in batches. Returns (translated, failed) counts.
|
||||
|
||||
When ``cat`` and ``po_path`` are provided and ``dry_run`` is False, the
|
||||
catalog is saved to disk after each batch that produced at least one
|
||||
successful translation. This means a crash mid-run only loses the in-flight
|
||||
batch rather than every batch translated so far.
|
||||
"""
|
||||
translated_count = 0
|
||||
failed_count = 0
|
||||
for batch_start in range(0, len(missing), batch_size):
|
||||
batch_entries = missing[batch_start : batch_start + batch_size]
|
||||
batch_items = _build_batch_items(batch_entries, index, lang)
|
||||
end = min(batch_start + batch_size, len(missing))
|
||||
print(
|
||||
f" Translating entries {batch_start + 1}–{end} of {len(missing)} …",
|
||||
file=sys.stderr,
|
||||
)
|
||||
try:
|
||||
translations = translate_batch(model, lang, batch_items, index)
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
print(f" ERROR in batch starting at {batch_start}: {exc}", file=sys.stderr)
|
||||
failed_count += len(batch_entries)
|
||||
continue
|
||||
batch_applied = 0
|
||||
for i, entry in enumerate(batch_entries):
|
||||
translation = translations.get(i)
|
||||
if translation is None:
|
||||
print(
|
||||
f" WARNING: no translation returned for index {i} "
|
||||
f"(msgid: {entry.msgid[:60]!r})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
failed_count += 1
|
||||
continue
|
||||
if dry_run:
|
||||
ctx = batch_items[i]["context_count"]
|
||||
ctx_tag = f" [ctx:{ctx}]" if ctx < 3 else ""
|
||||
print(
|
||||
f" [{lang}]{ctx_tag} {entry.msgid[:60]!r} → {translation[:60]!r}"
|
||||
)
|
||||
else:
|
||||
_apply_translation(
|
||||
entry, translation, batch_items[i], model, mark_fuzzy
|
||||
)
|
||||
batch_applied += 1
|
||||
translated_count += 1
|
||||
if (
|
||||
not dry_run
|
||||
and batch_applied > 0
|
||||
and cat is not None
|
||||
and po_path is not None
|
||||
):
|
||||
cat.save()
|
||||
print(
|
||||
f" Saved {po_path} ({batch_applied} entry(ies) in this batch).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return translated_count, failed_count
|
||||
|
||||
|
||||
def backfill(
|
||||
lang: str,
|
||||
*,
|
||||
batch_size: int = DEFAULT_BATCH_SIZE,
|
||||
limit: int | None = None,
|
||||
min_context: int = 0,
|
||||
model: str = DEFAULT_MODEL,
|
||||
index_path: Path = DEFAULT_INDEX,
|
||||
dry_run: bool = False,
|
||||
mark_fuzzy: bool = True,
|
||||
) -> None:
|
||||
"""Backfill missing translations in the target language's .po file."""
|
||||
# Defense against path traversal: ``lang`` lands in a filesystem path
|
||||
# without further sanitization, so reject anything that isn't an
|
||||
# ISO 639-1/639-2 code with an optional ISO 3166 region (e.g. ``pt_BR``).
|
||||
if not re.fullmatch(r"[a-z]{2,3}(_[A-Z]{2})?", lang):
|
||||
print(
|
||||
f"Invalid language code: {lang!r} "
|
||||
"(expected ISO 639 code, optionally with _<REGION>, e.g. 'fr' or 'pt_BR')",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
po_path = TRANSLATIONS_DIR / lang / "LC_MESSAGES" / "messages.po"
|
||||
if not po_path.exists():
|
||||
print(f"No .po file found for language '{lang}': {po_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not index_path.exists():
|
||||
print(
|
||||
f"Translation index not found at {index_path}.\n"
|
||||
"Run: python scripts/translations/build_translation_index.py",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print("Loading translation index …", file=sys.stderr)
|
||||
with open(index_path, encoding="utf-8") as f:
|
||||
index: dict[str, Any] = json.load(f)
|
||||
|
||||
_ensure_license_header(po_path, dry_run=dry_run)
|
||||
|
||||
print(f"Loading {po_path} …", file=sys.stderr)
|
||||
cat = polib.pofile(str(po_path))
|
||||
|
||||
missing: list[polib.POEntry] = [e for e in cat if e.msgid and _is_missing(e)]
|
||||
print(f"Found {len(missing)} untranslated entries for '{lang}'.", file=sys.stderr)
|
||||
|
||||
if min_context > 0:
|
||||
before = len(missing)
|
||||
missing = [
|
||||
e
|
||||
for e in missing
|
||||
if _context_count(
|
||||
{
|
||||
"index_key": (
|
||||
_plural_key(e.msgid, e.msgid_plural)
|
||||
if e.msgid_plural
|
||||
else e.msgid
|
||||
)
|
||||
},
|
||||
index,
|
||||
lang,
|
||||
)
|
||||
>= min_context
|
||||
]
|
||||
skipped = before - len(missing)
|
||||
print(
|
||||
f"Skipping {skipped} entries with fewer than {min_context} reference "
|
||||
f"translation(s) (use --min-context 0 to include them).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if limit is not None:
|
||||
missing = missing[:limit]
|
||||
print(f"Limiting to {limit} entries.", file=sys.stderr)
|
||||
|
||||
if not missing:
|
||||
print("Nothing to do.", file=sys.stderr)
|
||||
return
|
||||
|
||||
translated_count, failed_count = _process_batches(
|
||||
missing,
|
||||
index,
|
||||
lang,
|
||||
batch_size,
|
||||
model,
|
||||
dry_run,
|
||||
mark_fuzzy,
|
||||
cat=cat,
|
||||
po_path=po_path,
|
||||
)
|
||||
|
||||
print(
|
||||
f"\nDone. Translated: {translated_count}, Failed/skipped: {failed_count}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not dry_run and translated_count > 0:
|
||||
print(
|
||||
f"Translations written to {po_path} (marked #, fuzzy for review).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Parse CLI arguments and run translation backfill."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Backfill missing .po translations using Claude AI",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lang", required=True, help="ISO language code (e.g. fr, de, ja)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
type=int,
|
||||
default=DEFAULT_BATCH_SIZE,
|
||||
help=f"Strings per Claude request (default: {DEFAULT_BATCH_SIZE})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Maximum number of entries to translate (default: unlimited)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default=DEFAULT_MODEL,
|
||||
help=f"Claude model ID (default: {DEFAULT_MODEL})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--index",
|
||||
type=Path,
|
||||
default=DEFAULT_INDEX,
|
||||
help=f"Path to translation_index.json (default: {DEFAULT_INDEX})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print translations without modifying the .po file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-context",
|
||||
type=int,
|
||||
default=0,
|
||||
metavar="N",
|
||||
help=(
|
||||
"Skip entries with fewer than N reference translations in other languages "
|
||||
"(default: 0 = translate everything). Strings with low context are more "
|
||||
"likely to be ambiguous single words or fragments — set to e.g. 2 to only "
|
||||
"translate strings that have been confirmed in at least 2 other languages."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-fuzzy",
|
||||
dest="mark_fuzzy",
|
||||
action="store_false",
|
||||
default=True,
|
||||
help=(
|
||||
"Do not mark generated translations as #, fuzzy. "
|
||||
"WARNING: fuzzy entries are excluded from compiled .mo files. "
|
||||
"Removing this flag causes AI-generated translations to be served "
|
||||
"to end users without human review — only use after you have "
|
||||
"manually verified the .po file."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
backfill(
|
||||
lang=args.lang,
|
||||
batch_size=args.batch_size,
|
||||
limit=args.limit,
|
||||
min_context=args.min_context,
|
||||
model=args.model,
|
||||
index_path=args.index,
|
||||
dry_run=args.dry_run,
|
||||
mark_fuzzy=args.mark_fuzzy,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
153
scripts/translations/build_translation_index.py
Normal file
153
scripts/translations/build_translation_index.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# 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.
|
||||
"""Build a cross-language translation index from all .po files.
|
||||
|
||||
Outputs a JSON file structured as:
|
||||
{
|
||||
"<msgid>": {
|
||||
"<lang>": "<translated string or null>",
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
For plural entries the key is "<msgid>\x00<msgid_plural>" and the value
|
||||
is a dict mapping lang -> {0: "...", 1: "..."} (or null if untranslated).
|
||||
|
||||
Usage:
|
||||
python scripts/translations/build_translation_index.py
|
||||
python scripts/translations/build_translation_index.py \
|
||||
--translations-dir superset/translations \
|
||||
--output /tmp/translation_index.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import polib # type: ignore[import-untyped]
|
||||
except ImportError:
|
||||
print("polib is required. Install with: pip install polib", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
TRANSLATIONS_DIR = Path(__file__).parent.parent.parent / "superset" / "translations"
|
||||
DEFAULT_OUTPUT = (
|
||||
Path(__file__).parent.parent.parent
|
||||
/ "superset"
|
||||
/ "translations"
|
||||
/ "translation_index.json"
|
||||
)
|
||||
|
||||
|
||||
def _is_translated(entry: polib.POEntry) -> bool:
|
||||
"""Return True if the entry has a non-empty, non-fuzzy translation."""
|
||||
if "fuzzy" in entry.flags:
|
||||
return False
|
||||
if entry.msgid_plural:
|
||||
return any(v for v in entry.msgstr_plural.values())
|
||||
return bool(entry.msgstr)
|
||||
|
||||
|
||||
def _plural_key(entry: polib.POEntry) -> str:
|
||||
"""Build the combined key used for plural translation entries."""
|
||||
return f"{entry.msgid}\x00{entry.msgid_plural}"
|
||||
|
||||
|
||||
def build_index(translations_dir: Path) -> dict[str, Any]:
|
||||
"""Read all .po files and build a combined translation index."""
|
||||
index: dict[str, dict[str, Any]] = {}
|
||||
|
||||
langs = sorted(
|
||||
d
|
||||
for d in os.listdir(translations_dir)
|
||||
if (translations_dir / d / "LC_MESSAGES" / "messages.po").exists()
|
||||
and d != "en" # en has empty msgstr by convention (source = target)
|
||||
)
|
||||
|
||||
for lang in langs:
|
||||
po_path = translations_dir / lang / "LC_MESSAGES" / "messages.po"
|
||||
cat = polib.pofile(str(po_path))
|
||||
for entry in cat:
|
||||
if not entry.msgid:
|
||||
continue # skip header entry
|
||||
|
||||
if entry.msgid_plural:
|
||||
key = _plural_key(entry)
|
||||
if key not in index:
|
||||
index[key] = {}
|
||||
# Fuzzy entries are unreviewed (often machine-generated drafts),
|
||||
# so excluding them prevents feeding unverified translations
|
||||
# back into the AI backfill prompt as trusted context.
|
||||
index[key][lang] = (
|
||||
dict(entry.msgstr_plural) if _is_translated(entry) else None
|
||||
)
|
||||
else:
|
||||
key = entry.msgid
|
||||
if key not in index:
|
||||
index[key] = {}
|
||||
index[key][lang] = entry.msgstr if _is_translated(entry) else None
|
||||
|
||||
# Ensure every entry has a slot for every language (null if missing)
|
||||
for key in index:
|
||||
for lang in langs:
|
||||
index[key].setdefault(lang, None)
|
||||
|
||||
return index
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Parse arguments, build the translation index, and write it to disk."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build cross-language translation index"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--translations-dir",
|
||||
type=Path,
|
||||
default=TRANSLATIONS_DIR,
|
||||
help="Path to the translations directory (default: superset/translations)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=Path,
|
||||
default=DEFAULT_OUTPUT,
|
||||
help=(
|
||||
"Output JSON file path"
|
||||
" (default: superset/translations/translation_index.json)"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Reading .po files from {args.translations_dir} …", file=sys.stderr)
|
||||
index = build_index(args.translations_dir)
|
||||
print(f"Indexed {len(index)} message IDs.", file=sys.stderr)
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Written to {args.output}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
341
scripts/translations/check_translation_regression.py
Executable file
341
scripts/translations/check_translation_regression.py
Executable file
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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.
|
||||
"""
|
||||
Check that source-code changes don't cause translation regressions.
|
||||
|
||||
What counts as a regression
|
||||
---------------------------
|
||||
A regression is an *existing translation that a source change invalidated* —
|
||||
i.e. a string was renamed/reworded so its committed translation no longer
|
||||
applies. ``babel_update.sh`` (``pybabel update --ignore-obsolete``) surfaces
|
||||
exactly these as **newly fuzzy** entries: the old translation is fuzzy-matched
|
||||
onto the new ``msgid`` and flagged ``#, fuzzy``.
|
||||
|
||||
Crucially, *deleting* a translatable string is **not** a regression. With
|
||||
``--ignore-obsolete`` a removed string is dropped from the catalogs entirely;
|
||||
no fuzzy entry is created. So a PR that intentionally removes a string (e.g. a
|
||||
security fix that stops rendering a value) legitimately lowers the translated
|
||||
count without introducing any fuzzies, and must not be flagged. We therefore
|
||||
key the check on the **increase in fuzzy entries**, not on a drop in the
|
||||
translated count (a drop happens identically for a benign deletion and a real
|
||||
rename, so it cannot distinguish the two).
|
||||
|
||||
Usage
|
||||
-----
|
||||
Count translated + fuzzy entries in all .po files and write JSON to stdout:
|
||||
|
||||
python check_translation_regression.py --count
|
||||
|
||||
Compare the current .po state against a previously-recorded baseline and fail
|
||||
if a source change invalidated existing translations (new fuzzies):
|
||||
|
||||
python check_translation_regression.py --compare /path/to/before.json
|
||||
|
||||
Optionally write a markdown report to a file (used by CI to post a PR comment):
|
||||
|
||||
python check_translation_regression.py --compare before.json --report report.md
|
||||
|
||||
Use a translations directory other than the repo default (used by CI to count
|
||||
against a separate base-branch worktree):
|
||||
|
||||
python check_translation_regression.py --count \\
|
||||
--translations-dir /tmp/base-worktree/superset/translations
|
||||
|
||||
Typical CI workflow
|
||||
-------------------
|
||||
1. Create a base-branch worktree alongside the PR worktree
|
||||
2. Run babel_update.sh in the base worktree (extract from BASE source)
|
||||
3. Record baseline: python ... --count --translations-dir BASE_TREE > before.json
|
||||
4. Run babel_update.sh in the PR worktree (extract from PR source and keep
|
||||
any committed PR .po updates)
|
||||
5. Compare: python ... --compare before.json [--report report.md]
|
||||
|
||||
Running babel_update on the base branch first isolates regressions caused by
|
||||
the PR's source diff from any pre-existing drift on the base branch, while the
|
||||
PR worktree run still allows committed .po updates to resolve the fuzzies (and
|
||||
thus clear the regression) before merging.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
DEFAULT_TRANSLATIONS_DIR = (
|
||||
Path(__file__).resolve().parent.parent.parent / "superset" / "translations"
|
||||
)
|
||||
|
||||
# English .po files use empty msgstr by convention (source language == target),
|
||||
# so they always show 0 translated entries and should not be checked.
|
||||
SKIP_LANGS = {"en"}
|
||||
|
||||
|
||||
def count_stats(po_file: Path) -> dict[str, int]:
|
||||
"""Return ``{"translated": int, "fuzzy": int}`` for a .po file.
|
||||
|
||||
``translated`` is the number of non-fuzzy translated messages; ``fuzzy`` is
|
||||
the number of fuzzy translations. The fuzzy count is what the regression
|
||||
check keys on — a source rename invalidates an existing translation by
|
||||
making it fuzzy, whereas a deletion simply drops it (``--ignore-obsolete``).
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: if ``msgfmt`` fails (e.g. malformed
|
||||
.po file). The regression check exists to surface translation
|
||||
problems, so a silent zero would defeat its purpose — let the
|
||||
caller see a malformed file as a hard failure.
|
||||
"""
|
||||
import shutil # noqa: PLC0415
|
||||
|
||||
msgfmt = shutil.which("msgfmt") or "msgfmt"
|
||||
result = subprocess.run( # noqa: S603
|
||||
[msgfmt, "--statistics", "-o", "/dev/null", str(po_file)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
# stderr: "123 translated messages, 4 fuzzy translations, 56 untranslated messages."
|
||||
# The fuzzy and untranslated clauses are omitted by msgfmt when they are 0.
|
||||
translated_match = re.search(r"(\d+) translated message", result.stderr)
|
||||
if not translated_match:
|
||||
raise RuntimeError(
|
||||
f"Could not parse msgfmt --statistics output for {po_file}: "
|
||||
f"{result.stderr!r}"
|
||||
)
|
||||
fuzzy_match = re.search(r"(\d+) fuzzy translation", result.stderr)
|
||||
return {
|
||||
"translated": int(translated_match.group(1)),
|
||||
"fuzzy": int(fuzzy_match.group(1)) if fuzzy_match else 0,
|
||||
}
|
||||
|
||||
|
||||
def get_counts(
|
||||
translations_dir: Path,
|
||||
failures: Optional[set[str]] = None,
|
||||
) -> dict[str, dict[str, int]]:
|
||||
"""Count translated/fuzzy entries for every ``.po`` file in a directory.
|
||||
|
||||
If ``failures`` is provided, the name of each language whose ``.po`` file
|
||||
is present on disk but could not be counted (msgfmt non-zero exit, or
|
||||
unparseable output) is added to it. Such a language is deliberately absent
|
||||
from the returned mapping — but, unlike a language whose catalog was simply
|
||||
deleted, it must not be mistaken for an intentional removal: a caller that
|
||||
cares about the distinction (see :func:`cmd_compare`) can inspect
|
||||
``failures`` and treat it as a hard error.
|
||||
"""
|
||||
counts: dict[str, dict[str, int]] = {}
|
||||
for po_file in sorted(translations_dir.glob("*/LC_MESSAGES/messages.po")):
|
||||
lang = po_file.parent.parent.name
|
||||
if lang in SKIP_LANGS:
|
||||
continue
|
||||
try:
|
||||
counts[lang] = count_stats(po_file)
|
||||
except (subprocess.CalledProcessError, RuntimeError) as exc:
|
||||
# A malformed .po file (msgfmt non-zero exit, or stderr we
|
||||
# can't parse) is a real problem worth seeing, but it shouldn't
|
||||
# take the whole regression check down with it — that would
|
||||
# hide every other language's status. Skip and warn here; the
|
||||
# caller is told which langs failed via ``failures`` so it can
|
||||
# decide whether a present-but-uncountable catalog is fatal.
|
||||
if failures is not None:
|
||||
failures.add(lang)
|
||||
print(
|
||||
f"WARNING: skipping {lang} — {po_file} could not be counted: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return counts
|
||||
|
||||
|
||||
def _normalize(entry: object) -> dict[str, int]:
|
||||
"""Coerce a baseline entry into ``{"translated", "fuzzy"}``.
|
||||
|
||||
Tolerates the legacy baseline format where each language mapped directly to
|
||||
an integer translated count (no fuzzy data); such entries contribute a
|
||||
fuzzy baseline of 0.
|
||||
"""
|
||||
if isinstance(entry, dict):
|
||||
return {
|
||||
"translated": int(entry.get("translated", 0)),
|
||||
"fuzzy": int(entry.get("fuzzy", 0)),
|
||||
}
|
||||
if isinstance(entry, int):
|
||||
return {"translated": entry, "fuzzy": 0}
|
||||
raise TypeError(f"Unsupported baseline entry: {entry!r}")
|
||||
|
||||
|
||||
def build_regression_report(regressions: list[tuple[str, int, int]]) -> str:
|
||||
"""Build a markdown report for posting as a PR comment.
|
||||
|
||||
Each regression tuple is ``(lang, before_fuzzy, after_fuzzy)``.
|
||||
"""
|
||||
rows = "\n".join(
|
||||
f"| `{lang}` | {b} | {a} | +{a - b} |" for lang, b, a in regressions
|
||||
)
|
||||
affected = ", ".join(f"`{lang}`" for lang, _, _ in regressions)
|
||||
return (
|
||||
"## ⚠️ Translation Regression Detected\n\n"
|
||||
f"A source change in this PR renamed or reworded strings, invalidating "
|
||||
f"existing translations (they are now `#, fuzzy`) in {affected}. Please "
|
||||
f"resolve the affected `.po` files before merging.\n\n"
|
||||
"_Note: intentionally **deleting** a translatable string is not a "
|
||||
"regression and is not flagged here — only translations invalidated by "
|
||||
"a renamed/reworded source string are._\n\n"
|
||||
"| Language | Fuzzy before | Fuzzy after | New |\n"
|
||||
"|----------|-------------:|------------:|----:|\n"
|
||||
f"{rows}\n\n"
|
||||
"### How to fix\n\n"
|
||||
"**1. Install dependencies** (if not already set up):\n\n"
|
||||
"```bash\n"
|
||||
"pip install -r superset/translations/requirements.txt\n"
|
||||
"sudo apt-get install gettext # or: brew install gettext\n"
|
||||
"```\n\n"
|
||||
"**2. Re-extract strings and sync `.po` files:**\n\n"
|
||||
"```bash\n"
|
||||
"./scripts/translations/babel_update.sh\n"
|
||||
"```\n\n"
|
||||
"This rewrites `superset/translations/messages.pot` from the current "
|
||||
"source files and merges the changes into every `.po` file. Strings "
|
||||
"whose `msgid` changed will be marked `#, fuzzy`.\n\n"
|
||||
f"**3. Resolve the fuzzy entries** in the affected language files "
|
||||
f"({affected}):\n\n"
|
||||
"```bash\n"
|
||||
"grep -n '#, fuzzy' superset/translations/<lang>/LC_MESSAGES/messages.po\n"
|
||||
"```\n\n"
|
||||
"For each fuzzy entry, either rewrite the `msgstr` to match the new "
|
||||
"string and remove the `#, fuzzy` line, or clear the `msgstr` to "
|
||||
'`""` if you cannot provide a translation.\n\n'
|
||||
"**4. Commit your changes to the `.po` files.**\n"
|
||||
)
|
||||
|
||||
|
||||
def cmd_count(translations_dir: Path) -> None:
|
||||
counts = get_counts(translations_dir)
|
||||
print(json.dumps(counts, indent=2))
|
||||
|
||||
|
||||
def cmd_compare(
|
||||
before_path: str,
|
||||
translations_dir: Path,
|
||||
report_path: Optional[str] = None,
|
||||
) -> None:
|
||||
with open(before_path) as f:
|
||||
before_raw: dict[str, object] = json.load(f)
|
||||
before = {lang: _normalize(entry) for lang, entry in before_raw.items()}
|
||||
|
||||
failures: set[str] = set()
|
||||
after = get_counts(translations_dir, failures=failures)
|
||||
|
||||
# A baseline language whose catalog is *missing* from `after` is fine —
|
||||
# that's an intentional catalog deletion (handled below like any other
|
||||
# deletion). But a language whose .po file is still present yet could not
|
||||
# be counted (msgfmt failed / output unparseable) is a hard error: leaving
|
||||
# it out silently would let a corrupt catalog pass as "no regression".
|
||||
broken = sorted(lang for lang in failures if lang in before)
|
||||
if broken:
|
||||
print("Translation check failed!\n")
|
||||
for lang in broken:
|
||||
print(f" {lang}: catalog present but could not be counted (msgfmt error)")
|
||||
print(
|
||||
"\nFix the malformed .po file(s) above before merging — a catalog "
|
||||
"that cannot be parsed must not be silently dropped."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# A regression is an *increase* in fuzzy entries: the PR's source diff
|
||||
# renamed/reworded strings, leaving their committed translations stranded.
|
||||
# A plain drop in the translated count is NOT used — deleting a string
|
||||
# lowers it identically to a rename but is a legitimate change, and with
|
||||
# `pybabel update --ignore-obsolete` a deletion creates no fuzzy entry.
|
||||
regressions: list[tuple[str, int, int]] = []
|
||||
for lang, before_stats in sorted(before.items()):
|
||||
after_stats = after.get(lang, {"translated": 0, "fuzzy": 0})
|
||||
if after_stats["fuzzy"] > before_stats["fuzzy"]:
|
||||
regressions.append((lang, before_stats["fuzzy"], after_stats["fuzzy"]))
|
||||
|
||||
if regressions:
|
||||
print("Translation regression detected!\n")
|
||||
for lang, b, a in regressions:
|
||||
print(
|
||||
f" {lang}: {a - b} translation(s) invalidated "
|
||||
f"(fuzzy {b} -> {a}) by a renamed/reworded source string"
|
||||
)
|
||||
print(
|
||||
"\nResolve the newly-fuzzy entries in the affected .po files "
|
||||
"before merging."
|
||||
)
|
||||
if report_path:
|
||||
Path(report_path).write_text(
|
||||
build_regression_report(regressions), encoding="utf-8"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# All good — print a summary so it's easy to read in CI logs.
|
||||
print("No translation regressions.\n")
|
||||
for lang in sorted(after):
|
||||
before_stats = before.get(lang, {"translated": 0, "fuzzy": 0})
|
||||
after_stats = after[lang]
|
||||
t_delta = after_stats["translated"] - before_stats["translated"]
|
||||
f_delta = after_stats["fuzzy"] - before_stats["fuzzy"]
|
||||
print(
|
||||
f" {lang}: translated {before_stats['translated']} -> "
|
||||
f"{after_stats['translated']} ({t_delta:+d}), fuzzy "
|
||||
f"{before_stats['fuzzy']} -> {after_stats['fuzzy']} ({f_delta:+d})"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check for translation regressions in .po files."
|
||||
)
|
||||
action = parser.add_mutually_exclusive_group(required=True)
|
||||
action.add_argument(
|
||||
"--count",
|
||||
action="store_true",
|
||||
help="Output translation counts per language as JSON.",
|
||||
)
|
||||
action.add_argument(
|
||||
"--compare",
|
||||
metavar="BEFORE_JSON",
|
||||
help="Compare current counts against a baseline JSON file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
metavar="REPORT_MD",
|
||||
help="When --compare detects regressions, write a markdown report here.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--translations-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_TRANSLATIONS_DIR,
|
||||
help=(
|
||||
"Path to the translations directory containing per-language "
|
||||
"LC_MESSAGES/messages.po files (default: <repo>/superset/translations)."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.count:
|
||||
cmd_count(args.translations_dir)
|
||||
else:
|
||||
cmd_compare(args.compare, args.translations_dir, args.report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -22,15 +22,41 @@ set -e
|
||||
# If not already running in Docker, run this script inside Docker
|
||||
if [ -z "$RUNNING_IN_DOCKER" ]; then
|
||||
# Extract "current" Python version from CI config (single source of truth)
|
||||
PYTHON_VERSION=$(grep -A 1 'if.*"current"' .github/actions/setup-backend/action.yml | grep 'PYTHON_VERSION=' | sed 's/.*PYTHON_VERSION=\([0-9.]*\).*/\1/')
|
||||
PYTHON_VERSION=$(grep -A 1 'if.*"current"' .github/actions/setup-backend/action.yml | grep 'RESOLVED_VERSION=' | sed 's/.*RESOLVED_VERSION="\([0-9.]*\)".*/\1/')
|
||||
|
||||
if [ -z "$PYTHON_VERSION" ]; then
|
||||
echo "Failed to determine Python version from .github/actions/setup-backend/action.yml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running in Docker (Python ${PYTHON_VERSION} on Linux)..."
|
||||
|
||||
IMAGE="python:${PYTHON_VERSION}-slim"
|
||||
|
||||
# Pre-pull the image with a few retries to absorb transient Docker Hub
|
||||
# registry failures ("context deadline exceeded" / anonymous rate-limit blips
|
||||
# on shared CI runners). Without this a flaky pull fails the whole
|
||||
# check-python-deps job on an infrastructure hiccup rather than a real
|
||||
# dependency drift. The pull is in the `until` condition so `set -e` does not
|
||||
# abort on an individual failed attempt.
|
||||
attempt=1
|
||||
max_attempts=4
|
||||
until docker pull "$IMAGE"; do
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
echo "docker pull $IMAGE failed after ${max_attempts} attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
delay=$((attempt * 10))
|
||||
echo "docker pull $IMAGE failed (attempt ${attempt}/${max_attempts}); retrying in ${delay}s..." >&2
|
||||
sleep "$delay"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd)":/app \
|
||||
-w /app \
|
||||
-e RUNNING_IN_DOCKER=1 \
|
||||
python:${PYTHON_VERSION}-slim \
|
||||
"$IMAGE" \
|
||||
bash -c "pip install uv && ./scripts/uv-pip-compile.sh $*"
|
||||
|
||||
exit $?
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"pydantic>=2.8.0",
|
||||
"sqlalchemy>=1.4.0,<2.0",
|
||||
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
|
||||
"sqlglot>=28.10.0, <29",
|
||||
"sqlglot>=30.8.0, <31",
|
||||
"typing-extensions>=4.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ Embedding is done by inserting an iframe, containing a Superset page, into the h
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Activate the feature flag `EMBEDDED_SUPERSET`
|
||||
* Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py). Be aware that its default value must be changed in production.
|
||||
- Activate the feature flag `EMBEDDED_SUPERSET`
|
||||
- Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET` (see configuration file config.py). Be aware that its default value must be changed in production.
|
||||
|
||||
## Embedding a Dashboard
|
||||
|
||||
@@ -41,32 +41,37 @@ npm install --save @superset-ui/embedded-sdk
|
||||
```
|
||||
|
||||
```js
|
||||
import { embedDashboard } from "@superset-ui/embedded-sdk";
|
||||
import { embedDashboard } from '@superset-ui/embedded-sdk';
|
||||
|
||||
embedDashboard({
|
||||
id: "abc123", // given by the Superset embedding UI
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint: document.getElementById("my-superset-container"), // any html element that can contain an iframe
|
||||
id: 'abc123', // given by the Superset embedding UI
|
||||
supersetDomain: 'https://superset.example.com',
|
||||
mountPoint: document.getElementById('my-superset-container'), // any html element that can contain an iframe
|
||||
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
||||
dashboardUiConfig: { // dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional), urlParams (optional)
|
||||
hideTitle: true,
|
||||
filters: {
|
||||
expanded: true,
|
||||
},
|
||||
urlParams: {
|
||||
foo: 'value1',
|
||||
bar: 'value2',
|
||||
// ...
|
||||
}
|
||||
dashboardUiConfig: {
|
||||
// dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional), urlParams (optional)
|
||||
hideTitle: true,
|
||||
filters: {
|
||||
expanded: true,
|
||||
},
|
||||
urlParams: {
|
||||
foo: 'value1',
|
||||
bar: 'value2',
|
||||
// themeMode: 'dark', // set the initial theme: 'dark' | 'system' | 'default' (default: 'default')
|
||||
// ...
|
||||
},
|
||||
},
|
||||
// optional additional iframe sandbox attributes
|
||||
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'],
|
||||
iframeSandboxExtras: [
|
||||
'allow-top-navigation',
|
||||
'allow-popups-to-escape-sandbox',
|
||||
],
|
||||
// optional Permissions Policy features
|
||||
iframeAllowExtras: ['clipboard-write', 'fullscreen'],
|
||||
// optional config to enforce a particular referrerPolicy
|
||||
referrerPolicy: "same-origin",
|
||||
referrerPolicy: 'same-origin',
|
||||
// optional callback to customize permalink URLs
|
||||
resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`
|
||||
resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -97,7 +102,7 @@ Guest tokens can have Row Level Security rules which filter data for the user ca
|
||||
|
||||
The agent making the `POST` request must be authenticated with the `can_grant_guest_token` permission.
|
||||
|
||||
Within your app, using the Guest Token will then allow authentication to your Superset instance via creating an Anonymous user object. This guest anonymous user will default to the public role as per this setting `GUEST_ROLE_NAME = "Public"`.
|
||||
Within your app, using the Guest Token will then allow authentication to your Superset instance via creating an Anonymous user object. This guest anonymous user will default to the public role as per this setting `GUEST_ROLE_NAME = "Public"`.
|
||||
|
||||
The user parameters in the example below are optional and are provided as a means of passing user attributes that may be accessed in jinja templates inside your charts.
|
||||
|
||||
@@ -110,13 +115,13 @@ Example `POST /security/guest_token` payload:
|
||||
"first_name": "Stan",
|
||||
"last_name": "Lee"
|
||||
},
|
||||
"resources": [{
|
||||
"type": "dashboard",
|
||||
"id": "abc123"
|
||||
}],
|
||||
"rls": [
|
||||
{ "clause": "publisher = 'Nintendo'" }
|
||||
]
|
||||
"resources": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"id": "abc123"
|
||||
}
|
||||
],
|
||||
"rls": [{ "clause": "publisher = 'Nintendo'" }]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -152,15 +157,43 @@ In this example, the configuration file includes the following setting:
|
||||
GUEST_TOKEN_JWT_AUDIENCE="superset"
|
||||
```
|
||||
|
||||
### Setting the Initial Theme Mode
|
||||
|
||||
Use the `themeMode` URL parameter to control the embedded dashboard's initial colour scheme:
|
||||
|
||||
```js
|
||||
embedDashboard({
|
||||
id: 'abc123',
|
||||
supersetDomain: 'https://superset.example.com',
|
||||
mountPoint: document.getElementById('my-superset-container'),
|
||||
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
||||
dashboardUiConfig: {
|
||||
urlParams: {
|
||||
themeMode: 'dark', // 'dark' | 'system' | 'default' (default: 'default')
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The supported values are:
|
||||
|
||||
| Value | Behaviour |
|
||||
| --------- | --------------------------------------------------------- |
|
||||
| `default` | Light theme (Superset default) |
|
||||
| `dark` | Dark theme |
|
||||
| `system` | Follows the user's OS preference (`prefers-color-scheme`) |
|
||||
|
||||
The theme can also be changed at runtime via `embeddedDashboard.setThemeMode(mode)`.
|
||||
|
||||
### Sandbox iframe
|
||||
|
||||
The Embedded SDK creates an iframe with [sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox) mode by default
|
||||
which applies certain restrictions to the iframe's content.
|
||||
To pass additional sandbox attributes you can use `iframeSandboxExtras`:
|
||||
|
||||
```js
|
||||
// optional additional iframe sandbox attributes
|
||||
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox']
|
||||
// optional additional iframe sandbox attributes
|
||||
iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'];
|
||||
```
|
||||
|
||||
### Permissions Policy
|
||||
@@ -168,11 +201,12 @@ To pass additional sandbox attributes you can use `iframeSandboxExtras`:
|
||||
To enable specific browser features within the embedded iframe, use `iframeAllowExtras` to set the iframe's [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) (the `allow` attribute):
|
||||
|
||||
```js
|
||||
// optional Permissions Policy features
|
||||
iframeAllowExtras: ['clipboard-write', 'fullscreen']
|
||||
// optional Permissions Policy features
|
||||
iframeAllowExtras: ['clipboard-write', 'fullscreen'];
|
||||
```
|
||||
|
||||
Common permissions you might need:
|
||||
|
||||
- `clipboard-write` - Required for "Copy permalink to clipboard" functionality
|
||||
- `fullscreen` - Required for fullscreen chart viewing
|
||||
- `camera`, `microphone` - If your dashboards include media capture features
|
||||
@@ -191,16 +225,16 @@ When users click share buttons inside an embedded dashboard, Superset generates
|
||||
|
||||
```js
|
||||
embedDashboard({
|
||||
id: "abc123",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint: document.getElementById("my-superset-container"),
|
||||
id: 'abc123',
|
||||
supersetDomain: 'https://superset.example.com',
|
||||
mountPoint: document.getElementById('my-superset-container'),
|
||||
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
||||
|
||||
// Customize permalink URLs
|
||||
resolvePermalinkUrl: ({ key }) => {
|
||||
// key: the permalink key (e.g., "xyz789")
|
||||
return `https://my-app.com/analytics/share/${key}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -211,15 +245,15 @@ To restore the dashboard state from a permalink in your app:
|
||||
const permalinkKey = routeParams.key;
|
||||
|
||||
embedDashboard({
|
||||
id: "abc123",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint: document.getElementById("my-superset-container"),
|
||||
id: 'abc123',
|
||||
supersetDomain: 'https://superset.example.com',
|
||||
mountPoint: document.getElementById('my-superset-container'),
|
||||
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
||||
resolvePermalinkUrl: ({ key }) => `https://my-app.com/analytics/share/${key}`,
|
||||
dashboardUiConfig: {
|
||||
urlParams: {
|
||||
permalink_key: permalinkKey, // Restores filters, tabs, chart states, and scrolls to anchor
|
||||
}
|
||||
}
|
||||
permalink_key: permalinkKey, // Restores filters, tabs, chart states, and scrolls to anchor
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -226,7 +226,7 @@ def copy_frontend_dist(cwd: Path) -> str:
|
||||
def copy_backend_files(cwd: Path) -> None:
|
||||
"""Copy backend files based on pyproject.toml build configuration (validation already passed)."""
|
||||
dist_dir = cwd / "dist"
|
||||
backend_dir = cwd / "backend"
|
||||
backend_dir = (cwd / "backend").resolve()
|
||||
|
||||
# Read build config from pyproject.toml
|
||||
pyproject = read_toml(backend_dir / "pyproject.toml")
|
||||
@@ -239,11 +239,31 @@ def copy_backend_files(cwd: Path) -> None:
|
||||
|
||||
# Process include patterns
|
||||
for pattern in include_patterns:
|
||||
# Include patterns are only meant to select files within the backend
|
||||
# directory. Reject absolute patterns or ones that walk outside it via
|
||||
# parent ("..") components before handing them to glob().
|
||||
pattern_parts = Path(pattern).parts
|
||||
if Path(pattern).is_absolute() or ".." in pattern_parts:
|
||||
raise click.ClickException(
|
||||
f"Invalid include pattern {pattern!r}: patterns must be "
|
||||
"relative to the backend directory and may not contain '..'."
|
||||
)
|
||||
for f in backend_dir.glob(pattern):
|
||||
if not f.is_file():
|
||||
continue
|
||||
|
||||
# Check exclude patterns
|
||||
# Defense in depth: confirm the matched file resolves to a location
|
||||
# inside the backend directory before copying it into the bundle.
|
||||
resolved = f.resolve()
|
||||
if not resolved.is_relative_to(backend_dir):
|
||||
raise click.ClickException(
|
||||
f"Refusing to copy {f}: resolved path is outside the "
|
||||
f"backend directory {backend_dir}."
|
||||
)
|
||||
|
||||
# Use the matched path (not the resolved target) for the bundle
|
||||
# layout and exclude evaluation so symlinked files are staged at
|
||||
# their configured path rather than their symlink target.
|
||||
relative_path = f.relative_to(backend_dir)
|
||||
should_exclude = any(
|
||||
relative_path.match(excl_pattern) for excl_pattern in exclude_patterns
|
||||
|
||||
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import click
|
||||
import pytest
|
||||
from superset_extensions_cli.cli import (
|
||||
app,
|
||||
@@ -625,6 +626,155 @@ exclude = []
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_copy_backend_files_supports_legitimate_nested_patterns(isolated_filesystem):
|
||||
"""Test copy_backend_files copies deeply nested files via recursive globs."""
|
||||
backend_dir = isolated_filesystem / "backend"
|
||||
nested = backend_dir / "src" / "test_org" / "test_ext" / "deep" / "deeper"
|
||||
nested.mkdir(parents=True)
|
||||
(nested / "module.py").write_text("# nested module")
|
||||
|
||||
pyproject_content = """[project]
|
||||
name = "test_org-test_ext"
|
||||
version = "1.0.0"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[tool.apache_superset_extensions.build]
|
||||
include = [
|
||||
"src/test_org/test_ext/**/*.py",
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
(backend_dir / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
extension_data = {
|
||||
"publisher": "test-org",
|
||||
"name": "test-ext",
|
||||
"displayName": "Test Extension",
|
||||
"version": "1.0.0",
|
||||
"permissions": [],
|
||||
}
|
||||
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
|
||||
|
||||
clean_dist(isolated_filesystem)
|
||||
copy_backend_files(isolated_filesystem)
|
||||
|
||||
dist_dir = isolated_filesystem / "dist"
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "deep"
|
||||
/ "deeper"
|
||||
/ "module.py"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"bad_pattern",
|
||||
[
|
||||
"../../.ssh/*",
|
||||
"../config",
|
||||
"src/../../secret.txt",
|
||||
"/etc/passwd",
|
||||
],
|
||||
)
|
||||
def test_copy_backend_files_rejects_patterns_escaping_backend_dir(
|
||||
isolated_filesystem, bad_pattern
|
||||
):
|
||||
"""Test copy_backend_files refuses include patterns that escape backend_dir."""
|
||||
# Create a sensitive file outside the backend directory.
|
||||
(isolated_filesystem / "secret.txt").write_text("SECRET")
|
||||
(isolated_filesystem / "config").write_text("SECRET")
|
||||
|
||||
backend_dir = isolated_filesystem / "backend"
|
||||
backend_src = backend_dir / "src" / "test_org" / "test_ext"
|
||||
backend_src.mkdir(parents=True)
|
||||
(backend_src / "__init__.py").write_text("# init")
|
||||
|
||||
pyproject_content = f"""[project]
|
||||
name = "test_org-test_ext"
|
||||
version = "1.0.0"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[tool.apache_superset_extensions.build]
|
||||
include = [
|
||||
"{bad_pattern}",
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
(backend_dir / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
extension_data = {
|
||||
"publisher": "test-org",
|
||||
"name": "test-ext",
|
||||
"displayName": "Test Extension",
|
||||
"version": "1.0.0",
|
||||
"permissions": [],
|
||||
}
|
||||
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
|
||||
|
||||
clean_dist(isolated_filesystem)
|
||||
|
||||
with pytest.raises(click.ClickException):
|
||||
copy_backend_files(isolated_filesystem)
|
||||
|
||||
# Nothing outside the backend directory should have been staged into dist,
|
||||
# including paths reachable via ".." from inside dist/backend.
|
||||
dist_dir = isolated_filesystem / "dist"
|
||||
assert not (dist_dir / "secret.txt").exists()
|
||||
assert not (dist_dir / "config").exists()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_copy_backend_files_stages_symlink_at_matched_path(isolated_filesystem):
|
||||
"""Symlinked files inside backend are staged at the matched path, not the target."""
|
||||
backend_dir = isolated_filesystem / "backend"
|
||||
target_dir = backend_dir / "src" / "common"
|
||||
target_dir.mkdir(parents=True)
|
||||
(target_dir / "module.py").write_text("# shared module")
|
||||
|
||||
link_dir = backend_dir / "src" / "test_org" / "test_ext" / "common"
|
||||
link_dir.mkdir(parents=True)
|
||||
link = link_dir / "module.py"
|
||||
link.symlink_to(target_dir / "module.py")
|
||||
|
||||
pyproject_content = """[project]
|
||||
name = "test_org-test_ext"
|
||||
version = "1.0.0"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[tool.apache_superset_extensions.build]
|
||||
include = [
|
||||
"src/test_org/test_ext/**/*.py",
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
(backend_dir / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
extension_data = {
|
||||
"publisher": "test-org",
|
||||
"name": "test-ext",
|
||||
"displayName": "Test Extension",
|
||||
"version": "1.0.0",
|
||||
"permissions": [],
|
||||
}
|
||||
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_data))
|
||||
|
||||
clean_dist(isolated_filesystem)
|
||||
copy_backend_files(isolated_filesystem)
|
||||
|
||||
dist_dir = isolated_filesystem / "dist"
|
||||
# Staged at the configured (symlink) path, not the resolved target path.
|
||||
assert_file_exists(
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "common" / "module.py"
|
||||
)
|
||||
assert not (dist_dir / "backend" / "src" / "common" / "module.py").exists()
|
||||
|
||||
|
||||
# Removed obsolete tests:
|
||||
# - test_copy_backend_files_handles_no_backend_config: This scenario can't happen since copy_backend_files is only called when backend exists
|
||||
# - test_copy_backend_files_exits_when_extension_json_missing: Validation catches this before copy_backend_files is called
|
||||
|
||||
@@ -80,7 +80,7 @@ const restrictedImportsRules = {
|
||||
'no-jest-mock-console': {
|
||||
name: 'jest-mock-console',
|
||||
message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,100 +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 {
|
||||
waitForChartLoad,
|
||||
ChartSpec,
|
||||
getChartAliasesBySpec,
|
||||
} from 'cypress/utils';
|
||||
import { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls';
|
||||
import { WORLD_HEALTH_CHARTS } from './utils';
|
||||
import { isLegacyResponse } from '../../utils/vizPlugins';
|
||||
|
||||
describe('Dashboard top-level controls', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
});
|
||||
|
||||
// flaky test - query completes before assertion
|
||||
it.skip('should allow chart level refresh', () => {
|
||||
const mapSpec = WORLD_HEALTH_CHARTS.find(
|
||||
({ viz }) => viz === 'world_map',
|
||||
) as ChartSpec;
|
||||
waitForChartLoad(mapSpec).then(gridComponent => {
|
||||
const mapId = gridComponent.attr('data-test-chart-id');
|
||||
cy.get('[data-test="grid-container"]').find('.world_map').should('exist');
|
||||
cy.get(`#slice_${mapId}-controls`).click();
|
||||
cy.get(`[data-test="slice_${mapId}-menu"]`)
|
||||
.find('[data-test="refresh-chart-menu-item"]')
|
||||
.click({ force: true });
|
||||
// likely cause for flakiness:
|
||||
// The query completes before this assertion happens.
|
||||
// Solution: pause the network before clicking, assert, then unpause network.
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
waitForChartLoad(mapSpec);
|
||||
cy.get('[data-test="refresh-chart-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow dashboard level force refresh', () => {
|
||||
// when charts are not start loading, for example, under a secondary tab,
|
||||
// should allow force refresh
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
getChartAliasesBySpec(WORLD_HEALTH_CHARTS).then(aliases => {
|
||||
cy.get('[aria-label="ellipsis"]').click();
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').click({
|
||||
force: true,
|
||||
});
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||
'have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
|
||||
// wait all charts force refreshed.
|
||||
|
||||
cy.wait(aliases).then(xhrs => {
|
||||
xhrs.forEach(async ({ response, request }) => {
|
||||
const responseBody = response?.body;
|
||||
const isCached = isLegacyResponse(responseBody)
|
||||
? responseBody.is_cached
|
||||
: responseBody.result[0].is_cached;
|
||||
// request url should indicate force-refresh operation
|
||||
expect(request.url).to.have.string('force=true');
|
||||
// is_cached in response should be false
|
||||
expect(isCached).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
cy.get('[aria-label="ellipsis"]').click();
|
||||
cy.get('[data-test="refresh-dashboard-menu-item"]').and(
|
||||
'not.have.class',
|
||||
'ant-dropdown-menu-item-disabled',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,292 +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 { nativeFilters } from 'cypress/support/directories';
|
||||
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
applyNativeFilterValueWithIndex,
|
||||
enterNativeFilterEditModal,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
interceptFilterState,
|
||||
} from './utils';
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function openMoreFilters(waitFilterState = true) {
|
||||
interceptFilterState();
|
||||
// Wait for the dropdown button to appear when filters are overflowed
|
||||
// The button only appears when there are overflowed filters
|
||||
cy.getBySel('dropdown-container-btn', { timeout: 10000 })
|
||||
.should('exist')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
|
||||
if (waitFilterState) {
|
||||
cy.wait('@postFilterState');
|
||||
}
|
||||
}
|
||||
|
||||
function openVerticalFilterBar() {
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
cy.getBySel('filter-bar__expand-button').click();
|
||||
}
|
||||
|
||||
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
|
||||
cy.getBySel('filterbar-orientation-icon').click();
|
||||
cy.wait(250);
|
||||
cy.get('.filter-bar-orientation-submenu')
|
||||
.contains('Orientation of filter bar')
|
||||
.should('exist')
|
||||
.trigger('mouseover');
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Horizontal (Top)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
|
||||
cy.getBySel('dashboard-filters-panel').should('exist');
|
||||
} else {
|
||||
cy.get('.ant-dropdown-menu-item-selected')
|
||||
.contains('Vertical (Left)')
|
||||
.should('exist');
|
||||
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
|
||||
cy.getBySel('loading-indicator').should('exist');
|
||||
cy.getBySel('filter-bar').should('exist');
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Horizontal FilterBar', () => {
|
||||
it('should go from vertical to horizontal and the opposite', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
setFilterBarOrientation('vertical');
|
||||
});
|
||||
|
||||
it('should show all default actions in horizontal mode', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.getBySel('horizontal-filterbar-empty')
|
||||
.contains('No filters are currently added to this dashboard.')
|
||||
.should('exist');
|
||||
cy.get(nativeFilters.filtersPanel.filterGear).click({
|
||||
force: true,
|
||||
});
|
||||
cy.get('.ant-dropdown-menu').should('be.visible');
|
||||
cy.getBySel('filter-bar__create-filter').should('exist');
|
||||
cy.getBySel('filterbar-action-buttons').should('exist');
|
||||
});
|
||||
|
||||
it('should stay in horizontal mode when reloading', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.reload();
|
||||
cy.getBySel('dashboard-filters-panel').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show all filters in available space on load', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show "more filters" on window resizing up and down', () => {
|
||||
// Use 4 filters with unique columns to ensure overflow testing while allowing all to fit at large viewport
|
||||
prepareDashboardFilters([
|
||||
{ name: 'Country', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'Code', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'Region', column: 'region', datasetId: 2 },
|
||||
{ name: 'Year', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
// At full width, check how many filters are visible in main bar
|
||||
cy.get('.filter-item-wrapper').then($items => {
|
||||
cy.log(`Found ${$items.length} filter items at full width`);
|
||||
});
|
||||
|
||||
// Resize to force overflow
|
||||
cy.viewport(500, 1024);
|
||||
cy.wait(500); // Allow layout to stabilize after viewport change
|
||||
|
||||
// Should have some filters visible and dropdown button present
|
||||
cy.get('.filter-item-wrapper').should('have.length.lessThan', 4);
|
||||
cy.getBySel('dropdown-container-btn').should('exist');
|
||||
|
||||
// Open more filters and verify all are accessible in the dropdown
|
||||
openMoreFilters(false);
|
||||
// Check that the dropdown content contains filters
|
||||
cy.getBySel('dropdown-content').within(() => {
|
||||
cy.getBySel('form-item-value').should('have.length.greaterThan', 0);
|
||||
});
|
||||
|
||||
// Close the dropdown
|
||||
cy.getBySel('filter-bar').click();
|
||||
|
||||
// Test with medium viewport
|
||||
cy.viewport(800, 1024);
|
||||
cy.wait(500); // Allow layout to stabilize after viewport change
|
||||
|
||||
// May or may not have overflow at this size - test adaptively
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="dropdown-container-btn"]').length > 0) {
|
||||
openMoreFilters(false);
|
||||
cy.getBySel('dropdown-content').within(() => {
|
||||
cy.getBySel('form-item-value').should('have.length.greaterThan', 0);
|
||||
});
|
||||
cy.getBySel('filter-bar').click(); // Close dropdown
|
||||
}
|
||||
});
|
||||
|
||||
// At large viewport, all filters should fit
|
||||
cy.viewport(1300, 1024);
|
||||
cy.wait(500); // Allow layout to stabilize after viewport change
|
||||
cy.get('.filter-item-wrapper').then($items => {
|
||||
cy.log(`Found ${$items.length} filter items at large width`);
|
||||
// Just verify we have some filters, don't assert exact count
|
||||
expect($items.length).to.be.greaterThan(0);
|
||||
});
|
||||
cy.getBySel('dropdown-container-btn').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show "more filters" and scroll', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
cy.get('.filter-item-wrapper').should('have.length', 4);
|
||||
openMoreFilters();
|
||||
cy.getBySel('form-item-value').should('have.length', 12);
|
||||
cy.getBySel('filter-control-name').contains('test_3').should('be.visible');
|
||||
cy.getBySel('filter-control-name')
|
||||
.contains('test_12')
|
||||
.should('not.be.visible');
|
||||
cy.getBySel('filter-control-name').contains('test_12').scrollIntoView();
|
||||
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display newly added filter', () => {
|
||||
visitDashboard();
|
||||
openVerticalFilterBar();
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it.skip('should spot changes in "more filters" and apply their values', () => {
|
||||
cy.intercept(`**/api/v1/chart/data?form_data=**`).as('chart');
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
cy.get('.ant-scroll-number.ant-badge-count').should(
|
||||
'have.attr',
|
||||
'title',
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
it.skip('should focus filter and open "more filters" programmatically', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_2', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_3', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_4', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_5', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_6', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_7', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_8', column: 'year', datasetId: 2 },
|
||||
{ name: 'test_9', column: 'country_name', datasetId: 2 },
|
||||
{ name: 'test_10', column: 'country_code', datasetId: 2 },
|
||||
{ name: 'test_11', column: 'region', datasetId: 2 },
|
||||
{ name: 'test_12', column: 'year', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
openMoreFilters();
|
||||
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.getBySel('slice-header').within(() => {
|
||||
cy.get('.filter-counts').trigger('mouseover');
|
||||
});
|
||||
cy.getBySel('filter-status-popover').contains('test_9').click();
|
||||
cy.getBySel('dropdown-content').should('be.visible');
|
||||
cy.get('.ant-select-focused').should('be.visible');
|
||||
});
|
||||
|
||||
it.skip('should show tag count and one plain tag on focus and only count on blur in select ', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'test_1', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue('Albania');
|
||||
cy.get('.ant-select-selection-search-input').clear({ force: true });
|
||||
inputNativeFilterDefaultValue('Algeria', true);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.getBySel('filter-bar').within(() => {
|
||||
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
|
||||
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
|
||||
cy.get('.ant-select-selection-search-input').click();
|
||||
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,53 +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 qs from 'querystringify';
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls';
|
||||
import { WORLD_HEALTH_CHARTS } from './utils';
|
||||
|
||||
interface QueryString {
|
||||
native_filters_key: string;
|
||||
}
|
||||
|
||||
describe('nativefilter url param key', () => {
|
||||
// const urlParams = { param1: '123', param2: 'abc' };
|
||||
|
||||
let initialFilterKey: string;
|
||||
it('should have cachekey in nativefilter param', () => {
|
||||
// things in `before` will not retry and the `waitForChartLoad` check is
|
||||
// especially flaky and may need more retries
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
cy.wait(1000); // wait for key to be published (debounced)
|
||||
cy.location().then(loc => {
|
||||
const queryParams = qs.parse(loc.search) as QueryString;
|
||||
expect(typeof queryParams.native_filters_key).eq('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have different key when page reloads', () => {
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
cy.wait(1000); // wait for key to be published (debounced)
|
||||
cy.location().then(loc => {
|
||||
const queryParams = qs.parse(loc.search) as QueryString;
|
||||
expect(queryParams.native_filters_key).not.equal(initialFilterKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +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 { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls';
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import { WORLD_HEALTH_CHARTS, interceptLog } from './utils';
|
||||
|
||||
describe('Dashboard load', () => {
|
||||
it('should load dashboard', () => {
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('should load in edit mode', () => {
|
||||
cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`);
|
||||
cy.getBySel('discard-changes-button').should('be.visible');
|
||||
});
|
||||
|
||||
it('should load in standalone mode', () => {
|
||||
cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`);
|
||||
cy.get('#app-menu').should('not.exist');
|
||||
});
|
||||
|
||||
it('should load in edit/standalone mode', () => {
|
||||
cy.visit(`${WORLD_HEALTH_DASHBOARD}?edit=true&standalone=true`);
|
||||
cy.getBySel('discard-changes-button').should('be.visible');
|
||||
cy.get('#app-menu').should('not.exist');
|
||||
});
|
||||
|
||||
// TODO flaky test. skipping to unblock CI
|
||||
it.skip('should send log data', () => {
|
||||
interceptLog();
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
cy.wait('@logs', { timeout: 15000 });
|
||||
});
|
||||
});
|
||||
@@ -1,385 +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 qs from 'querystring';
|
||||
import {
|
||||
dashboardView,
|
||||
nativeFilters,
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import {
|
||||
addCountryNameFilter,
|
||||
applyAdvancedTimeRangeFilterOnDashboard,
|
||||
applyNativeFilterValueWithIndex,
|
||||
cancelNativeFilterSettings,
|
||||
deleteNativeFilter,
|
||||
enterNativeFilterEditModal,
|
||||
fillNativeFilterForm,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
undoDeleteNativeFilter,
|
||||
validateFilterContentOnDashboard,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
WORLD_HEALTH_CHARTS,
|
||||
} from './utils';
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
// function selectFilter(index: number) {
|
||||
// cy.get("[data-test='filter-title-container'] [draggable='true']")
|
||||
// .eq(index)
|
||||
// .click();
|
||||
// }
|
||||
|
||||
// function closeFilterModal() {
|
||||
// cy.get('body').then($body => {
|
||||
// if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
|
||||
// cy.getBySel('native-filter-modal-cancel-button').click();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
describe('Native filters', () => {
|
||||
describe('Nativefilters initial state not required', () => {
|
||||
it("User can check 'Filter has default value'", () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
});
|
||||
|
||||
it('User can add a new native filter', () => {
|
||||
prepareDashboardFilters([]);
|
||||
|
||||
let filterKey: string;
|
||||
const removeFirstChar = (search: string) =>
|
||||
search.split('').slice(1, search.length).join('');
|
||||
|
||||
cy.location().then(loc => {
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
const queryParams = qs.parse(removeFirstChar(loc.search));
|
||||
filterKey = queryParams.native_filters_key as string;
|
||||
expect(typeof filterKey).eq('string');
|
||||
});
|
||||
enterNativeFilterEditModal();
|
||||
addCountryNameFilter();
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.location().then(loc => {
|
||||
cy.url().should('contain', 'native_filters_key');
|
||||
const queryParams = qs.parse(removeFirstChar(loc.search));
|
||||
const newfilterKey = queryParams.native_filters_key;
|
||||
expect(newfilterKey).eq(filterKey);
|
||||
});
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
});
|
||||
|
||||
it('User can restore a deleted native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_code', column: 'country_code', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.get('[data-test="restore-filter-button"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.should(
|
||||
'have.attr',
|
||||
'value',
|
||||
testItems.topTenChart.filterColumnCountryCode,
|
||||
);
|
||||
});
|
||||
|
||||
it('User can create a time grain filter', () => {
|
||||
prepareDashboardFilters([]);
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeGrain,
|
||||
testItems.filterType.timeGrain,
|
||||
testItems.datasetForNativeFilter,
|
||||
);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
applyNativeFilterValueWithIndex(0, testItems.filterTimeGrain);
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
});
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeGrain);
|
||||
validateFilterContentOnDashboard(testItems.filterTimeGrain);
|
||||
});
|
||||
|
||||
it.skip('User can create a time range filter', () => {
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeRange,
|
||||
testItems.filterType.timeRange,
|
||||
);
|
||||
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
|
||||
cy.get(dashboardView.salesDashboardSpecific.vehicleSalesFilterTimeRange)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
applyAdvancedTimeRangeFilterOnDashboard('2005-12-17', '2006-12-17');
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
});
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeRange);
|
||||
cy.get(nativeFilters.filterFromDashboardView.timeRangeFilterContent)
|
||||
.contains('2005-12-17')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it.skip('User can create a time column filter', () => {
|
||||
enterNativeFilterEditModal();
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.timeColumn,
|
||||
testItems.filterType.timeColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
);
|
||||
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
|
||||
cy.intercept(`**/api/v1/chart/data?form_data=**`).as('chart');
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
// assert that native filter is created
|
||||
validateFilterNameOnDashboard(testItems.filterType.timeColumn);
|
||||
applyNativeFilterValueWithIndex(
|
||||
0,
|
||||
testItems.topTenChart.filterColumnYear,
|
||||
);
|
||||
cy.get(nativeFilters.applyFilter).click({ force: true });
|
||||
cy.wait('@chart');
|
||||
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
|
||||
});
|
||||
|
||||
describe.only('Numerical Range Filter - Display Modes', () => {
|
||||
beforeEach(() => {
|
||||
visitDashboard();
|
||||
});
|
||||
|
||||
const expandFilterConfiguration = () => {
|
||||
cy.get('.ant-collapse-header')
|
||||
.contains('Filter Configuration')
|
||||
.should('be.visible')
|
||||
.then($header => {
|
||||
cy.wrap($header)
|
||||
.closest('.ant-collapse-item')
|
||||
.invoke('hasClass', 'ant-collapse-item-active')
|
||||
.then(isExpanded => {
|
||||
if (!isExpanded) cy.wrap($header).click();
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.ant-collapse-content-box').should('be.visible');
|
||||
};
|
||||
|
||||
const selectRangeTypeOption = (label: string) => {
|
||||
cy.contains('Range Type')
|
||||
.should('be.visible')
|
||||
.closest('.ant-form-item')
|
||||
.within(() => {
|
||||
cy.get('.ant-select-selector').click();
|
||||
});
|
||||
|
||||
cy.get('.ant-select-dropdown:visible')
|
||||
.contains('.ant-select-item-option', label)
|
||||
.click();
|
||||
};
|
||||
|
||||
const applyAndAssertInputs = (from: string, to: string) => {
|
||||
// Set 'from' input
|
||||
cy.get('[data-test="range-filter-from-input"]').clear();
|
||||
cy.get('[data-test="range-filter-from-input"]').type(from);
|
||||
cy.get('[data-test="range-filter-from-input"]').blur();
|
||||
|
||||
// Set 'to' input
|
||||
cy.get('[data-test="range-filter-to-input"]').clear();
|
||||
cy.get('[data-test="range-filter-to-input"]').type(to);
|
||||
cy.get('[data-test="range-filter-to-input"]').blur();
|
||||
|
||||
// Assert values without chaining after .invoke()
|
||||
cy.get('[data-test="range-filter-from-input"]')
|
||||
.invoke('val')
|
||||
.then(val => {
|
||||
expect(val).to.equal(from);
|
||||
});
|
||||
|
||||
cy.get('[data-test="range-filter-to-input"]')
|
||||
.invoke('val')
|
||||
.then(val => {
|
||||
expect(val).to.equal(to);
|
||||
});
|
||||
};
|
||||
|
||||
it('User can create a numerical range filter with "Range Inputs" display mode', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
|
||||
expandFilterConfiguration();
|
||||
selectRangeTypeOption('Range Inputs');
|
||||
|
||||
saveNativeFilterSettings([]);
|
||||
cy.wait(500); // allow filter to mount
|
||||
|
||||
applyAndAssertInputs('40', '70');
|
||||
});
|
||||
|
||||
it('User can change the display mode to "Slider"', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
|
||||
expandFilterConfiguration();
|
||||
|
||||
cy.contains('Range Type')
|
||||
.should('be.visible')
|
||||
.closest('.ant-form-item')
|
||||
.within(() => {
|
||||
cy.get('.ant-select-selector').click({ force: true });
|
||||
});
|
||||
|
||||
cy.get('.ant-select-dropdown:visible .ant-select-item-option')
|
||||
.contains(/^Slider$/)
|
||||
.click({ force: true });
|
||||
|
||||
cy.get('.ant-select-selector').should('contain.text', 'Slider');
|
||||
|
||||
saveNativeFilterSettings([]);
|
||||
|
||||
cy.get('.ant-slider', { timeout: 10000 }).should('be.visible');
|
||||
|
||||
cy.get('[data-test="range-filter-from-input"]', {
|
||||
timeout: 5000,
|
||||
}).should('not.exist');
|
||||
cy.get('[data-test="range-filter-to-input"]', { timeout: 5000 }).should(
|
||||
'not.exist',
|
||||
);
|
||||
});
|
||||
|
||||
it('User can change the display mode to "Slider and range input"', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
|
||||
// Re-create filter
|
||||
fillNativeFilterForm(
|
||||
testItems.filterType.numerical,
|
||||
testItems.filterNumericalColumn,
|
||||
testItems.datasetForNativeFilter,
|
||||
testItems.filterNumericalColumn,
|
||||
);
|
||||
|
||||
expandFilterConfiguration();
|
||||
selectRangeTypeOption('Slider and range input');
|
||||
|
||||
saveNativeFilterSettings([]);
|
||||
cy.wait(500);
|
||||
|
||||
applyAndAssertInputs('40', '70');
|
||||
});
|
||||
});
|
||||
|
||||
it('User can undo deleting a native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
undoDeleteNativeFilter();
|
||||
cy.get(nativeFilters.modal.container)
|
||||
.find(nativeFilters.filtersPanel.filterName)
|
||||
.should('have.attr', 'value', testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('User can cancel changes in native filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
cy.getBySel('filters-config-modal__name-input').type('|EDITED', {
|
||||
force: true,
|
||||
});
|
||||
cancelNativeFilterSettings();
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.contains('You have removed this filter.').should('be.visible');
|
||||
});
|
||||
|
||||
it('User can create a value filter', () => {
|
||||
visitDashboard();
|
||||
enterNativeFilterEditModal(false);
|
||||
addCountryNameFilter();
|
||||
cy.get(nativeFilters.filtersPanel.filterTypeInput)
|
||||
.find(nativeFilters.filtersPanel.filterTypeItem)
|
||||
.should('have.text', testItems.filterType.value);
|
||||
saveNativeFilterSettings([]);
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
});
|
||||
|
||||
it('User can apply value filter with selected values', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
applyNativeFilterValueWithIndex(0, testItems.filterDefaultValue);
|
||||
cy.get(nativeFilters.applyFilter).click();
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('User can stop filtering when filter is removed', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
cy.get(nativeFilters.filterItem)
|
||||
.contains(testItems.filterDefaultValue)
|
||||
.should('be.visible');
|
||||
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
|
||||
enterNativeFilterEditModal(false);
|
||||
deleteNativeFilter();
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,431 +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 {
|
||||
nativeFilters,
|
||||
exploreView,
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import {
|
||||
addParentFilterWithValue,
|
||||
applyNativeFilterValueWithIndex,
|
||||
cancelNativeFilterSettings,
|
||||
checkNativeFilterTooltip,
|
||||
clickOnAddFilterInModal,
|
||||
collapseFilterOnLeftPanel,
|
||||
enterNativeFilterEditModal,
|
||||
expandFilterOnLeftPanel,
|
||||
getNativeFilterPlaceholderWithIndex,
|
||||
inputNativeFilterDefaultValue,
|
||||
saveNativeFilterSettings,
|
||||
nativeFilterTooltips,
|
||||
validateFilterContentOnDashboard,
|
||||
valueNativeFilterOptions,
|
||||
validateFilterNameOnDashboard,
|
||||
testItems,
|
||||
} from './utils';
|
||||
import {
|
||||
prepareDashboardFilters,
|
||||
SAMPLE_CHART,
|
||||
visitDashboard,
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function selectFilter(index: number) {
|
||||
cy.get("[data-test='filter-title-container'] [role='tab']").eq(index).click();
|
||||
}
|
||||
|
||||
function closeFilterModal() {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
|
||||
cy.getBySel('native-filter-modal-cancel-button').click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('Native filters', () => {
|
||||
describe('Nativefilters tests initial state required', () => {
|
||||
beforeEach(() => {
|
||||
cy.createSampleDashboards([0]);
|
||||
});
|
||||
|
||||
it.skip('Verify that default value is respected after revisit', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(nativeFilters.filterItem)
|
||||
.contains(testItems.filterDefaultValue)
|
||||
.should('be.visible');
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
|
||||
// reload dashboard
|
||||
cy.reload();
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('not.exist');
|
||||
});
|
||||
validateFilterContentOnDashboard(testItems.filterDefaultValue);
|
||||
});
|
||||
|
||||
it('User can create parent filters using "Values are dependent on other filters"', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region' },
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
selectFilter(1);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
[
|
||||
testItems.topTenChart.filterColumnRegion,
|
||||
testItems.topTenChart.filterColumn,
|
||||
].forEach(it => {
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterName)
|
||||
.contains(it)
|
||||
.should('be.visible');
|
||||
});
|
||||
getNativeFilterPlaceholderWithIndex(1)
|
||||
.invoke('text')
|
||||
.should('equal', '214 options', { timeout: 20000 });
|
||||
// apply first filter value and validate 2nd filter is depden on 1st filter.
|
||||
applyNativeFilterValueWithIndex(0, 'North America');
|
||||
getNativeFilterPlaceholderWithIndex(0).should('have.text', '3 options', {
|
||||
timeout: 20000,
|
||||
});
|
||||
});
|
||||
|
||||
it('user can delete dependent filter', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region' },
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
selectFilter(1);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
// remove year native filter to cause it disappears from parent filter input in global sales
|
||||
cy.get(nativeFilters.modal.tabsList.removeTab)
|
||||
.should('be.visible')
|
||||
.first()
|
||||
.click();
|
||||
// make sure you are seeing global sales filter which had parent filter
|
||||
cy.get(nativeFilters.modal.tabsList.filterItemsContainer)
|
||||
.children()
|
||||
.last()
|
||||
.click();
|
||||
//
|
||||
cy.wait(1000);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters').should(
|
||||
'not.exist',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('user cannot create bi-directional dependencies between filters', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region' },
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
{ name: 'country_code', column: 'country_code' },
|
||||
{ name: 'year', column: 'year' },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
|
||||
// First, make country_name dependent on region
|
||||
selectFilter(1);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
|
||||
// Second, make country_code dependent on country_name
|
||||
selectFilter(2);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumn);
|
||||
|
||||
// Now select region filter and try to add dependency
|
||||
selectFilter(0);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
// Verify that only 'year' is available as dependency for region
|
||||
// 'country_name' and 'country_code' should not be available (would create circular dependency)
|
||||
cy.get('input[aria-label^="Limit type"]').click({ force: true });
|
||||
cy.get('[role="listbox"]').should('be.visible');
|
||||
cy.get('[role="listbox"]').should('contain', 'year');
|
||||
cy.get('[role="listbox"]').should('not.contain', 'country_name');
|
||||
cy.get('[role="listbox"]').should('not.contain', 'country_code');
|
||||
cy.get('[role="listbox"]').contains('year').click();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('Dependent filter selects first item based on parent filter selection', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region' },
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
]);
|
||||
|
||||
enterNativeFilterEditModal();
|
||||
|
||||
selectFilter(0);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Select first filter value by default')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Can select multiple values ')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
|
||||
selectFilter(1);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Can select multiple values ')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Select first filter value by default')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
|
||||
// cannot use saveNativeFilterSettings because there is a bug which
|
||||
// sometimes does not allow charts to load when enabling the 'Select first filter value by default'
|
||||
// to be saved when using dependent filters so,
|
||||
// you reload the window.
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
cy.reload();
|
||||
|
||||
applyNativeFilterValueWithIndex(0, 'North America');
|
||||
|
||||
// Check that dependent filter auto-selects the first item
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterContent)
|
||||
.eq(1)
|
||||
.should('contain.text', 'Bermuda');
|
||||
});
|
||||
|
||||
it('User can create filter depend on 2 other filters', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region' },
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
{ name: 'country_code', column: 'country_code' },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
selectFilter(2);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(exploreView.controlPanel.addFieldValue).click();
|
||||
},
|
||||
);
|
||||
// add value to the first input
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
// add value to the second input
|
||||
addParentFilterWithValue(1, testItems.topTenChart.filterColumn);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
// filters should be displayed in the left panel
|
||||
[
|
||||
testItems.topTenChart.filterColumnRegion,
|
||||
testItems.topTenChart.filterColumn,
|
||||
testItems.topTenChart.filterColumnCountryCode,
|
||||
].forEach(it => {
|
||||
validateFilterNameOnDashboard(it);
|
||||
});
|
||||
|
||||
// initially first filter shows 39 options
|
||||
getNativeFilterPlaceholderWithIndex(0).should('have.text', '7 options');
|
||||
// initially second filter shows 409 options
|
||||
getNativeFilterPlaceholderWithIndex(1).should('have.text', '214 options');
|
||||
// verify third filter shows 409 options
|
||||
getNativeFilterPlaceholderWithIndex(2).should('have.text', '214 options');
|
||||
|
||||
// apply first filter value
|
||||
applyNativeFilterValueWithIndex(0, 'North America');
|
||||
|
||||
// verify second filter shows 409 options available still
|
||||
getNativeFilterPlaceholderWithIndex(0).should('have.text', '214 options');
|
||||
|
||||
// verify second filter shows 69 options available still
|
||||
getNativeFilterPlaceholderWithIndex(1).should('have.text', '3 options');
|
||||
|
||||
// apply second filter value
|
||||
applyNativeFilterValueWithIndex(1, 'United States');
|
||||
|
||||
// verify number of available options for third filter - should be decreased to only one
|
||||
getNativeFilterPlaceholderWithIndex(0).should('have.text', '1 option');
|
||||
});
|
||||
|
||||
it('User can remove parent filters', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region' },
|
||||
{ name: 'country_name', column: 'country_name' },
|
||||
]);
|
||||
enterNativeFilterEditModal();
|
||||
selectFilter(1);
|
||||
// Select dependent option and auto use platform for genre
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.get(nativeFilters.modal.tabsList.removeTab)
|
||||
.should('be.visible')
|
||||
.first()
|
||||
.click({
|
||||
force: true,
|
||||
});
|
||||
saveNativeFilterSettings([SAMPLE_CHART]);
|
||||
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
|
||||
cy.contains(testItems.filterDefaultValue).should('be.visible');
|
||||
cy.contains(testItems.filterOtherCountry).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nativefilters basic interactions', () => {
|
||||
before(() => {
|
||||
visitDashboard();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.createSampleDashboards([0]);
|
||||
closeFilterModal();
|
||||
});
|
||||
|
||||
it('User can expand / retract native filter sidebar on a dashboard', () => {
|
||||
expandFilterOnLeftPanel();
|
||||
cy.get(nativeFilters.filtersPanel.filterGear).click({
|
||||
force: true,
|
||||
});
|
||||
cy.get('.ant-dropdown-menu').should('be.visible');
|
||||
cy.get(nativeFilters.filterFromDashboardView.createFilterButton).should(
|
||||
'be.visible',
|
||||
);
|
||||
cy.get(nativeFilters.filterFromDashboardView.expand).should(
|
||||
'not.be.visible',
|
||||
);
|
||||
collapseFilterOnLeftPanel();
|
||||
});
|
||||
|
||||
it('User can enter filter edit pop-up by clicking on native filter edit icon', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
});
|
||||
|
||||
it('User can delete a native filter', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.contains('Restore filter').should('not.exist', { timeout: 10000 });
|
||||
});
|
||||
|
||||
it('User can cancel creating a new filter', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
cancelNativeFilterSettings();
|
||||
});
|
||||
|
||||
it('Verify setting options and tooltips for value filter', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.contains('Filter value is required').scrollIntoView();
|
||||
cy.get('body').trigger('mousemove', { clientX: 0, clientY: 0 });
|
||||
cy.wait(300);
|
||||
|
||||
cy.contains('Filter value is required').should('be.visible').click({
|
||||
force: true,
|
||||
});
|
||||
checkNativeFilterTooltip(0, nativeFilterTooltips.preFilter);
|
||||
checkNativeFilterTooltip(1, nativeFilterTooltips.defaultValue);
|
||||
cy.get(nativeFilters.modal.container).should('be.visible');
|
||||
valueNativeFilterOptions.forEach(el => {
|
||||
cy.contains(el);
|
||||
});
|
||||
cy.contains('Values are dependent on other filters').should('not.exist');
|
||||
cy.get(
|
||||
nativeFilters.filterConfigurationSections.checkedCheckbox,
|
||||
).contains('Can select multiple values');
|
||||
checkNativeFilterTooltip(2, nativeFilterTooltips.required);
|
||||
checkNativeFilterTooltip(3, nativeFilterTooltips.defaultToFirstItem);
|
||||
checkNativeFilterTooltip(4, nativeFilterTooltips.searchAllFilterOptions);
|
||||
checkNativeFilterTooltip(5, nativeFilterTooltips.inverseSelection);
|
||||
clickOnAddFilterInModal();
|
||||
cy.contains('Values are dependent on other filters').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,194 +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 {
|
||||
parsePostForm,
|
||||
waitForChartLoad,
|
||||
getChartAliasBySpec,
|
||||
} from 'cypress/utils';
|
||||
import { TABBED_DASHBOARD } from 'cypress/utils/urls';
|
||||
import { expandFilterOnLeftPanel } from './utils';
|
||||
|
||||
const TREEMAP = { name: 'Treemap', viz: 'treemap_v2' };
|
||||
const LINE_CHART = { name: 'Growth Rate', viz: 'echarts_timeseries_line' };
|
||||
const BOX_PLOT = { name: 'Box plot', viz: 'box_plot' };
|
||||
const BIG_NUMBER = { name: 'Number of Girls', viz: 'big_number_total' };
|
||||
const TABLE = { name: 'Names Sorted by Num in California', viz: 'table' };
|
||||
|
||||
function topLevelTabs() {
|
||||
cy.getBySel('dashboard-component-tabs')
|
||||
.first()
|
||||
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||
.as('top-level-tabs');
|
||||
}
|
||||
|
||||
function resetTabs() {
|
||||
topLevelTabs();
|
||||
cy.get('@top-level-tabs').first().click();
|
||||
waitForChartLoad(TREEMAP);
|
||||
waitForChartLoad(BIG_NUMBER);
|
||||
waitForChartLoad(TABLE);
|
||||
}
|
||||
|
||||
describe.skip('Dashboard tabs', () => {
|
||||
before(() => {
|
||||
cy.visit(TABBED_DASHBOARD);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetTabs();
|
||||
});
|
||||
|
||||
it('should switch tabs', () => {
|
||||
topLevelTabs();
|
||||
|
||||
cy.get('@top-level-tabs').first().click();
|
||||
cy.get('@top-level-tabs')
|
||||
.first()
|
||||
.should('have.class', 'ant-tabs-tab-active');
|
||||
cy.get('@top-level-tabs')
|
||||
.last()
|
||||
.should('not.have.class', 'ant-tabs-tab-active');
|
||||
cy.get('[data-test-chart-name="Box plot"]').should('not.exist');
|
||||
cy.get('[data-test-chart-name="Trends"]').should('not.exist');
|
||||
|
||||
cy.get('@top-level-tabs').last().click();
|
||||
cy.get('@top-level-tabs')
|
||||
.last()
|
||||
.should('have.class', 'ant-tabs-tab-active');
|
||||
cy.get('@top-level-tabs')
|
||||
.first()
|
||||
.should('not.have.class', 'ant-tabs-tab-active');
|
||||
waitForChartLoad(BOX_PLOT);
|
||||
|
||||
cy.get('[data-test-chart-name="Box plot"]').should('exist');
|
||||
|
||||
resetTabs();
|
||||
|
||||
// click row level tab, see 1 more chart
|
||||
cy.getBySel('dashboard-component-tabs')
|
||||
.eq(2)
|
||||
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||
.as('row-level-tabs');
|
||||
|
||||
cy.get('@row-level-tabs').last().click();
|
||||
waitForChartLoad(LINE_CHART);
|
||||
cy.get('[data-test-chart-name="Trends"]').should('exist');
|
||||
cy.get('@row-level-tabs').first().click();
|
||||
});
|
||||
|
||||
it.skip('should send new queries when tab becomes visible', () => {
|
||||
// landing in first tab
|
||||
waitForChartLoad(TREEMAP);
|
||||
|
||||
getChartAliasBySpec(TREEMAP).then(treemapAlias => {
|
||||
// apply filter
|
||||
cy.get('.Select__control').first().should('be.visible').click();
|
||||
cy.get('.Select__control input[type=text]').first().focus();
|
||||
cy.focused().type('South');
|
||||
cy.get('.Select__option').contains('South Asia').click();
|
||||
cy.get('.filter button:not(:disabled)').contains('Apply').click();
|
||||
|
||||
// send new query from same tab
|
||||
cy.wait(treemapAlias).then(({ request }) => {
|
||||
const requestBody = parsePostForm(request.body);
|
||||
const requestParams = JSON.parse(requestBody.form_data as string);
|
||||
expect(requestParams.extra_filters[0]).deep.eq({
|
||||
col: 'region',
|
||||
op: 'IN',
|
||||
val: ['South Asia'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept('**/superset/explore_json/?*').as('legacyChartData');
|
||||
// click row level tab, send 1 more query
|
||||
cy.get('.ant-tabs-tab').contains('row tab 2').click();
|
||||
|
||||
cy.wait('@legacyChartData').then(({ request }) => {
|
||||
const requestBody = parsePostForm(request.body);
|
||||
const requestParams = JSON.parse(requestBody.form_data as string);
|
||||
expect(requestParams.extra_filters[0]).deep.eq({
|
||||
col: 'region',
|
||||
op: 'IN',
|
||||
val: ['South Asia'],
|
||||
});
|
||||
expect(requestParams.viz_type).eq(LINE_CHART.viz);
|
||||
});
|
||||
|
||||
cy.intercept('POST', '**/api/v1/chart/data?*').as('v1ChartData');
|
||||
|
||||
// click top level tab, send 1 more query
|
||||
cy.get('.ant-tabs-tab').contains('Tab B').click();
|
||||
|
||||
cy.wait('@v1ChartData').then(({ request }) => {
|
||||
expect(request.body.queries[0].filters[0]).deep.eq({
|
||||
col: 'region',
|
||||
op: 'IN',
|
||||
val: ['South Asia'],
|
||||
});
|
||||
});
|
||||
|
||||
getChartAliasBySpec(BOX_PLOT).then(boxPlotAlias => {
|
||||
// navigate to filter and clear filter
|
||||
cy.get('.ant-tabs-tab').contains('Tab A').click();
|
||||
cy.get('.ant-tabs-tab').contains('row tab 1').click();
|
||||
|
||||
cy.get('.Select__clear-indicator').click();
|
||||
cy.get('.filter button:not(:disabled)').contains('Apply').click();
|
||||
|
||||
// trigger 1 new query
|
||||
waitForChartLoad(TREEMAP);
|
||||
// make sure query API not requested multiple times
|
||||
cy.on('fail', err => {
|
||||
expect(err.message).to.include('timed out waiting');
|
||||
return false;
|
||||
});
|
||||
|
||||
cy.wait(boxPlotAlias, { timeout: 1000 }).then(() => {
|
||||
throw new Error('Unexpected API call.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update size when switch tab', () => {
|
||||
cy.get('@top-level-tabs').last().click();
|
||||
cy.get('@top-level-tabs')
|
||||
.last()
|
||||
.should('have.class', 'ant-tabs-tab-active');
|
||||
|
||||
expandFilterOnLeftPanel();
|
||||
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('@top-level-tabs').first().click();
|
||||
cy.get('@top-level-tabs')
|
||||
.first()
|
||||
.should('have.class', 'ant-tabs-tab-active');
|
||||
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get("[data-test-viz-type='treemap_v2'] .chart-container").then(
|
||||
$chartContainer => {
|
||||
expect($chartContainer.get(0).scrollWidth).eq(
|
||||
$chartContainer.get(0).offsetWidth,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,45 +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 { parsePostForm, JsonObject, waitForChartLoad } from 'cypress/utils';
|
||||
import { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls';
|
||||
import { WORLD_HEALTH_CHARTS } from './utils';
|
||||
|
||||
describe('Dashboard form data', () => {
|
||||
const urlParams = { param1: '123', param2: 'abc' };
|
||||
before(() => {
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD, { qs: urlParams });
|
||||
});
|
||||
|
||||
it('should apply url params to slice requests', () => {
|
||||
cy.intercept('**/api/v1/chart/data?*', request => {
|
||||
// TODO: export url params to chart data API
|
||||
request.body.queries.forEach((query: { url_params: JsonObject }) => {
|
||||
expect(query.url_params).deep.eq(urlParams);
|
||||
});
|
||||
});
|
||||
cy.intercept('**/superset/explore_json/*', request => {
|
||||
const requestParams = JSON.parse(
|
||||
parsePostForm(request.body).form_data as string,
|
||||
);
|
||||
expect(requestParams.url_params).deep.eq(urlParams);
|
||||
});
|
||||
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
});
|
||||
@@ -1,67 +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 { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
|
||||
import { interceptFav, interceptUnfav } from './utils';
|
||||
|
||||
describe('Dashboard actions', () => {
|
||||
beforeEach(() => {
|
||||
cy.createSampleDashboards([0]);
|
||||
cy.visit(SAMPLE_DASHBOARD_1);
|
||||
});
|
||||
it('should allow to favorite/unfavorite dashboard', () => {
|
||||
interceptFav();
|
||||
interceptUnfav();
|
||||
|
||||
// Find and click StarOutlined (adds to favorites)
|
||||
cy.getBySel('dashboard-header-container')
|
||||
.find("[aria-label='unstarred']")
|
||||
.as('starIconOutlined')
|
||||
.should('exist')
|
||||
.click();
|
||||
|
||||
cy.wait('@select');
|
||||
|
||||
// After clicking, StarFilled should appear
|
||||
cy.getBySel('dashboard-header-container')
|
||||
.find("[aria-label='starred']")
|
||||
.as('starIconFilled')
|
||||
.should('exist');
|
||||
|
||||
// Verify the color of the filled star (gold)
|
||||
cy.get('@starIconFilled')
|
||||
.should('have.css', 'color')
|
||||
.and('eq', 'rgb(252, 199, 0)');
|
||||
|
||||
// Click on StarFilled (removes from favorites)
|
||||
cy.get('@starIconFilled').click();
|
||||
|
||||
cy.wait('@unselect');
|
||||
|
||||
// After clicking, StarOutlined should reappear
|
||||
cy.getBySel('dashboard-header-container')
|
||||
.find("[aria-label='unstarred']")
|
||||
.as('starIconOutlinedAfter')
|
||||
.should('exist');
|
||||
|
||||
// Verify the color of the outlined star (gray)
|
||||
cy.get('@starIconOutlinedAfter')
|
||||
.should('have.css', 'color')
|
||||
.and('eq', 'rgba(0, 0, 0, 0.45)');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -160,18 +160,6 @@ export function interceptLog() {
|
||||
cy.intercept('**/superset/log/?explode=events&dashboard_id=*').as('logs');
|
||||
}
|
||||
|
||||
export function interceptFav() {
|
||||
cy.intercept({ url: `**/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
|
||||
'select',
|
||||
);
|
||||
}
|
||||
|
||||
export function interceptUnfav() {
|
||||
cy.intercept({ url: `**/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
|
||||
'unselect',
|
||||
);
|
||||
}
|
||||
|
||||
export function interceptDataset() {
|
||||
cy.intercept('GET', `**/api/v1/dataset/*`).as('getDataset');
|
||||
}
|
||||
|
||||
@@ -1,109 +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('AdhocFilters', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '**/api/v1/datasource/table/*/column/name/values').as(
|
||||
'filterValues',
|
||||
);
|
||||
cy.intercept('POST', '**/superset/explore_json/**').as('postJson');
|
||||
cy.intercept('GET', '**/superset/explore_json/**').as('getJson');
|
||||
cy.visitChartByName('Boys'); // a table chart
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
});
|
||||
|
||||
let numScripts = 0;
|
||||
|
||||
it('Should load AceEditor scripts when needed', () => {
|
||||
cy.get('script').then(nodes => {
|
||||
numScripts = nodes.length;
|
||||
});
|
||||
|
||||
cy.get('[data-test=adhoc_filters]').within(() => {
|
||||
cy.get('.Select__control').scrollIntoView();
|
||||
cy.get('.Select__control').click();
|
||||
cy.get('input[type=text]').focus();
|
||||
cy.focused().type('name{enter}');
|
||||
cy.get("div[role='button']").first().click();
|
||||
});
|
||||
|
||||
// antd tabs do lazy loading, so we need to click on tab with ace editor
|
||||
cy.get('#filter-edit-popover').within(() => {
|
||||
cy.get('.ant-tabs-tab').contains('Custom SQL').click();
|
||||
cy.get('.ant-tabs-tab').contains('Simple').click();
|
||||
});
|
||||
|
||||
cy.get('script').then(nodes => {
|
||||
// should load new script chunks for SQL editor
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
});
|
||||
});
|
||||
|
||||
it('Set simple adhoc filter', () => {
|
||||
cy.get('[aria-label="Comparator option"] .Select__control').click();
|
||||
cy.get('[data-test=adhoc-filter-simple-value] input[type=text]').focus();
|
||||
cy.focused().type('Jack{enter}', { delay: 20 });
|
||||
|
||||
cy.get('[data-test="adhoc-filter-edit-popover-save-button"]').click();
|
||||
|
||||
cy.get(
|
||||
'[data-test=adhoc_filters] .Select__control span.option-label',
|
||||
).contains('name = Jack');
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
});
|
||||
|
||||
it('Set custom adhoc filter', () => {
|
||||
const filterType = 'name';
|
||||
const filterContent = "'Amy' OR name = 'Donald'";
|
||||
|
||||
cy.get('[data-test=adhoc_filters] .Select__control').scrollIntoView();
|
||||
cy.get('[data-test=adhoc_filters] .Select__control').click();
|
||||
|
||||
// remove previous input
|
||||
cy.get('[data-test=adhoc_filters] input[type=text]').focus();
|
||||
cy.focused().type('{backspace}');
|
||||
|
||||
cy.get('[data-test=adhoc_filters] input[type=text]').focus();
|
||||
cy.focused().type(`${filterType}{enter}`);
|
||||
|
||||
cy.wait('@filterValues');
|
||||
|
||||
// selecting a new filter should auto-open the popup,
|
||||
// so the tab should be visible by now
|
||||
cy.get('#filter-edit-popover #adhoc-filter-edit-tabs-tab-SQL').click();
|
||||
cy.get('#filter-edit-popover .ace_content').click();
|
||||
cy.get('#filter-edit-popover .ace_text-input').type(filterContent);
|
||||
cy.get('[data-test="adhoc-filter-edit-popover-save-button"]').click();
|
||||
|
||||
// check if the filter was saved correctly
|
||||
cy.get(
|
||||
'[data-test=adhoc_filters] .Select__control span.option-label',
|
||||
).contains(`${filterType} = ${filterContent}`);
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,123 +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 { interceptChart } from 'cypress/utils';
|
||||
|
||||
describe('AdhocMetrics', () => {
|
||||
beforeEach(() => {
|
||||
interceptChart({ legacy: false }).as('chartData');
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
});
|
||||
|
||||
it('Clear metric and set simple adhoc metric', () => {
|
||||
const metric = 'sum(num_girls)';
|
||||
const metricName = 'Sum Girls';
|
||||
cy.get('[data-test=metrics]')
|
||||
.find('[data-test="remove-control-button"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-test=metrics]')
|
||||
.contains('Drop columns/metrics here or click')
|
||||
.click();
|
||||
|
||||
// Title edit for saved metrics is disabled - switch to Simple
|
||||
cy.get('[id="adhoc-metric-edit-tabs-tab-SIMPLE"]').click();
|
||||
|
||||
cy.get('[data-test="AdhocMetricEditTitle#trigger"]').click();
|
||||
cy.get('[data-test="AdhocMetricEditTitle#input"]').type(metricName);
|
||||
|
||||
cy.get('input[aria-label="Select column"]').click();
|
||||
cy.get('input[aria-label="Select column"]').type('num_girls{enter}');
|
||||
cy.get('input[aria-label="Select aggregate options"]').click();
|
||||
cy.get('input[aria-label="Select aggregate options"]').type('sum{enter}');
|
||||
|
||||
cy.get('[data-test="AdhocMetricEdit#save"]').contains('Save').click();
|
||||
|
||||
cy.get('[data-test="control-label"]').contains(metricName);
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: `${metric} AS "${metricName}"`, // SQL statement
|
||||
});
|
||||
});
|
||||
|
||||
xit('Switch from simple to custom sql', () => {
|
||||
cy.get('[data-test=metrics]')
|
||||
.find('[data-test="metric-option"]')
|
||||
.should('have.length', 1);
|
||||
|
||||
// select column "num"
|
||||
cy.get('[data-test=metrics]').find('.Select__clear-indicator').click();
|
||||
|
||||
cy.get('[data-test=metrics]').find('.Select__control').click();
|
||||
|
||||
cy.get('[data-test=metrics]').find('.Select__control input').type('num');
|
||||
|
||||
cy.get('[data-test=metrics]')
|
||||
.find('.option-label')
|
||||
.first()
|
||||
.should('have.text', 'num')
|
||||
.click();
|
||||
|
||||
// add custom SQL
|
||||
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
|
||||
cy.get('[data-test=metrics-edit-popover]').within(() => {
|
||||
cy.get('.ace_content').click();
|
||||
cy.get('.ace_text-input').type('/COUNT(DISTINCT name)', { force: true });
|
||||
cy.get('[data-test="AdhocMetricEdit#save"]').contains('Save').click();
|
||||
});
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
|
||||
const metric = 'SUM(num)/COUNT(DISTINCT name)';
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: `${metric} AS "${metric}"`,
|
||||
});
|
||||
});
|
||||
|
||||
xit('Switch from custom sql tabs to simple', () => {
|
||||
cy.get('[data-test=metrics]').within(() => {
|
||||
cy.get('.Select__dropdown-indicator').click();
|
||||
cy.get('input[type=text]').type('num_girls{enter}');
|
||||
});
|
||||
cy.get('[data-test=metrics]')
|
||||
.find('[data-test="metric-option"]')
|
||||
.should('have.length', 2);
|
||||
|
||||
cy.get('#metrics-edit-popover').within(() => {
|
||||
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
|
||||
cy.get('.ace_identifier').contains('num_girls');
|
||||
cy.get('.ace_content').click();
|
||||
cy.get('.ace_text-input').type('{selectall}{backspace}SUM(num)');
|
||||
cy.get('#adhoc-metric-edit-tabs-tab-SIMPLE').click();
|
||||
cy.get('.Select__single-value').contains(/^num$/);
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
|
||||
const metric = 'SUM(num)';
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: `${metric} AS "${metric}"`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +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 { interceptV1ChartData } from './utils';
|
||||
|
||||
describe('Advanced analytics', () => {
|
||||
beforeEach(() => {
|
||||
interceptV1ChartData();
|
||||
cy.intercept('PUT', '**/api/v1/explore/**').as('putExplore');
|
||||
cy.intercept('GET', '**/explore/**').as('getExplore');
|
||||
});
|
||||
|
||||
it('Create custom time compare', () => {
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@v1Data' });
|
||||
|
||||
cy.get('.ant-collapse-header')
|
||||
.contains('Advanced analytics')
|
||||
.click({ force: true });
|
||||
|
||||
cy.get('[data-test=time_compare]').find('.ant-select').click();
|
||||
cy.get('[data-test=time_compare]')
|
||||
.find('input[type=search]')
|
||||
.type('28 days{enter}');
|
||||
|
||||
cy.get('[data-test=time_compare]').find('input[type=search]').clear();
|
||||
cy.get('[data-test=time_compare]')
|
||||
.find('input[type=search]')
|
||||
.type('1 year{enter}');
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.wait('@v1Data');
|
||||
cy.wait('@putExplore');
|
||||
|
||||
cy.reload();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@v1Data',
|
||||
});
|
||||
cy.wait('@getExplore');
|
||||
cy.get('.ant-collapse-header')
|
||||
.contains('Advanced analytics')
|
||||
.click({ force: true });
|
||||
cy.get('[data-test=time_compare]')
|
||||
.find('.ant-select-selector')
|
||||
.contains('28 days');
|
||||
cy.get('[data-test=time_compare]')
|
||||
.find('.ant-select-selector')
|
||||
.contains('1 year');
|
||||
});
|
||||
});
|
||||
@@ -1,48 +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 { interceptChart } from 'cypress/utils';
|
||||
|
||||
describe('Annotations', () => {
|
||||
beforeEach(() => {
|
||||
interceptChart({ legacy: false }).as('chartData');
|
||||
});
|
||||
|
||||
it('Create formula annotation y-axis goal line', () => {
|
||||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
const layerLabel = 'Goal line';
|
||||
|
||||
// get by text Annotations and Layers
|
||||
cy.get('span').contains('Annotations and Layers').click();
|
||||
|
||||
cy.get('[data-test=annotation_layers]').click();
|
||||
|
||||
cy.get('[data-test="popover-content"]').within(() => {
|
||||
cy.get('[aria-label=Name]').type(layerLabel);
|
||||
cy.get('[aria-label=Formula]').type('y=1400000');
|
||||
cy.get('button').contains('OK').click();
|
||||
});
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.get('[data-test=annotation_layers]').contains(layerLabel);
|
||||
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
// ***********************************************
|
||||
// Tests for links in the explore UI
|
||||
// ***********************************************
|
||||
|
||||
import rison from 'rison';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { interceptChart } from 'cypress/utils';
|
||||
import { HEALTH_POP_FORM_DATA_DEFAULTS } from './visualizations/shared.helper';
|
||||
|
||||
const apiURL = (endpoint: string, queryObject: Record<string, unknown>) =>
|
||||
`${endpoint}?q=${rison.encode(queryObject)}`;
|
||||
|
||||
describe('Test explore links', () => {
|
||||
beforeEach(() => {
|
||||
interceptChart({ legacy: false }).as('chartData');
|
||||
});
|
||||
|
||||
it('Open and close view query modal', () => {
|
||||
cy.visitChartByName('Growth Rate');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[aria-label="Menu actions trigger"]').click();
|
||||
cy.get('span').contains('View query').parent().click();
|
||||
cy.wait('@chartData').then(() => {
|
||||
cy.get('code');
|
||||
});
|
||||
cy.get('.ant-modal-content').within(() => {
|
||||
cy.get('button.ant-modal-close').first().click({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('Test iframe link', () => {
|
||||
cy.visitChartByName('Growth Rate');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[aria-label="Menu actions trigger"]').click();
|
||||
cy.get('div[role="menuitem"]').within(() => {
|
||||
cy.contains('Share').parent().click();
|
||||
});
|
||||
cy.getBySel('embed-code-button').click();
|
||||
cy.get('#embed-code-popover').within(() => {
|
||||
cy.get('textarea[name=embedCode]').contains('iframe');
|
||||
});
|
||||
});
|
||||
|
||||
it('Test chart save as AND overwrite', () => {
|
||||
interceptChart({ legacy: false }).as('tableChartData');
|
||||
|
||||
const formData = {
|
||||
...HEALTH_POP_FORM_DATA_DEFAULTS,
|
||||
viz_type: 'table',
|
||||
metrics: ['sum__SP_POP_TOTL'],
|
||||
groupby: ['country_name'],
|
||||
};
|
||||
const newChartName = `Test chart [${nanoid()}]`;
|
||||
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
||||
cy.url().then(() => {
|
||||
cy.get('[data-test="query-save-button"]').click();
|
||||
cy.get('[data-test="saveas-radio"]').check();
|
||||
cy.get('[data-test="new-chart-name"]').type(newChartName, {
|
||||
force: true,
|
||||
});
|
||||
cy.get('[data-test="btn-modal-save"]').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
||||
cy.visitChartByName(newChartName);
|
||||
|
||||
// Overwriting!
|
||||
cy.get('[data-test="query-save-button"]').click();
|
||||
cy.get('[data-test="save-overwrite-radio"]').check();
|
||||
cy.get('[data-test="btn-modal-save"]').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
||||
const query = {
|
||||
filters: [
|
||||
{
|
||||
col: 'slice_name',
|
||||
opr: 'eq',
|
||||
value: newChartName,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
cy.request(apiURL('/api/v1/chart/', query)).then(response => {
|
||||
expect(response.body.count).equals(1);
|
||||
});
|
||||
cy.deleteChartByName(newChartName, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('Test chart save as and add to new dashboard', () => {
|
||||
const chartName = 'Growth Rate';
|
||||
const newChartName = `${chartName} [${nanoid()}]`;
|
||||
const dashboardTitle = `Test dashboard [${nanoid()}]`;
|
||||
|
||||
cy.visitChartByName(chartName);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test="query-save-button"]').click();
|
||||
cy.get('[data-test="saveas-radio"]').check();
|
||||
cy.get('[data-test="new-chart-name"]').click();
|
||||
cy.get('[data-test="new-chart-name"]').clear();
|
||||
cy.get('[data-test="new-chart-name"]').type(newChartName);
|
||||
// Add a new option using the "CreatableSelect" feature
|
||||
cy.get('[data-test="save-chart-modal-select-dashboard-form"]')
|
||||
.find('input[aria-label="Select a dashboard"]')
|
||||
.type(`${dashboardTitle}`, { force: true });
|
||||
|
||||
cy.get(`.ant-select-item[title="${dashboardTitle}"]`).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get('[data-test="btn-modal-save"]').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
let query = {
|
||||
filters: [
|
||||
{
|
||||
col: 'dashboard_title',
|
||||
opr: 'eq',
|
||||
value: dashboardTitle,
|
||||
},
|
||||
],
|
||||
};
|
||||
cy.request(apiURL('/api/v1/dashboard/', query)).then(response => {
|
||||
expect(response.body.count).equals(1);
|
||||
});
|
||||
|
||||
cy.visitChartByName(newChartName);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test="query-save-button"]').click();
|
||||
cy.get('[data-test="save-overwrite-radio"]').check();
|
||||
cy.get('[data-test="new-chart-name"]').click();
|
||||
cy.get('[data-test="new-chart-name"]').clear();
|
||||
cy.get('[data-test="new-chart-name"]').type(newChartName);
|
||||
// This time around, typing the same dashboard name
|
||||
// will select the existing one
|
||||
cy.get('[data-test="save-chart-modal-select-dashboard-form"]')
|
||||
.find('input[aria-label^="Select a dashboard"]')
|
||||
.type(`${dashboardTitle}{enter}`, { force: true });
|
||||
|
||||
cy.get(`.ant-select-item[title="${dashboardTitle}"]`).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get('[data-test="btn-modal-save"]').click();
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
query = {
|
||||
filters: [
|
||||
{
|
||||
col: 'slice_name',
|
||||
opr: 'eq',
|
||||
value: chartName,
|
||||
},
|
||||
],
|
||||
};
|
||||
cy.request(apiURL('/api/v1/chart/', query)).then(response => {
|
||||
expect(response.body.count).equals(1);
|
||||
});
|
||||
query = {
|
||||
filters: [
|
||||
{
|
||||
col: 'dashboard_title',
|
||||
opr: 'eq',
|
||||
value: dashboardTitle,
|
||||
},
|
||||
],
|
||||
};
|
||||
cy.request(apiURL('/api/v1/dashboard/', query)).then(response => {
|
||||
expect(response.body.count).equals(1);
|
||||
});
|
||||
cy.deleteDashboardByName(dashboardTitle, true);
|
||||
});
|
||||
});
|
||||
@@ -1,80 +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 { interceptChart } from 'cypress/utils';
|
||||
|
||||
describe('Visualization > Big Number with Trendline', () => {
|
||||
beforeEach(() => {
|
||||
interceptChart({ legacy: false }).as('chartData');
|
||||
});
|
||||
|
||||
const BIG_NUMBER_FORM_DATA = {
|
||||
datasource: '2__table',
|
||||
viz_type: 'big_number',
|
||||
slice_id: 42,
|
||||
granularity_sqla: 'year',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '2000 : 2014-01-02',
|
||||
metric: 'sum__SP_POP_TOTL',
|
||||
adhoc_filters: [],
|
||||
compare_lag: '10',
|
||||
compare_suffix: 'over 10Y',
|
||||
y_axis_format: '.3s',
|
||||
show_trend_line: true,
|
||||
start_y_axis_at_zero: true,
|
||||
color_picker: {
|
||||
r: 0,
|
||||
g: 122,
|
||||
b: 135,
|
||||
a: 1,
|
||||
},
|
||||
};
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
chartSelector: '.superset-legacy-chart-big-number',
|
||||
});
|
||||
}
|
||||
|
||||
it('should work', () => {
|
||||
verify(BIG_NUMBER_FORM_DATA);
|
||||
cy.get('.chart-container .header-line');
|
||||
cy.get('.chart-container canvas');
|
||||
});
|
||||
|
||||
it('should work without subheader', () => {
|
||||
verify({
|
||||
...BIG_NUMBER_FORM_DATA,
|
||||
compare_lag: null,
|
||||
});
|
||||
cy.get('.chart-container .header-line');
|
||||
cy.get('.chart-container .subtitle-line').should('not.exist');
|
||||
cy.get('.chart-container canvas');
|
||||
});
|
||||
|
||||
it('should not render trendline when hidden', () => {
|
||||
verify({
|
||||
...BIG_NUMBER_FORM_DATA,
|
||||
show_trend_line: false,
|
||||
});
|
||||
cy.get('[data-test="chart-container"] .header-line');
|
||||
cy.get('[data-test="chart-container"] canvas').should('not.exist');
|
||||
});
|
||||
});
|
||||
@@ -1,79 +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 { interceptChart } from 'cypress/utils';
|
||||
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
|
||||
|
||||
describe('Visualization > Big Number Total', () => {
|
||||
beforeEach(() => {
|
||||
interceptChart({ legacy: false }).as('chartData');
|
||||
});
|
||||
|
||||
const BIG_NUMBER_DEFAULTS = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
viz_type: 'big_number_total',
|
||||
};
|
||||
|
||||
it('Test big number chart with adhoc metric', () => {
|
||||
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC };
|
||||
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@chartData',
|
||||
querySubstring: NUM_METRIC.label,
|
||||
});
|
||||
});
|
||||
|
||||
it('Test big number chart with simple filter', () => {
|
||||
const filters = [
|
||||
{
|
||||
expressionType: 'SIMPLE',
|
||||
subject: 'name',
|
||||
operator: 'IN',
|
||||
comparator: ['Aaron', 'Amy', 'Andrea'],
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
filterOptionName: 'filter_4y6teao56zs_ebjsvwy48c',
|
||||
},
|
||||
];
|
||||
|
||||
const formData = {
|
||||
...BIG_NUMBER_DEFAULTS,
|
||||
metric: 'count',
|
||||
adhoc_filters: filters,
|
||||
};
|
||||
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
});
|
||||
|
||||
it('Test big number chart ignores groupby', () => {
|
||||
const formData = {
|
||||
...BIG_NUMBER_DEFAULTS,
|
||||
metric: NUM_METRIC,
|
||||
groupby: ['state'],
|
||||
};
|
||||
|
||||
cy.visitChartByParams(formData);
|
||||
cy.wait(['@chartData']).then(async ({ response }) => {
|
||||
cy.verifySliceContainer();
|
||||
const responseBody = response?.body;
|
||||
expect(responseBody.result[0].query).not.contains(formData.groupby[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +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 { getDatasetId } from './shared.helper';
|
||||
|
||||
describe('Visualization > Box Plot', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('POST', '**/api/v1/chart/data*').as('getJson');
|
||||
});
|
||||
|
||||
const getBoxPlotFormData = datasetId => ({
|
||||
datasource: `${datasetId}__table`,
|
||||
viz_type: 'box_plot',
|
||||
granularity_sqla: 'year',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '1960-01-01 : now',
|
||||
metrics: ['sum__SP_POP_TOTL'],
|
||||
adhoc_filters: [],
|
||||
groupby: ['region'],
|
||||
limit: '25',
|
||||
color_scheme: 'bnbColors',
|
||||
whisker_options: 'Min/max (no outliers)',
|
||||
});
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson' });
|
||||
}
|
||||
|
||||
it('should work', () => {
|
||||
getDatasetId('wb_health_population').then(datasetId => {
|
||||
verify(getBoxPlotFormData(datasetId));
|
||||
cy.get('.chart-container .box_plot canvas').should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow type to search color schemes', () => {
|
||||
getDatasetId('wb_health_population').then(datasetId => {
|
||||
verify(getBoxPlotFormData(datasetId));
|
||||
|
||||
cy.get('#controlSections-tab-CUSTOMIZE').click();
|
||||
cy.get('.Control[data-test="color_scheme"]').scrollIntoView();
|
||||
cy.get('.Control[data-test="color_scheme"] input[type="search"]').focus();
|
||||
cy.focused().type('supersetColors{enter}');
|
||||
cy.get(
|
||||
'.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="supersetColors"]',
|
||||
).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,108 +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 { getDatasetId } from './shared.helper';
|
||||
|
||||
describe('Visualization > Bubble', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
|
||||
});
|
||||
|
||||
const getBubbleFormData = datasetId => ({
|
||||
datasource: `${datasetId}__table`,
|
||||
viz_type: 'bubble',
|
||||
granularity_sqla: 'year',
|
||||
time_grain_sqla: 'P1D',
|
||||
time_range: '2011-01-01 : 2011-01-02',
|
||||
series: 'region',
|
||||
entity: 'country_name',
|
||||
x: 'sum__SP_RUR_TOTL_ZS',
|
||||
y: 'sum__SP_DYN_LE00_IN',
|
||||
size: 'sum__SP_POP_TOTL',
|
||||
max_bubble_size: '50',
|
||||
limit: 0,
|
||||
color_scheme: 'bnbColors',
|
||||
show_legend: true,
|
||||
x_axis_label: '',
|
||||
left_margin: 'auto',
|
||||
x_axis_format: '.3s',
|
||||
x_ticks_layout: 'auto',
|
||||
x_log_scale: false,
|
||||
x_axis_showminmax: false,
|
||||
y_axis_label: '',
|
||||
bottom_margin: 'auto',
|
||||
y_axis_format: '.3s',
|
||||
y_log_scale: false,
|
||||
y_axis_showminmax: false,
|
||||
});
|
||||
|
||||
function verify(formData) {
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
}
|
||||
|
||||
it('should work with filter', () => {
|
||||
getDatasetId('wb_health_population').then(datasetId => {
|
||||
verify({
|
||||
...getBubbleFormData(datasetId),
|
||||
adhoc_filters: [
|
||||
{
|
||||
expressionType: 'SIMPLE',
|
||||
subject: 'region',
|
||||
operator: '==',
|
||||
comparator: 'South Asia',
|
||||
clause: 'WHERE',
|
||||
sqlExpression: null,
|
||||
filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd',
|
||||
},
|
||||
],
|
||||
});
|
||||
cy.get('[data-test="chart-container"]').should('be.visible');
|
||||
cy.get('[data-test="chart-container"]').within(() => {
|
||||
cy.get('svg').find('.nv-point-clips circle').should('have.length', 8);
|
||||
});
|
||||
cy.get('[data-test="chart-container"]').then(nodeList => {
|
||||
// Check that all circles have same color.
|
||||
const color = nodeList[0].getAttribute('fill');
|
||||
const circles = Array.prototype.slice.call(nodeList);
|
||||
expect(circles.every(c => c.getAttribute('fill') === color)).to.equal(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow type to search color schemes and apply the scheme', () => {
|
||||
getDatasetId('wb_health_population').then(datasetId => {
|
||||
cy.visitChartByParams(getBubbleFormData(datasetId));
|
||||
|
||||
cy.get('.Control[data-test="color_scheme"]').scrollIntoView();
|
||||
cy.get('.Control[data-test="color_scheme"] input[type="search"]').focus();
|
||||
cy.focused().type('supersetColors{enter}');
|
||||
cy.get(
|
||||
'.Control[data-test="color_scheme"] .ant-select-selection-item [data-test="supersetColors"]',
|
||||
).should('exist');
|
||||
cy.get('[data-test=run-query-button]').click();
|
||||
cy.get('.bubble .nv-legend .nv-legend-symbol').should(
|
||||
'have.css',
|
||||
'fill',
|
||||
'rgb(31, 168, 201)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user