diff --git a/.dockerignore b/.dockerignore index edce6e9e78aef..c7fb2e60c7bb1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -47,6 +47,7 @@ !helm-tests !kubernetes-tests !task-sdk-tests +!airflow-ctl-tests !shared/ # Add scripts so that we can use them inside the container diff --git a/.gitattributes b/.gitattributes index a2bdd2ec133e0..b38c5f047383d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,7 @@ scripts export-ignore Dockerfile.ci export-ignore CONTRIBUTING.rst export-ignore ISSUE_TRIAGE_PROCESS.rst export-ignore +.github/PULL_REQUEST_TEMPLATE.md export-ignore .asf.yaml export-ignore .bash_completion export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5f3b5a3effada..f6adec118d5ce 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,11 +15,8 @@ # Helm Chart /chart/ @dstandish @jedcunningham @hussein-awala -# Docs (without Providers) -/docs/*.py @potiuk -/docs/apache-airflow @potiuk -/docs/docker-stack @potiuk -/docs/helm-chart @dstandish @jedcunningham +# Docs +/docs/*.py @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 # API /airflow-core/src/airflow/api/ @ephraimbuddy @pierrejeambrun @rawwar @jason810496 @@ -29,29 +26,42 @@ # Airflow CTL /airflow-ctl/ @bugraoz93 @kaxil @potiuk +# Airflow CLI +/airflow-core/src/airflow/cli/ @bugraoz93 @potiuk + # Auth manager /airflow-core/src/airflow/api_fastapi/auth/ @vincbeck # UI /airflow-core/src/airflow/ui/ @bbovenzi @pierrejeambrun @ryanahamilton @jscheffl @shubhamraj-git +# UI e2e tests +/airflow-core/src/airflow/ui/tests/e2e/ @vatsrahul1001 + # Translation Owners (i18n) # Note: Non committer engaged translators are listed in comments prevent making file syntax invalid # See: https://github.com/apache/airflow/blob/main/airflow-core/src/airflow/ui/public/i18n/README.md#43-engaged-translator airflow-core/src/airflow/ui/public/i18n/locales/ar/ @shahar1 @hussein-awala # + @ahmadtfarhan -airflow-core/src/airflow/ui/public/i18n/locales/ca/ @jscheffl @bugraoz93 # + @ecodina +airflow-core/src/airflow/ui/public/i18n/locales/ca/ @jscheffl @bugraoz93 # + @ecodina @oscarhernandezrodriguez airflow-core/src/airflow/ui/public/i18n/locales/de/ @jscheffl # + @TJaniF @m1racoli +airflow-core/src/airflow/ui/public/i18n/locales/el/ @ashb # + @PApostol airflow-core/src/airflow/ui/public/i18n/locales/es/ @bbovenzi # + @aoelvp94 airflow-core/src/airflow/ui/public/i18n/locales/fr/ @pierrejeambrun @vincbeck airflow-core/src/airflow/ui/public/i18n/locales/he/ @eladkal @shahar1 @romsharon98 # +@Dev-iL airflow-core/src/airflow/ui/public/i18n/locales/hi/ @vatsrahul1001 airflow-core/src/airflow/ui/public/i18n/locales/hu/ @jscheffl @potiuk # +@majorosdonat +airflow-core/src/airflow/ui/public/i18n/locales/it/ @bbovenzi # + @aoelvp94 +airflow-core/src/airflow/ui/public/i18n/locales/ja/ @uranusjr @sekikn # + @rsanda airflow-core/src/airflow/ui/public/i18n/locales/ko/ @jscheffl @potiuk # + @choo121600 @kgw7401 @0ne-stone airflow-core/src/airflow/ui/public/i18n/locales/nl/ @BasPH # + @DjVinnii airflow-core/src/airflow/ui/public/i18n/locales/pl/ @potiuk @mobuchowski # + @kacpermuda +airflow-core/src/airflow/ui/public/i18n/locales/pt/ @potiuk # + @aoelvp94 @victoru2 +airflow-core/src/airflow/ui/public/i18n/locales/th/ @potiuk # + @zkan @blackbass64 @lifnaja @Aphinan-Th @chonla @Srabasti airflow-core/src/airflow/ui/public/i18n/locales/tr/ @bugraoz93 # +@hasancatalgol +airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/ @potiuk # + @Fortytwoo @gyli airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/ @Lee-W @jason810496 # + @RoyLee1224 @guan404ming + # Security/Permissions /airflow-core/src/airflow/security/permissions.py @vincbeck @@ -74,7 +84,7 @@ airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/ @Lee-W @jason810496 # + @ /docs/apache-airflow/authoring-and-scheduling/deferring.rst @dstandish @hussein-awala # Secrets Backends -/airflow-core/src/airflow/secrets @dstandish @potiuk @ashb +/airflow-core/src/airflow/secrets @dstandish @ashb # Providers /providers/amazon/ @eladkal @o-nikolas @@ -90,26 +100,27 @@ airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/ @Lee-W @jason810496 # + @ /providers/openlineage/ @mobuchowski /providers/slack/ @eladkal /providers/smtp/ @hussein-awala -/providers/snowflake/ @potiuk @mik-laj +/providers/snowflake/ @potiuk /providers/apache/iceberg/ @Fokko # Dev tools -/.github/workflows/ @potiuk @ashb @gopidesupavan -/dev/ @potiuk @ashb @jedcunningham @gopidesupavan @amoghrajesh +/.github/workflows/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 +/dev/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 @jedcunningham @ephraimbuddy /dev/react-plugin-tools/ @pierrejeambrun @bbovenzi /docker-tests/ @potiuk @ashb @gopidesupavan @jason810496 /kubernetes-tests/ @potiuk @ashb @gopidesupavan @jason810496 /helm-tests/ @dstandish @jedcunningham -/scripts/ @potiuk @ashb @gopidesupavan -Dockerfile @potiuk @ashb @gopidesupavan -Dockerfile.ci @potiuk @ashb @gopidesupavan - -# Releasing Guides & Project Guidelines -/dev/PROJECT_GUIDELINES.md @kaxil -/dev/PROVIDER_DISTRIBUTIONS_DETAILS.md @eladkal -/dev/README.md @kaxil -/dev/README_RELEASE_*.md @kaxil @pierrejeambrun -/dev/README_RELEASE_PROVIDERS.md @eladkal +/scripts/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 +Dockerfile @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 +Dockerfile.ci @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 + +# E2E integration tests +/airflow-e2e-tests/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 + +# Task SDK integration tests +/task-sdk-tests/ @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @kaxil @eladkal @jason810496 + +# Issue triage process ISSUE_TRIAGE_PROCESS.rst @eladkal # AIP-52 - Setup and Teardown diff --git a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml index f04a2ef0ea150..6bf54207a50ac 100644 --- a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml @@ -25,15 +25,15 @@ body: the latest release or main to see if the issue is fixed before reporting it. multiple: false options: - - "3.0.6" + - "3.1.3" - "2.11.0" - "main (development)" - - "Other Airflow 2 version (please specify below)" + - "Other Airflow 2/3 version (please specify below)" validations: required: true - type: input attributes: - label: If "Other Airflow 2 version" selected, which one? + label: If "Other Airflow 2/3 version" selected, which one? # yamllint disable rule:line-length description: > On what 2.X version of Airflow are you currently experiencing the issue? Remember, you are encouraged to diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index adbee6faeae11..adb6fb75fc6f1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,44 +1,39 @@ +Thank you for contributing! - + +--- + +##### Was generative AI tooling used to co-author this PR? -How to write a good git commit message: -http://chris.beams.io/posts/git-commit/ + +- [ ] Yes (please specify the tool below) + - --- -**^ Add meaningful description above** -Read the **[Pull Request Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)** for more information. -In case of fundamental code changes, an Airflow Improvement Proposal ([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals)) is needed. -In case of a new dependency, check compliance with the [ASF 3rd Party License Policy](https://www.apache.org/legal/resolved.html#category-x). -In case of backwards incompatible changes please leave a note in a newsfragment file, named `{pr_number}.significant.rst` or `{issue_number}.significant.rst`, in [airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments). + +* Read the **[Pull Request Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)** for more information. Note: commit author/co-author name and email in commits become permanently public when merged. +* For fundamental code changes, an Airflow Improvement Proposal ([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals)) is needed. +* When adding dependency, check compliance with the [ASF 3rd Party License Policy](https://www.apache.org/legal/resolved.html#category-x). +* For significant user-facing changes create newsfragment: `{pr_number}.significant.rst` or `{issue_number}.significant.rst`, in [airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments). diff --git a/.github/actions/breeze/action.yml b/.github/actions/breeze/action.yml index a7fff7dc397fe..dd1fd5d76f174 100644 --- a/.github/actions/breeze/action.yml +++ b/.github/actions/breeze/action.yml @@ -22,6 +22,9 @@ inputs: python-version: description: 'Python version to use' default: "3.10" + uv-version: + description: 'uv version to use' + default: "0.9.26" # Keep this comment to allow automatic replacement of uv version outputs: host-python-version: description: Python version used in host @@ -30,9 +33,14 @@ runs: using: "composite" steps: - name: "Setup python" - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ inputs.python-version }} + - name: "Install uv" + shell: bash + run: curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh + env: + UV_VERSION: ${{ inputs.uv-version }} # NOTE! Installing Breeze without using cache is FASTER than when using cache - uv is so fast and has # so low overhead, that just running upload cache/restore cache is slower than installing it from scratch - name: "Install Breeze" diff --git a/.github/actions/install-prek/action.yml b/.github/actions/install-prek/action.yml index 3bf30da69c9ae..73b4ad4cae9a0 100644 --- a/.github/actions/install-prek/action.yml +++ b/.github/actions/install-prek/action.yml @@ -24,13 +24,10 @@ inputs: default: "3.10" uv-version: description: 'uv version to use' - default: "0.8.15" # Keep this comment to allow automatic replacement of uv version + default: "0.9.26" # Keep this comment to allow automatic replacement of uv version prek-version: description: 'prek version to use' - default: "0.1.6" # Keep this comment to allow automatic replacement of prek version - skip-prek-hooks: - description: "Skip some prek hooks from installation" - default: "" + default: "0.3.0" # Keep this comment to allow automatic replacement of prek version save-cache: description: "Whether to save prek cache" required: true @@ -40,14 +37,17 @@ inputs: runs: using: "composite" steps: - - name: Install uv and prek + - name: "Install uv" + shell: bash + run: curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh + env: + UV_VERSION: ${{ inputs.uv-version }} + - name: Install prek shell: bash env: - UV_VERSION: ${{inputs.uv-version}} PREK_VERSION: ${{inputs.prek-version}} - SKIP: ${{ inputs.skip-prek-hooks }} + UV_VERSION: ${{ inputs.uv-version }} run: | - curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh uv tool install prek==${PREK_VERSION} --with uv==${UV_VERSION} working-directory: ${{ github.workspace }} # We need to use tar file with archive to restore all the permissions and symlinks @@ -64,7 +64,7 @@ runs: uses: apache/infrastructure-actions/stash/restore@1c35b5ccf8fba5d4c3fdf25a045ca91aa0cbc468 with: # yamllint disable rule:line-length - key: cache-prek-v6-${{ inputs.platform }}-${{ inputs.python-version }}-${{inputs.skip-prek-hooks}}-${{ hashFiles('.pre-commit-config.yaml') }} + key: cache-prek-v9-${{ inputs.platform }}-python${{ inputs.python-version }}-uv${{ inputs.uv-version }}-${{ hashFiles('**/.pre-commit-config.yaml') }} path: /tmp/ id: restore-prek-cache - name: "Check if prek cache tarball exists" @@ -88,12 +88,21 @@ runs: echo shell: bash if: steps.restore-prek-cache.outputs.stash-hit == 'true' + - name: "Make sure cache is cleared on cache miss" + run: | + echo "Cleaning up prek cache in case of cache miss (in case of pre-installed-cache from the system)" + ls -la ~/.cache/prek || true + rm -rf ~/.cache/prek + shell: bash + if: steps.restore-prek-cache.outputs.stash-hit != 'true' - name: Install prek hooks shell: bash - run: prek install-hooks || (cat ~/.cache/prek/prek.log && exit 1) + run: prek install-hooks working-directory: ${{ github.workspace }} - env: - SKIP: ${{ inputs.skip-prek-hooks }} + - name: "Show prek log" + shell: bash + run: cat ~/.cache/prek/prek.log || true + if: always() - name: "Prepare .tar file from prek cache" run: | tar -C ~ -czf /tmp/cache-prek.tar.gz .cache/prek @@ -103,7 +112,7 @@ runs: uses: apache/infrastructure-actions/stash/save@1c35b5ccf8fba5d4c3fdf25a045ca91aa0cbc468 with: # yamllint disable rule:line-length - key: cache-prek-v6-${{ inputs.platform }}-${{ inputs.python-version }}-${{ inputs.skip-prek-hooks }}-${{ hashFiles('.pre-commit-config.yaml') }} + key: cache-prek-v9-${{ inputs.platform }}-python${{ inputs.python-version }}-uv${{ inputs.uv-version }}-${{ hashFiles('**/.pre-commit-config.yaml') }} path: /tmp/cache-prek.tar.gz if-no-files-found: 'error' retention-days: '2' diff --git a/.github/actions/migration_tests/action.yml b/.github/actions/migration_tests/action.yml index 93e966de8a847..447af1c6368ff 100644 --- a/.github/actions/migration_tests/action.yml +++ b/.github/actions/migration_tests/action.yml @@ -28,7 +28,7 @@ runs: - name: "Test migration file 2 to 3 migration: ${{env.BACKEND}}" shell: bash run: | - breeze shell "${AIRFLOW_2_CMD}" --use-airflow-version 2.11.0 --answer y && + breeze shell "${AIRFLOW_2_CMD}" --use-airflow-version 2.11.0 --airflow-extras pydantic --answer y && breeze shell "export AIRFLOW__DATABASE__EXTERNAL_DB_MANAGERS=${DB_MANGERS} ${AIRFLOW_3_CMD}" --no-db-cleanup env: @@ -57,7 +57,7 @@ runs: - name: "Test ORM migration 2 to 3: ${{env.BACKEND}}" shell: bash run: > - breeze shell "${AIRFLOW_2_CMD}" --use-airflow-version 2.11.0 --answer y && + breeze shell "${AIRFLOW_2_CMD}" --use-airflow-version 2.11.0 --airflow-extras pydantic --answer y && breeze shell "export AIRFLOW__DATABASE__EXTERNAL_DB_MANAGERS=${DB_MANGERS} ${AIRFLOW_3_CMD}" --no-db-cleanup env: diff --git a/.github/actions/post_tests_failure/action.yml b/.github/actions/post_tests_failure/action.yml index 5586138c95194..e6bbf03ad9416 100644 --- a/.github/actions/post_tests_failure/action.yml +++ b/.github/actions/post_tests_failure/action.yml @@ -22,21 +22,21 @@ runs: using: "composite" steps: - name: "Upload airflow logs" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: airflow-logs-${{env.JOB_ID}} path: './files/airflow_logs*' retention-days: 7 if-no-files-found: ignore - name: "Upload container logs" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: container-logs-${{env.JOB_ID}} path: "./files/container_logs*" retention-days: 7 if-no-files-found: ignore - name: "Upload other logs" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: container-logs-${{env.JOB_ID}} path: "./files/other_logs*" diff --git a/.github/actions/post_tests_success/action.yml b/.github/actions/post_tests_success/action.yml index 234cde900e4d8..7298fadaf7cb5 100644 --- a/.github/actions/post_tests_success/action.yml +++ b/.github/actions/post_tests_success/action.yml @@ -31,7 +31,7 @@ runs: using: "composite" steps: - name: "Upload artifact for warnings" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-warnings-${{ env.JOB_ID }} path: ./files/warnings-*.txt diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index bafa93822095f..07016c3524f2a 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -332,23 +332,23 @@ labelPRBasedOnFilePath: - .rat-excludes - .readthedocs.yml - # This should be copy of the "area:dev-tools" above and should be updated when we switch maintenance branch - backport-to-v3-0-test: - - scripts/**/* - - dev/**/* - - .github/**/* - - Dockerfile.ci - - CONTRIBUTING.rst - - contributing-docs/**/* - - yamllint-config.yml - - .asf.yaml - - .bash_completion - - .dockerignore - - .hadolint.yaml - - .pre-commit-config.yaml - - .rat-excludes - - .readthedocs.yml - + # # This should be copy of the "area:dev-tools" above and should be updated when + # # we switch maintenance branch + # backport-to-v3-1-test: + # - scripts/**/* + # - dev/**/* + # - .github/**/* + # - Dockerfile.ci + # - CONTRIBUTING.rst + # - contributing-docs/**/* + # - yamllint-config.yml + # - .asf.yaml + # - .bash_completion + # - .dockerignore + # - .hadolint.yaml + # - .pre-commit-config.yaml + # - .rat-excludes + # - .readthedocs.yml kind:documentation: - airflow-core/docs/**/* @@ -381,6 +381,9 @@ labelPRBasedOnFilePath: translation:de: - airflow-core/src/airflow/ui/public/i18n/locales/de/* + translation:el: + - airflow-core/src/airflow/ui/public/i18n/locales/el/* + translation:es: - airflow-core/src/airflow/ui/public/i18n/locales/es/* @@ -396,6 +399,12 @@ labelPRBasedOnFilePath: translation:hu: - airflow-core/src/airflow/ui/public/i18n/locales/hu/* + translation:it: + - airflow-core/src/airflow/ui/public/i18n/locales/it/* + + translation:ja: + - airflow-core/src/airflow/ui/public/i18n/locales/ja/* + translation:ko: - airflow-core/src/airflow/ui/public/i18n/locales/ko/* @@ -405,9 +414,18 @@ labelPRBasedOnFilePath: translation:pl: - airflow-core/src/airflow/ui/public/i18n/locales/pl/* + translation:pt: + - airflow-core/src/airflow/ui/public/i18n/locales/pt/* + + translation:th: + - airflow-core/src/airflow/ui/public/i18n/locales/th/* + translation:tr: - airflow-core/src/airflow/ui/public/i18n/locales/tr/* + translation:zh-CN: + - airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/* + translation:zh-TW: - airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/* @@ -424,7 +442,7 @@ labelPRBasedOnFilePath: area:Logging: - airflow-core/src/airflow/config_templates/airflow_local_settings.py - - airflow-core/tests/unit/core/test_logging_config.py + - shared/logging/**/* - airflow-core/src/airflow/utils/log/**/* - airflow-core/docs/administration-and-deployment/logging-monitoring/logging-*.rst - airflow-core/tests/unit/utils/log/**/* diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0251dc0060c23..8437776faf95a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,8 @@ version: 2 updates: - package-ecosystem: pip + cooldown: + default-days: 4 directories: - /airflow-core - /airflow-ctl @@ -30,8 +32,14 @@ updates: - / schedule: interval: daily + groups: + pip-dependency-updates: + patterns: + - "*" - package-ecosystem: npm + cooldown: + default-days: 4 directories: - /airflow-core/src/airflow/ui schedule: @@ -42,6 +50,8 @@ updates: - "*" - package-ecosystem: npm + cooldown: + default-days: 4 directories: - /airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui schedule: @@ -52,6 +62,20 @@ updates: - "*" - package-ecosystem: npm + cooldown: + default-days: 4 + directories: + - /dev/react-plugin-tools/react_plugin_template + schedule: + interval: daily + groups: + ui-plugin-template-package-updates: + patterns: + - "*" + + - package-ecosystem: npm + cooldown: + default-days: 4 directories: - /providers/edge3/src/airflow/providers/edge3/plugins/www schedule: @@ -62,6 +86,8 @@ updates: - "*" - package-ecosystem: npm + cooldown: + default-days: 4 directories: - /providers/fab/src/airflow/providers/fab/www schedule: @@ -71,8 +97,10 @@ updates: patterns: - "*" - # Repeat dependency updates on v3-0-test branch as well + # Repeat dependency updates on v3-1-test branch as well - package-ecosystem: pip + cooldown: + default-days: 4 directories: - /airflow-core - /airflow-ctl @@ -85,14 +113,20 @@ updates: - / schedule: interval: daily - target-branch: v3-0-test + target-branch: v3-1-test + groups: + pip-dependency-updates: + patterns: + - "*" - package-ecosystem: npm + cooldown: + default-days: 4 directories: - /airflow-core/src/airflow/ui schedule: interval: daily - target-branch: v3-0-test + target-branch: v3-1-test groups: core-ui-package-updates: patterns: @@ -100,6 +134,8 @@ updates: # Repeat dependency updates on 2.11 branch as well - package-ecosystem: pip + cooldown: + default-days: 4 directories: - /clients/python - /dev/breeze @@ -108,8 +144,14 @@ updates: schedule: interval: daily target-branch: v2-11-test + groups: + pip-dependency-updates: + patterns: + - "*" - package-ecosystem: npm + cooldown: + default-days: 4 directories: - /airflow/www/ schedule: @@ -119,3 +161,14 @@ updates: core-ui-package-updates: patterns: - "*" + + - package-ecosystem: "uv" + cooldown: + default-days: 4 + directory: "/dev/breeze" + schedule: + interval: "weekly" + groups: + uv-dependency-updates: + patterns: + - "*" diff --git a/.github/workflows/additional-prod-image-tests.yml b/.github/workflows/additional-prod-image-tests.yml index 714761bceb3cc..c7d4bde61c8db 100644 --- a/.github/workflows/additional-prod-image-tests.yml +++ b/.github/workflows/additional-prod-image-tests.yml @@ -191,3 +191,34 @@ jobs: id: breeze - name: "Run Task SDK integration tests" run: breeze testing task-sdk-integration-tests + + airflow-ctl-integration-tests: + timeout-minutes: 60 + name: "Airflow CTL integration tests with PROD image" + runs-on: ${{ fromJSON(inputs.runners) }} + env: + PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}" + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USERNAME: ${{ github.actor }} + VERBOSE: "true" + steps: + - name: "Cleanup repo" + shell: bash + run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" + - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 2 + persist-credentials: false + - name: "Prepare breeze & PROD image: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}" + uses: ./.github/actions/prepare_breeze_and_image + with: + platform: ${{ inputs.platform }} + image-type: "prod" + python: ${{ env.PYTHON_MAJOR_MINOR_VERSION }} + use-uv: ${{ inputs.use-uv }} + make-mnt-writeable-and-cleanup: true + id: breeze + - name: "Run airflowctl integration tests" + run: breeze testing airflow-ctl-integration-tests diff --git a/.github/workflows/airflow-distributions-tests.yml b/.github/workflows/airflow-distributions-tests.yml index beda0fcd8036a..90fd8cdda7f6f 100644 --- a/.github/workflows/airflow-distributions-tests.yml +++ b/.github/workflows/airflow-distributions-tests.yml @@ -113,7 +113,7 @@ jobs: USE_LOCAL_HATCH: "${{ inputs.use-local-venv }}" run: | uv tool uninstall hatch || true - uv tool install hatch==1.14.1 + uv tool install hatch==1.16.3 breeze release-management "${DISTRIBUTION_TYPE}" --distribution-format wheel if: ${{ matrix.python-version == inputs.default-python-version }} - name: "Verify wheel packages with twine" diff --git a/.github/workflows/automatic-backport.yml b/.github/workflows/automatic-backport.yml index 4f861ddd58118..986c31cae4a51 100644 --- a/.github/workflows/automatic-backport.yml +++ b/.github/workflows/automatic-backport.yml @@ -35,6 +35,14 @@ jobs: id: get-sha run: echo "COMMIT_SHA=${GITHUB_SHA}" >> $GITHUB_ENV + # Adding a slight delay to allow GitHub's API to associate the merge commit with the PR. + # This is needed because GH has a consistency of 6-10+ seconds + # to process the commit and PR association after a merge based on some of our past runs. + # Without this delay, the listPullRequestsAssociatedWithCommit API call may return an empty array + # even though a PR was just merged, causing backports to be skipped. + - name: Add delay for GitHub to process PR merge + run: sleep 15 + - name: Find PR information id: pr-info uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 @@ -57,7 +65,7 @@ jobs: console.log(`Backport branches: ${backportBranches}`); core.setOutput('branches', JSON.stringify(backportBranches)); } else { - console.log('No pull request found for this commit.'); + console.log('⚠️ No pull request found for this commit.'); core.setOutput('branches', '[]'); } diff --git a/.github/workflows/basic-tests.yml b/.github/workflows/basic-tests.yml index 548b9ce5b0fae..899a7220de171 100644 --- a/.github/workflows/basic-tests.yml +++ b/.github/workflows/basic-tests.yml @@ -66,7 +66,7 @@ on: # yamllint disable-line rule:truthy type: string uv-version: description: 'uv version to use' - default: "0.8.15" # Keep this comment to allow automatic replacement of uv version + default: "0.9.26" # Keep this comment to allow automatic replacement of uv version type: string platform: description: 'Platform for the build - linux/amd64 or linux/arm64' @@ -100,8 +100,6 @@ jobs: matrix: shared-distribution: ${{ fromJSON(inputs.shared-distributions-as-json) }} runs-on: ${{ fromJSON(inputs.runners) }} - env: - UV_VERSION: ${{inputs.uv-version}} steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -110,6 +108,8 @@ jobs: persist-credentials: false - name: "Install uv" run: curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh + env: + UV_VERSION: ${{ inputs.uv-version }} - name: "Run shared ${{ matrix.shared-distribution }} tests" run: uv run --group dev pytest --color=yes -n auto working-directory: shared/${{ matrix.shared-distribution }} @@ -188,22 +188,10 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - name: "Install prek" - uses: ./.github/actions/install-prek - id: prek - with: - python-version: ${{steps.breeze.outputs.host-python-version}} - skip-prek-hooks: ${{ inputs.skip-prek-hooks }} - platform: ${{ inputs.platform }} - save-cache: false + - name: "Install Breeze" + uses: ./.github/actions/breeze - name: "Check translation completeness" - run: > - prek --show-diff-on-failure --color always - --hook-stage manual --verbose --all-files - check-translations-completeness - env: - SKIP: ${{ inputs.skip-prek-hooks }} - COLUMNS: "202" + run: breeze check-translations-completeness || true # Those checks are run if no image needs to be built for checks. This is for simple changes that # Do not touch any of the python code or any of the important files that might require building @@ -229,9 +217,8 @@ jobs: id: prek with: python-version: ${{ steps.breeze.outputs.host-python-version }} - skip-prek-hooks: ${{ inputs.skip-prek-hooks }} platform: ${{ inputs.platform }} - save-cache: false + save-cache: true - name: Fetch incoming commit ${{ github.sha }} with its parent uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -241,7 +228,7 @@ jobs: - name: "Static checks: basic checks only" run: > prek --show-diff-on-failure --color always - --from-ref "${{ github.sha }}" --to-ref "${{ github.sha }}" + --from-ref "${{ github.sha }}"^ --to-ref "${{ github.sha }}" env: VERBOSE: "false" SKIP_BREEZE_PREK_HOOKS: "true" @@ -282,15 +269,21 @@ jobs: id: prek with: python-version: ${{ steps.breeze.outputs.host-python-version }} - skip-prek-hooks: ${{ inputs.skip-prek-hooks }} platform: ${{ inputs.platform }} save-cache: false - name: "Autoupdate all prek hooks" - run: prek autoupdate --freeze + run: prek auto-update --cooldown-days 4 --freeze - name: "Autoupdate Lucas-C hooks to bleeding edge" - run: prek autoupdate --bleeding-edge --freeze --repo https://github.com/Lucas-C/pre-commit-hooks - - name: "Check if there are any changes in pre-commit hooks" - run: git diff --exit-code + run: prek auto-update --bleeding-edge --freeze --repo https://github.com/Lucas-C/pre-commit-hooks + - name: "Autoupdate Octopin to bleeding edge" + run: prek auto-update --bleeding-edge --freeze --repo https://github.com/eclipse-csi/octopin + - name: "Check if there are any changes in prek hooks" + run: | + if ! git diff --exit-code; then + echo -e "\n\033[0;31mThere are changes in prek hooks after upgrade check.\033[0m" + echo -e "\n\033[0;33mHow to fix:\033[0m Run \`breeze ci upgrade\` locally to fix it!.\n" + exit 1 + fi - name: "Run automated upgrade for chart dependencies" run: > prek @@ -308,36 +301,32 @@ jobs: --hook-stage manual upgrade-important-versions || true if: always() env: - UPGRADE_UV: "true" UPGRADE_PIP: "false" UPGRADE_PYTHON: "false" - UPGRADE_GOLANG: "false" - UPGRADE_PREK: "true" UPGRADE_NODE_LTS: "false" UPGRADE_HATCH: "false" UPGRADE_PYYAML: "false" UPGRADE_GITPYTHON: "false" UPGRADE_RICH: "false" UPGRADE_RUFF: "false" + UPGRADE_MYPY: "false" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: "Run automated upgrade for important versions minus uv(failing if needed)" - run: > - prek - --all-files --show-diff-on-failure --color always --verbose - --hook-stage manual upgrade-important-versions + - name: "Run automated upgrade for important versions minus uv (failing if needed)" + run: | + if ! prek \ + --all-files --show-diff-on-failure --color always --verbose \ + --hook-stage manual upgrade-important-versions; then + echo -e "\n\033[0;31mThere are changes in prek hooks after upgrade check.\033[0m" + echo -e "\n\033[0;33mHow to fix:\033[0m Run \`breeze ci upgrade\` locally to fix it!.\n" + exit 1 + fi if: always() env: UPGRADE_UV: "false" - UPGRADE_PIP: "true" - UPGRADE_PYTHON: "true" - UPGRADE_GOLANG: "true" UPGRADE_PREK: "false" - UPGRADE_NODE_LTS: "true" - UPGRADE_HATCH: "true" - UPGRADE_PYYAML: "true" - UPGRADE_GITPYTHON: "true" - UPGRADE_RICH: "true" - UPGRADE_RUFF: "true" + UPGRADE_MPROCS: "false" + UPGRADE_PROTOC: "false" + UPGRADE_OPENAPI_GENERATOR: "false" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} test-airflow-release-commands: @@ -370,17 +359,15 @@ jobs: - name: Install twine run: pip install twine - name: "Check Airflow create minor branch command" - run: > - breeze release-management create-minor-branch - --version-branch 3-1 --answer yes --dry-run + run: breeze release-management create-minor-branch --version-branch 3-1 --answer yes --dry-run - name: "Check Airflow RC process command" run: > - breeze release-management start-rc-process - --version 3.1.0rc1 --previous-version 3.0.0 --task-sdk-version 1.0.0rc1 --answer yes --dry-run + breeze release-management start-rc-process --version 3.1.0rc1 --previous-version 3.0.0 + --task-sdk-version 1.0.0rc1 --sync-branch v3-1-test --answer yes --dry-run - name: "Check Airflow release process command" run: > - breeze release-management start-release --release-candidate 3.1.0rc1 - --previous-release 3.0.0 --answer yes --dry-run + breeze release-management start-release --version 3.1.6 + --answer yes --dry-run - name: "Test providers metadata generation" run: | git remote add apache https://github.com/apache/airflow.git @@ -394,3 +381,56 @@ jobs: run: | breeze release-management generate-issue-content-core \ --limit-pr-count 2 --previous-release 3.0.1 --current-release 3.0.2 --verbose + + + test-airflow-standalone: + timeout-minutes: 30 + name: "Test Airflow standalone commands" + runs-on: ${{ fromJSON(inputs.runners) }} + env: + AIRFLOW_HOME: ~/airflow + FORCE_COLOR: 1 + steps: + - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: "Install uv" + run: curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh + env: + UV_VERSION: ${{ inputs.uv-version }} + - name: "Set up Airflow home directory" + run: | + echo "Setting AIRFLOW_HOME to $AIRFLOW_HOME" + mkdir -p $AIRFLOW_HOME + - name: "Install Airflow from current repo (simulating user installation)" + run: | + uv venv + set -x + uv pip install -e ./airflow-core + - name: "Test airflow standalone command" + run: | + uv run --no-sync airflow standalone 2>&1 | tee airflow_startup.log & + AIRFLOW_PID=$! + + # Wait for ready message till timeout (10 minutes) + for i in {1..600}; do + if ! kill -0 $AIRFLOW_PID 2>/dev/null; then + wait $AIRFLOW_PID + EXIT_CODE=$? + echo "FAILED: Airflow standalone exited with code $EXIT_CODE" + exit $EXIT_CODE + fi + + if grep -q "Airflow is ready" airflow_startup.log; then + echo "SUCCESS: Airflow standalone is ready!" + kill $AIRFLOW_PID + exit 0 + fi + + sleep 1 + done + + echo "FAILED: Airflow standalone did not become ready in time" + kill $AIRFLOW_PID 2>/dev/null || true + exit 1 diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd-arm.yml similarity index 88% rename from .github/workflows/ci-amd.yml rename to .github/workflows/ci-amd-arm.yml index e65bc71e0fc38..f64e2bbd90e91 100644 --- a/.github/workflows/ci-amd.yml +++ b/.github/workflows/ci-amd-arm.yml @@ -16,10 +16,10 @@ # under the License. # --- -name: Tests AMD +name: Tests on: # yamllint disable-line rule:truthy schedule: - - cron: '28 1,7,13,19 * * *' + - cron: '28 1,3,7,9,13,15,19,21 * * *' push: branches: - v[0-9]+-[0-9]+-test @@ -43,7 +43,7 @@ env: VERBOSE: "true" concurrency: - group: ci-amd-${{ github.event.pull_request.number || github.ref }} + group: ci-amd-arm-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -57,7 +57,6 @@ jobs: outputs: all-python-versions-list-as-string: >- ${{ steps.selective-checks.outputs.all-python-versions-list-as-string }} - amd-runners: ${{ steps.selective-checks.outputs.amd-runners }} arm-runners: ${{ steps.selective-checks.outputs.arm-runners }} basic-checks-only: ${{ steps.selective-checks.outputs.basic-checks-only }} canary-run: ${{ steps.source-run-info.outputs.canary-run }} @@ -94,6 +93,7 @@ jobs: mypy-checks: ${{ steps.selective-checks.outputs.mypy-checks }} mysql-exclude: ${{ steps.selective-checks.outputs.mysql-exclude }} mysql-versions: ${{ steps.selective-checks.outputs.mysql-versions }} + platform: ${{ steps.selective-checks.outputs.platform }} postgres-exclude: ${{ steps.selective-checks.outputs.postgres-exclude }} postgres-versions: ${{ steps.selective-checks.outputs.postgres-versions }} prod-image-build: ${{ steps.selective-checks.outputs.prod-image-build }} @@ -116,6 +116,7 @@ jobs: run-mypy: ${{ steps.selective-checks.outputs.run-mypy }} run-system-tests: ${{ steps.selective-checks.outputs.run-system-tests }} run-task-sdk-tests: ${{ steps.selective-checks.outputs.run-task-sdk-tests }} + runner-type: ${{ steps.selective-checks.outputs.runner-type }} run-ui-tests: ${{ steps.selective-checks.outputs.run-ui-tests }} run-unit-tests: ${{ steps.selective-checks.outputs.run-unit-tests }} run-www-tests: ${{ steps.selective-checks.outputs.run-www-tests }} @@ -165,18 +166,30 @@ jobs: env: PR_LABELS: ${{ steps.source-run-info.outputs.pr-labels }} GITHUB_CONTEXT: ${{ toJson(github) }} - - name: "Install and cache prek" - uses: ./.github/actions/install-prek - id: prek - with: - python-version: ${{ steps.breeze.outputs.host-python-version }} - skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }} - platform: "linux/amd64" - save-cache: true + + print-platform-arm: + name: "Platform: ARM" + needs: [build-info] + runs-on: ["ubuntu-22.04"] + if: needs.build-info.outputs.platform == 'linux/arm64' + steps: + - name: "Print architecture" + run: "echo '## Architecture: ARM' >> $GITHUB_STEP_SUMMARY" + + print-platform-amd: + name: "Platform: AMD" + needs: [build-info] + runs-on: ["ubuntu-22.04"] + if: needs.build-info.outputs.platform == 'linux/amd64' + steps: + - name: "Print architecture" + run: "echo '## Architecture: AMD' >> $GITHUB_STEP_SUMMARY" + run-pin-versions-hook: - name: "Run pin-versions hook" + name: "Pin actions" needs: [build-info] - runs-on: ${{ fromJSON(needs.build-info.outputs.amd-runners) }} + runs-on: ${{ fromJSON(needs.build-info.outputs.runner-type) }} + if: needs.build-info.outputs.platform == 'linux/amd64' steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -188,13 +201,15 @@ jobs: with: # octopin needs python 3.11 python-version: "3.11" - platform: "linux/amd64" + platform: ${{ needs.build-info.outputs.platform }} save-cache: true - skip-prek-hooks: "" - name: "Run pin-versions" - run: > - prek -c .github/.pre-commit-config.yaml --all-files --verbose --hook-stage manual - pin-versions + run: | + if ! prek --all-files --verbose --hook-stage manual pin-versions; then + echo -e "\n\033[0;31mThere are changes in actions hooks after upgrade check.\033[0m" + echo -e "\n\033[0;33mHow to fix:\033[0m Run \`breeze ci upgrade\` locally to fix it!.\n" + exit 1 + fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -203,7 +218,7 @@ jobs: needs: [build-info] uses: ./.github/workflows/basic-tests.yml with: - runners: ${{ needs.build-info.outputs.amd-runners }} + runners: ${{ needs.build-info.outputs.runner-type }} run-ui-tests: ${{needs.build-info.outputs.run-ui-tests}} run-www-tests: ${{needs.build-info.outputs.run-www-tests}} run-api-codegen: ${{needs.build-info.outputs.run-api-codegen}} @@ -213,7 +228,7 @@ jobs: canary-run: ${{needs.build-info.outputs.canary-run}} latest-versions-only: ${{needs.build-info.outputs.latest-versions-only}} use-uv: ${{needs.build-info.outputs.use-uv}} - platform: "linux/amd64" + platform: ${{ needs.build-info.outputs.platform }} shared-distributions-as-json: ${{needs.build-info.outputs.shared-distributions-as-json}} build-ci-images: @@ -226,8 +241,8 @@ jobs: # from forks. This is to prevent malicious PRs from creating images in the "apache/airflow" repo. packages: write with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} push-image: "false" upload-image-artifact: "true" upload-mount-cache-artifact: ${{ needs.build-info.outputs.canary-run }} @@ -249,8 +264,8 @@ jobs: packages: write id-token: write with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} python-versions: ${{ needs.build-info.outputs.python-versions }} branch: ${{ needs.build-info.outputs.default-branch }} constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} @@ -271,8 +286,8 @@ jobs: uses: ./.github/workflows/generate-constraints.yml if: needs.build-info.outputs.ci-image-build == 'true' with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} python-versions: ${{ needs.build-info.outputs.python-versions }} generate-pypi-constraints: "true" @@ -290,8 +305,8 @@ jobs: id-token: write contents: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} run-mypy: ${{ needs.build-info.outputs.run-mypy }} mypy-checks: ${{ needs.build-info.outputs.mypy-checks }} python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} @@ -329,8 +344,8 @@ jobs: needs.build-info.outputs.latest-versions-only != 'true' && needs.build-info.outputs.run-unit-tests == 'true' with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} canary-run: ${{ needs.build-info.outputs.canary-run }} default-python-version: "${{ needs.build-info.outputs.default-python-version }}" upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} @@ -352,8 +367,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} helm-test-packages: ${{ needs.build-info.outputs.helm-test-packages }} default-python-version: "${{ needs.build-info.outputs.default-python-version }}" use-uv: ${{ needs.build-info.outputs.use-uv }} @@ -370,8 +385,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "postgres" test-name: "Postgres" test-scope: "DB" @@ -399,8 +414,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "postgres" test-name: "Postgres" test-scope: "DB" @@ -428,8 +443,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "mysql" test-name: "MySQL" test-scope: "DB" @@ -447,7 +462,7 @@ jobs: skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} use-uv: ${{ needs.build-info.outputs.use-uv }} default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' + if: needs.build-info.outputs.run-unit-tests == 'true' && needs.build-info.outputs.platform == 'linux/amd64' tests-mysql-providers: name: "MySQL tests: providers" @@ -457,8 +472,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "mysql" test-name: "MySQL" test-scope: "DB" @@ -476,7 +491,7 @@ jobs: skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} use-uv: ${{ needs.build-info.outputs.use-uv }} default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' + if: needs.build-info.outputs.run-unit-tests == 'true' && needs.build-info.outputs.platform == 'linux/amd64' tests-sqlite-core: @@ -487,8 +502,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "sqlite" test-name: "Sqlite" test-name-separator: "" @@ -518,8 +533,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "sqlite" test-name: "Sqlite" test-name-separator: "" @@ -550,8 +565,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "sqlite" test-name: "" test-name-separator: "" @@ -580,8 +595,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} backend: "sqlite" test-name: "" test-name-separator: "" @@ -616,8 +631,8 @@ jobs: needs.build-info.outputs.full-tests-needed == 'true') with: default-branch: ${{ needs.build-info.outputs.default-branch }} - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} core-test-types-list-as-strings-in-json: > ${{ needs.build-info.outputs.core-test-types-list-as-strings-in-json }} providers-test-types-list-as-strings-in-json: > @@ -642,8 +657,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} testable-core-integrations: ${{ needs.build-info.outputs.testable-core-integrations }} testable-providers-integrations: ${{ needs.build-info.outputs.testable-providers-integrations }} run-system-tests: ${{ needs.build-info.outputs.run-system-tests }} @@ -666,8 +681,8 @@ jobs: if: > needs.build-info.outputs.run-unit-tests == 'true' with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} test-name: "LowestDeps" force-lowest-dependencies: "true" test-scope: "All" @@ -696,8 +711,8 @@ jobs: packages: read if: needs.build-info.outputs.run-unit-tests == 'true' with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} test-name: "LowestDeps" force-lowest-dependencies: "true" test-scope: "All" @@ -727,8 +742,8 @@ jobs: # from forks. This is to prevent malicious PRs from creating images in the "apache/airflow" repo. packages: write with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} build-type: "Regular" push-image: "false" upload-image-artifact: "true" @@ -748,8 +763,8 @@ jobs: needs: [build-info, build-prod-images, generate-constraints] uses: ./.github/workflows/additional-prod-image-tests.yml with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} default-branch: ${{ needs.build-info.outputs.default-branch }} constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} @@ -768,8 +783,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} use-uv: ${{ needs.build-info.outputs.use-uv }} @@ -787,8 +802,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} default-python-version: "${{ needs.build-info.outputs.default-python-version }}" python-versions: ${{ needs.build-info.outputs.python-versions }} use-uv: ${{ needs.build-info.outputs.use-uv }} @@ -803,7 +818,7 @@ jobs: tests-go-sdk: name: "Go SDK tests" needs: [build-info] - runs-on: ${{ fromJSON(needs.build-info.outputs.amd-runners) }} + runs-on: ${{ fromJSON(needs.build-info.outputs.runner-type) }} timeout-minutes: 15 permissions: contents: read @@ -829,13 +844,13 @@ jobs: - name: Setup Gotestsum shell: bash run: | - go install gotest.tools/gotestsum@ddd0b05a6878e2e8257a2abe6e7df66cebc53d0e # v1.12.3 + go install gotest.tools/gotestsum@c4a0df2e75a225d979a444342dd3db752b53619f # v1.13.0 gotestsum --version - name: "Cleanup dist files" run: rm -fv ./dist/* - name: Run Go tests working-directory: ./go-sdk - run: gotestsum --format testname ./... + run: gotestsum --format github-actions ./... tests-airflow-ctl: name: "Airflow CTL tests" @@ -845,8 +860,8 @@ jobs: contents: read packages: read with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} default-python-version: "${{ needs.build-info.outputs.default-python-version }}" python-versions: ${{ needs.build-info.outputs.python-versions }} use-uv: ${{ needs.build-info.outputs.use-uv }} @@ -883,7 +898,6 @@ jobs: - tests-non-db-providers - tests-postgres-core - tests-postgres-providers - # - tests-special - tests-sqlite-core - tests-sqlite-providers - tests-task-sdk @@ -893,8 +907,8 @@ jobs: - tests-with-lowest-direct-resolution-providers uses: ./.github/workflows/finalize-tests.yml with: - runners: ${{ needs.build-info.outputs.amd-runners }} - platform: "linux/amd64" + runners: ${{ needs.build-info.outputs.runner-type }} + platform: ${{ needs.build-info.outputs.platform }} python-versions: ${{ needs.build-info.outputs.python-versions }} python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} branch: ${{ needs.build-info.outputs.default-branch }} @@ -911,6 +925,7 @@ jobs: notify-slack-failure: name: "Notify Slack on Failure" needs: + - build-info - finalize-tests if: github.event_name == 'schedule' && failure() && github.run_attempt == 1 runs-on: ["ubuntu-22.04"] @@ -924,18 +939,33 @@ jobs: # yamllint disable rule:line-length payload: | channel: "internal-airflow-ci-cd" - text: "🚨🕒 Failure Alert: Scheduled CI (AMD) on branch *${{ github.ref_name }}* 🕒🚨\n\n*Details:* " + text: "🚨🕒 Failure Alert: Scheduled CI (${{ needs.build-info.outputs.platform }}) on branch *${{ github.ref_name }}* 🕒🚨\n\n*Details:* " blocks: - type: "section" text: type: "mrkdwn" - text: "🚨🕒 Failure Alert: Scheduled CI (AMD) 🕒🚨\n\n*Details:* " + text: "🚨🕒 Failure Alert: Scheduled CI (${{ needs.build-info.outputs.platform }}) 🕒🚨\n\n*Details:* " # yamllint enable rule:line-length summarize-warnings: timeout-minutes: 15 name: "Summarize warnings" - runs-on: ${{ fromJSON(needs.build-info.outputs.amd-runners) }} + needs: + - build-info + - tests-mysql-core + - tests-mysql-providers + - tests-non-db-core + - tests-non-db-providers + - tests-postgres-core + - tests-postgres-providers + - tests-sqlite-core + - tests-sqlite-providers + - tests-task-sdk + - tests-airflow-ctl + - tests-special + - tests-with-lowest-direct-resolution-core + - tests-with-lowest-direct-resolution-providers + runs-on: ${{ fromJSON(needs.build-info.outputs.runner-type) }} if: needs.build-info.outputs.run-unit-tests == 'true' steps: - name: "Cleanup repo" @@ -965,7 +995,7 @@ jobs: - name: "Upload artifact for summarized warnings" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: test-summarized-amd-runner-warnings + name: "test-summarized-warnings" path: ./files/warn-summary-*.txt retention-days: 7 if-no-files-found: ignore diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml deleted file mode 100644 index 28dd6967dae8f..0000000000000 --- a/.github/workflows/ci-arm.yml +++ /dev/null @@ -1,615 +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. -# ---- -name: Tests ARM -on: # yamllint disable-line rule:truthy - schedule: - - cron: '28 3,9,15,21 * * *' - push: - branches: - - v[0-9]+-[0-9]+-test - - providers-[a-z]+-?[a-z]*/v[0-9]+-[0-9]+ - workflow_dispatch: -permissions: - # All other permissions are set to none by default - contents: read -env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_USERNAME: ${{ github.actor }} - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - VERBOSE: "true" - -concurrency: - group: ci-arm-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - - build-info: - name: "Build info" - # At build-info stage we do not yet have outputs so we need to hard-code the runs-on to public runners - runs-on: ["ubuntu-22.04"] - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - outputs: - all-python-versions-list-as-string: >- - ${{ steps.selective-checks.outputs.all-python-versions-list-as-string }} - amd-runners: ${{ steps.selective-checks.outputs.amd-runners }} - arm-runners: ${{ steps.selective-checks.outputs.arm-runners }} - basic-checks-only: ${{ steps.selective-checks.outputs.basic-checks-only }} - canary-run: ${{ steps.source-run-info.outputs.canary-run }} - ci-image-build: ${{ steps.selective-checks.outputs.ci-image-build }} - core-test-types-list-as-strings-in-json: >- - ${{ steps.selective-checks.outputs.core-test-types-list-as-strings-in-json }} - debug-resources: ${{ steps.selective-checks.outputs.debug-resources }} - default-branch: ${{ steps.selective-checks.outputs.default-branch }} - default-constraints-branch: ${{ steps.selective-checks.outputs.default-constraints-branch }} - default-helm-version: ${{ steps.selective-checks.outputs.default-helm-version }} - default-kind-version: ${{ steps.selective-checks.outputs.default-kind-version }} - default-kubernetes-version: ${{ steps.selective-checks.outputs.default-kubernetes-version }} - default-mysql-version: ${{ steps.selective-checks.outputs.default-mysql-version }} - default-postgres-version: ${{ steps.selective-checks.outputs.default-postgres-version }} - default-python-version: ${{ steps.selective-checks.outputs.default-python-version }} - disable-airflow-repo-cache: ${{ steps.selective-checks.outputs.disable-airflow-repo-cache }} - docker-cache: ${{ steps.selective-checks.outputs.docker-cache }} - docs-build: ${{ steps.selective-checks.outputs.docs-build }} - docs-list-as-string: ${{ steps.selective-checks.outputs.docs-list-as-string }} - excluded-providers-as-string: ${{ steps.selective-checks.outputs.excluded-providers-as-string }} - force-pip: ${{ steps.selective-checks.outputs.force-pip }} - full-tests-needed: ${{ steps.selective-checks.outputs.full-tests-needed }} - has-migrations: ${{ steps.selective-checks.outputs.has-migrations }} - helm-test-packages: ${{ steps.selective-checks.outputs.helm-test-packages }} - include-success-outputs: ${{ steps.selective-checks.outputs.include-success-outputs }} - individual-providers-test-types-list-as-strings-in-json: >- - ${{ steps.selective-checks.outputs.individual-providers-test-types-list-as-strings-in-json }} - kubernetes-combos: ${{ steps.selective-checks.outputs.kubernetes-combos }} - kubernetes-combos-list-as-string: >- - ${{ steps.selective-checks.outputs.kubernetes-combos-list-as-string }} - kubernetes-versions-list-as-string: >- - ${{ steps.selective-checks.outputs.kubernetes-versions-list-as-string }} - latest-versions-only: ${{ steps.selective-checks.outputs.latest-versions-only }} - mypy-checks: ${{ steps.selective-checks.outputs.mypy-checks }} - mysql-exclude: ${{ steps.selective-checks.outputs.mysql-exclude }} - mysql-versions: ${{ steps.selective-checks.outputs.mysql-versions }} - postgres-exclude: ${{ steps.selective-checks.outputs.postgres-exclude }} - postgres-versions: ${{ steps.selective-checks.outputs.postgres-versions }} - prod-image-build: ${{ steps.selective-checks.outputs.prod-image-build }} - # yamllint disable rule:line-length - providers-compatibility-tests-matrix: > - ${{ steps.selective-checks.outputs.providers-compatibility-tests-matrix }} - providers-test-types-list-as-strings-in-json: >- - ${{ steps.selective-checks.outputs.providers-test-types-list-as-strings-in-json }} - pull-request-labels: ${{ steps.source-run-info.outputs.pr-labels }} - python-versions-list-as-string: ${{ steps.selective-checks.outputs.python-versions-list-as-string }} - python-versions: ${{ steps.selective-checks.outputs.python-versions }} - run-airflow-ctl-tests: ${{ steps.selective-checks.outputs.run-airflow-ctl-tests }} - run-amazon-tests: ${{ steps.selective-checks.outputs.run-amazon-tests }} - run-api-codegen: ${{ steps.selective-checks.outputs.run-api-codegen }} - run-api-tests: ${{ steps.selective-checks.outputs.run-api-tests }} - run-coverage: ${{ steps.source-run-info.outputs.run-coverage }} - run-go-sdk-tests: ${{ steps.selective-checks.outputs.run-go-sdk-tests }} - run-helm-tests: ${{ steps.selective-checks.outputs.run-helm-tests }} - run-kubernetes-tests: ${{ steps.selective-checks.outputs.run-kubernetes-tests }} - run-mypy: ${{ steps.selective-checks.outputs.run-mypy }} - run-system-tests: ${{ steps.selective-checks.outputs.run-system-tests }} - run-task-sdk-tests: ${{ steps.selective-checks.outputs.run-task-sdk-tests }} - run-ui-tests: ${{ steps.selective-checks.outputs.run-ui-tests }} - run-unit-tests: ${{ steps.selective-checks.outputs.run-unit-tests }} - run-www-tests: ${{ steps.selective-checks.outputs.run-www-tests }} - selected-providers-list-as-string: >- - ${{ steps.selective-checks.outputs.selected-providers-list-as-string }} - shared-distributions-as-json: ${{ steps.selective-checks.outputs.shared-distributions-as-json }} - skip-prek-hooks: ${{ steps.selective-checks.outputs.skip-prek-hooks }} - skip-providers-tests: ${{ steps.selective-checks.outputs.skip-providers-tests }} - source-head-repo: ${{ steps.source-run-info.outputs.source-head-repo }} - sqlite-exclude: ${{ steps.selective-checks.outputs.sqlite-exclude }} - testable-core-integrations: ${{ steps.selective-checks.outputs.testable-core-integrations }} - testable-providers-integrations: ${{ steps.selective-checks.outputs.testable-providers-integrations }} - use-uv: ${{ steps.selective-checks.outputs.force-pip == 'true' && 'false' || 'true' }} - upgrade-to-newer-dependencies: ${{ steps.selective-checks.outputs.upgrade-to-newer-dependencies }} - steps: - - name: "Cleanup repo" - shell: bash - run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm -rf /workspace/*" - - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - name: Fetch incoming commit ${{ github.sha }} with its parent - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - persist-credentials: false - - name: "Install Breeze" - uses: ./.github/actions/breeze - id: breeze - - name: "Get information about the Workflow" - id: source-run-info - run: breeze ci get-workflow-info 2>> ${GITHUB_OUTPUT} - env: - SKIP_BREEZE_SELF_UPGRADE_CHECK: "true" - - name: Selective checks - id: selective-checks - env: - PR_LABELS: "${{ steps.source-run-info.outputs.pr-labels }}" - COMMIT_REF: "${{ github.sha }}" - VERBOSE: "false" - run: breeze ci selective-check 2>> ${GITHUB_OUTPUT} - - name: env - run: printenv - env: - PR_LABELS: ${{ steps.source-run-info.outputs.pr-labels }} - GITHUB_CONTEXT: ${{ toJson(github) }} - - name: "Install and cache prek" - uses: ./.github/actions/install-prek - id: prek - with: - python-version: ${{ steps.breeze.outputs.host-python-version }} - skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }} - platform: "linux/arm64" - save-cache: true - basic-tests: - name: "Basic tests" - needs: [build-info] - uses: ./.github/workflows/basic-tests.yml - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - run-ui-tests: ${{needs.build-info.outputs.run-ui-tests}} - run-www-tests: ${{needs.build-info.outputs.run-www-tests}} - run-api-codegen: ${{needs.build-info.outputs.run-api-codegen}} - default-python-version: "${{ needs.build-info.outputs.default-python-version }}" - basic-checks-only: ${{ needs.build-info.outputs.basic-checks-only }} - skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }} - canary-run: ${{needs.build-info.outputs.canary-run}} - latest-versions-only: ${{needs.build-info.outputs.latest-versions-only}} - use-uv: ${{needs.build-info.outputs.use-uv}} - platform: "linux/arm64" - shared-distributions-as-json: ${{needs.build-info.outputs.shared-distributions-as-json}} - - build-ci-images: - name: Build CI images - needs: [build-info] - uses: ./.github/workflows/ci-image-build.yml - permissions: - contents: read - # This write is only given here for `push` events from "apache/airflow" repo. It is not given for PRs - # from forks. This is to prevent malicious PRs from creating images in the "apache/airflow" repo. - packages: write - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - push-image: "false" - upload-image-artifact: "true" - upload-mount-cache-artifact: ${{ needs.build-info.outputs.canary-run }} - python-versions: ${{ needs.build-info.outputs.python-versions }} - branch: ${{ needs.build-info.outputs.default-branch }} - constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} - docker-cache: ${{ needs.build-info.outputs.docker-cache }} - disable-airflow-repo-cache: ${{ needs.build-info.outputs.disable-airflow-repo-cache }} - if: needs.build-info.outputs.ci-image-build == 'true' - - additional-ci-image-checks: - name: "Additional CI image checks" - needs: [build-info, build-ci-images] - uses: ./.github/workflows/additional-ci-image-checks.yml - permissions: - contents: read - packages: write - id-token: write - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - python-versions: ${{ needs.build-info.outputs.python-versions }} - branch: ${{ needs.build-info.outputs.default-branch }} - constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} - default-python-version: "${{ needs.build-info.outputs.default-python-version }}" - upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} - skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }} - docker-cache: ${{ needs.build-info.outputs.docker-cache }} - disable-airflow-repo-cache: ${{ needs.build-info.outputs.disable-airflow-repo-cache }} - canary-run: ${{ needs.build-info.outputs.canary-run }} - latest-versions-only: ${{ needs.build-info.outputs.latest-versions-only }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - - generate-constraints: - name: "Generate constraints" - needs: [build-info, build-ci-images] - uses: ./.github/workflows/generate-constraints.yml - if: needs.build-info.outputs.ci-image-build == 'true' - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} - python-versions: ${{ needs.build-info.outputs.python-versions }} - generate-pypi-constraints: "true" - # generate no providers constraints only in canary builds - they take quite some time to generate - # they are not needed for regular builds, they are only needed to update constraints in canaries - generate-no-providers-constraints: ${{ needs.build-info.outputs.canary-run }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - - providers: - name: "provider distributions tests" - uses: ./.github/workflows/test-providers.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - if: > - needs.build-info.outputs.skip-providers-tests != 'true' && - needs.build-info.outputs.latest-versions-only != 'true' - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - canary-run: ${{ needs.build-info.outputs.canary-run }} - default-python-version: "${{ needs.build-info.outputs.default-python-version }}" - upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} - selected-providers-list-as-string: ${{ needs.build-info.outputs.selected-providers-list-as-string }} - # yamllint disable rule:line-length - providers-compatibility-tests-matrix: > - ${{ needs.build-info.outputs.providers-compatibility-tests-matrix }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - python-versions: ${{ needs.build-info.outputs.python-versions }} - providers-test-types-list-as-strings-in-json: > - ${{ needs.build-info.outputs.providers-test-types-list-as-strings-in-json }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - - tests-helm: - name: "Helm tests" - uses: ./.github/workflows/helm-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - helm-test-packages: ${{ needs.build-info.outputs.helm-test-packages }} - default-python-version: "${{ needs.build-info.outputs.default-python-version }}" - use-uv: ${{ needs.build-info.outputs.use-uv }} - if: > - needs.build-info.outputs.run-helm-tests == 'true' && - needs.build-info.outputs.default-branch == 'main' && - needs.build-info.outputs.latest-versions-only != 'true' - - tests-postgres-core: - name: "Postgres tests: core" - uses: ./.github/workflows/run-unit-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - backend: "postgres" - test-name: "Postgres" - test-scope: "DB" - test-group: "core" - python-versions: ${{ needs.build-info.outputs.python-versions }} - backend-versions: ${{ needs.build-info.outputs.postgres-versions }} - excluded-providers-as-string: ${{ needs.build-info.outputs.excluded-providers-as-string }} - excludes: ${{ needs.build-info.outputs.postgres-exclude }} - test-types-as-strings-in-json: > - ${{ needs.build-info.outputs.core-test-types-list-as-strings-in-json }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - run-migration-tests: "true" - run-coverage: ${{ needs.build-info.outputs.run-coverage }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' - - tests-postgres-providers: - name: "Postgres tests: providers" - uses: ./.github/workflows/run-unit-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - backend: "postgres" - test-name: "Postgres" - test-scope: "DB" - test-group: "providers" - python-versions: ${{ needs.build-info.outputs.python-versions }} - backend-versions: ${{ needs.build-info.outputs.postgres-versions }} - excluded-providers-as-string: ${{ needs.build-info.outputs.excluded-providers-as-string }} - excludes: ${{ needs.build-info.outputs.postgres-exclude }} - test-types-as-strings-in-json: > - ${{ needs.build-info.outputs.providers-test-types-list-as-strings-in-json }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - run-migration-tests: "true" - run-coverage: ${{ needs.build-info.outputs.run-coverage }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' - - tests-sqlite-core: - name: "Sqlite tests: core" - uses: ./.github/workflows/run-unit-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - backend: "sqlite" - test-name: "Sqlite" - test-name-separator: "" - test-scope: "DB" - test-group: "core" - python-versions: ${{ needs.build-info.outputs.python-versions }} - # No versions for sqlite - backend-versions: "['']" - excluded-providers-as-string: ${{ needs.build-info.outputs.excluded-providers-as-string }} - excludes: ${{ needs.build-info.outputs.sqlite-exclude }} - test-types-as-strings-in-json: > - ${{ needs.build-info.outputs.core-test-types-list-as-strings-in-json }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - run-coverage: ${{ needs.build-info.outputs.run-coverage }} - run-migration-tests: "true" - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' - - tests-sqlite-providers: - name: "Sqlite tests: providers" - uses: ./.github/workflows/run-unit-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - backend: "sqlite" - test-name: "Sqlite" - test-name-separator: "" - test-scope: "DB" - test-group: "providers" - python-versions: ${{ needs.build-info.outputs.python-versions }} - # No versions for sqlite - backend-versions: "['']" - excluded-providers-as-string: ${{ needs.build-info.outputs.excluded-providers-as-string }} - excludes: ${{ needs.build-info.outputs.sqlite-exclude }} - test-types-as-strings-in-json: > - ${{ needs.build-info.outputs.providers-test-types-list-as-strings-in-json }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - run-coverage: ${{ needs.build-info.outputs.run-coverage }} - run-migration-tests: "true" - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' - - - tests-non-db-core: - name: "Non-DB tests: core" - uses: ./.github/workflows/run-unit-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - backend: "sqlite" - test-name: "" - test-name-separator: "" - test-scope: "Non-DB" - test-group: "core" - python-versions: ${{ needs.build-info.outputs.python-versions }} - # No versions for non-db - backend-versions: "['']" - excluded-providers-as-string: ${{ needs.build-info.outputs.excluded-providers-as-string }} - excludes: ${{ needs.build-info.outputs.sqlite-exclude }} - test-types-as-strings-in-json: > - ${{ needs.build-info.outputs.core-test-types-list-as-strings-in-json }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - run-coverage: ${{ needs.build-info.outputs.run-coverage }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' - - tests-non-db-providers: - name: "Non-DB tests: providers" - uses: ./.github/workflows/run-unit-tests.yml - needs: [build-info, build-ci-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - backend: "sqlite" - test-name: "" - test-name-separator: "" - test-scope: "Non-DB" - test-group: "providers" - python-versions: ${{ needs.build-info.outputs.python-versions }} - # No versions for non-db - backend-versions: "['']" - excluded-providers-as-string: ${{ needs.build-info.outputs.excluded-providers-as-string }} - excludes: ${{ needs.build-info.outputs.sqlite-exclude }} - test-types-as-strings-in-json: > - ${{ needs.build-info.outputs.providers-test-types-list-as-strings-in-json }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - run-coverage: ${{ needs.build-info.outputs.run-coverage }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - skip-providers-tests: ${{ needs.build-info.outputs.skip-providers-tests }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - default-branch: ${{ needs.build-info.outputs.default-branch }} - if: needs.build-info.outputs.run-unit-tests == 'true' - - build-prod-images: - name: Build PROD images - needs: [build-info, build-ci-images, generate-constraints] - uses: ./.github/workflows/prod-image-build.yml - permissions: - contents: read - # This write is only given here for `push` events from "apache/airflow" repo. It is not given for PRs - # from forks. This is to prevent malicious PRs from creating images in the "apache/airflow" repo. - packages: write - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - build-type: "Regular" - push-image: "false" - upload-image-artifact: "true" - upload-package-artifact: "true" - python-versions: ${{ needs.build-info.outputs.python-versions }} - default-python-version: "${{ needs.build-info.outputs.default-python-version }}" - branch: ${{ needs.build-info.outputs.default-branch }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} - constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} - docker-cache: ${{ needs.build-info.outputs.docker-cache }} - disable-airflow-repo-cache: ${{ needs.build-info.outputs.disable-airflow-repo-cache }} - prod-image-build: ${{ needs.build-info.outputs.prod-image-build }} - - tests-kubernetes: - name: "Kubernetes tests" - uses: ./.github/workflows/k8s-tests.yml - needs: [build-info, build-prod-images] - permissions: - contents: read - packages: read - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - kubernetes-combos: ${{ needs.build-info.outputs.kubernetes-combos }} - if: > - ( needs.build-info.outputs.run-kubernetes-tests == 'true' || - needs.build-info.outputs.run-helm-tests == 'true') - - tests-go-sdk: - name: "Go SDK tests" - needs: [build-info, build-ci-images] - runs-on: ${{ fromJSON(needs.build-info.outputs.arm-runners) }} - timeout-minutes: 15 - permissions: - contents: read - packages: read - if: needs.build-info.outputs.run-go-sdk-tests == 'true' - env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_USERNAME: ${{ github.actor }} - VERBOSE: "true" - steps: - - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - # keep this in sync with go.mod in go-sdk/ - - name: Setup Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version: 1.24 - cache-dependency-path: go-sdk/go.sum - - name: "Cleanup dist files" - run: rm -fv ./dist/* - - name: Run Go tests - working-directory: ./go-sdk - run: go test -v ./... - - finalize-tests: - name: Finalize tests - permissions: - contents: write - packages: write - # This will fire when all the jobs from "needs" are either successful or skipped - if: always() && !failure() && !cancelled() - needs: - - additional-ci-image-checks - - basic-tests - - build-info - - basic-tests - - generate-constraints - - build-prod-images - - providers - - tests-helm - - tests-kubernetes - - tests-non-db-core - - tests-non-db-providers - - tests-postgres-core - - tests-postgres-providers - - tests-sqlite-core - - tests-sqlite-providers - uses: ./.github/workflows/finalize-tests.yml - with: - runners: ${{ needs.build-info.outputs.arm-runners }} - platform: "linux/arm64" - python-versions: ${{ needs.build-info.outputs.python-versions }} - python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }} - branch: ${{ needs.build-info.outputs.default-branch }} - constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }} - default-python-version: "${{ needs.build-info.outputs.default-python-version }}" - upgrade-to-newer-dependencies: ${{ needs.build-info.outputs.upgrade-to-newer-dependencies }} - include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }} - docker-cache: ${{ needs.build-info.outputs.docker-cache }} - disable-airflow-repo-cache: ${{ needs.build-info.outputs.disable-airflow-repo-cache }} - canary-run: ${{ needs.build-info.outputs.canary-run }} - use-uv: ${{ needs.build-info.outputs.use-uv }} - debug-resources: ${{ needs.build-info.outputs.debug-resources }} - - notify-slack-failure: - name: "Notify Slack on Failure" - needs: - - finalize-tests - if: github.event_name == 'schedule' && failure() && github.run_attempt == 1 - runs-on: ["ubuntu-22.04"] - steps: - - name: Notify Slack - id: slack - uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0 - with: - method: chat.postMessage - token: ${{ env.SLACK_BOT_TOKEN }} - # yamllint disable rule:line-length - payload: | - channel: "internal-airflow-ci-cd" - text: "🚨🕒 Failure Alert: Scheduled CI (ARM) on branch *${{ github.ref_name }}* 🕒🚨\n\n*Details:* " - blocks: - - type: "section" - text: - type: "mrkdwn" - text: "🚨🕒 Failure Alert: Scheduled CI (ARM) 🕒🚨\n\n*Details:* " - # yamllint enable rule:line-length diff --git a/.github/workflows/ci-image-build.yml b/.github/workflows/ci-image-build.yml index b7e09a38648e6..1acb191d21bc8 100644 --- a/.github/workflows/ci-image-build.yml +++ b/.github/workflows/ci-image-build.yml @@ -72,6 +72,7 @@ on: # yamllint disable-line rule:truthy description: "JSON-formatted array of Python versions to build images from" required: true type: string + default: '[""]' branch: description: "Branch used to run the CI jobs in (main/v*_*_test)." required: true @@ -99,7 +100,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ${{ fromJSON(inputs.python-versions) || fromJSON('[""]') }} + python-version: ${{ fromJSON(inputs.python-versions) }} timeout-minutes: 110 name: "Build CI ${{ inputs.platform }} image ${{ matrix.python-version }}" runs-on: ${{ fromJSON(inputs.runners) }} @@ -205,3 +206,5 @@ jobs: if-no-files-found: 'error' retention-days: 2 if: inputs.upload-mount-cache-artifact == 'true' + - name: "Check disk space after build" + run: df -H diff --git a/.github/workflows/ci-image-checks.yml b/.github/workflows/ci-image-checks.yml index a4954d0344579..f4e302ab17036 100644 --- a/.github/workflows/ci-image-checks.yml +++ b/.github/workflows/ci-image-checks.yml @@ -153,16 +153,19 @@ jobs: with: python-version: ${{steps.breeze.outputs.host-python-version}} platform: ${{ inputs.platform }} - save-cache: false + save-cache: true - name: "Static checks" run: prek --all-files --show-diff-on-failure --color always env: - VERBOSE: "false" + VERBOSE: "true" SKIP: ${{ inputs.skip-prek-hooks }} COLUMNS: "202" SKIP_GROUP_OUTPUT: "true" DEFAULT_BRANCH: ${{ inputs.branch }} RUFF_FORMAT: "github" + - name: "Show prek log on failure" + run: cat ~/.cache/prek/prek.log || true + if: failure() mypy: timeout-minutes: 45 @@ -184,6 +187,9 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Free up disk space" + shell: bash + run: ./scripts/tools/free_up_disk_space.sh - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" uses: ./.github/actions/prepare_breeze_and_image with: diff --git a/.github/workflows/ci-notification.yml b/.github/workflows/ci-notification.yml index 60c2476c2dd81..3b5fdfd31e5a5 100644 --- a/.github/workflows/ci-notification.yml +++ b/.github/workflows/ci-notification.yml @@ -36,8 +36,8 @@ jobs: workflow-status: strategy: matrix: - branch: ["v3-0-test"] - workflow-id: ["ci-amd.yml", "ci-arm.yml"] + branch: ["v3-1-test"] + workflow-id: ["ci-amd-arm.yml"] runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" @@ -45,11 +45,6 @@ jobs: with: persist-credentials: false - - name: "Install Python 3.11 as 3.11+ is needed by pin-versions pre-commit" - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 - with: - python-version: 3.11 - - name: "Find workflow run status" id: find-workflow-run-status run: | diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index 1770f44bb553f..47bd7ba83211e 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -83,6 +83,9 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Free up disk space" + shell: bash + run: ./scripts/tools/free_up_disk_space.sh # env.PYTHON_MAJOR_MINOR_VERSION, env.KUBERNETES_VERSION are set in the previous # step id: prepare-versions - name: "Prepare breeze & PROD image: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}" diff --git a/.github/workflows/prod-image-build.yml b/.github/workflows/prod-image-build.yml index 31127a8e132cb..93eb251cf0d63 100644 --- a/.github/workflows/prod-image-build.yml +++ b/.github/workflows/prod-image-build.yml @@ -76,6 +76,7 @@ on: # yamllint disable-line rule:truthy description: "JSON-formatted array of Python versions to build images from" required: true type: string + default: '[""]' default-python-version: description: "Which version of python should be used by default" required: true @@ -184,7 +185,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ${{ fromJSON(inputs.python-versions) || fromJSON('[""]') }} + python-version: ${{ fromJSON(inputs.python-versions) }} timeout-minutes: 80 name: "Build PROD ${{ inputs.build-type }} image ${{ matrix.python-version }}" runs-on: ${{ fromJSON(inputs.runners) }} @@ -227,6 +228,10 @@ jobs: with: name: prod-packages path: ./docker-context-files + - name: "Remove fab provider for python 3.13" + shell: bash + run: rm -vf ./docker-context-files/apache_airflow_providers_fab-*.whl + if: matrix.python-version == '3.13' - name: "Show downloaded packages" run: ls -la ./docker-context-files - name: "Download constraints" diff --git a/.github/workflows/publish-docs-to-s3.yml b/.github/workflows/publish-docs-to-s3.yml index 97bce1515f52f..ddac8d7532598 100644 --- a/.github/workflows/publish-docs-to-s3.yml +++ b/.github/workflows/publish-docs-to-s3.yml @@ -95,14 +95,16 @@ jobs: default-python-version: "3.10" if: contains(fromJSON('[ "ashb", + "bugraoz93", "eladkal", "ephraimbuddy", "jedcunningham", + "jscheffl", "kaxil", "pierrejeambrun", "potiuk", "utkarsharma2", - "bugraoz93" + "vincbeck", ]'), github.event.sender.login) steps: - name: "Input parameters summary" @@ -189,6 +191,13 @@ jobs: ref: ${{ inputs.ref }} fetch-depth: 0 fetch-tags: true + - name: "Free up disk space" + shell: bash + run: ./scripts/tools/free_up_disk_space.sh + - name: "Make /mnt writeable" + run: ./scripts/ci/make_mnt_writeable.sh + - name: "Move docker to /mnt" + run: ./scripts/ci/move_docker_to_mnt.sh - name: "Apply patch commits if provided" run: | if [[ "${APPLY_COMMITS}" != "" ]]; then @@ -305,7 +314,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_USERNAME: ${{ github.actor }} INCLUDE_SUCCESS_OUTPUTS: false - PYTHON_MAJOR_MINOR_VERSION: 3.10 + PYTHON_MAJOR_MINOR_VERSION: "3.10" VERBOSE: "true" steps: - name: "Cleanup repo" @@ -318,6 +327,9 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Make /mnt writeable and cleanup" + shell: bash + run: ./scripts/ci/make_mnt_writeable.sh - name: "Install Breeze" uses: ./.github/actions/breeze - name: "Download docs prepared as artifacts" diff --git a/.github/workflows/push-image-cache.yml b/.github/workflows/push-image-cache.yml index 9458b536e68e7..69ee1145361a0 100644 --- a/.github/workflows/push-image-cache.yml +++ b/.github/workflows/push-image-cache.yml @@ -116,6 +116,8 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Free up disk space" + run: ./scripts/tools/free_up_disk_space.sh - name: "Install Breeze" uses: ./.github/actions/breeze - name: Login to ghcr.io @@ -183,6 +185,8 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Free up disk space" + run: ./scripts/tools/free_up_disk_space.sh - name: "Install Breeze" uses: ./.github/actions/breeze - name: "Cleanup dist and context file" diff --git a/.github/workflows/release_dockerhub_image.yml b/.github/workflows/release_dockerhub_image.yml index 4c1349f20f070..c32bcc686758d 100644 --- a/.github/workflows/release_dockerhub_image.yml +++ b/.github/workflows/release_dockerhub_image.yml @@ -58,16 +58,19 @@ jobs: AIRFLOW_VERSION: ${{ github.event.inputs.airflowVersion }} AMD_ONLY: ${{ github.event.inputs.amdOnly }} LIMIT_PYTHON_VERSIONS: ${{ github.event.inputs.limitPythonVersions }} - UV_VERSION: "0.8.15" # Keep this comment to allow automatic replacement of uv version + UV_VERSION: "0.9.26" # Keep this comment to allow automatic replacement of uv version if: contains(fromJSON('[ "ashb", + "bugraoz93", "eladkal", "ephraimbuddy", "jedcunningham", + "jscheffl", "kaxil", "pierrejeambrun", "potiuk", - "utkarsharma2" + "utkarsharma2", + "vincbeck", ]'), github.event.sender.login) steps: - name: "Input parameters summary" @@ -87,19 +90,19 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - name: "Install uv" - run: curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh - - name: "Check airflow version" - id: check-airflow-version - shell: bash - run: uv run scripts/ci/airflow_version_check.py "${AIRFLOW_VERSION}" >> "${GITHUB_OUTPUT}" - name: "Install Breeze" uses: ./.github/actions/breeze + with: + uv-version: ${{ env.UV_VERSION }} - name: Selective checks id: selective-checks env: VERBOSE: "false" run: breeze ci selective-check 2>> ${GITHUB_OUTPUT} + - name: "Check airflow version" + id: check-airflow-version + shell: bash + run: uv run scripts/ci/airflow_version_check.py "${AIRFLOW_VERSION}" >> "${GITHUB_OUTPUT}" - name: "Determine build matrix" shell: bash id: determine-matrix diff --git a/.github/workflows/test-providers.yml b/.github/workflows/test-providers.yml index 338c6f395bc1a..264f75e759be6 100644 --- a/.github/workflows/test-providers.yml +++ b/.github/workflows/test-providers.yml @@ -92,6 +92,9 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Free up disk space" + shell: bash + run: ./scripts/tools/free_up_disk_space.sh - name: "Prepare breeze & CI image: ${{ inputs.default-python-version }}" uses: ./.github/actions/prepare_breeze_and_image with: @@ -188,6 +191,9 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: "Free up disk space" + shell: bash + run: ./scripts/tools/free_up_disk_space.sh - name: "Prepare breeze & CI image: ${{ matrix.compat.python-version }}" uses: ./.github/actions/prepare_breeze_and_image with: diff --git a/.gitignore b/.gitignore index d3f1810a46e48..f255d7e01a09b 100644 --- a/.gitignore +++ b/.gitignore @@ -171,8 +171,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* -.vscode/* -!.vscode/extensions.json /.vite/ # Exclude the ui .vite dir airflow-core/src/airflow/ui/.vite/ @@ -235,8 +233,8 @@ pip-wheel-metadata .pypirc # Dev files -/dev/packages.txt -/dev/Dockerfile.pmc +packages.txt +Dockerfile.pmc # Generated UI licenses 3rd-party-licenses/LICENSES-ui.txt @@ -275,3 +273,9 @@ _api/ #while running go tests inside the go-sdk, it can generate log files for dags, ignore all logs go-sdk/**/*.log + +# E2e tests +_e2e_test_report.json + +# UV cache +.uv-cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aac71bbb1793f..1f6a0dad88be9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,11 +16,11 @@ # under the License. --- default_stages: [pre-commit, pre-push] +minimum_prek_version: '0.2.22' default_language_version: python: python3 node: 22.19.0 golang: 1.24.0 -minimum_prek_version: '0.1.3' exclude: ^.*/.*_vendor/ repos: - repo: meta @@ -30,8 +30,16 @@ repos: description: Print input to the static check hooks for troubleshooting - id: check-hooks-apply name: Check if all hooks apply to the repository + - repo: https://github.com/eclipse-csi/octopin + rev: 4cf60063ce94dc61e8760462a9b223aa89676544 + hooks: + - id: pin-versions + name: Pin versions of dependencies in CI workflows (manual) + stages: ['manual'] + language: python + language_version: python311 - repo: https://github.com/thlorenz/doctoc.git - rev: 70fdcd39ef919754011a827bd25f23a0b141c3c3 # frozen: v2.2.0 + rev: 68f070c98b9a053eabfa7f8899d1f42b9919f98c # frozen: v2.2.0 hooks: - id: doctoc name: Add TOC for Markdown and RST files @@ -45,12 +53,14 @@ repos: ^docs/README\.md$| ^\.github/.*\.md$| ^airflow-core/tests/system/README\.md$ + exclude: + (?x) + .github/PULL_REQUEST_TEMPLATE\.md$ args: - "--maxlevel" - "2" - repo: https://github.com/Lucas-C/pre-commit-hooks - # replace hash with version once PR #103 merged comes in a release - rev: abdd8b62891099da34162217ecb3872d22184a51 + rev: ad1b27d73581aa16cca06fc4a0761fc563ffe8e8 # frozen: v1.5.6 hooks: - id: insert-license name: Add license for all SQL files @@ -124,20 +134,12 @@ repos: - --license-filepath - scripts/ci/license-templates/LICENSE.txt - --fuzzy-match-generates-todo - - id: insert-license - name: Add license for all Helm template files - files: ^chart/templates/.* - args: - - --comment-style - - "{{/*||*/}}" - - --license-filepath - - scripts/ci/license-templates/LICENSE.txt - - --fuzzy-match-generates-todo - id: insert-license name: Add license for all YAML files except Helm templates exclude: > (?x) - ^\.github/.*$|^chart/templates/.*| + ^\.github/.*$| + ^chart/templates/.*| .*reproducible_build\.yaml$| ^.*/v2.*\.yaml$| ^.*/openapi/_private_ui.*\.yaml$| @@ -154,13 +156,15 @@ repos: - id: insert-license name: Add license for all Markdown files files: \.md$ - exclude: PROVIDER_CHANGES.*\.md$ args: - --comment-style - "" - --license-filepath - scripts/ci/license-templates/LICENSE.txt - --fuzzy-match-generates-todo + exclude: + (?x) + .github/PULL_REQUEST_TEMPLATE\.md$ - id: insert-license name: Add license for all other files exclude: ^\.github/.*$ @@ -172,18 +176,6 @@ repos: - --fuzzy-match-generates-todo files: > \.cfg$|\.conf$|\.ini$|\.ldif$|\.properties$|\.service$|\.tf$|Dockerfile.*$ - - id: insert-license - name: Add license for all Go files - types: [go] - exclude: mocks/.*\.go$ - args: - - --comment-style - - "|//|" - - --license-filepath - - scripts/ci/license-templates/LICENSE.txt - - --insert-license-after-regex - # We need this 'generated by' line at the top for `golines` to not format it - - '// Code generated by .*' - repo: local hooks: - id: check-min-python-version @@ -191,6 +183,18 @@ repos: entry: ./scripts/ci/prek/check_min_python_version.py language: python require_serial: true + - id: check-version-consistency + name: Check version consistency + entry: ./scripts/ci/prek/check_version_consistency.py + language: python + files: > + (?x) + ^airflow-core/src/airflow/__init__\.py$| + ^airflow-core/pyproject\.toml$| + ^task-sdk/src/airflow/sdk/__init__\.py$| + ^pyproject\.toml$ + pass_filenames: false + require_serial: true - id: upgrade-important-versions name: Upgrade important versions (manual) entry: ./scripts/ci/prek/upgrade_important_versions.py @@ -203,21 +207,6 @@ repos: ^scripts/ci/prek/update_installers_and_prek\.py$ pass_filenames: false require_serial: true - - id: check-translations-completeness - name: Check translation completeness (manual) - entry: ./dev/i18n/check_translations_completeness.py - stages: ['manual'] - language: python - pass_filenames: false - require_serial: true - - id: update-chart-dependencies - name: Update chart dependencies to latest (manual) - entry: ./scripts/ci/prek/update_chart_dependencies.py - stages: ['manual'] - language: python - files: ^\.pre-commit-config\.yaml$|^scripts/ci/prek/update_build_dependencies\.py$ - pass_filenames: false - require_serial: true - id: check-taskinstance-tis-attrs name: Check that TI and TIS have the same attributes entry: ./scripts/ci/prek/check_ti_vs_tis_attributes.py @@ -225,15 +214,8 @@ repos: files: ^airflow-core/src/airflow/models/taskinstance\.py$|^airflow-core/src/airflow/models/taskinstancehistory\.py$ pass_filenames: false require_serial: true - - id: check-deferrable-default - name: Check and fix default value of default_deferrable - language: python - entry: ./scripts/ci/prek/check_deferrable_default.py - pass_filenames: false - # libcst doesn't have source wheels for all PY except PY3.12, excluding it - files: ^(providers/.*/)?airflow/.*/(sensors|operators)/.*\.py$ - repo: https://github.com/adamchainz/blacken-docs - rev: dda8db18cfc68df532abf33b185ecd12d5b7b326 # frozen: 1.20.0 + rev: fda77690955e9b63c6687d8806bafd56a526e45f # frozen: 1.20.0 hooks: - id: blacken-docs name: Run black on docs @@ -245,7 +227,7 @@ repos: - --target-version=py313 alias: blacken-docs additional_dependencies: - - 'black==25.1.0' + - 'black==25.9.0' - repo: https://github.com/pre-commit/pre-commit-hooks rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: @@ -284,15 +266,6 @@ repos: ^dev/breeze/doc/images/output.*$| ^.*/openapi-gen/.*$| ^airflow-ctl/docs/images/.*\.svg$ - - id: pretty-format-json - name: Format JSON files - args: - - --autofix - - --no-sort-keys - - --indent - - "4" - files: ^chart/values\.schema\.json$|^chart/values_schema\.schema\.json$ - pass_filenames: true - repo: https://github.com/pre-commit/pygrep-hooks rev: 3a6eb0fadf60b3cccfd80bad9dbb6fae7e47b316 # frozen: v1.10.0 hooks: @@ -301,7 +274,7 @@ repos: - id: python-no-log-warn name: Check if there are no deprecate log warn - repo: https://github.com/adrienverge/yamllint - rev: 79a6b2b1392eaf49cdd32ac4f14be1a809bbd8f7 # frozen: v1.37.1 + rev: cba56bcde1fdd01c1deb3f945e69764c291a6530 # frozen: v1.38.0 hooks: - id: yamllint name: Check YAML files with yamllint @@ -319,7 +292,7 @@ repos: ^.*reproducible_build\.yaml$| ^.*pnpm-lock\.yaml$ - repo: https://github.com/ikamensh/flynt - rev: '97be693bf18bc2f050667dd282d243e2824b81e2' # frozen: 1.0.6 + rev: 97be693bf18bc2f050667dd282d243e2824b81e2 # frozen: 1.0.6 hooks: - id: flynt name: Run flynt string format converter for Python @@ -355,7 +328,7 @@ repos: - --skip=providers/.*/src/airflow/providers/*/*.rst,providers/*/docs/changelog.rst,docs/*/commits.rst,providers/*/docs/commits.rst,providers/*/*/docs/commits.rst,docs/apache-airflow/tutorial/pipeline_example.csv,*.min.js,*.lock,INTHEWILD.md,*.svg - --exclude-file=.codespellignorelines - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: 5fee7cc03a2498e0ce36a18f36dfb558a30a426c # frozen: v1.12.1 + rev: b546b77c44c466a54a42af5499dcc0dcc1a3193f # frozen: v1.22.0 hooks: - id: zizmor name: Run zizmor to check for github workflow syntax errors @@ -372,12 +345,6 @@ repos: # changes quickly - especially when we want the early modifications from the first local group # to be applied before the non-local prek hooks are run hooks: - - id: update-providers-dependencies - name: Update dependencies for providers - entry: ./scripts/ci/prek/update_providers_dependencies.py - language: python - always_run: true - pass_filenames: false - id: check-shared-distributions-structure name: Check shared distributions structure entry: ./scripts/ci/prek/check_shared_distributions_structure.py @@ -390,34 +357,6 @@ repos: language: python pass_filenames: false files: ^shared/.*$|^.*/pyproject.toml$|^.*/_shared/.*$ - - id: validate-operators-init - name: No templated field logic checks in operator __init__ - description: Prevent templated field logic checks in operators' __init__ - language: python - entry: ./scripts/ci/prek/validate_operators_init.py - pass_filenames: true - files: ^providers/.*/src/airflow/providers/.*/(operators|transfers|sensors)/.*\.py$ - - id: update-providers-build-files - name: Update providers build files - entry: ./scripts/ci/prek/update_providers_build_files.py - language: python - pass_filenames: true - files: | - (?x) - ^providers/[^\/]*/src/airflow/providers/[^\/]*/__init__\.py$| - ^providers/[^\/]*/[^\/]*/src/airflow/providers/[^\/]*/[^\/]*/__init__\.py$| - ^providers/.*/pyproject\.toml$| - ^providers/.*/provider\.yaml$| - ^airflow_breeze/templates/PROVIDER__INIT__PY_TEMPLATE\.py\.jinja2$| - ^airflow_breeze/templates/get_provider_info_TEMPLATE\.py\.jinja2$| - ^airflow_breeze/templates/PROVIDER_README_TEMPLATE\.rst\.jinja2$ - require_serial: true - - id: check-airflow-v-imports-in-tests - name: Check AIRFLOW_V imports in tests - language: python - entry: ./scripts/ci/prek/check_airflow_v_imports_in_tests.py - pass_filenames: true - files: ^providers/.*/tests/.+\.py$ - id: ruff name: Run 'ruff' for extremely fast Python linting description: "Run 'ruff' for extremely fast Python linting" @@ -426,7 +365,7 @@ repos: types_or: [python, pyi] args: [--fix] require_serial: true - additional_dependencies: ['ruff==0.12.12'] + additional_dependencies: ['ruff==0.14.14'] exclude: ^airflow-core/tests/unit/dags/test_imports\.py$|^performance/tests/test_.*\.py$ - id: ruff-format name: Run 'ruff format' @@ -507,12 +446,6 @@ repos: language: python files: ^dev/breeze/src/airflow_breeze/utils/docker_command_utils\.py$|^scripts/ci/docker_compose/local\.yml$ pass_filenames: false - - id: check-sql-dependency-common-data-structure - name: Check dependency of SQL providers - description: Check dependency of SQL Providers with common data structure - entry: ./scripts/ci/prek/check_common_sql_dependency.py - language: python - files: ^providers/.*/src/airflow/providers/.*/hooks/.*\.py$ - id: check-extra-packages-references name: Checks setup extra packages description: Checks if all the extras defined in hatch_build.py are listed in extra-packages-ref.rst file @@ -533,15 +466,9 @@ repos: files: > (?x) ^airflow-core/docs/.*/diagram_[^/]*\.py$| - ^docs/images/.*\.py$ + ^docs/images/.*\.py$| + ^airflow-ctl/docs/images/diagrams/.*\.py$ pass_filenames: true - - id: generate-volumes-for-sources - name: Generate volumes for docker compose - entry: ./scripts/ci/prek/generate_volumes_for_sources.py - language: python - files: ^providers/.*/provider\.yaml$ - pass_filenames: false - require_serial: true - id: prevent-deprecated-sqlalchemy-usage name: Prevent deprecated sqlalchemy usage entry: ./scripts/ci/prek/prevent_deprecated_sqlalchemy_usage.py @@ -550,7 +477,6 @@ repos: (?x) ^airflow-ctl.*\.py$| ^airflow-core/src/airflow/models/.*\.py$| - ^providers/fab/.*\.py$| ^task_sdk.*\.py$ pass_filenames: true - id: update-supported-versions @@ -567,10 +493,9 @@ repos: files: > (?x) ^scripts/ci/prek/version_heads_map\.py$| - ^airflow-core/src/airflow/migrations/versions/.*$|^airflow-core/src/airflow/migrations/versions| - ^providers/fab/src/airflow/providers/fab/migrations/versions/.*$|^providers/fab/src/airflow/providers/fab/migrations/versions| - ^airflow-core/src/airflow/utils/db\.py$| - ^providers/fab/src/airflow/providers/fab/auth_manager/models/db\.py$ + ^airflow-core/src/airflow/migrations/versions/.*$| + ^airflow-core/src/airflow/migrations/versions| + ^airflow-core/src/airflow/utils/db\.py$ - id: update-version name: Update versions in docs entry: ./scripts/ci/prek/update_versions.py @@ -583,75 +508,6 @@ repos: entry: "pydevd.*settrace\\(" pass_filenames: true files: \.py$ - - id: check-pytest-mark-db-test-in-providers - language: pygrep - name: Check pytest.mark.db_test use in providers - entry: pytest\.mark\.db_test - pass_filenames: true - # Here we should add providers that are already free from the pytest.mark.db_test - # and we want to keep them clean and only use non-db-tests - files: > - (?x) - ^providers/airbyte/.*\.py$| - ^providers/apache/beam/.*\.py$| - ^providers/apache/flink/.*\.py$| - ^providers/apache/iceberg/.*\.py$| - ^providers/apache/kafka/.*\.py$| - ^providers/apprise/.*\.py$| - ^providers/arangodb/.*\.py$| - ^providers/asana/.*\.py$| - ^providers/atlassian/jira/.*\.py$| - ^providers/cloudant/.*\.py$| - ^providers/cohere/.*\.py$| - ^providers/common/compat/.*\.py$| - ^providers/common/messaging/.*\.py$| - ^providers/datadog/.*\.py$| - ^providers/dingding/.*\.py$| - ^providers/discord/.*\.py$| - ^providers/exasol/.*\.py$| - ^providers/facebook/.*\.py$| - ^providers/ftp/.*\.py$| - ^providers/grpc/.*\.py$| - ^providers/hashicorp/.*\.py$| - ^providers/imap/.*\.py$| - ^providers/influxdb/.*\.py$| - ^providers/jdbc/.*\.py$| - ^providers/jenkins/.*\.py$| - ^providers/mongo/.*\.py$| - ^providers/microsoft/psrp/.*\.py$| - ^providers/microsoft/winrm/.*\.py$| - ^providers/neo4j/.*\.py$| - ^providers/odbc/.*\.py$| - ^providers/openai/.*\.py$| - ^providers/openfaas/.*\.py$| - ^providers/opsgenie/.*\.py$| - ^providers/oracle/.*\.py$| - ^providers/pagerduty/.*\.py$| - ^providers/pgvector/.*\.py$| - ^providers/pinecone/.*\.py$| - ^providers/postgres/.*\.py$| - ^providers/presto/.*\.py$| - ^providers/segment/.*\.py$| - ^providers/sendgrid/.*\.py$| - ^providers/singularity/.*\.py$| - ^providers/slack/.*\.py$| - ^providers/smtp/.*\.py$| - ^providers/tableau/.*\.py$| - ^providers/teradata/.*\.py$| - ^providers/trino/.*\.py$| - ^providers/vertica/.*\.py$| - ^providers/yandex/.*\.py$| - ^providers/zendesk/.*\.py$ - - id: check-links-to-example-dags-do-not-use-hardcoded-versions - name: Verify no hard-coded version in example dags - description: The links to example dags should use |version| as version specification - language: pygrep - entry: > - (?i) - .*https://github.*/main/providers/.*/src/airflow/providers/.*/example_dags/| - .*https://github.*/master/providers/.*/src/airflow/providers/.*/example_dags/ - pass_filenames: true - files: ^providers/.*/docs/.*\.rst - id: check-safe-filter-usage-in-html language: pygrep name: Don't use safe in templates @@ -666,13 +522,6 @@ repos: entry: "^\\s*from airflow\\.providers.(?!standard.)" pass_filenames: true files: ^airflow-core/src/airflow/example_dags/.*\.py$ - - id: check-no-airflow-deprecation-in-providers - language: pygrep - name: Do not use DeprecationWarning in providers - description: Use AirflowProviderDeprecationWarning in providers - entry: "^\\s*DeprecationWarning*" - pass_filenames: true - files: ^providers/.*/src/airflow/providers/.*\.py$ - id: check-urlparse-usage-in-code language: pygrep name: Don't use urlparse in code @@ -705,91 +554,89 @@ repos: pass_filenames: true exclude: > (?x) - ^airflow-core/src/airflow/ui/src/i18n/config\.ts$| - ^airflow-core/src/airflow/ui/openapi-gen/| - ^airflow-core/src/airflow/ui/public/i18n/locales/de/README\.md$| + ^README\.md$| + ^generated/PYPI_README\.md$| + ^airflow-core/docs/.*commits\.rst$| + ^airflow-core/newsfragments/41368\.significant\.rst$| + ^airflow-core/newsfragments/41761.significant\.rst$| + ^airflow-core/newsfragments/43349\.significant\.rst$| + ^airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock\.yaml$| ^airflow-core/src/airflow/cli/commands/local_commands/fastapi_api_command\.py$| ^airflow-core/src/airflow/config_templates/| ^airflow-core/src/airflow/models/baseoperator\.py$| ^airflow-core/src/airflow/operators/__init__\.py$| - ^providers/common/sql/tests/provider_tests/common/sql/operators/test_sql_execute\.py$| + ^airflow-core/src/airflow/serialization/serialized_objects\.py$| + ^airflow-core/src/airflow/ui/openapi-gen/| + ^airflow-core/src/airflow/ui/pnpm-lock\.yaml$| + ^airflow-core/src/airflow/ui/public/i18n/locales/de/README\.md$| + ^airflow-core/src/airflow/ui/src/i18n/config\.ts$| + ^airflow-core/src/airflow/utils/db\.py$| + ^airflow-core/src/airflow/utils/trigger_rule\.py$| + ^airflow-core/tests/| + ^.*changelog\.(rst|txt)$| + ^.*CHANGELOG\.(rst|txt)$| + ^chart/values.schema\.json$| + ^.*commits\.(rst|txt)$| + ^.*/conf_constants\.py$| + ^.*/conf\.py$| + ^contributing-docs/03_contributors_quick_start\.rst$| + ^dev/| + ^devel-common/src/docs/README\.rst$| + ^devel-common/src/sphinx_exts/removemarktransform\.py| + ^devel-common/src/tests_common/test_utils/db\.py| + .*/dist/.*| + ^docs/apache-airflow-providers-amazon/secrets-backends/aws-ssm-parameter-store\.rst$| + git| + ^helm-tests/tests/chart_utils/helm_template_generator\.py$| + package-lock\.json$| + ^.*\.(png|gif|jp[e]?g|svg|tgz|lock)$| + ^\.pre-commit-config\.yaml$| + ^.*/provider_conf\.py$| + ^providers/\.pre-commit-config\.yaml$| ^providers/amazon/src/airflow/providers/amazon/aws/hooks/emr\.py$| ^providers/amazon/src/airflow/providers/amazon/aws/operators/emr\.py$| ^providers/apache/cassandra/src/airflow/providers/apache/cassandra/hooks/cassandra\.py$| + ^providers/apache/hdfs/docs/connections\.rst$| ^providers/apache/hive/src/airflow/providers/apache/hive/operators/hive_stats\.py$| ^providers/apache/hive/src/airflow/providers/apache/hive/transfers/vertica_to_hive\.py$| + ^providers/apache/kafka/docs/connections/kafka\.rst$| + ^providers/apache/spark/docs/decorators/pyspark\.rst$| ^providers/apache/spark/src/airflow/providers/apache/spark/decorators/| ^providers/apache/spark/src/airflow/providers/apache/spark/hooks/| ^providers/apache/spark/src/airflow/providers/apache/spark/operators/| + ^providers/cncf/kubernetes/docs/operators\.rst$| + ^providers/common/sql/tests/provider_tests/common/sql/operators/test_sql_execute\.py$| + ^providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml$| ^providers/exasol/src/airflow/providers/exasol/hooks/exasol\.py$| + ^providers/fab/docs/auth-manager/webserver-authentication\.rst$| ^providers/fab/src/airflow/providers/fab/auth_manager/security_manager/| ^providers/fab/src/airflow/providers/fab/www/static/| ^providers/fab/src/airflow/providers/fab/www/templates/| + ^providers/google/docs/operators/cloud/kubernetes_engine\.rst$| ^providers/google/src/airflow/providers/google/cloud/hooks/bigquery\.py$| ^providers/google/src/airflow/providers/google/cloud/operators/cloud_build\.py$| ^providers/google/src/airflow/providers/google/cloud/operators/dataproc\.py$| ^providers/google/src/airflow/providers/google/cloud/operators/mlengine\.py$| ^providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/definition.py| + ^providers/microsoft/azure/docs/connections/azure_cosmos\.rst$| ^providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/cosmos\.py$| ^providers/microsoft/winrm/src/airflow/providers/microsoft/winrm/hooks/winrm\.py$| - ^airflow-core/docs/.*commits\.rst$| ^providers/microsoft/winrm/src/airflow/providers/microsoft/winrm/operators/winrm\.py$| ^providers/opsgenie/src/airflow/providers/opsgenie/hooks/opsgenie\.py$| ^providers/redis/src/airflow/providers/redis/provider\.yaml$| - ^airflow-core/src/airflow/serialization/serialized_objects\.py$| - ^airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock\.yaml$| - ^airflow-core/src/airflow/ui/pnpm-lock\.yaml$| - ^airflow-core/src/airflow/utils/db\.py$| - ^airflow-core/src/airflow/utils/trigger_rule\.py$| - ^chart/values.schema\.json$| - ^helm-tests/tests/chart_utils/helm_template_generator\.py$| - ^helm-tests/tests/chart_utils/ingress-networking-v1beta1\.json$| - ^dev/| - ^devel-common/src/docs/README\.rst$| - ^docs/apache-airflow-providers-amazon/secrets-backends/aws-ssm-parameter-store\.rst$| - ^providers/apache/kafka/docs/connections/kafka\.rst$| - ^providers/apache/hdfs/docs/connections\.rst$| - ^providers/apache/spark/docs/decorators/pyspark\.rst$| - ^providers/microsoft/azure/docs/connections/azure_cosmos\.rst$| - ^providers/fab/docs/auth-manager/webserver-authentication\.rst$| - ^providers/google/docs/operators/cloud/kubernetes_engine\.rst$| - ^providers/cncf/kubernetes/docs/operators\.rst$| - ^.*/conf\.py$| - ^.*/conf_constants\.py$| - ^.*/provider_conf\.py$| - ^devel-common/src/sphinx_exts/removemarktransform\.py| - ^devel-common/src/tests_common/test_utils/db\.py| - ^airflow-core/newsfragments/41761.significant\.rst$| - ^scripts/ci/prek/vendor_k8s_json_schema\.py$| - ^scripts/ci/docker-compose/integration-keycloak\.yml$| - ^scripts/ci/docker-compose/keycloak/keycloak-entrypoint\.sh$| - ^airflow-core/tests/| ^providers/.*/tests/| - ^\.pre-commit-config\.yaml$| - ^.*CHANGELOG\.(rst|txt)$| - ^.*changelog\.(rst|txt)$| - ^.*commits\.(rst|txt)$| + .rat-excludes| ^.*RELEASE_NOTES\.rst$| - ^contributing-docs/03_contributors_quick_start\.rst$| - ^.*\.(png|gif|jp[e]?g|svg|tgz|lock)$| - git| - ^airflow-core/newsfragments/43349\.significant\.rst$| - ^airflow-core/newsfragments/41368\.significant\.rst$| - .*/dist/.*| - package-lock\.json$| - ^providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml$ + ^scripts/ci/docker-compose/integration-keycloak\.yml$| + ^scripts/ci/docker-compose/keycloak/keycloak-entrypoint\.sh$| + ^scripts/ci/prek/upgrade_important_versions.py$| + ^scripts/ci/prek/vendor_k8s_json_schema\.py$ - id: check-base-operator-partial-arguments name: Check BaseOperator and partial() arguments language: python entry: ./scripts/ci/prek/check_base_operator_partial_arguments.py pass_filenames: false files: ^airflow-core/src/airflow/models/(?:base|mapped)operator\.py$ - - id: check-init-decorator-arguments - name: Sync model __init__ and decorator arguments - language: python - entry: ./scripts/ci/prek/check_init_decorator_arguments.py - pass_filenames: false - files: ^task-sdk/src/airflow/sdk/definitions/dag\.py$|^task-sdk/src/airflow/sdk/definitions/decorators/task_group\.py$ - id: check-template-context-variable-in-sync name: Sync template context variable refs language: python @@ -822,27 +669,6 @@ repos: ^airflow-core/src/airflow/operators/.*$| ^providers/.*/src/airflow/providers/.*$| ^providers/.*/src/airflow/providers/standard/sensors/.*$ - - id: check-base-operator-usage - language: pygrep - name: Check BaseOperator other imports - description: Make sure BaseOperator is imported from airflow.models outside of core - entry: "from airflow\\.models\\.baseoperator import.* BaseOperator" - pass_filenames: true - files: > - (?x) - ^providers/.*/src/airflow/providers/.*\.py$ - exclude: ^providers/standard/.*/.*\.py$ - - id: check-get-lineage-collector-providers - language: python - name: Check providers import hook lineage code from compat - description: Make sure you import from airflow.provider.common.compat.lineage.hook instead of - airflow.lineage.hook. - entry: ./scripts/ci/prek/check_airflow_imports.py - --pattern '^airflow\.lineage\.hook' - --message "Only TYPE_CHECKING imports from `airflow.lineage.hook` are allowed in providers." - --only_top_level - files: ^providers/.*/src/airflow/providers/.*\.py$ - exclude: ^providers/common/compat/src/airflow/providers/common/compat/.*\.py$ - id: check-decorated-operator-implements-custom-name name: Check @task decorator implements custom_operator_name language: python @@ -881,13 +707,6 @@ repos: language: python files: ^LICENSE$ pass_filenames: false - - id: check-aiobotocore-optional - name: Check if aiobotocore is an optional dependency only - entry: ./scripts/ci/prek/check_aiobotocore_optional.py - language: python - files: ^providers/.*/provider\.yaml$ - pass_filenames: true - require_serial: true - id: check-boring-cyborg-configuration name: Checks for Boring Cyborg configuration consistency language: python @@ -915,26 +734,6 @@ repos: files: ^\.pre-commit-config\.yaml$|^docs/spelling_wordlist\.txt$ require_serial: true pass_filenames: false - - id: lint-helm-chart - name: Lint Helm Chart - entry: ./scripts/ci/prek/lint_helm.py - language: python - pass_filenames: false - files: ^chart - require_serial: true - - id: validate-chart-annotations - name: Validate chart annotations - entry: ./scripts/ci/prek/validate_chart_annotations.py - language: python - pass_filenames: false - files: ^chart/Chart\.yaml$ - - id: kubeconform - name: Kubeconform check on our helm chart - entry: ./scripts/ci/prek/check_kubeconform.py - language: python - pass_filenames: false - files: ^chart - require_serial: true - id: shellcheck name: Check Shell scripts syntax correctness language: docker_image @@ -949,21 +748,7 @@ repos: files: ^airflow-core/src/airflow/ui/|^airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/ entry: ./scripts/ci/prek/compile_ui_assets.py pass_filenames: false - additional_dependencies: ['pnpm@9.7.1'] - - id: compile-fab-assets - name: Compile FAB provider assets - language: node - files: ^providers/fab/.*/www/ - entry: ./scripts/ci/prek/compile_provider_assets.py fab - pass_filenames: false - additional_dependencies: ['yarn@1.22.21'] - - id: compile-edge-assets - name: Compile Edge provider assets - language: node - files: ^providers/edge3/.*/www/ - entry: ./scripts/ci/prek/compile_provider_assets.py edge - pass_filenames: false - additional_dependencies: ['yarn@1.22.21'] + additional_dependencies: ['pnpm@10.25.0'] - id: compile-ui-assets-dev name: Compile ui assets in dev mode (manual) language: node @@ -972,14 +757,7 @@ repos: files: ^airflow-core/src/airflow/ui/|^airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/ entry: ./scripts/ci/prek/compile_ui_assets_dev.py pass_filenames: false - additional_dependencies: ['pnpm@9.7.1'] - - id: check-providers-subpackages-init-file-exist - name: Provider subpackage init files are there - pass_filenames: false - always_run: true - entry: ./scripts/ci/prek/check_providers_subpackages_all_have_init.py - language: python - require_serial: true + additional_dependencies: ['pnpm@10.25.0'] - id: check-integrations-list-consistent name: Sync integrations list with docs entry: ./scripts/ci/prek/check_integrations_list.py @@ -987,13 +765,6 @@ repos: files: ^scripts/ci/docker-compose/integration-.*\.yml$|^contributing-docs/testing/integration_tests\.rst$ require_serial: true pass_filenames: false - - id: update-breeze-readme-config-hash - name: Update Breeze README.md with config files hash - language: python - entry: ./scripts/ci/prek/update_breeze_config_hash.py - files: ^dev/breeze/pyproject\.toml$|^dev/breeze/README\.md$ - pass_filenames: false - require_serial: true - id: update-pyproject-toml name: Update Airflow's meta-package pyproject.toml language: python @@ -1082,34 +853,6 @@ repos: ^scripts/ci/docker-compose/gremlin/.| ^scripts/ci/docker-compose/.+-config\.ya?ml$ require_serial: true - - id: lint-json-schema - name: Lint chart/values.schema.json - entry: ./scripts/ci/prek/lint_json_schema.py - args: - - --spec-file - - chart/values_schema.schema.json - - chart/values.schema.json - language: python - pass_filenames: false - files: ^chart/values\.schema\.json$|^chart/values_schema\.schema\.json$ - require_serial: true - - id: update-vendored-in-k8s-json-schema - name: Vendor k8s definitions into values.schema.json - entry: ./scripts/ci/prek/vendor_k8s_json_schema.py - language: python - files: ^chart/values\.schema\.json$ - - id: lint-json-schema - name: Lint chart/values.yaml - entry: ./scripts/ci/prek/lint_json_schema.py - args: - - --enforce-defaults - - --spec-file - - chart/values.schema.json - - chart/values.yaml - language: python - pass_filenames: false - files: ^chart/values\.yaml$|^chart/values\.schema\.json$ - require_serial: true - id: lint-json-schema name: Lint config_templates/config.yml entry: ./scripts/ci/prek/lint_json_schema.py @@ -1147,13 +890,6 @@ repos: language: python pass_filenames: true files: ^airflow-core/src/airflow/.*\.py$ - - id: lint-chart-schema - name: Lint chart/values.schema.json file - entry: ./scripts/ci/prek/chart_schema.py - language: python - pass_filenames: false - files: ^chart/values\.schema\.json$ - require_serial: true - id: update-inlined-dockerfile-scripts name: Inline Dockerfile and Dockerfile.ci scripts entry: ./scripts/ci/prek/inline_scripts_in_docker.py @@ -1212,12 +948,6 @@ repos: pass_filenames: true files: ^airflow-core/docs/.*example-dags\.rst$|^docs/.*index\.rst$^airflow-core/docs/.*index\.rst$ always_run: true - - id: check-system-tests-tocs - name: Check that system tests is properly added - entry: ./scripts/ci/prek/check_system_tests_hidden_in_index.py - language: python - pass_filenames: true - files: ^providers/.*/docs/index\.rst$ - id: check-lazy-logging name: Check that all logging methods are lazy entry: ./scripts/ci/prek/check_lazy_logging.py @@ -1237,13 +967,6 @@ repos: language: python pass_filenames: true files: ^airflow-core/tests/.*\.py$ - - id: check-provider-docs-valid - name: Validate provider doc files - entry: ./scripts/ci/prek/check_provider_docs.py - language: python - files: ^providers/.*/provider\.yaml$|^.*/docs/.* - require_serial: true - pass_filenames: false - id: bandit name: bandit description: "Bandit is a tool for finding common security issues in Python code" @@ -1261,25 +984,7 @@ repos: - "B101,B301,B324,B403,B404,B603" - "--severity-level" - "high" # TODO: remove this line when we fix all the issues - - id: pylint - name: pylint - description: "Pylint is a static code analyser for Python 2 or 3." - entry: pylint - language: python - language_version: python3 - types: [python] - additional_dependencies: ['pylint==3.1.0'] - require_serial: true - files: ^airflow-core/src/airflow/.* - exclude: - airflow/example_dags/.* - args: - # Use pylint only for the specific check, which are not available into the ruff - - "--disable=all" - # W0133: "Exception statement has no effect" - # see: https://github.com/astral-sh/ruff/issues/10145 - - "--enable=W0133" - - id: check-fab-migrations + - id: check-no-fab-migrations language: pygrep name: Check no migration is done on FAB related table description: > @@ -1303,36 +1008,6 @@ repos: files: ^airflow-core/src/airflow/migrations/versions/.*\.py$ exclude: ^airflow-core/src/airflow/migrations/versions/0028_3_0_0_drop_ab_user_id_foreign_key.py$ - - id: go-mockery - name: Generate mocks for go - entry: -w /src/go-sdk vektra/mockery:3 - files: ^go-sdk/ - exclude: mocks/.*\.go$ - types: [go] - pass_filenames: false - language: docker_image - - id: go-mod-tidy - name: Run go mod tidy - entry: bash -c "cd go-sdk && go mod tidy" - files: ^go-sdk/ - exclude: mocks/.*\.go$ - pass_filenames: false - language: system - - id: gofmt - name: Format go code - entry: golines --base-formatter=gofumpt --write-output --max-len=100 --chain-split-dots - additional_dependencies: [github.com/segmentio/golines@latest, mvdan.cc/gofumpt@v0.8.0] - files: ^go-sdk/ - types: [go] - language: golang - - id: gci - name: Consistent import ordering for Go files - # Since this is invoked from the root folder, not go-sdk/, gci can't auto-detect the prefix - entry: gci write --skip-generated -s standard -s default -s "prefix(github.com/apache/airflow)" - additional_dependencies: [github.com/daixiang0/gci@v0.13.6] - files: ^go-sdk/ - types: [go] - language: golang - id: ts-compile-lint-ui name: Compile / format / lint UI description: TS types generation / ESLint / Prettier new UI files @@ -1347,7 +1022,7 @@ repos: ^airflow-core/src/airflow/ui/node-modules/.*| ^airflow-core/src/airflow/ui/.pnpm-store entry: ./scripts/ci/prek/ts_compile_lint_ui.py - additional_dependencies: ['pnpm@9.7.1'] + additional_dependencies: ['pnpm@10.25.0'] pass_filenames: true require_serial: true - id: ts-compile-lint-simple-auth-manager-ui @@ -1364,28 +1039,13 @@ repos: ^airflow-core/src/airflow/api_fastapi/node-modules/.*| ^airflow-core/src/airflow/api_fastapi/.pnpm-store entry: ./scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py - additional_dependencies: ['pnpm@9.7.1'] - pass_filenames: true - require_serial: true - - id: ts-compile-lint-edge-ui - name: Compile / format / lint edge UI - description: TS types generation / ESLint / Prettier new UI files in Edge Provider - language: node - files: | - (?x) - ^providers/edge3/src/airflow/providers/edge3/plugins/www/.*\.(js|ts|tsx|yaml|css|json)$| - ^providers/edge3/src/airflow/providers/edge3/openapi/v2-edge-generated.yaml$ - exclude: | - (?x) - ^providers/edge3/src/airflow/providers/edge3/plugins/www/node-modules/.*| - ^providers/edge3/src/airflow/providers/edge3/plugins/www/.pnpm-store - entry: ./scripts/ci/prek/ts_compile_lint_edge.py - additional_dependencies: ['pnpm@9.7.1'] + additional_dependencies: ['pnpm@10.25.0'] pass_filenames: true require_serial: true ## ADD MOST PREK HOOK ABOVE THAT LINE # The below prek hooks are those requiring CI image to be built - id: mypy-dev + stages: ['pre-push'] name: Run mypy for dev language: python entry: ./scripts/ci/prek/mypy.py @@ -1395,11 +1055,12 @@ repos: stages: ['manual'] name: Run mypy for dev (manual) language: python - entry: ./scripts/ci/prek/mypy_folder.py dev + entry: ./scripts/ci/prek/mypy_folder.py dev scripts pass_filenames: false files: ^.*\.py$ require_serial: true - id: mypy-airflow-core + stages: ['pre-push'] name: Run mypy for airflow-core language: python entry: ./scripts/ci/prek/mypy.py @@ -1413,35 +1074,8 @@ repos: pass_filenames: false files: ^airflow-core/.*\.py$ require_serial: true - - id: mypy-providers - name: Run mypy for providers - language: python - entry: ./scripts/ci/prek/mypy.py - files: ^providers/.*\.py$ - require_serial: true - - id: mypy-providers - stages: ['manual'] - name: Run mypy for providers (manual) - language: python - entry: ./scripts/ci/prek/mypy_folder.py providers - pass_filenames: false - files: ^.*\.py$ - require_serial: true - - id: mypy-task-sdk - name: Run mypy for task-sdk - language: python - entry: ./scripts/ci/prek/mypy.py - files: ^task-sdk/.*\.py$ - require_serial: true - - id: mypy-task-sdk - stages: ['manual'] - name: Run mypy for task-sdk (manual) - language: python - entry: ./scripts/ci/prek/mypy_folder.py task-sdk - pass_filenames: false - files: ^.*\.py$ - require_serial: true - id: mypy-devel-common + stages: ['pre-push'] name: Run mypy for devel-common language: python entry: ./scripts/ci/prek/mypy.py @@ -1455,46 +1089,13 @@ repos: pass_filenames: false files: ^.*\.py$ require_serial: true - - id: mypy-airflow-ctl - name: Run mypy for airflow-ctl - language: python - entry: ./scripts/ci/prek/mypy.py - files: ^airflow-ctl/src/airflowctl/.*\.py$|^airflow-ctl/tests/.*\.py$ - exclude: .*generated.py - require_serial: true - - id: mypy-airflow-ctl - stages: ['manual'] - name: Run mypy for airflow-ctl (manual) - language: python - entry: ./scripts/ci/prek/mypy_folder.py airflow-ctl - pass_filenames: false - files: ^.*\.py$ - require_serial: true - id: generate-openapi-spec name: Generate the FastAPI API spec language: python entry: ./scripts/ci/prek/generate_openapi_spec.py pass_filenames: false - files: ^airflow-core/src/airflow/api_fastapi/.*\.py$|^airflow-core/src/airflow/api_fastapi/auth/managers/simple/.*\.py$|^providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/.*\.py$ + files: ^airflow-core/src/airflow/api_fastapi/.*\.py$|^airflow-core/src/airflow/api_fastapi/auth/managers/simple/.*\.py$ exclude: ^airflow-core/src/airflow/api_fastapi/execution_api/.* - - id: generate-openapi-spec-fab - name: Generate the FastAPI API spec for FAB - language: python - entry: ./scripts/ci/prek/generate_openapi_spec_providers.py fab - pass_filenames: false - files: ^providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/.*\.py$ - - id: generate-openapi-spec-edge - name: Generate the FastAPI API spec for Edge - language: python - entry: ./scripts/ci/prek/generate_openapi_spec_providers.py edge - pass_filenames: false - files: ^providers/edge3/src/airflow/providers/edge3/worker_api/.*\.py$ - - id: generate-openapi-spec-keycloak - name: Generate the FastAPI API spec for Keycloak - language: python - entry: ./scripts/ci/prek/generate_openapi_spec_providers.py keycloak - pass_filenames: false - files: ^providers/keycloak/src/airflow/providers/keycloak/auth_manager/.*\.py$ - id: check-i18n-json name: Check i18n files validity description: Check i18n files are valid json, have no TODOs, and auto-format them @@ -1502,13 +1103,6 @@ repos: files: ^airflow-core/src/airflow/ui/public/i18n/locales/.*\.json$ entry: ./scripts/ci/prek/check_i18n_json.py pass_filenames: false - - id: check-provider-yaml-valid - name: Validate provider.yaml files - entry: ./scripts/ci/prek/check_provider_yaml_files.py - language: python - files: ^providers/.*/provider\.yaml$ - exclude: ^providers/.*/.venv/.*$ - require_serial: true - id: check-template-fields-valid name: Check templated fields mapped in operators/sensors language: python @@ -1551,26 +1145,6 @@ repos: require_serial: true pass_filenames: false files: ^airflow-core/src/airflow/config_templates/config\.yml$ - - id: generate-airflowctl-help-images - name: Generate SVG from Airflow CTL Commands - entry: ./scripts/ci/prek/capture_airflowctl_help.py - language: python - pass_filenames: false - files: - ^airflow-ctl/src/airflowctl/ctl/cli_config.py$|airflow-ctl/src/airflowctl/api/operations.py|airflow-ctl/src/airflowctl/ctl/commands/.*\.py - - id: check-imports-in-providers - name: Check imports in providers - entry: ./scripts/ci/prek/check_imports_in_providers.py - language: python - files: ^providers/.*/src/airflow/providers/.*version_compat.*\.py$ - require_serial: true - - id: provider-version-compat - name: Check for correct version_compat imports in providers - entry: ./scripts/ci/prek/check_provider_version_compat.py - language: python - types: [python] - files: ^providers/.*/src/airflow/providers/.*\.py$ - require_serial: true - id: check-airflow-version-checks-in-core language: pygrep name: No AIRFLOW_V_* imports in airflow-core @@ -1586,7 +1160,6 @@ repos: ^airflow-core/tests/unit/core/test_configuration\.py$| ^airflow-core/tests/unit/models/test_renderedtifields\.py$| ^airflow-core/tests/unit/models/test_variable\.py$ - - id: check-sdk-imports name: Check for SDK imports in core files entry: ./scripts/ci/prek/check_sdk_imports.py @@ -1623,6 +1196,7 @@ repos: ^airflow-core/src/airflow/dag_processing/collection\.py$| ^airflow-core/src/airflow/dag_processing/manager\.py$| ^airflow-core/src/airflow/dag_processing/processor\.py$| + ^airflow-core/src/airflow/dag_processing/dagbag\.py$| ^airflow-core/src/airflow/datasets/metadata\.py$| ^airflow-core/src/airflow/exceptions\.py$| ^airflow-core/src/airflow/executors/local_executor\.py$| @@ -1634,9 +1208,10 @@ repos: ^airflow-core/src/airflow/models/__init__\.py$| ^airflow-core/src/airflow/models/asset\.py$| ^airflow-core/src/airflow/models/baseoperator\.py$| + ^airflow-core/src/airflow/models/callback\.py$| ^airflow-core/src/airflow/models/connection\.py$| ^airflow-core/src/airflow/models/dag\.py$| - ^airflow-core/src/airflow/models/dagbag\.py$| + ^airflow-core/src/airflow/models/dagbag.py$| ^airflow-core/src/airflow/models/dagrun\.py$| ^airflow-core/src/airflow/models/deadline\.py$| ^airflow-core/src/airflow/models/expandinput\.py$| @@ -1656,6 +1231,7 @@ repos: ^airflow-core/src/airflow/operators/subdag\.py$| ^airflow-core/src/airflow/plugins_manager\.py$| ^airflow-core/src/airflow/providers_manager\.py$| + ^airflow-core/src/airflow/secrets/__init__.py$| ^airflow-core/src/airflow/serialization/definitions/[_a-z]+\.py$| ^airflow-core/src/airflow/serialization/enums\.py$| ^airflow-core/src/airflow/serialization/helpers\.py$| @@ -1692,3 +1268,9 @@ repos: files: ^airflow-core/src/airflow/serialization/schema\.json$|^airflow-core/src/airflow/serialization/serialized_objects\.py$ pass_filenames: false require_serial: true + - id: check-contextmanager-class-decorators + name: Check for problematic context manager class decorators + entry: ./scripts/ci/prek/check_contextmanager_class_decorators.py + language: python + files: .*test.*\.py$ + pass_filenames: true diff --git a/.rat-excludes b/.rat-excludes index 129d7210f07b1..a18505e25f092 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -19,6 +19,7 @@ .rat-excludes .stylelintignore .stylelintrc +.env .venv requirements requirements.txt @@ -48,6 +49,7 @@ generated/* .gitmodules prod_image_installed_providers.txt airflow_pre_installed_providers.txt +Dockerfile.pmc # Generated doc files .*html @@ -176,9 +178,150 @@ auth_generated.py www-hash.txt # go setup files -go.mod -go.sum -mocks/* +**/go.mod +**/go.sum +**/protov1/* + +# go mocks +**/mocks/* + +# Generated protobuf files +.*proto +.*pb.go +.*_grpc.pb.go # Kubernetes env .env + +# SVG files +**/*.svg + +# Doc only change marker file +**/.latest-doc-only-change.txt +**/*-gen/* + + +# Redirects +**/redirects.txt + +# Ignore files + +**/.git-blame-ignore-revs +**/.gitattributes +**/.rat-excludes +**/.gitignore +**/.prettierignore +**/.prettierrc +**/.airflowignore +**/.airflowignore_glob + + +# Vendor includes +**/_vendor/ + +# Generated files +**/*-generated.yaml +**/*-generated.py +**/generated.py +**/generated/* +**/auth_generated.py + +# Lock files +**/pnpm-lock.yaml +**/yarn.lock +**/Chart.lock +**/uv.lock + +# Generated UI files +**/ui/index.html +**/ui/dev/index.html +**/ui/dist/index.html +**/_private_ui.yaml +**/dist/** +**/www/index.html + +# PNG files +**/*.png + +# CSV files +**/*.csv + +# LICENCE files +**/LICENCE*.txt +**/LICENSE*.txt + + +# Checksum files +**/*.sha256 +**/*.md5sum + +# Requirement files +**/requirements.txt + +# Hashes +**/command_hashes.txt +**/www-hash.txt + +# Spelling wordlist +**/spelling_wordlist.txt +**/dictionary.txt + +# Empty files +**/empty.txt + +# Script files +**/script +**/script.bteq +**/script_utf16.bteq + +# Reproducible build files +**/reproducible_build.yaml + +# Other files +**/test_notifier.txt +**/email.html +**/*.log +**/example_upload.txt +**/dummy.pdf +**/java_streaming_src/* +**/kube_config +**/prod_image_installed_providers.txt +**/text.txt +**/newsfragments/** +**/warnings.txt +**/rtd-deprecation/404.html +**/.env +**/*.jsonl + +# API files +**/_api/** +**/node_modules/** + +# Doc files +/docs/.latest-doc-only-change.txt +/docs/redirects.txt +/docs/integration-logos/*.svg +/docs/img/*.md5sum +/docs/img/*.svg + +# Log files +*.log + +# md5 sum files +.*\.md5sum + +# Generated files +*generated.* +/src/airflow/providers/keycloak/auth_manager/openapi/v2-keycloak-auth-manager-generated.yaml +/src/airflow/providers/edge3/plugins/www/* +/src/airflow/providers/edge3/openapi/v2-edge-generated.yaml +/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml +/src/airflow/providers/fab/www/static/dist/* +/any/dag_id=dag_for_testing_redis_task_handler/run_id=test/task_id=task_for_testing_redis_log_handler/attempt=1.log +/src/airflow/providers/google/ads/.gitignore + +# Vendored-in code +/src/airflow/providers/google/_vendor/* + +# Git ignore file +.gitignore diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000..0e6d5fd64e4df --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..aeae611e1fe46 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,38 @@ +{ + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, +} diff --git a/COMMITTERS.rst b/COMMITTERS.rst index 992f671107d9b..ef503bd6d35f0 100644 --- a/COMMITTERS.rst +++ b/COMMITTERS.rst @@ -215,6 +215,12 @@ To be able to merge PRs, committers have to integrate their GitHub ID with Apach GitHub ID to your Apache account. You should see 5 green checks in GitBox. 3. Wait at least 30 minutes for an email inviting you to Apache GitHub Organization and accept invitation. 4. After accepting the GitHub Invitation verify that you are a member of the `Airflow committers team on GitHub `__. + + Additionally, as a committer you can view the team membership at: + + * https://github.com/orgs/apache/teams/airflow-committers/members + * https://whimsy.apache.org/roster/committee/airflow + 5. Ask in ``#internal-airflow-ci-cd`` channel to be `configured in self-hosted runners `_ by the CI team. Wait for confirmation that this is done and some helpful tips from the CI team (Temporarily disabled) 6. After confirming that step 5 is done, open a PR to include your GitHub ID in: diff --git a/Dockerfile b/Dockerfile index 2bef0b0887adf..716c11c36bffc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,17 +46,17 @@ ARG AIRFLOW_UID="50000" ARG AIRFLOW_USER_HOME_DIR=/home/airflow # latest released version here -ARG AIRFLOW_VERSION="3.0.6" +ARG AIRFLOW_VERSION="3.1.3" ARG BASE_IMAGE="debian:bookworm-slim" - +ARG AIRFLOW_PYTHON_VERSION="3.12.12" # You can swap comments between those two args to test pip from the main version # When you attempt to test if the version of `pip` from specified branch works for our builds # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` -ARG AIRFLOW_PIP_VERSION=25.2 +ARG AIRFLOW_PIP_VERSION=25.3 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.8.15 +ARG AIRFLOW_UV_VERSION=0.9.26 ARG AIRFLOW_USE_UV="false" ARG UV_HTTP_TIMEOUT="300" ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow" @@ -143,7 +143,6 @@ libev4 \ libffi-dev \ libgdbm-compat-dev \ libgdbm-dev \ -libgdbm-dev \ libgeos-dev \ libkrb5-dev \ libldap2-dev \ @@ -400,7 +399,7 @@ function install_python() { find /usr/python -depth \ \( \ \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ - -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \ + -o \( -type f -a \( -name 'libpython*.a' \) \) \ \) -exec rm -rf '{}' + link_python } @@ -998,6 +997,7 @@ function install_airflow_and_providers_from_docker_context_files(){ "${install_airflow_distribution[@]}" "${install_airflow_core_distribution[@]}" "${airflow_distributions[@]}" set +x common::install_packaging_tools + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check } @@ -1116,6 +1116,7 @@ function install_from_sources() { --editable ./airflow-core --editable ./task-sdk --editable ./airflow-ctl \ --editable ./kubernetes-tests --editable ./docker-tests --editable ./helm-tests \ --editable ./task-sdk-tests \ + --editable ./airflow-ctl-tests \ --editable ./devel-common[all] --editable ./dev \ --group dev --group docs --group docs-gen --group leveldb" local -a projects_with_devel_dependencies @@ -1236,6 +1237,7 @@ function install_airflow_when_building_images() { echo echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check } @@ -1271,6 +1273,7 @@ function install_additional_dependencies() { echo echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check else echo @@ -1285,6 +1288,7 @@ function install_additional_dependencies() { echo echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check fi } @@ -1619,6 +1623,11 @@ if [[ ${AIRFLOW_COMMAND} =~ ^(scheduler|celery)$ ]] \ wait_for_celery_broker fi +if [[ "$#" -eq 0 && "${_AIRFLOW_DB_MIGRATE}" == "true" ]]; then + echo "[INFO] No commands passed and _AIRFLOW_DB_MIGRATE=true. Exiting script with code 0." + exit 0 +fi + exec "airflow" "${@}" EOF @@ -1693,14 +1702,14 @@ ARG ADDITIONAL_DEV_APT_DEPS="" ARG DEV_APT_COMMAND="" ARG ADDITIONAL_DEV_APT_COMMAND="" ARG ADDITIONAL_DEV_APT_ENV="" +ARG AIRFLOW_PYTHON_VERSION ENV DEV_APT_DEPS=${DEV_APT_DEPS} \ ADDITIONAL_DEV_APT_DEPS=${ADDITIONAL_DEV_APT_DEPS} \ DEV_APT_COMMAND=${DEV_APT_COMMAND} \ ADDITIONAL_DEV_APT_COMMAND=${ADDITIONAL_DEV_APT_COMMAND} \ - ADDITIONAL_DEV_APT_ENV=${ADDITIONAL_DEV_APT_ENV} - -ENV AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION} + ADDITIONAL_DEV_APT_ENV=${ADDITIONAL_DEV_APT_ENV} \ + AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION} COPY --from=scripts install_os_dependencies.sh /scripts/docker/ RUN bash /scripts/docker/install_os_dependencies.sh dev diff --git a/Dockerfile.ci b/Dockerfile.ci index b56d94bf7e8c5..2a848074ef1b9 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -83,7 +83,6 @@ libev4 \ libffi-dev \ libgdbm-compat-dev \ libgdbm-dev \ -libgdbm-dev \ libgeos-dev \ libkrb5-dev \ libldap2-dev \ @@ -340,7 +339,7 @@ function install_python() { find /usr/python -depth \ \( \ \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ - -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \ + -o \( -type f -a \( -name 'libpython*.a' \) \) \ \) -exec rm -rf '{}' + link_python } @@ -870,6 +869,7 @@ function install_from_sources() { --editable ./airflow-core --editable ./task-sdk --editable ./airflow-ctl \ --editable ./kubernetes-tests --editable ./docker-tests --editable ./helm-tests \ --editable ./task-sdk-tests \ + --editable ./airflow-ctl-tests \ --editable ./devel-common[all] --editable ./dev \ --group dev --group docs --group docs-gen --group leveldb" local -a projects_with_devel_dependencies @@ -990,6 +990,7 @@ function install_airflow_when_building_images() { echo echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check } @@ -1025,6 +1026,7 @@ function install_additional_dependencies() { echo echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check else echo @@ -1039,6 +1041,7 @@ function install_additional_dependencies() { echo echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check fi } @@ -1207,6 +1210,27 @@ function environment_initialization() { ssh-keyscan -H localhost >> ~/.ssh/known_hosts 2>/dev/null fi + if [[ ${INTEGRATION_LOCALSTACK:-"false"} == "true" ]]; then + echo + echo "${COLOR_BLUE}Configuring LocalStack integration${COLOR_RESET}" + echo + + # Define LocalStack AWS configuration + declare -A localstack_config=( + ["AWS_ENDPOINT_URL"]="http://localstack:4566" + ["AWS_ACCESS_KEY_ID"]="test" + ["AWS_SECRET_ACCESS_KEY"]="test" + ["AWS_DEFAULT_REGION"]="us-east-1" + ) + + # Export each configuration variable and log it + for key in "${!localstack_config[@]}"; do + export "$key"="${localstack_config[$key]}" + echo " * ${COLOR_BLUE}${key}:${COLOR_RESET} ${localstack_config[$key]}" + done + echo + fi + cd "${AIRFLOW_SOURCES}" # Temporarily add /opt/airflow/providers/standard/tests to PYTHONPATH in order to see example dags @@ -1216,10 +1240,18 @@ function environment_initialization() { fi if [[ ${START_AIRFLOW:="false"} == "true" || ${START_AIRFLOW} == "True" ]]; then + if [[ ${BREEZE_DEBUG_CELERY_WORKER=} == "true" ]]; then + export AIRFLOW__CELERY__POOL=${AIRFLOW__CELERY__POOL:-solo} + fi export AIRFLOW__CORE__LOAD_EXAMPLES=${LOAD_EXAMPLES} wait_for_asset_compilation - # shellcheck source=scripts/in_container/bin/run_tmux - exec run_tmux + if [[ ${USE_MPROCS:="false"} == "true" || ${USE_MPROCS} == "True" ]]; then + # shellcheck source=scripts/in_container/bin/run_mprocs + exec run_mprocs + else + # shellcheck source=scripts/in_container/bin/run_tmux + exec run_tmux + fi fi } @@ -1234,7 +1266,7 @@ function handle_mount_sources() { function determine_airflow_to_use() { USE_AIRFLOW_VERSION="${USE_AIRFLOW_VERSION:=""}" - if [[ "${USE_AIRFLOW_VERSION}" == "" && "${USE_DISTRIBUTIONS_FROM_DIST}" != "true" ]]; then + if [[ "${USE_AIRFLOW_VERSION}" == "" && "${USE_DISTRIBUTIONS_FROM_DIST=}" != "true" ]]; then export PYTHONPATH=${AIRFLOW_SOURCES} echo echo "${COLOR_BLUE}Using airflow version from current sources${COLOR_RESET}" @@ -1294,14 +1326,18 @@ function check_boto_upgrade() { echo # shellcheck disable=SC2086 ${PACKAGING_TOOL_CMD} uninstall ${EXTRA_UNINSTALL_FLAGS} aiobotocore s3fs || true + + # Urllib 2.6.0 breaks kubernetes client because kubernetes client uses deprecated in 2.0.0 and + # removed in 2.6.0 `getheaders()` call (instead of `headers` property. + # Tracked in https://github.com/kubernetes-client/python/issues/2477 # shellcheck disable=SC2086 - ${PACKAGING_TOOL_CMD} install ${EXTRA_INSTALL_FLAGS} --upgrade "boto3<1.38.3" "botocore<1.38.3" + ${PACKAGING_TOOL_CMD} install ${EXTRA_INSTALL_FLAGS} --upgrade "boto3<1.38.3" "botocore<1.38.3" "urllib3<2.6.0" } function check_upgrade_sqlalchemy() { # The python version constraint is a TEMPORARY WORKAROUND to exclude all FAB tests. Is should be removed once we # upgrade FAB to v5 (PR #50960). - if [[ "${UPGRADE_SQLALCHEMY}" != "true" || ${PYTHON_MAJOR_MINOR_VERSION} != "3.13" ]]; then + if [[ "${UPGRADE_SQLALCHEMY=}" != "true" || ${PYTHON_MAJOR_MINOR_VERSION} != "3.13" ]]; then return fi echo @@ -1322,6 +1358,10 @@ function check_downgrade_sqlalchemy() { echo # shellcheck disable=SC2086 ${PACKAGING_TOOL_CMD} install ${EXTRA_INSTALL_FLAGS} "sqlalchemy[asyncio]==${min_sqlalchemy_version}" + echo + echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" + echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check } @@ -1336,6 +1376,10 @@ function check_downgrade_pendulum() { echo # shellcheck disable=SC2086 ${PACKAGING_TOOL_CMD} install ${EXTRA_INSTALL_FLAGS} "pendulum==${min_pendulum_version}" + echo + echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}" + echo + # We use pip check here to make sure that whatever `uv` installs, is also "correct" according to `pip` pip check } @@ -1518,9 +1562,9 @@ ENV DEV_APT_COMMAND=${DEV_APT_COMMAND} \ ADDITIONAL_DEV_APT_DEPS=${ADDITIONAL_DEV_APT_DEPS} \ ADDITIONAL_DEV_APT_COMMAND=${ADDITIONAL_DEV_APT_COMMAND} -ARG AIRFLOW_PYTHON_VERSION="3.12.11" +ARG AIRFLOW_PYTHON_VERSION="3.12.12" ENV AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION} -ENV GOLANG_MAJOR_MINOR_VERSION="1.25.1" +ENV GOLANG_MAJOR_MINOR_VERSION="1.25.5" COPY --from=scripts install_os_dependencies.sh /scripts/docker/ @@ -1573,6 +1617,16 @@ RUN SYSTEM=$(uname -s | tr '[:upper:]' '[:lower:]') \ && curl --silent --location "${HELM_URL}" | tar -xz -O "${SYSTEM}-${PLATFORM}/helm" > /usr/local/bin/helm \ && chmod +x /usr/local/bin/helm +# Install mprocs - a modern process manager for managing multiple Airflow components +ARG MPROCS_VERSION="0.8.3" + +RUN SYSTEM=$(uname -s | tr '[:upper:]' '[:lower:]') \ + && PLATFORM="$(uname -m)" \ + && MPROCS_URL="https://github.com/pvolok/mprocs/releases/download/v${MPROCS_VERSION}/mprocs-${MPROCS_VERSION}-${SYSTEM}-${PLATFORM}-musl.tar.gz" \ + && echo "Downloading mprocs from ${MPROCS_URL}" \ + && curl --silent --location "${MPROCS_URL}" | tar -xz -C /usr/local/bin/ mprocs \ + && chmod +x /usr/local/bin/mprocs + WORKDIR ${AIRFLOW_SOURCES} RUN mkdir -pv ${AIRFLOW_HOME} && \ @@ -1653,10 +1707,10 @@ COPY --from=scripts common.sh install_packaging_tools.sh install_additional_depe # You can swap comments between those two args to test pip from the main version # When you attempt to test if the version of `pip` from specified branch works for our builds # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` -ARG AIRFLOW_PIP_VERSION=25.2 +ARG AIRFLOW_PIP_VERSION=25.3 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.8.15 -ARG AIRFLOW_PREK_VERSION="0.1.6" +ARG AIRFLOW_UV_VERSION=0.9.26 +ARG AIRFLOW_PREK_VERSION="0.3.0" # UV_LINK_MODE=copy is needed since we are using cache mounted from the host ENV AIRFLOW_PIP_VERSION=${AIRFLOW_PIP_VERSION} \ @@ -1718,10 +1772,12 @@ RUN chmod a+x /entrypoint /entrypoint-exec # Install autocomplete for airflow and kubectl +# hadolint ignore=SC2028 RUN if command -v airflow; then \ register-python-argcomplete airflow >> ~/.bashrc ; \ fi; \ - echo "source /etc/bash_completion" >> ~/.bashrc + echo "source /etc/bash_completion" >> ~/.bashrc ; \ + echo 'export PS1="\[\033[1;36m\][Breeze:\$(python --version 2>&1 | cut -d\" \" -f2)]\[\033[0m\] \[\033[1;32m\]\u@\h\[\033[0m\]:\[\033[1;34m\]\w\[\033[0m\]\$ "' >> ~/.bashrc WORKDIR ${AIRFLOW_SOURCES} diff --git a/INSTALL b/INSTALL index 7d4cc066d983e..0a907c6e6a6bf 100644 --- a/INSTALL +++ b/INSTALL @@ -47,6 +47,21 @@ you get all sources in one place. This is the most convenient way to develop Air Otherwise, you have to install Airflow and Providers separately from sources in the same environment, which is not as convenient. + +Content of the source archive +----------------------------- + +The archive contains a complete snapshot of the whole "apache-airflow" repository, including all +distributions that can be built from the sources: `apache-airflow` meta-distribution (that you can use to install +all other distributions, `apache-airflow-core`, `apache-airflow-task-sdk`, `apache-airflow-ctl`, +`apache-airflow-go-sdk`, more than 90 `apache-airflow-providers-` distributions, and +all the other distributions that are part of the monorepo and are needed to build other packages, +their documentation and also allows to run tests for all those distributions. + +We are using `uv` and workspace tooling to build and manage the packages together, see below for more details. +Whatever distribution you choose to install, you need to localise the right `pyproject.toml` file in the +repository and this is the one that you should use to build the distribution you need.` + Using ``uv`` to manage your Python, virtualenvs, and install airflow for development (recommended) ================================================================================================== @@ -62,6 +77,7 @@ Installing ``uv`` You can install uv following the instructions: https://docs.astral.sh/uv/getting-started/installation/ + Using ``uv`` to manage your project dependencies ------------------------------------------------ @@ -107,8 +123,8 @@ You can run any command in the virtual environment created by `uv` by prefixing This will automatically synchronize your dependencies to latest dependencies needed. -Compiling front-end assets --------------------------- +Compiling front-end assets for Airflow Core +------------------------------------------- In order to see UI in Airflow, you need to compile front-end assets first. @@ -142,12 +158,25 @@ it in the UI) that should contain the git commit hash of the build and it will g The result of this command is airflow sdist package built in the `dist` folder of `airflow-core` package as well. It requires ``prek`` to be installed in your system. +There are also similar ``prek`` commands for other packages in the repository - for example: + +``` +compile-edge-assets -- Compile Edge provider assets +compile-fab-assets -- Compile FAB provider assets +``` + +However, the compiled, generated assets for those are checked in the repository and you do not need to +compile them manually before building the packages - you only need to do it when you modify the original +UI files for those packages. + Using pip and manually managing your virtualenv =============================================== -While `uv` manages dependencies and venv automatically you might want to manage both manually with -pip and virtualenv. You need to have Python installed in your preferred way for that to work. It is also -way slower than with `uv` and you need to manage your environment manually. +While `uv` manages dependencies and venv automatically and manage workspace automatically, you might want +to manage both manually with pip and virtualenv. You need to have Python installed in your preferred +way for that to work. It is also way slower than with `uv` and you need to manage your environment manually +and sometimes install several distributions together to make tests and documentation work - emulating what +workspace tooling does automatically. Creating virtualenv ------------------- @@ -193,16 +222,19 @@ It's important to keep your hatch up to date. You can do this by running: uv tool upgrade hatch -Using Hatch to build your packages ----------------------------------- +Using Hatch to build packages +----------------------------- You can use Hatch to build installable packages from the Airflow sources. Such package will include all metadata configured in `pyproject.toml` and will be installable with ``pip`` and and any other PEP-compliant packaging front-end. You can run those commands in: -* root folder of the repository to build "meta" airflow package that install other packages -* `airflow-core` folder to build the core airflow package -* any of the `providers` folders that has a pyproject.toml file to build the provider package +* root folder of the repository to build "meta" airflow distribution that install other distribution +* `airflow-core` folder to build the core airflow distribution +* any of the `providers` folders that has a pyproject.toml file to build the provider distribution +* task-sdk to build the task-sdk distribution +* airflow-ctl to build the airflow-ctl distribution +* task-go-sdk to build the task-go-sdk distribution The packages will have pre-installed dependencies for providers that are available when Airflow is i onstalled from PyPI. Both `wheel` and `sdist` packages are built by default. @@ -214,11 +246,13 @@ You can also build only `wheel` or `sdist` packages: hatch build -t wheel hatch build -t sdist -In the `airflow-core` folder, you can also build the package with the `custom` target that will clean -the build directory, update the `git_version` file, and build the assets: +In the `airflow-core` folder, you should also build the package with the `custom` target that will clean +the build directory, update the `git_version` file, and build the assets (in case you have not built +them already manually with `prek`): hatch build -t custom -t wheel -t sdist + Installing recommended version of dependencies ============================================== @@ -231,15 +265,15 @@ that are used in main CI tests and by other contributors. There are different constraint files for different Python versions. For example, this command will install all basic devel requirements and requirements of Google provider as last successfully tested for Python 3.10: - uv pip install -e ".[devel,google]"" \ + pip install -e ".[devel,google]"" \ --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-main/constraints-3.10.txt" Using the 'constraints-no-providers' constraint files, you can upgrade Airflow without paying attention to the provider's dependencies. This allows you to keep installed provider dependencies and install the latest supported ones using pure Airflow core. - uv pip install -e ".[devel]" \ + pip install -e ".[devel]" \ --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-main/constraints-no-providers-3.10.txt" -Note that you can also use `pip install` if you do not use `uv`. +Note that you can also use `uv pip install` if you use `uv`. Airflow extras ============== diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000..e02aab0589f0d --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache Airflow +Copyright 2016-2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md index 1d9e08a763e97..6ee4a01d5c48c 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,20 @@ # Apache Airflow -| Badges | | -|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| License | [![License](https://img.shields.io/:license-Apache%202-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.txt) | -| PyPI | [![PyPI version](https://badge.fury.io/py/apache-airflow.svg)](https://badge.fury.io/py/apache-airflow) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/apache-airflow.svg)](https://pypi.org/project/apache-airflow/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/apache-airflow)](https://pypi.org/project/apache-airflow/) | -| Containers | [![Docker Pulls](https://img.shields.io/docker/pulls/apache/airflow.svg)](https://hub.docker.com/r/apache/airflow) [![Docker Stars](https://img.shields.io/docker/stars/apache/airflow.svg)](https://hub.docker.com/r/apache/airflow) [![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/apache-airflow)](https://artifacthub.io/packages/search?repo=apache-airflow) | -| Community | [![Contributors](https://img.shields.io/github/contributors/apache/airflow)](https://github.com/apache/airflow/graphs/contributors) [![Slack Status](https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&style=social)](https://s.apache.org/airflow-slack) ![Commit Activity](https://img.shields.io/github/commit-activity/m/apache/airflow) [![OSSRank](https://shields.io/endpoint?url=https://ossrank.com/shield/6)](https://ossrank.com/p/6) | +| Category | Badges | +|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| License | [![License](https://img.shields.io/:license-Apache%202-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.txt) | +| PyPI | [![PyPI version](https://badge.fury.io/py/apache-airflow.svg)](https://badge.fury.io/py/apache-airflow) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/apache-airflow.svg)](https://pypi.org/project/apache-airflow/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/apache-airflow)](https://pypi.org/project/apache-airflow/) | +| Containers | [![Docker Pulls](https://img.shields.io/docker/pulls/apache/airflow.svg)](https://hub.docker.com/r/apache/airflow) [![Docker Stars](https://img.shields.io/docker/stars/apache/airflow.svg)](https://hub.docker.com/r/apache/airflow) [![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/apache-airflow)](https://artifacthub.io/packages/search?repo=apache-airflow) | +| Community | [![Contributors](https://img.shields.io/github/contributors/apache/airflow)](https://github.com/apache/airflow/graphs/contributors) [![Slack Status](https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&style=social)](https://s.apache.org/airflow-slack) ![Commit Activity](https://img.shields.io/github/commit-activity/m/apache/airflow) [![LFX Health Score](https://insights.linuxfoundation.org/api/badge/health-score?project=apache-airflow)](https://insights.linuxfoundation.org/project/apache-airflow) | +| Dev tools | [![prek](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/assets/badge/v0.json)](https://github.com/j178/prek) | - -| Version | Build Status | -|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Main | [![GitHub Build main](https://github.com/apache/airflow/actions/workflows/ci-amd.yml/badge.svg)](https://github.com/apache/airflow/actions) [![GitHub Build main](https://github.com/apache/airflow/actions/workflows/ci-arm.yml/badge.svg)](https://github.com/apache/airflow/actions) | -| 3.x | [![GitHub Build 3.0](https://github.com/apache/airflow/actions/workflows/ci-amd.yml/badge.svg?branch=v3-0-test)](https://github.com/apache/airflow/actions) [![GitHub Build 3.0](https://github.com/apache/airflow/actions/workflows/ci-arm.yml/badge.svg?branch=v3-0-test)](https://github.com/apache/airflow/actions) | -| 2.x | [![GitHub Build 2.11](https://github.com/apache/airflow/actions/workflows/ci.yml/badge.svg?branch=v2-11-test)](https://github.com/apache/airflow/actions) | +| Version | Build Status | +|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Main | [![GitHub Build main](https://github.com/apache/airflow/actions/workflows/ci-amd-arm.yml/badge.svg)](https://github.com/apache/airflow/actions) | +| 3.x | [![GitHub Build 3.1](https://github.com/apache/airflow/actions/workflows/ci-amd-arm.yml/badge.svg?branch=v3-1-test)](https://github.com/apache/airflow/actions) | +| 2.x | [![GitHub Build 2.11](https://github.com/apache/airflow/actions/workflows/ci.yml/badge.svg?branch=v2-11-test)](https://github.com/apache/airflow/actions) | @@ -48,7 +48,7 @@ When workflows are defined as code, they become more maintainable, versionable, testable, and collaborative. -Use Airflow to author workflows (Dags) that orchestrate tasks. The Airflow scheduler executes your tasks on an array of workers while following the specified dependencies. Rich command line utilities make performing complex surgeries on DAGs a snap. The rich user interface makes it easy to visualize pipelines running in production, monitor progress, and troubleshoot issues when needed. +Use Airflow to author workflows (Dags) that orchestrate tasks. The Airflow scheduler executes your tasks on an array of workers while following the specified dependencies. Rich command line utilities make performing complex surgeries on Dags a snap. The rich user interface makes it easy to visualize pipelines running in production, monitor progress, and troubleshoot issues when needed. @@ -82,7 +82,7 @@ Use Airflow to author workflows (Dags) that orchestrate tasks. The Airflow sched ## Project Focus -Airflow works best with workflows that are mostly static and slowly changing. When the DAG structure is similar from one run to the next, it clarifies the unit of work and continuity. Other similar projects include [Luigi](https://github.com/spotify/luigi), [Oozie](https://oozie.apache.org/) and [Azkaban](https://azkaban.github.io/). +Airflow works best with workflows that are mostly static and slowly changing. When the Dag structure is similar from one run to the next, it clarifies the unit of work and continuity. Other similar projects include [Luigi](https://github.com/spotify/luigi), [Oozie](https://oozie.apache.org/) and [Azkaban](https://azkaban.github.io/). Airflow is commonly used to process data, but has the opinion that tasks should ideally be idempotent (i.e., results of the task will be the same, and will not create duplicated data in a destination system), and should not pass large quantities of data from one task to the next (though tasks can pass metadata using Airflow's [XCom feature](https://airflow.apache.org/docs/apache-airflow/stable/concepts/xcoms.html)). For high-volume, data-intensive tasks, a best practice is to delegate to external services specializing in that type of work. @@ -99,14 +99,14 @@ Airflow is not a streaming solution, but it is often used to process real-time d Apache Airflow is tested with: -| | Main version (dev) | Stable version (3.0.6) | -|------------|------------------------|------------------------| -| Python | 3.10, 3.11, 3.12, 3.13 | 3.9, 3.10, 3.11, 3.12 | -| Platform | AMD64/ARM64(\*) | AMD64/ARM64(\*) | -| Kubernetes | 1.30, 1.31, 1.32, 1.33 | 1.30, 1.31, 1.32, 1.33 | -| PostgreSQL | 13, 14, 15, 16, 17 | 13, 14, 15, 16, 17 | -| MySQL | 8.0, 8.4, Innovation | 8.0, 8.4, Innovation | -| SQLite | 3.15.0+ | 3.15.0+ | +| | Main version (dev) | Stable version (3.1.6) | +|------------|------------------------------|------------------------| +| Python | 3.10, 3.11, 3.12, 3.13 | 3.10, 3.11, 3.12, 3.13 | +| Platform | AMD64/ARM64(\*) | AMD64/ARM64(\*) | +| Kubernetes | 1.30, 1.31, 1.32, 1.33, 1.34 | 1.30, 1.31, 1.32, 1.33 | +| PostgreSQL | 13, 14, 15, 16, 17 | 13, 14, 15, 16, 17 | +| MySQL | 8.0, 8.4, Innovation | 8.0, 8.4, Innovation | +| SQLite | 3.15.0+ | 3.15.0+ | \* Experimental @@ -177,15 +177,15 @@ them to the appropriate format and workflow that your tool requires. ```bash -pip install 'apache-airflow==3.0.6' \ - --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.0.6/constraints-3.10.txt" +pip install 'apache-airflow==3.1.6' \ + --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.1.6/constraints-3.10.txt" ``` 2. Installing with extras (i.e., postgres, google) ```bash -pip install 'apache-airflow[postgres,google]==3.0.6' \ - --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.0.6/constraints-3.10.txt" +pip install 'apache-airflow[postgres,google]==3.1.6' \ + --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-3.1.6/constraints-3.10.txt" ``` For information on installing provider distributions, check @@ -236,19 +236,19 @@ following the ASF Policy. ## User Interface -- **DAGs**: Overview of all DAGs in your environment. +- **Dags**: Overview of all Dags in your environment. - ![DAGs](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/dags.png) + ![Dags](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/dags.png) - **Assets**: Overview of Assets with dependencies. ![Asset Dependencies](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/assets_graph.png) -- **Grid**: Grid representation of a DAG that spans across time. +- **Grid**: Grid representation of a Dag that spans across time. ![Grid](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/grid.png) -- **Graph**: Visualization of a DAG's dependencies and their current status for a specific run. +- **Graph**: Visualization of a Dag's dependencies and their current status for a specific run. ![Graph](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/graph.png) @@ -256,11 +256,11 @@ following the ASF Policy. ![Home](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/home.png) -- **Backfill**: Backfilling a DAG for a specific date range. +- **Backfill**: Backfilling a Dag for a specific date range. ![Backfill](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/backfill.png) -- **Code**: Quick way to view source code of a DAG. +- **Code**: Quick way to view source code of a Dag. ![Code](https://raw.githubusercontent.com/apache/airflow/main/airflow-core/docs/img/ui-dark/code.png) @@ -275,7 +275,7 @@ packages: Changing limits for versions of Airflow dependencies is not a breaking change on its own. * **Airflow Providers**: SemVer rules apply to changes in the particular provider's code only. SemVer MAJOR and MINOR versions for the packages are independent of the Airflow version. - For example, `google 4.1.0` and `amazon 3.0.6` providers can happily be installed + For example, `google 4.1.0` and `amazon 3.1.1` providers can happily be installed with `Airflow 2.1.2`. If there are limits of cross-dependencies between providers and Airflow packages, they are present in providers as `install_requires` limitations. We aim to keep backwards compatibility of providers with all previously released Airflow 2 versions but @@ -299,7 +299,7 @@ Apache Airflow version life cycle: | Version | Current Patch/Minor | State | First Release | Limited Maintenance | EOL/Terminated | |-----------|-----------------------|-----------|-----------------|-----------------------|------------------| -| 3 | 3.0.6 | Supported | Apr 22, 2025 | TBD | TBD | +| 3 | 3.1.6 | Supported | Apr 22, 2025 | TBD | TBD | | 2 | 2.11.0 | Supported | Dec 17, 2020 | Oct 22, 2025 | Apr 22, 2026 | | 1.10 | 1.10.15 | EOL | Aug 27, 2018 | Dec 17, 2020 | June 17, 2021 | | 1.9 | 1.9.0 | EOL | Jan 03, 2018 | Aug 27, 2018 | Aug 27, 2018 | @@ -367,7 +367,7 @@ this image in the `main` branch. Airflow has a lot of dependencies - direct and transitive, also Airflow is both - library and application, therefore our policies to dependencies has to include both - stability of installation of application, -but also ability to install newer version of dependencies for those users who develop DAGs. We developed +but also ability to install newer version of dependencies for those users who develop Dags. We developed the approach where `constraints` are used to make sure airflow can be installed in a repeatable way, while we do not limit our users to upgrade most of the dependencies. As a result we decided not to upper-bound version of Airflow dependencies by default, unless we have good reasons to believe upper-bounding them is @@ -432,7 +432,7 @@ might decide to add additional limits (and justify them with comment). Want to help build Apache Airflow? Check out our [contributors' guide](https://github.com/apache/airflow/blob/main/contributing-docs/README.rst) for a comprehensive overview of how to contribute, including setup instructions, coding standards, and pull request guidelines. -If you can't wait to contribute, and want to get started asap, check out the [contribution quickstart](https://github.com/apache/airflow/blob/main/contributing-docs/03_contributors_quick_start.rst) here! +If you can't wait to contribute, and want to get started asap, check out the [contribution quickstart](https://github.com/apache/airflow/blob/main/contributing-docs/03a_contributors_quick_start_beginners.rst) here! Official Docker (container) images for Apache Airflow are described in [images](https://github.com/apache/airflow/blob/main/dev/breeze/doc/ci/02_images.md). diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 97310ce20cc5d..66158c85ed7fd 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -24,6 +24,971 @@ .. towncrier release notes start +Airflow 3.1.6 (2026-01-13) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +``is_authorized_hitl_task()`` method now available in auth managers(#59399). +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +This method is now available in auth managers to check whether a user is authorized to approve a HITL task + +``proxy`` and ``proxies`` added to ``DEFAULT_SENSITIVE_FIELDS`` (#59688) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +``proxy`` and ``proxies`` have been added to ``DEFAULT_SENSITIVE_FIELDS`` in secrets_masker to treat proxy configurations as sensitive by default + +Bug Fixes +^^^^^^^^^ +- Protect against hanging thread in aiosqlite 0.22+ (#60217) (#60245) +- Fix log task instance sqlalchemy join query (#59973) (#60222) +- Fix invalid uri created when extras contains non string elements (#59339) (#60219) +- Fix operator template fields via callable serialization that causes unstable DAG serialization (#60065) (#60221) +- Fix real-time extra links updates for TriggerDagRunOperator (#59507) (#60225) +- Fix signal handling in triggerer job runner (#60190) (#60214) +- Added state validation to delete dag run endpoint (#60195) (#60207) +- Fix text overflow issue (#60080) +- UI: Add toggle functionality to Dags state filters (#59089) +- Fix ``deprecated_options`` entry for ``dag_file_processor_timeout`` (#59181) (#60162) +- Fix ``ApprovalOperator`` with ``SimpleAuthManager`` when ``all_admins=True`` (#59399) (#60116) +- Record missing ``ti_failure`` metrics for tasks (#59731) (#59964) +- Fix missing ``TaskInstanceHistory`` on scheduler ``TI`` resets (#59639) (#59752) +- Add ``proxy`` and ``proxies`` as sensitive fields in ``DEFAULT_SENSITIVE_FIELDS`` (#59688) (#59792) +- Fix compat deprecation handling for ``[webserver] base_url`` (#59659) (#59781) +- Fix Execution API refresh token (#58782) (#59713) +- Fix eager-loading DagRun asset relationships before creating ``DagRunContext`` (#59714) (#59732) +- Redact secrets in rendered templates properly when truncating it (#59566) (#59704) +- Add ``Content-Type`` to request headers in Task SDK calls when missing (#59676) (#59687) +- UI: Fix Expand+Collapse Translation Key (#59672) (#59674) +- Fix server context for connections (#59624) (#59652) +- Fix clear task instance dialog tasks states (#59363) (#59580) +- Add log record when listening dag is partitioned but run has no key (#59375) (#59582) +- Fix Dag Processor logging crash (#59317) (#59581) +- Flush session before processing Event Buffer in dag test (#59314) (#59559) +- Add task group ID filtering support to task instance query (#58092) (#59511) +- Fix message of ``_read_from_logs_server`` when status_code is 403 (#59489) (#59504) +- Fix import errors not cleared for files without Dags (#58242) (#59500) +- Fix backfill ``run_on_latest_version`` defaulting to False instead of True (#59304) (#59328) +- Add toaster notifications for Connection Test (#59354) (#59368) +- Fix ``.airflowignore`` negation not working in subfolders (#58740) (#59305) +- Fix XCom key handling when keys contain special characters like slash (#58344) (#59311) +- Fix an odd import of pendulum from sqlalchemy_utils instead of elsewhere. (#59258) (#59265) +- Fix links for DurationChart (#59095) (#59237) +- Fix misleading error message when GitHook creation fails (#59236) +- Show asset extra in asset list (#59195) (#59201) +- Prevent dag processor crash on encountering excel files in the Dag directory (#59069) (#59170) +- Fix ``DagRun.queued_at`` not updating when clearing (#59066) (#59177) +- Fix Rendered Templates not showing dictionary items (#58071) (#59176) +- UI: Change task log source display to hidden by default (#58749) (#59045) +- Fix button to go back from FAB iframe (#58997) (#59007) +- Fix task instance and run tooltips in Grid view (#58359) (#59013) + +Miscellaneous +^^^^^^^^^^^^^ +- Don't depend upon FastAPI inside Task-SDK client (#59250) (#59257) +- Align the term Dag in all translations (#59155) + +Doc Only Changes +^^^^^^^^^^^^^^^^ +- Bump Sphinx Airflow theme to ``0.3.0`` (#59538) +- Translations updates [French: (#60157) (#60167), German: (#59673), PL: (#59675) (#59251) (#59256), Japanese: (#59557),(#59313), + Taiwanese Mandarin (#59513) (#59515), Hebrew: (#59133) (#59255), Ca: (#59216) (#60199), TR: (#59169) (#60191)] +- Update webserver probe health check doc (#59942) (#59982) +- Update API auth. instructions in Docker running docs (#59830) (#59832) +- Improve CLI date argument help text documentation (#59797) (#59810) +- Add fast client-side search to Airflow documentation (#59658) +- Fix broken ``permalink`` icon (#58763) +- Add Refresh Token logic to auth manager docs (#54196) (#59482) +- Update json to JSON for consistency in translations (#59323) (#59333) +- Fix outdated dependency documentation (#58970) (#59219) +- Add UI/API performance tips (#59004) (#59052) +- Provide a clear naming and description for the attribute caching ``get_template_context`` (#59023) (#59036) +- Update the documentation for the LocalExecutor (#58990) (#59022) + + + +Airflow 3.1.5 (2025-12-12) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +No significant changes. + +Bug Fixes +^^^^^^^^^ + +- Handle invalid token in JWTRefreshMiddleware (#56904) +- Fix inconsistent Dag hashes when template fields contain unordered dicts (#59091) (#59175) +- Fix assets used only as inlets being incorrectly orphaned (#58986) +- Fix exception when logging stdout with a custom %-format string (#58963) +- Fix backfill max_active_runs race condition with concurrent schedulers (#58935) +- Fix LocalExecutor memory spike by applying ``gc.freeze`` (#58934) +- Fix string to datetime pydantic conversion (#58916) +- Fix deadlines being incorrectly pruned for DAG runs with the same run_id (#58910) +- Fix handling of ``pre-AIP-39`` DAG runs (#58773) +- Mask secrets properly when using deprecated import path (#58726) +- Preserve Asset.extra when using AssetAlias (#58712) +- Fix timeout_after in run_trigger method of TriggerRunner (#58703) +- Fix connection retrieval from secrets backend without conn_type (#58664) +- Fix task retry logic to respect retries for all exit codes (#58478) +- Respect default_args in DAG when set to a "falsy" value (#58396) +- Fix airflow config list output for multi-line values (#58378) +- Fix TriggerDagRunOperator stuck in deferred state with reset_dag_run=True (#58333) +- Fix HITLTrigger params serialization (#58297) +- Fix atomicity issue in SerializedDagModel.write_dag preventing orphaned DAG versions (#58281) +- Mask kwargs when illegal arguments are passed (#58283) +- Fix supervisor communications not reconnecting when using ``dag.test()`` (#58266) +- Fix supervisor communications and logs not reconnecting in task subprocesses (#58263) +- Make pool description optional when patching pools (#58169) +- Fix check_files.py script after source tarball was renamed (#58192) +- Fix db cleanup logging behavior and docstrings (#58523) +- Fix Asset URI normalization for user info without password (#58485) +- UI: Fix object rendering in Human-in-the-Loop (HITL) interface (#58611) +- UI: Fix "Consuming Tasks" section not in asset header (#58060) +- UI: Fix timezone string parsing to use ``dayjs`` correctly (#57880) +- UI: Ensure task instance ``endDate`` is not null (#58435) +- UI: Fix trigger parameter field showing as dict when param.value is null (#58899) +- UI: Remove unnecessary refresh state consumption for DAG header (#58692) +- UI: Fix mobile responsiveness of Dashboard sections (#58853) +- UI: Fix incorrect backfill duration calculation in Grid view (#58816) +- UI: Redact secrets in rendered templates to not expose them in UI (#58772) +- UI: Add fallback value of 1 for number of DAG runs in Grid view (#58735) +- UI: Update refresh token flow (#58649) +- UI: Fix 404 handling with fallback route for invalid URLs (#58629) +- UI: Fix excessive database queries in UI grid endpoint by adding query count guard (#57977, #58632) +- UI: Fix DAG documentation markdown display issue (#58627) +- UI: Fix duration chart duration format (#58564) +- UI: Fix TaskGroup nodes not being properly highlighted when selected in Graph view (#58559) +- UI: Fix tag filter with special characters (#58558) +- UI: Fix group task instance tab memory leak (#58557) +- UI: Fix popup automatically closing when DAG run completes (#58538) +- UI: Fix operator extra links not appearing on failed tasks (#58508) +- UI: Fix TypeError in ``parseStreamingLogContent`` for non-string data (#58399) +- UI: Fix Dag tag order (#58904) + +Miscellaneous +^^^^^^^^^^^^^ + +- Do not remove ``.pyc`` and ``.pyo`` files after building Python (#58947) +- Improve cross-distribution dependency management (#58472) +- Bump glob from 10.4.5 to 10.5.0 in simple auth manager UI (#58463) +- Bump glob in React core UI (#58461) + +Doc Only Changes +^^^^^^^^^^^^^^^^ +- Fix Chinese (Traditional) translations for trigger-related terminology (#58989) +- Close translation gaps in German (#58971) +- Add missing Polish translations (#58939) +- Clarify that Connection extra JSON masking is keyword-dependent (#58587) +- Add migration guide for Airflow 2 users accessing database in tasks (#57479) +- Update UIAlert import path and usage for v3 (#58891) +- Add clarifying documentation for TaskGroup parameters (#58880) +- Enhance asset extra field documentation (#58830) +- Update mask_secret documentation to use the latest import path (#58534) +- Improve disable_bundle_versioning configuration documentation (#58405) +- Fix documentation for installing from sources (#58373) +- Fix broken link on installing-from-sources page (#58324) +- Add missing DAG run table translations (#58572) + + + +Airflow 3.1.3 (2025-11-13) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +Fix Connection & Variable access in API server contexts (plugins, log handlers)(#56583) +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +Previously, hooks used in API server contexts (plugins, middlewares, log handlers) would fail with an ``ImportError`` +for ``SUPERVISOR_COMMS``, because ``SUPERVISOR_COMMS`` only exists in task runner child processes. + +This has been fixed by implementing automatic context detection with three separate secrets backend chains: + +**Context Detection:** + +1. **Client contexts** (task runner in worker): Detected via ``SUPERVISOR_COMMS`` presence +2. **Server contexts** (API server, scheduler): Explicitly marked with ``_AIRFLOW_PROCESS_CONTEXT=server`` environment variable +3. **Fallback contexts** (supervisor, unknown contexts): Neither marker present, uses minimal safe chain + +**Backend Chains:** + +- **Client**: ``EnvironmentVariablesBackend`` → ``ExecutionAPISecretsBackend`` (routes to Execution API via SUPERVISOR_COMMS) +- **Server**: ``EnvironmentVariablesBackend`` → ``MetastoreBackend`` (direct database access) +- **Fallback**: ``EnvironmentVariablesBackend`` only (+ external backends from config like AWS Secrets Manager, Vault) + +The fallback chain is crucial for supervisor processes (worker-side, before task runner starts) which need to access +external secrets for remote logging setup but should not use ``MetastoreBackend`` (to maintain worker isolation). + +**Architecture Benefits:** + +- Workers (supervisor + task runner) never use ``MetastoreBackend``, maintaining strict isolation +- External secrets backends (AWS Secrets Manager, Vault, etc.) work in all three contexts +- Supervisor falls back to Execution API client for connections not found in external backends +- API server and scheduler have direct database access for optimal performance + +**Impact:** + +- Hooks like ``GCSHook``, ``S3Hook`` now work correctly in log handlers and plugins +- No code changes required for existing plugins or hooks +- Workers remain isolated from direct database access (network-level DB blocking fully supported) +- External secrets work everywhere (workers, supervisor, API server) +- Robust handling of unknown contexts with safe minimal chain + +See: `#56120 `__, `#56583 `__, `#51816 `__ + +Remove insecure dag reports API endpoint that executed user code in API server (#56609) +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + + The ``/api/v2/dagReports`` endpoint has been removed because it loaded user DAG files directly in the API server process, + violating Airflow's security architecture. This endpoint was not used in the UI and had no known consumers. + Use the ``airflow dags report`` CLI command instead for DAG loading reports. + +Bug Fixes +^^^^^^^^^ +- Fix HITL tasks not properly validating params (#57547) (#58144) +- Fix secrets being exposed in Jinja template rendering error messages (#57467) (#57962) +- UI: Fix slow loading on next run assets page (#58052) (#58064) +- Fix logout not working in airflow-core (#57990) (#58043) +- Fix slow loading on UI [(#57820) (#57856), (#57956) (#57973), (#57957) (#57972),(#57869) (#57882), (#57868) (#57918),(#57624) (#57757)] +- UI: Fix log download to include .txt file extension (#57991) (#58040) +- Fix scheduler using incorrect max_active_runs value from cached DAG (#57619) (#57959) +- Fix database migration failures when XCom contains NaN values (#57866) (#57893) +- Fix incorrect task context in trigger rule scenarios (#57884) (#57892) +- UI: Fix test connection not working (#57811) (#57852) +- Fix worker ``healthcheck`` timeout not respecting worker-timeout CLI option (#57731) (#57854) +- Fix provider hooks not loading when FAB provider is not installed (#57717) (#57830) +- Fix slow API responses for task instances list [(#57645) (#57794), (#57646) (#57664),(#57500) (#57735), (#57549) (#57738), (#57450) (#57736),(#57647) (#57732)] +- Fix task instance errors when tasks are triggered by trigger rules (#57474) (#57786) +- Fix type consistency for extra field in Asset, AssetAlias, and AssetEvent (#57352) (#57728) +- Fix upgrade failures when XCom contains NaN in string values (#57614) + +Miscellaneous +^^^^^^^^^^^^^ + +- UI: Add resize functionality to DAG run and task instance notes (#57897) (#58068) +- Add Taiwan translation for UI (#58121) +- UI: Shorten German translation of Asset in navigation (#57671) (#57690) +- Fix code formatting via ruff preview (#57641) (#57670) +- Remove remnants from unlimited parallelism in local executor (#57579) (#57644) + +Doc Only Changes +^^^^^^^^^^^^^^^^ + +- Add learning from Airflow 3 migration guide (#57989) (#58083) +- Fix duplicate mention of 'DAGs' and 'tasks' in overview documentation (#57524) (#57793) +- Document asset event extra storage behavior (#57727) (#57734) + + +Airflow 3.1.2 (2025-11-05) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +No significant changes. + +Bug Fixes +^^^^^^^^^ + +- Fix import error when upgrading structlog to 25.5.0+ (#57335) +- Fix connection retrieval in ``DagProcessorManager`` for bundle initialization (#57459) +- Fix incorrect task instance counts displayed in task group headers (#55670) +- Fix task retry execution after tasks are killed by external signals (#55767) +- Fix triggerer errors after Airflow 2 to 3 migration (#55884) +- Fix tasks unable to access ``triggering_user_name`` context variable (#56193) +- Fix outlet event extra data being empty in task instance success listener callbacks (#57031) +- UI: Fix panel button spacing and alignment issues (#57062) +- UI: Fix broken grid view links for tasks with retries (#57097) +- Fix DAG processor crash when renaming DAG tag case on MySQL (#57113) +- Fix iteration errors when using ``ObjectStoragePath`` (#57156) +- Fix auto-refresh not working on Required Actions page (#57207) +- Fix DAG processor crash by ignoring callbacks from other bundles (#57330) +- Fix asset name text overflow in DAG list view (#57363) +- Fix memory leak caused by repeated SSL context creation in API client (#57374) +- Fix performance issues loading DAG list page with many DAGs (#57444) +- Fix text selection jumping unexpectedly in log viewer (#57453) +- Fix DAG documentation pane not scroll-able when content is too long (#57518) +- Fix incorrect macro listings in template reference documentation (#57529) +- Fix Human-In-The-Loop operators failing when using notifiers (#57551) +- Fix n+1 query issues in XCom API endpoints (#57554) +- Fix n+1 query issues in Event Logs API endpoint (#57558) +- Fix n+1 query to fetch tags in the dags list page (#57570) +- Optimize database query to prevent "Out of sort memory" errors with many DAG versions (#57042) +- Optimize DAG list query for users with limited access (#57460) +- Optimize dynamic DAG updates to avoid loading large serialized DAGs (#57592) +- Reduce serialized DAG size by optimizing callback serialization in ``default_args`` (#57397) + +Miscellaneous +^^^^^^^^^^^^^ + +- UI: Improve global navigation visual design, interaction, and accessibility (#57565) +- UI: Add button to download all task logs at once (#56771) +- UI: Add timestamp column to ``XCom`` viewer and standardize task instance columns (#57447) +- UI: Improve highlighting of selected task instances and edges in grid view (#57560) +- Improve retry logic by migrating from ``retryhttp`` to ``tenacity`` library (#56762) +- Improve exception logging for task instance heartbeat failures (#57179) +- Add ``Content-Type`` header to Task SDK API requests (#57386) +- Log execution API server URL at task startup (#57409) +- Reduce log noise by changing "Connection not found" from error to debug level (#57548) +- Add ``task_display_name`` alias in event log API responses (#57609) +- Improve Pydantic model validation strictness in serialization (#57616) +- Fix systemd service files issues (#57231) + +Doc Only Changes +^^^^^^^^^^^^^^^^ +- Improve plugin system documentation for clarity and completeness (#57068) +- Improve clarity on api workers recommendation in docs (#57404) +- Fix ``instance_name`` in UI docs (#57523) +- Fix airflow macros list in template document (#57529) + +Airflow 3.1.1 (2025-10-27) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +No significant changes. + +Bug Fixes +^^^^^^^^^ + +- Fix execution failures with NULL ``dag_run.conf`` during upgrades from earlier versions (#56729) +- Fix memory leak in remote logging connection cache (#56695) +- Fix DAG processor crash with pre-import module optimization enabled (#56779) +- Fix scheduler crash with email notifications (#56431) +- Fix scheduler crash during 3.0 to 3.1 migration when ``retry_delay`` is ``None`` (#56236) +- Fix task retries executing wrong method after deferred state (#56737) +- Fix retry callbacks not executing for externally killed tasks (#56607) +- Fix custom timetable ``generate_run_id`` not called for manual triggers (#56699) +- Fix ``KeyError`` when accessing ``retry_delay`` on ``MappedOperator`` without explicit value (#56605) +- Fix ``task-sdk`` connection error handling to match ``airflow-core`` behavior (#56653) +- Fix topological sort for Grid View (#56963) +- Fix ``get_ti_count`` and ``get_task_states`` access in callback requests (#56860) +- Fix ``Connection`` or ``Variable`` access in Server context (#56602) +- Fix ``.airflowignore`` order precedence (#56832) +- Fix migration errors for Pydantic 2.12.0 compatibility (#56581) +- Fix: Correctly parse JSON for ``--dag_run_conf`` in ``airflow dags backfill`` CLI (#56599) +- UI: Fix note modal does not change markdown text after change (#56092) +- UI: Fix Grid for cleared runs when tasks were removed (#56297) +- UI: Fix log text selection contrast in light mode (#56893) +- UI: Fix Advanced Search button overlap in DAG List View (#56777) +- UI: Fix view for many DAG tags (#55604) +- UI: Fix asset name text overflow in DAGs list view (#55914) +- UI: Fix auto refresh when only 1 dag run is running (#56649) +- UI: Fix UI keeps poking pools API when no permission (#56626) +- UI: Fix multi-line drag selection in task log view (#56300) +- UI: Fix task named ``'root'`` causes blue screen on hover (#56926) +- UI: Fix cron expression display for ``Day-of-Month`` and ``Day-of-Week`` conflicts (#56255) +- UI: Fix Grid view performance issues with ``SerializedDagModel`` query optimization (#56938) +- Fix: Gracefully handle FastAPI plugins with empty ``url_prefix`` (#55262) +- Fix: Allow mapped tasks to accept zero-length inputs on rerun (#56162) +- Fix: Enable API to clear task instances by specifying map indexes (#56945) +- Fix: Add ``max_retry_delay`` to ``MappedOperator`` model (#56951) +- Fix: Use name passed to ``@asset`` decorator when fetching the asset (#56611) +- UI: Add English as a fallback locale (#56934) + +Miscellaneous +^^^^^^^^^^^^^ + +- Add Greek UI translation (#56724) +- Add Thai UI translation (#56946) +- Add Polish translations (#56825) +- Close German translation gaps for full UI translation (#56981) +- Fix Hebrew typo in translations (#56168) +- Improve DAG and task missing error handling in callbacks (#56725) +- Improve UI retry strategy on client errors (#56638) +- Improve get dag grid structure endpoint speed (#56937) +- Optimize grid structure query with ``DISTINCT`` for ``dag_version_id`` lookup (#56565) +- Add configurable timeout for Execution API requests (#56969) +- Prevent unnecessary kubernetes client imports in workers (#56692) +- Lazy import ``PodGenerator`` for deserialization (#56733) +- Serialize pydantic models in json mode for JSON serialization compatibility (#56939) +- Update authentication to handle JWT token in backend (#56677) +- Update bulk API permission check to handle ``action_on_existence`` (#56672) +- Migrate ``CreateAssetEventsBody`` to Pydantic v2 ``ConfigDict`` (#56772) +- Restore timetable ``active_runs_limit`` check (#56922) +- Add ``is_favorite`` to UI dags list (#56341) +- Add ``executor``, ``hostname``, and ``queue`` columns to ``TaskInstances`` page (#55922) +- Add resize function for DAG Documentation (#56344) +- Add optional pending dag runs check to auto refresh (#56648) +- Add auto refresh to backfill banner (#56774) +- UI: Add Expand/Collapse all to ``XComs`` page (#56285) +- UI: Update recent runs bar chart and improve responsiveness (#56314) +- UI: Update duration format to show milliseconds (#56961) +- UI: Modify min width for task names in grid view (#56952) +- UI: Use Task Display Name in Graph if existing (#56511) +- UI: Use Task Display Name in Grid if existing (#56410) +- UI: Use TI duration from database instead of UI calculated (#56329) +- UI: Make DAG Run ID visible in DAG Header Card (#56409) +- UI: Modify calendar cell colors (#56161) +- UI: Modify log highlight color (#56894) +- UI: Fix show appropriate time units in grid view (#56414) +- UI: Reduce default columns of DAG Run and Task Instance lists (#55968) +- UI: Add expand and collapse functionality for task groups (#56334) +- UI: Avoid using rem for icons for Safari compatibility (#56304) +- UI: Add ANSI support to log viewer (#56721) +- UI: Support Dynamic UI Alerts (#56259) +- UI: Disable Gantt view by default (#56242) +- UI: Use welcome on dashboard instead of airflow (#56074) +- UI: Improve clipboard button visibility with hover effect (#56484) +- UI: Allow sub-pages in React UI plugins (#56485) +- Include task instance id in log printed by supervisor (#56383) +- Emit log stream stopped warning as ``ndjson`` (#56480) +- Detect interactive terminal to set colored logging with override env variable support (#56157) +- Add back deprecation warning for ``sla_miss_callback`` (#56127) +- Move ``natsort`` dependency to ``airflow-core`` (#56582) +- Added missing ``babel`` dependency in Task SDK (#56592) +- Remove unused ``dagReports`` API endpoint (#56621) + +Doc Only Changes +^^^^^^^^^^^^^^^^ + +- Improve API sort documentation (#56617) +- Improve API doc for ordering query param (#55988) +- Add Audit Logs detailed documentation (#56719) +- Update serializer document to reflect latest changes in codebase (#56857) +- Update ASF logos in documentation to the new Oak logo (#56601) +- Enhance ``triggering_asset_event`` retrieval documentation in DAGs (#56957) +- Remove self-reference in best practices documentation (#56111) +- Fix supported Python versions in README (#56734) + +Airflow 3.1.0 (2025-09-25) +-------------------------- + +Significant Changes +^^^^^^^^^^^^^^^^^^^ + +Human in the Loop (HITL) +"""""""""""""""""""""""" + +Airflow 3.1 introduces :doc:`Human-in-the-Loop (HITL) ` functionality that enables +workflows to pause and wait for human decision-making. This powerful feature is particularly valuable for +AI/ML workflows, content moderation, and approval processes where human judgment is essential. + +HITL tasks pause execution in a ``deferred`` state while waiting for human input via the Airflow UI. Users +with appropriate roles can see pending tasks, review context (including ``XCom`` data and ``DAG`` parameters), and +complete actions through intuitive web forms. The feature also supports API-driven interactions for custom +UIs and notification integration. + +For detailed usage instructions, see :doc:`/tutorial/hitl`. + +**Note**: HITL operators require ``apache-airflow-providers-standard`` package and Airflow 3.1+. + +Task SDK Decoupling for Independent Upgrades +""""""""""""""""""""""""""""""""""""""""""""" + +Airflow 3.1 advances the decoupling of the Task SDK from Airflow Core through +improved DAG serialization with versioned contracts. While complete code separation is planned for Airflow 3.2.0, +the serialization foundation enables independent upgrades when components are deployed separately. + +**For DAG Authors**: Import constructs from ``airflow.sdk`` namespace: + +- ``from airflow.sdk import DAG, task, asset`` +- Access to latest authoring features with forward compatibility +- Reduced dependency on server-side Airflow versions + +**For Platform Teams**: Foundation for independent upgrades: + +- Schema compliance ensures compatibility across versions +- Deployment flexibility when components are separated +- Reduced coordination overhead between development and operations teams + +For technical details on the serialization contract, see :doc:`/administration-and-deployment/dag-serialization`. + +Deadline Alerts +""""""""""""""" + +Deadline Alerts provide proactive monitoring for DAG execution by automatically triggering notifications +when time thresholds are exceeded. This helps ensure SLA compliance and timely completion of critical workflows. + +Configure deadline monitoring by specifying: + +- **Reference point**: Choose from DAG run queued time, logical date, or fixed datetime +- **Interval**: Time threshold relative to the reference point (positive or negative) +- **Callback**: Response action using Airflow Notifiers or custom functions + +Example use cases: + +- Alert if a daily ETL hasn't completed 1 hour after its scheduled time +- Notify stakeholders 30 minutes before a critical deadline +- Escalate when resource-constrained DAGs remain queued too long + +**Current Limitations**: Deadline Alerts currently support only asynchronous callbacks (``AsyncCallback``). +Support for synchronous callbacks (``SyncCallback``) is planned for a future release. + +For configuration details and examples, see :doc:`/howto/deadline-alerts`. + +.. warning:: + + Deadline Alerts are experimental in 3.1 and may change in future versions based on user feedback. + +UI Internationalization +""""""""""""""""""""""" + +Airflow 3.1 delivers comprehensive internationalization (``i18n``) support, making the web interface +accessible to users worldwide. The React-based UI now supports 17 languages with robust translation infrastructure. + +**Supported Languages**: + +- Arabic +- Catalan +- Dutch +- English +- French +- German +- Hebrew +- Hindi +- Hungarian +- Italian +- Korean +- Polish +- Portuguese +- Simplified Chinese +- Spanish +- Traditional Chinese +- Turkish + +The translation system includes automated completeness checking and clear contribution guidelines for community translators. + +React Plugin System (AIP-68) +""""""""""""""""""""""""""""" + +Airflow 3.1 introduces a modern plugin architecture enabling rich integrations through React components and +external views. This extensibility framework allows organizations to embed custom dashboards, +monitoring tools, and domain-specific interfaces directly within the Airflow UI. + +**New Plugin Capabilities**: + +- **React Apps**: Full-featured React applications integrated into Airflow navigation +- **External Views**: Embed external web applications via iframe with seamless authentication +- **Dashboard Integration**: Custom widgets and panels for operational dashboards +- **Menu Integration**: Add custom navigation items and organize tools logically + +**Developer Experience**: + +- Hot reloading during development with ``airflow-react-plugin`` dev tools +- TypeScript support and modern React patterns +- Standardized plugin loading and validation +- Comprehensive documentation and boilerplate generation + +This plugin system replaces legacy Flask-based approaches with modern web standards, improving performance, +maintainability, and user experience. + +For more details and examples, see :doc:`/howto/custom-view-plugin`. + +Enhanced UI Views and Filtering +"""""""""""""""""""""""""""""""" + +Airflow 3.1 brings significant UI improvements including rebuilt Calendar and Gantt chart views for the modern React UI, +comprehensive filtering capabilities, and a refreshed visual design system. + +**Visual Design Improvements** + +The UI now features an updated color palette leveraging Chakra UI semantic tokens, providing better consistency, +accessibility, and theme support across the interface. This modernization improves readability and creates +a more cohesive visual experience throughout Airflow. + +**Rebuilt Views and Enhanced Filtering** + +The Calendar and Gantt views from Airflow 2.x have been rebuilt for the modern React UI, along with enhanced +filtering capabilities across all views. These improvements provide better performance and a more consistent +user experience with the rest of the modern Airflow interface. + +**DAG Dashboard Organization** + +Users can now pin and favorite DAGs for better dashboard organization, making it easier to find and prioritize +frequently used workflows. This feature is particularly valuable for teams managing large numbers of DAGs, +providing quick access to critical workflows without searching through extensive DAG lists. + +Inference Execution (Synchronous DAGs) +"""""""""""""""""""""""""""""""""""""" + +Airflow 3.1 introduces a new streaming API endpoint that allows applications to watch DAG runs until completion, +enabling more responsive integration patterns for real-time and inference workflows. + +**New Streaming Endpoint**: +The ``/dags/{dag_id}/dagRuns/{dag_run_id}/wait`` endpoint repeatedly emits JSON updates at specified intervals until the DAG run reaches a finished state. + +.. code-block:: bash + + # Watch a DAG run with 2-second polling interval, including XCom results + curl -X GET "http://localhost:8080/api/v2/dags/ml_pipeline/dagRuns/manual_2024_01_15/wait?result=inference_task" \ + -H "Accept: application/x-ndjson" + +This enables use cases like: + +- **ML Inference Monitoring**: Trigger inference DAGs and wait for completion before returning results +- **Real-time Processing**: Monitor event-driven workflows with immediate response requirements +- **API Integration**: Build responsive services that react to DAG completion without polling +- **Synchronous Workflows**: Create quasi-synchronous behavior for workflows that need immediate feedback + +New Trigger Rule: ``ALL_DONE_MIN_ONE_SUCCESS`` +"""""""""""""""""""""""""""""""""""""""""""""" + +``ALL_DONE_MIN_ONE_SUCCESS``: This rule triggers when all upstream tasks are done (success, failed) and +at least one has succeeded, filling a gap between existing trigger rules for complex workflow patterns. +Skipped upstream tasks work as usually - they skip downstream task. + +Enhanced DAG Processing Visibility +""""""""""""""""""""""""""""""""""" + +DAG parsing duration is now exposed in the UI, providing better visibility into DAG processing +performance and helping identify parsing bottlenecks. This information is displayed alongside +other DAG metadata to assist with performance optimization. + +Python 3.13 support added & 3.9 support removed +""""""""""""""""""""""""""""""""""""""""""""""" + +Support for Python 3.9 has been removed, as it has reached end-of-life. +Airflow 3.1.0 requires Python 3.10, 3.11, 3.12 or 3.13. + +Configuration Changes and Cleanup +"""""""""""""""""""""""""""""""""" + +**Webserver Configuration Reorganization** + +Several webserver configuration options have been moved to the ``api`` section for better organization: + +- ``[webserver] log_fetch_timeout_sec`` → ``[api] log_fetch_timeout_sec`` +- ``[webserver] hide_paused_dags_by_default`` → ``[api] hide_paused_dags_by_default`` +- ``[webserver] page_size`` → ``[api] page_size`` +- ``[webserver] default_wrap`` → ``[api] default_wrap`` +- ``[webserver] require_confirmation_dag_change`` → ``[api] require_confirmation_dag_change`` +- ``[webserver] auto_refresh_interval`` → ``[api] auto_refresh_interval`` + +Unused configuration options have been removed: + +- ``[webserver] instance_name_has_markup`` +- ``[webserver] warn_deployment_exposure`` + +**API Server Logging Configuration** + +The API server configuration option ``[api] access_logfile`` has been replaced with ``[api] log_config`` to align with uvicorn's logging configuration. The new option accepts a path to a logging configuration file compatible with ``logging.config.fileConfig``, providing more flexible logging configuration. + +**Security Improvement: XCom Deserialization** + +The ``enable_xcom_deserialize_support`` configuration option has been removed as a security improvement. This option previously allowed deserializing unknown objects in the API, which posed a security risk due to potential remote code execution vulnerabilities when deserializing arbitrary Python objects. + +The XCom display improvements now handle showing non-native XComs (like custom objects, Assets, datetime objects) in a human-readable way through safer methods that don't require deserializing unknown objects in the API server. This provides better user experience when viewing XCom data in the Airflow UI while eliminating the security risk. + +API Changes +""""""""""" + +**Asset API Key Rename** + +The ``consuming_dags`` key in asset API responses has been renamed to ``scheduled_dags`` to better reflect its purpose. This key contains only DAGs that use the asset in their ``schedule`` argument, not all DAGs that technically use the asset. + +Task SDK Interface Changes +"""""""""""""""""""""""""" + +**Removed Functions** + +The following functions have been removed from the task-sdk (``airflow.sdk.definitions.taskgroup``) and moved to server-side API services: + +- ``get_task_group_children_getter`` +- ``task_group_to_dict`` + +These functions are now internal to Airflow's API layer and should not be imported directly by users. + +Reduce default API server workers to 1 +"""""""""""""""""""""""""""""""""""""" + +The default number of API server workers (``[api] workers``) has been reduced from ``4`` to ``1``. + +With FastAPI, sync code runs in external thread pools, making multiple workers within a single +process less necessary. Additionally, with uvicorn's spawn behavior instead of fork, there is +no shared copy-on-write memory between workers, so horizontal scaling with multiple API server +instances is now the recommended approach for better resource utilization and fault isolation. + +A good starting point for the number of workers is to set it to the number of CPU cores available. +If you do have multiple CPU cores available for the API server, consider deploying multiple API +server instances instead of increasing the number of workers. + +Airflow now uses `structlog `_ everywhere +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +Most users should not notice the difference, but it is now possible to emit structured log key/value pairs from tasks. + +If your class subclasses ``LoggingMixin`` (which all ``BaseHook`` and ``BaseOperator`` do -- i.e. all hooks and +operators) then ``self.log`` is now a ``_. + +The advantage of using structured logging is that it is much easier to find specific information about log +message, especially when using a central store such as ``OpenSearch``/``Elastic``/``Splunk`` etc. +You don't have to make any changes, but you can now take advantage of this. + +.. code-block:: python + + # Inside a Task/Hook etc. + + # Before: + # self.log.info("Registering adapter %r", item.name) + # Now: + self.log.info("Registering adapter", name=item.name) + +This will produce a log that (in the UI) will look something like this:: + + [2025-09-16 10:36:13] INFO - Registering adapter name="adapter1" + +or in JSON (i.e. the log files on disk):: + + {"timestamp": "2025-09-16T10:36:13Z", "log_level": "info", "event": "Registering adapter", "name": "adapter1"} + +You can also use ``structlog`` loggers at the top level of modules etc, and ``stdlib`` both continue to work: + +.. code-block:: python + + import logging + import structlog + + log1 = logging.getLogger(__name__) + log2 = strcutlog.get_logger(__name__) + + log1.info("Loading something from %s", __name__) + log2.info("Loading something", source=__name__) + +(You can't add arbitrary key/value pairs to ``stdlib``, but the normal ``percent-formatter`` approaches still work fine.) + +Serialization Interface Changes +""""""""""""""""""""""""""""""" + +The deserializer interface in ``airflow.serialization.serializers`` has changed for improved security. + +**Before 3.1.0:** + +``def deserialize(classname: str, version: int, data: Any)`` + +**Starting with 3.1.0:** + +``def deserialize(cls: type, version: int, data: Any)`` + +The class loading is now handled in ``serde.py``, and the deserializer receives the loaded class directly rather than a ``classname`` string. +This update avoids the use of ``import_string`` in the deserializer, making deserialization more secure. + +New Features +^^^^^^^^^^^^ + +- Add Calendar and Gantt chart views to modern React UI with enhanced filtering (#54252, #51667) +- Add Python 3.13 support for Airflow runtime and dependencies (#46891) +- Add ``SQLAlchemy 2.0`` support with various compatibility fixes for ``Python 3.13`` (#52233, #52518, #54940) +- Add support for the ``psycopg3`` postgres driver (#52976) +- Add ability to track & display user who triggers DAG runs (#51738, #53510, #54164, #55112) +- Add toggle for log grouping in task log viewer for better organization (#51146) +- Add tag filtering improvements with Any/All selection options (#51162) +- Add comprehensive filtering for DAG runs, task instances, and audit logs (#53652, #54210, #55082) +- Add ``XCom`` browsing with filtering and improved navigation (#54049) +- Add bulk task instance actions and deletion endpoints (#50443, #50165, #50235) +- Add DAG run deletion functionality through UI (#50368) +- Add test connection button for connection validation (#51055) +- Add hyperlink support for URLs in XCom values (#54288) +- Add pool column to task instances list and improve pool integration (#51185, #51031) +- Add drag-and-drop log grouping and improved log visualization (#51146) +- Add color support for XCom JSON display (#51323) +- Add configuration column to DAG runs page (#51270) +- Add enhanced note visibility and management in task headers (#51764, #54163) +- Introduce React plugin system (AIP-68) for modern UI extensions (#52255) +- Add support for external view plugins via iframe integration (#51003, #51889) +- Add dashboard integration capabilities for custom React apps (#54131, #54144) +- Add comprehensive plugin development tools and documentation (#53643) +- Implement complete HITL operator suite (``HITLOperator``, ``ApprovalOperator``, ``HITLEntryOperator``) for human decision workflows (#52868) +- Add HITL UI integration with role-based access and form handling (#53035) +- Add HITL API endpoints with filtering and query support (#53376, #53923) +- Add HITL utility functions for generating URLs to required actions page (#54827) +- Improve HITL user experience with bug fixes, UI enhancements, and data model consistency (#55463, #55539, #55575, #55546, #55543, #55536, #55535) +- Add ordering and filtering support for HITL details endpoints (#55217) +- Add "No Response Received" required action state (#55149) +- Add operator filter for HITL task instances (#54773) +- Implement deadline alert system for proactive DAG monitoring (AIP-86) (#53951, #53903, #53201, #55086) +- Add configurable reference points and notification callbacks (#50677, #50093) +- Add deadline calculation and tracking in DAG execution lifecycle (#51638, #50925) +- Add comprehensive UI translation support for 16 languages (#51266, #51038, #51219, #50929, #50981, #51793 and more) +- Add right-to-left (RTL) layout support for Arabic and Hebrew (#51376) +- Add language selection interface and browser preference detection (#51369) +- Add translation completeness validation and automated checks (#51166, #51131) +- Add calendar data API endpoints for DAG execution visualization (#52748) +- Add endpoint to watch DAG runs until completion (#51920, #53346) +- Add DAG run ID pattern search functionality (#52437) +- Add multi-sorting capabilities for improved data navigation (#53408) +- Add bulk connection deletion API and UI (#51201) +- Add task group detail pages across DAG runs (#50412, #50309) +- Add asset event tracking with last event timestamps (#50060, #50279) +- Add ``has_import_errors`` filter to Core API GET ``/dags`` endpoint (#54563) +- Add dag_version filter to get_dag_runs endpoint (#54882) +- Add pattern search for event log endpoint (#55114) +- Add dry_run support with consistent audit log handling (#55116) +- Add utility functions for generic filter counting (#54817) +- Add keyboard navigation for Grid view interface (#51784) +- Add improved error handling for plugin import failures (#49643) +- Add plugin validation in ``/plugins`` API with warnings for invalid plugins (#55673) +- Improve accessibility for screen readers and assistive technologies with proper language detection (#55839) +- Add enhanced variable management with upsert operations (#48547) +- Add favorites/pinning support for DAG dashboard organization (#51264) +- Add system theme support with automatic OS preference detection (#52649) +- Add hotkey shortcut to toggle between Grid and Graph views (#54667) +- Add queued DAGs filter button to DAGs page (#55052) +- Add DAG parsing duration visibility in UI (#54752) +- Add owner links support in DAG Header UI for better navigation (#50627) +- Add ``dag_display_name`` aliases for improved API consistency (#50332, #50065, #50014, #49933, #49641) +- Add enhanced search capabilities with SearchParamsKeys constants (#55218) +- Add ALL_DONE_MIN_ONE_SUCCESS trigger rule for flexible task dependencies (#53959) +- Add fail_when_dag_is_paused parameter to TriggerDagRunOperator for better control (#48214) +- Add ``XCom`` validation to prevent empty keys in ``XCom.set()`` and ``XCom.get()`` operations (#46929) +- Add collapsible plugin menu when multiple plugins are present (#55265) +- Add external view plugin categories (admin, browse, docs, user) (#52737) +- Add iframe plugins integration to DAG pages (#52795) +- Add plugin error display in UI with comprehensive error handling (#49643, #49436) +- Add collapsible failed task logs to prevent React error overflow (#54377) +- Add dynamic legend system for calendar view (#55155) +- Add React UI for Edge functionality (#53563) +- Add pending actions display to DAG UI (#55041) +- Add description field for filter parameters (#54903) +- Add Catalan language support to Airflow UI (#55013) +- Add Hungarian language support to Airflow UI (#54716) +- Add map_index validation in categorize_task_instances (#54791) +- Add Grid view UX improvements (#54846) +- Add HITL UX improvements for better user experience (#54990) +- Add async support for Notifiers (AIP-86) (#53831) +- Add filtering capabilities for tasks view (#54484) +- Add asset-based filtering support to DAG API endpoint (#54263) +- Add iframe plugins to navigation (#51706) +- Add RTL (right-to-left) layout support for Arabic and Hebrew (#51376) +- Add test connection button to UI (#51055) +- Add task instance bulk actions endpoint (#50443) +- Add connection bulk deletion functionality (#51201) +- Add pool column to task instances list (#51185) +- Add ``iframe_views`` to backend plugin support (#51003) +- Add keyboard shortcuts to clear and mark state for task instances and DAG runs (#50885) +- Add deadline relationship to DAG runs and deadline model (#50925, #50093) +- Add DAG run deletion UI (#50368) +- Add task instance deletion UI and endpoint (#50235, #50165) +- Switch all airflow logging to structlog (#52651, #55434, #55431, #55638) +- Add Filter Bar to Audit Log (#55487) +- Add Filters UI for Asset View (#54640) +- Update color palette and leverage Chakra semantic tokens (#53981, #55739) +- Improve calendar view UI with enhanced tooltips and visual fixes (#55476) + +Bug Fixes +^^^^^^^^^ + +- Fix DAG list filtering to include ``QUEUED`` runs with null ``start_date`` (#52668) +- Fix XCom deletion failure for mapped task instances through bulk deletion API (#51850) +- Fix XCom deletion failure for mapped task instances (#54954) +- Fix task timeout handling within task SDK (#54089) +- Fix task instance tries API duplicate entries (#50597) +- Fix connection validation and type checking during construction (#54759) +- Fix mapped task instance index display in Task Instances tab (#55363) +- Fix Gantt chart state mismatch with Grid view (#55300) +- Fix Gantt chart status color display issues (#55296) +- Fix XCom mapping for dynamically-mapped task groups (#51556) +- Fix missing ``ti_successes`` and related metrics in Airflow 3.0 Task SDK (#55322) +- Fix bulk operation permissions for connection, pool and variable (#55278) +- Fix ``clearTaskInstances`` API: Restore ``include_past``/``future`` support on UI (#54416) +- Fix migration when XCom has NaN values (#53812) +- Fix HITL related UI schema generated by prek hooks (#55204) +- Fix consistent no-log handling for tasks with try_number=0 in API and UI (#55035) +- Fix timezone conversion in datetime trigger parameters (#54593) +- Fix audit log payload for DAG pause/unpause actions (#55091) +- Fix pushing None as an XCom value (#55080) +- Fix scheduler processing of cleared running tasks stuck in RESTARTING state (#55084) +- Fix XCom deletion failure for mapped task instances (#54954) +- Fix outgoing graph edges should exit opposite of incoming edges (#54789) +- Fix external links in Navigation buttons (#52220) +- Fix Error when viewing DAG details of a no longer configured bundle (#52086) +- Fix compatibility with new numpy and pandas versions (#52071) +- Fix connection recovery from URI when host has protocol (#51953) +- Fix last DAG run not showing on DAG listing (#51115) +- Fix task instance tries API returning duplicate entries (#50597) +- Fix Graph view vanishing and loading issues (#53886, #54756) +- Fix rendered template display formatting for better readability (#53657) +- Fix Grid view expand/collapse button functionality (#54257) +- Fix tooltip visibility and positioning issues (#53913) +- Fix grid keyboard navigation focus management (#54271) +- Fix plugin registration for invalid objects and middleware registration (#55264, #55399) +- Fix external links for plugins with undefined URL routes (#55221) +- Fix language display consistency and flag representation (#51560, #51177) +- Fix RTL layout rendering for Arabic and Hebrew interfaces (#51853) +- Fix graph export cropping when view is partial (#55012) +- Fix log viewer "Toggle Source" to hide only source fields, not all structured log fields (#55474) +- Output on stdout/stderr from within tasks is now filterable in the Sources list in the UI log view (#55508) +- Redact JWT tokens in task logs (#55499) +- Fix grid view to handle long task name (#55332) +- Allow slash characters in Variable keys similar to Airflow 2.x (#55324) +- Fix Grid cache invalidation for multi-run task operations (#55504) +- Fix Gantt chart rendering issues (#55554) +- Fix ``XCom`` access in DAG processor callbacks for notifiers (#55542) +- Fix alignment of arrows in RTL mode for right-to-left languages (#55619) +- Fix connection form extras not inferring correct type in UI (#55492) +- Fix incorrect log timestamps in UI when ``default_timezone`` is not UTC (#54431) +- Fix handling of priority_weight for DAG processor callbacks (#55436) +- Fix pointless requests from Gantt view when there is no Run ID (#55668) +- Ensure filename and ``lineno`` of logger calls are present in Task Logs (#55581) +- Fix DAG disappearing after callback execution in stale detection (#55698) +- Fix DB downgrade to Airflow 2 when fab tables exists (#55738) +- Fix UI stats endpoint causing dashboard loading issues (#55733) +- Fix unintended console output when DAG not found in ``serialized_dag`` table (#54972) +- Fix scheduler handling of orphaned tasks from Airflow 2 during upgrade (#55848) +- Fix logging format to respect existing configuration during upgrade to prevent unexpected log format changes (#55824) +- Fix Grid view crashes when DAG version information is missing (#55771) +- Fix compatibility for custom triggers migrating from Airflow 2.x that use synchronous connection calls (#55799) +- Fix DAG runs triggered from UI incorrectly marked as REST API triggers instead of UI triggers (#54650) +- Fix XCom API responses failing when encountering non-serializable objects by falling back to string representation (#55880) +- Fix asset queue display in UI showing incorrect timestamps for deleted queue events (#54652) +- Fix SQLite database migrations failing due to foreign key constraint handling (#55883) +- Fix DAG deserialization failure when using non-default weight_rule values like 'absolute' (#55906) +- Fix async connection retrieval in triggerer context preventing event loop blocking (#55812) +- Fix Airflow downgrade compatibility by handling serialized DAG format conversion from v3 to v2 (#55975) +- Fix 'All Log Levels' filter not working in task log viewer (#55851) +- Fix Grid view scrollbar overlapping issues on Firefox browser (#55960) +- Fix Gantt chart misalignment with Grid view layout (#55995) +- Fix Grid view task names being extremely collapsed and unreadable when displaying many DAG runs (#55997) +- Fix ``LocalExecutor`` race condition where tasks could start before database state was committed (#56010) + +Miscellaneous +^^^^^^^^^^^^^ + +- Move secrets masker to shared distribution for better modularity (#54449) +- Move email notifications from scheduler to DAG processor for better architecture (#55238) +- Add graph UI load optimization with latest run info endpoint (#53429) +- Optimize UI bundle size by moving translations to dynamic loading (#51735) +- Relocate Task SDK components for improved separation (#55174, #54795) +- Refactor trigger rule utilities and weight rule consolidation (#54797, #53393) +- Remove deprecated Airflow 2.x modules and legacy imports (#50482) +- Clean up unused code and improve module organization (#52176, #52173, #53031) +- Add SQLAlchemy 2.0 CI support for future compatibility (#52233) +- Improve test fixtures and SDK communication testing (#54795, #50603) +- Add translation completeness linting and validation tools (#51166) +- Upgrade to latest versions of important dependencies (#55350) +- Move webserver configuration options to API section (#50693, #50656) +- Improve DAG bundle handling and versioning support (#47592) +- Add database management CLI tools for external database operations (#50657) +- Add comprehensive HITL operator documentation and examples (#54618) +- Add guards for registering middlewares from plugins (#55399) +- Optimize Gantt group expansion with de-bouncing and deferred rendering (#55334) +- Differentiate between triggers and watchers currently running for better visibility (#55376) +- Removed unused config: ``dag_stale_not_seen_duration`` (#55601, #55684) +- Update UI's query client strategy for improved performance (#55528) +- Unify datetime format across the UI for consistency (#55572) +- Mark React Apps as Experimental for Airflow 3.1 release (#55478) +- Improve OOM error messaging for clearer task failure diagnosis (#55602) +- Display responder username for better audit trail in HITL workflows (#55509) +- The constraint file do not contain developer dependencies anymore (#53631) +- Add hyperlinks to ``dag_id`` column in DAG Runs and Task Instances pages for better navigation (#55648) +- Add responsive web design (RWD) support to Grid view (#55745) + +Doc Only Changes +^^^^^^^^^^^^^^^^ + +- Add comprehensive Human-in-the-Loop operator tutorial and examples (#54618) +- Add deadline alerts configuration and usage documentation (#53727) +- Make term Dag consistent in docs task-sdk (#55100) +- Add migration guide for upgrading from legacy SLA functionality to deadline alerts (#55743) +- Add DAG bundles triggerer limitation documentation (#55232) +- Add deadline alerts usage guides and best practices (#53727) +- Remove ``--preview`` flag from ``ruff check`` instructions for Airflow 3 upgrade path (#55516) +- Add documentation for context parameter (#55377) + Airflow 3.0.6 (2025-08-29) -------------------------- @@ -332,6 +1297,7 @@ Bug Fixes - Fix OpenAPI schema for ``get_log`` API (#50547) - Remove ``logical_date`` check when validating inlets and outlets (#51464) - Guard ``ti`` update state and set task to fail if exception encountered (#51295) +- Fix task log URL generation with various ``base_url`` formats (#55699) Miscellaneous """"""""""""" @@ -692,7 +1658,7 @@ simplify onboarding: - ``catchup_by_default`` is now set to ``False`` by default. DAGs will not automatically backfill unless explicitly configured to do so. - ``create_cron_data_intervals`` is now set to ``False`` by default. As a result, cron expressions will be interpreted using the ``CronTriggerTimetable`` instead of the legacy ``CronDataIntervalTimetable``. -- ``SimpleAuthManager`` is now the default ``auth_manager``. To continue using Flask AppBuilder-based authentication, install the ``apache-airflow-providers-flask-appbuilder`` provider and explicitly set ``auth_manager = airflow.providers.fab.auth_manager.FabAuthManager``. +- ``SimpleAuthManager`` is now the default ``auth_manager``. To continue using Flask AppBuilder-based authentication, install the ``apache-airflow-providers-fab`` provider and explicitly set ``auth_manager = airflow.providers.fab.auth_manager.FabAuthManager``. These changes represent the most significant evolution of the Airflow platform since the release of 2.0 — setting the stage for more scalable, event-driven, and language-agnostic orchestration in the years ahead. diff --git a/airflow-core/docs/administration-and-deployment/dag-serialization.rst b/airflow-core/docs/administration-and-deployment/dag-serialization.rst index 89384f3436b89..986821d7e2b28 100644 --- a/airflow-core/docs/administration-and-deployment/dag-serialization.rst +++ b/airflow-core/docs/administration-and-deployment/dag-serialization.rst @@ -153,7 +153,7 @@ Serialized Dags now include a ``client_defaults`` section that contains common d .. code-block:: json { - "__version": 2, + "__version": 3, "client_defaults": { "tasks": { "retry_delay": 300.0, diff --git a/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst b/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst index c803791762ef0..c9b1966714af4 100644 --- a/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst +++ b/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst @@ -36,7 +36,7 @@ Webserver Health Check Endpoint ------------------------------- To check the health status of your Airflow instance, you can simply access the endpoint -``/api/v2/monitor/health``. It will return a JSON object in which a high-level glance is provided. +``/api/v2/monitor/health``. It will return a JSON object that provides a high-level glance at the health status across multiple Airflow components. .. code-block:: JSON @@ -86,9 +86,12 @@ Served by the web server, this health check endpoint is independent of the newer .. note:: - For this check to work, at least one working web server is required. Suppose you use this check for scheduler - monitoring, then in case of failure of the web server, you will lose the ability to monitor scheduler, which means - that it can be restarted even if it is in good condition. For greater confidence, consider using :ref:`CLI Check for Scheduler ` or :ref:`Scheduler Health Check Server `. + * For this check to work, at least one working web server is required. Suppose you use this check for scheduler + monitoring, then in case of failure of the web server, you will lose the ability to monitor scheduler, which means + that it can be restarted even if it is in good condition. For greater confidence, consider using :ref:`CLI Check for Scheduler ` or :ref:`Scheduler Health Check Server `. + + * Using this endpoint as webserver probes (liveness/readiness) makes it contingent on Airflow core components' availability (database, scheduler, etc). + Webservers will be frequently restarted if any of these core components are down. To make Webservers less prone to other components' failures, consider using endpoints like ``api/v2/version``. .. _check-health/scheduler-health-check-server: diff --git a/airflow-core/docs/administration-and-deployment/plugins.rst b/airflow-core/docs/administration-and-deployment/plugins.rst index a93eb48ffdbd3..51f10859703f6 100644 --- a/airflow-core/docs/administration-and-deployment/plugins.rst +++ b/airflow-core/docs/administration-and-deployment/plugins.rst @@ -24,6 +24,9 @@ Airflow has a simple plugin manager built-in that can integrate external features to its core by simply dropping files in your ``$AIRFLOW_HOME/plugins`` folder. +Since Airflow 3.1, the plugin system supports new features such as React apps, FastAPI endpoints, +and middleware, making it easier to extend Airflow and build rich custom integrations. + The python modules in the ``plugins`` folder get imported, and **macros** and web **views** get integrated to Airflow's main collections and become available for use. @@ -48,7 +51,7 @@ Examples: * A set of tools to parse Hive logs and expose Hive metadata (CPU /IO / phases/ skew /...) * An anomaly detection framework, allowing people to collect metrics, set thresholds and alerts -* An auditing tool, helping understand who accesses what +* An auditing tool, helping to understand who accesses what * A config-driven SLA monitoring tool, allowing you to set monitored tables and at what time they should land, alert people, and expose visualizations of outages @@ -61,11 +64,24 @@ Airflow has many components that can be reused when building an application: * A metadata database to store your models * Access to your databases, and knowledge of how to connect to them * An array of workers that your application can push workload to -* Airflow is deployed, you can just piggy back on its deployment logistics +* Airflow is deployed, you can just piggyback on its deployment logistics * Basic charting capabilities, underlying libraries and abstractions .. _plugins:loading: +Available Building Blocks +------------------------- + +Airflow plugins can register the following components: + +* *External Views* – Add buttons/tabs linking to new pages in the UI. +* *React Apps* – Embed custom React apps inside the Airflow UI (new in Airflow 3.1). +* *FastAPI Apps* – Add custom API endpoints. +* *FastAPI Middlewares* – Intercept and modify API requests/responses. +* *Macros* – Define reusable Python functions available in DAG templates. +* *Operator Extra Links* – Add custom buttons in the task details view. +* *Timetables & Listeners* – Implement custom scheduling logic and event hooks. + When are plugins (re)loaded? ---------------------------- @@ -73,7 +89,7 @@ Plugins are by default lazily loaded and once loaded, they are never reloaded (e automatically loaded in Webserver). To load them at the start of each Airflow process, set ``[core] lazy_load_plugins = False`` in ``airflow.cfg``. -This means that if you make any changes to plugins and you want the webserver or scheduler to use that new +This means that if you make any changes to plugins, and you want the webserver or scheduler to use that new code you will need to restart those processes. However, it will not be reflected in new running tasks until after the scheduler boots. By default, task execution uses forking. This avoids the slowdown associated with creating a new Python interpreter @@ -111,6 +127,8 @@ looks like: # A list of dictionaries containing external views and some metadata. See the example below. external_views = [] # A list of dictionaries containing react apps and some metadata. See the example below. + # Note: React apps are only supported in Airflow 3.1 and later. + # Note: The React app integration is experimental and interfaces might change in future versions. Particularly, dependency and state interactions between the UI and plugins may need to be refactored for more complex plugin apps. react_apps = [] # A callback to perform actions when Airflow starts and the plugin is loaded. @@ -149,6 +167,19 @@ additional initialization. Please note ``name`` inside this class must be specif Make sure you restart the webserver and scheduler after making changes to plugins so that they take effect. +Plugin Management Interface +--------------------------- + +Airflow 3.1 introduces a Plugin Management Interface, available under *Admin → Plugins* in the Airflow UI. +This page allows you to view installed plugins. + +... + +External Views +-------------- + +External views can also be embedded directly into the Airflow UI using iframes by providing a ``url_route`` value. +This allows you to render the view inline instead of opening it in a new browser tab. .. _plugin-example: @@ -218,6 +249,7 @@ definitions in Airflow. "category": "browse", } + # Note: The React app integration is experimental and interfaces might change in future versions. react_app_with_metadata = { # Name of the React app, this will be displayed in the UI. "name": "Name of the React App", diff --git a/airflow-core/docs/administration-and-deployment/production-deployment.rst b/airflow-core/docs/administration-and-deployment/production-deployment.rst index 78d0e0966bc42..e4a6c1c814b2a 100644 --- a/airflow-core/docs/administration-and-deployment/production-deployment.rst +++ b/airflow-core/docs/administration-and-deployment/production-deployment.rst @@ -90,7 +90,7 @@ e.g. metadata DB, password, etc. You can accomplish this using the format :envva .. code-block:: bash AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=my_conn_id - AIRFLOW__WEBSERVER__BASE_URL=http://host:port + AIRFLOW__API__BASE_URL=http://host:port Some configurations such as the Airflow Backend connection URI can be derived from bash commands as well: diff --git a/airflow-core/docs/authoring-and-scheduling/asset-scheduling.rst b/airflow-core/docs/authoring-and-scheduling/asset-scheduling.rst index 51ae64c62c7a5..4feaf7e24a97c 100644 --- a/airflow-core/docs/authoring-and-scheduling/asset-scheduling.rst +++ b/airflow-core/docs/authoring-and-scheduling/asset-scheduling.rst @@ -152,16 +152,35 @@ Fetching information from a triggering asset event A triggered Dag can fetch information from the asset that triggered it using the ``triggering_asset_events`` template or parameter. See more at :ref:`templates-ref`. -Example: +The ``triggering_asset_events`` is a dictionary that looks like this: .. code-block:: python - example_snowflake_asset = Asset("snowflake://my_db/my_schema/my_table") + { + Asset("s3://asset-bucket/example.csv"): [ + AssetEvent(uri="s3://asset-bucket/example.csv", source_dag_run=DagRun(...), ...), + ..., + ], + Asset("s3://another-bucket/another.csv"): [ + AssetEvent(uri="s3://another-bucket/another.csv", source_dag_run=DagRun(...), ...), + ..., + ], + } - with DAG(dag_id="load_snowflake_data", schedule="@hourly", ...): - SQLExecuteQueryOperator( - task_id="load", conn_id="snowflake_default", outlets=[example_snowflake_asset], ... - ) +You can access this information in your tasks using Jinja templating or directly in Python functions. + +Accessing triggering asset events with Jinja +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use Jinja templating to pass information from the triggering asset events to your operators. + +**Example: Single Triggering Asset** + +If your DAG is triggered by a single asset, you can access its information like this: + +.. code-block:: python + + example_snowflake_asset = Asset("snowflake://my_db/my_schema/my_table") with DAG(dag_id="query_snowflake_data", schedule=[example_snowflake_asset], ...): SQLExecuteQueryOperator( @@ -175,13 +194,58 @@ Example: """, ) - @task - def print_triggering_asset_events(triggering_asset_events=None): - for asset, asset_list in triggering_asset_events.items(): - print(asset, asset_list) - print(asset_list[0].source_dag_run.dag_id) +In this example, ``triggering_asset_events.values() | first | first`` does the following: +1. ``triggering_asset_events.values()``: Gets a list of all lists of asset events. +2. ``| first``: Gets the first list of asset events (since we only have one triggering asset). +3. ``| first``: Gets the first ``AssetEvent`` from that list. + +**Example: Multiple Triggering Assets** + +When your DAG is triggered by multiple assets, you can iterate through them in your Jinja template. + +.. code-block:: python + + with DAG(dag_id="process_assets", schedule=[asset1, asset2], ...): + BashOperator( + task_id="process", + bash_command=""" + {% for asset_uri, events in triggering_asset_events.items() %} + echo "Processing asset: {{ asset_uri }}" + {% for event in events %} + echo " Triggered by DAG: {{ event.source_dag_run.dag_id }}" + echo " Data interval start: {{ event.source_dag_run.data_interval_start }}" + echo " Data interval end: {{ event.source_dag_run.data_interval_end }}" + {% endfor %} + {% endfor %} + """, + ) + + +Accessing triggering asset events in Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also access the ``triggering_asset_events`` directly in a Python function by passing it as a parameter. + +.. code-block:: python + + @task + def print_triggering_asset_events(triggering_asset_events=None): + if triggering_asset_events: + for asset, asset_events in triggering_asset_events.items(): + print(f"Asset: {asset.uri}") + for event in asset_events: + print(f" - Triggered by DAG run: {event.source_dag_run.dag_id}") + print( + f" Data interval: {event.source_dag_run.data_interval_start} to {event.source_dag_run.data_interval_end}" + ) + print(f" Run ID: {event.source_dag_run.run_id}") + print(f" Timestamp: {event.timestamp}") + + + print_triggering_asset_events() - print_triggering_asset_events() +.. note:: + When a DAG is scheduled by multiple assets, there may be multiple asset events for each asset. The logic for handling these events can be complex. It is up to the DAG author to decide how to process them. For example, you might want to process all new data since the last run, or you might want to process each triggering event individually. Note that this example is using `(.values() | first | first) `_ to fetch the first of one asset given to the Dag, and the first of one AssetEvent for that asset. An implementation can be quite complex if you diff --git a/airflow-core/docs/authoring-and-scheduling/assets.rst b/airflow-core/docs/authoring-and-scheduling/assets.rst index 22aea6faf34fa..dcdf15e4e3b5f 100644 --- a/airflow-core/docs/authoring-and-scheduling/assets.rst +++ b/airflow-core/docs/authoring-and-scheduling/assets.rst @@ -90,10 +90,10 @@ The identifier does not have to be absolute; it can be a scheme-less, relative U Non-absolute identifiers are considered plain strings that do not carry any semantic meanings to Airflow. -Extra information on asset +Extra information on assets ---------------------------- -If needed, you can include an extra dictionary in an asset: +If needed, you can include an additional dictionary in an asset using the ``extra`` parameter: .. code-block:: python @@ -102,9 +102,49 @@ If needed, you can include an extra dictionary in an asset: extra={"team": "trainees"}, ) -This can be used to supply custom description to the asset, such as who has ownership to the target file, or what the file is for. The extra information does not affect an asset's identity. +This allows you to provide custom metadata about the asset, such as ownership information or the purpose of the file. The ``extra`` field does **NOT** affect the identity of an asset. +Thus, maintaining the uniqueness of the ``extra`` value is the user responsibility. It suggested to have only one single set of ``extra`` value per asset. + +For example, in the following snippet, only one of the ``extra`` dictionaries will ultimately be stored, but it does guaranteed which one will be stored. + +.. code-block:: python + + Asset("s3://asset/example.csv", extra={"d": "e"}) + Asset("s3://asset/example.csv", extra={"f": "g"}) + +This behavior also applies to dynamically generated assets created through ``AssetAlias``. +In the example below, the final stored ``extra`` value is not guaranteed and it might vary based on Dag processor settings. + +.. code-block:: python + + from airflow.sdk import AssetAlias + + + @dag(schedule=None) + def my_dag_1(): + + @task(outlets=[AssetAlias("my-task-outputs")]) + def my_task_with_outlet_events(*, outlet_events): + outlet_events[AssetAlias("my-task-outputs")].add( + # Asset extra set as {"from": "asset alias"} + Asset("s3://bucket/my-task", extra={"from": "asset alias"}) + ) + + my_task_with_outlet_events() + + + # Asset extra set as {"key": "value"} + @dag(schedule=Asset("s3://bucket/my-task", extra={"key": "value"})) + def my_dag_2(): ... + + + my_dag_1() + my_dag_2() + + # It's not guaranteed which extra will be the one stored + +.. note:: **Security Note:** Asset URIs and values in the ``extra`` field are stored in cleartext in Airflow's metadata database. These fields are **not encrypted**. **DO NOT** store sensitive information, especially credentials, in either the asset URI or the ``extra`` dictionary. -.. note:: **Security Note:** Asset URI and extra fields are not encrypted, they are stored in cleartext in Airflow's metadata database. Do NOT store any sensitive values, especially credentials, in either asset URIs or extra key values! Creating a task to emit asset events ------------------------------------ @@ -149,7 +189,7 @@ Attaching extra information to an emitting asset event .. versionadded:: 2.10.0 A task with an asset outlet can optionally attach extra information before it emits an asset event. This is different -from `Extra information on asset`_. Extra information on an asset statically describes the entity pointed to by the asset URI; extra information on the *asset event* instead should be used to annotate the triggering data change, such as how many rows in the database are changed by the update, or the date range covered by it. +from `Extra information on assets`_. Extra information on an asset statically describes the entity pointed to by the asset URI; extra information on the *asset event* instead should be used to annotate the triggering data change, such as how many rows in the database are changed by the update, or the date range covered by it. The easiest way to attach extra information to the asset event is by ``yield``-ing a ``Metadata`` object from a task: @@ -178,6 +218,8 @@ Another way to achieve the same is by accessing ``outlet_events`` in a task's ex There's minimal magic here---Airflow simply writes the yielded values to the exact same accessor. This also works in classic operators, including ``execute``, ``pre_execute``, and ``post_execute``. +.. note:: Asset event extra information can only contain JSON-serializable values (list and dict nesting is possible). This is due to the value being stored in the database. + .. _fetching_information_from_previously_emitted_asset_events: Fetching information from previously emitted asset events diff --git a/airflow-core/docs/authoring-and-scheduling/serializers.rst b/airflow-core/docs/authoring-and-scheduling/serializers.rst index 433485bb3b8af..fb19a111d9177 100644 --- a/airflow-core/docs/authoring-and-scheduling/serializers.rst +++ b/airflow-core/docs/authoring-and-scheduling/serializers.rst @@ -74,8 +74,7 @@ Airflow Object @staticmethod def deserialize(data: dict[str, Any], version: int): - f = Foo(a=data["a"]) - f.b = data["b"] + f = Foo(a=data["a"], v=data["b"]) return f @@ -86,17 +85,18 @@ Registered from __future__ import annotations - from decimal import Decimal from typing import TYPE_CHECKING from airflow.utils.module_loading import qualname if TYPE_CHECKING: + import decimal + from airflow.serialization.serde import U serializers = [ - Decimal + "decimal.Decimal" ] # this can be a type or a fully qualified str. Str can be used to prevent circular imports deserializers = serializers # in some cases you might not have a deserializer (e.g. k8s pod) @@ -105,25 +105,28 @@ Registered # the serializer expects output, classname, version, is_serialized? def serialize(o: object) -> tuple[U, str, int, bool]: - if isinstance(o, Decimal): - name = qualname(o) - _, _, exponent = o.as_tuple() - if exponent >= 0: # No digits after the decimal point. - return int(o), name, __version__, True - # Technically lossy due to floating point errors, but the best we - # can do without implementing a custom encode function. - return float(o), name, __version__, True + from decimal import Decimal - return "", "", 0, False + if not isinstance(o, Decimal): + return "", "", 0, False + name = qualname(o) + _, _, exponent = o.as_tuple() + if isinstance(exponent, int) and exponent >= 0: # No digits after the decimal point. + return int(o), name, __version__, True + # Technically lossy due to floating point errors, but the best we + # can do without implementing a custom encode function. + return float(o), name, __version__, True # the deserializer sanitizes the data for you, so you do not need to deserialize values yourself - def deserialize(classname: str, version: int, data: object) -> Decimal: + def deserialize(cls: type, version: int, data: object) -> Decimal: + from decimal import Decimal + # always check version compatibility if version > __version__: - raise TypeError(f"serialized {version} of {classname} > {__version__}") + raise TypeError(f"serialized {version} of {qualname(cls)} > {__version__}") - if classname != qualname(Decimal): - raise TypeError(f"{classname} != {qualname(Decimal)}") + if cls is not Decimal: + raise TypeError(f"do not know how to deserialize {qualname(cls)}") return Decimal(str(data)) diff --git a/airflow-core/docs/best-practices.rst b/airflow-core/docs/best-practices.rst index 5c8fdf43dc6a7..8ebf130402305 100644 --- a/airflow-core/docs/best-practices.rst +++ b/airflow-core/docs/best-practices.rst @@ -291,7 +291,7 @@ When you execute that code you will see: .. code-block:: bash - root@cf85ab34571e:/opt/airflow# python /files/test_python.py + [Breeze:3.10.19] root@cf85ab34571e:/opt/airflow# python /files/test_python.py Executing 1 This means that the ``get_array`` is not executed as top-level code, but ``get_task_id`` is. @@ -310,13 +310,13 @@ Installing and Using ruff .. code-block:: bash - pip install "ruff>=0.12.12" + pip install "ruff>=0.14.14" 2. **Running ruff**: Execute ``ruff`` to check your Dags for potential issues: .. code-block:: bash - ruff check dags/ --select AIR3 --preview + ruff check dags/ --select AIR3 This command will analyze your Dags located in the ``dags/`` directory and report any issues related to the specified rules. @@ -347,8 +347,6 @@ Running ``ruff`` will produce: By integrating ``ruff`` into your development workflow, you can proactively address deprecations and maintain code quality, facilitating smoother transitions between Airflow versions. -For more information on ``ruff`` and its integration with Airflow, refer to the `official Airflow documentation `_. - .. _best_practices/dynamic_dag_generation: Dynamic Dag Generation diff --git a/airflow-core/docs/conf.py b/airflow-core/docs/conf.py index c9886a172b328..99e0527fcdb4f 100644 --- a/airflow-core/docs/conf.py +++ b/airflow-core/docs/conf.py @@ -27,12 +27,6 @@ from pathlib import Path from typing import Any -from packaging.version import Version, parse as parse_version - -import airflow -from airflow.api_fastapi.auth.managers.simple.openapi import __file__ as sam_openapi_file -from airflow.api_fastapi.core_api.openapi import __file__ as main_openapi_file -from airflow.configuration import retrieve_configuration_description from docs.utils.conf_constants import ( AIRFLOW_CORE_DOC_STATIC_PATH, AIRFLOW_CORE_DOCKER_COMPOSE_PATH, @@ -61,6 +55,12 @@ get_rst_filepath_from_path, skip_util_classes_extension, ) +from packaging.version import Version, parse as parse_version + +import airflow +from airflow.api_fastapi.auth.managers.simple.openapi import __file__ as sam_openapi_file +from airflow.api_fastapi.core_api.openapi import __file__ as main_openapi_file +from airflow.configuration import retrieve_configuration_description PACKAGE_NAME = "apache-airflow" PACKAGE_VERSION = airflow.__version__ @@ -244,13 +244,20 @@ def add_airflow_core_exclude_patterns_to_sphinx(exclude_patterns: list[str]): config_descriptions = retrieve_configuration_description(include_providers=False) configs, deprecated_options = get_configs_and_deprecations(airflow_version, config_descriptions) +# TODO: remove it when we start releasing task-sdk separately from airflow-core +airflow_version_split = PACKAGE_VERSION.split(".") +TASK_SDK_VERSION = f"1.{airflow_version_split[1]}.{airflow_version_split[2]}" + jinja_contexts = { "config_ctx": {"configs": configs, "deprecated_options": deprecated_options}, "quick_start_ctx": {"doc_root_url": f"https://airflow.apache.org/docs/apache-airflow/{PACKAGE_VERSION}/"}, "official_download_page": { "base_url": f"https://downloads.apache.org/airflow/{PACKAGE_VERSION}", + "base_url_task_sdk": f"https://downloads.apache.org/airflow/task-sdk/{TASK_SDK_VERSION}", "closer_lua_url": f"https://www.apache.org/dyn/closer.lua/airflow/{PACKAGE_VERSION}", + "closer_lua_url_task_sdk": f"https://www.apache.org/dyn/closer.lua/airflow/task-sdk/{TASK_SDK_VERSION}", "airflow_version": PACKAGE_VERSION, + "task_sdk_version": TASK_SDK_VERSION, }, } @@ -261,6 +268,14 @@ def add_airflow_core_exclude_patterns_to_sphinx(exclude_patterns: list[str]): "experimental": "This is an :ref:`experimental feature `.", } +# Pagefind search configuration +pagefind_exclude_patterns = [ + "_api/**", # Exclude auto-generated API documentation + "_modules/**", # Exclude source code modules + "release_notes.html", # Exclude changelog aggregation page + "genindex.html", # Exclude generated index +] + # -- Options for sphinx.ext.autodoc -------------------------------------------- # See: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html @@ -365,3 +380,7 @@ def add_airflow_core_exclude_patterns_to_sphinx(exclude_patterns: list[str]): def setup(sphinx): sphinx.connect("autoapi-skip-member", skip_util_classes_extension) + + +# Fix for broken permalink icon in Sphinx 7.x+ +html_permalinks_icon = '' diff --git a/airflow-core/docs/core-concepts/auth-manager/index.rst b/airflow-core/docs/core-concepts/auth-manager/index.rst index fffc89fc4d572..be626edde7fe4 100644 --- a/airflow-core/docs/core-concepts/auth-manager/index.rst +++ b/airflow-core/docs/core-concepts/auth-manager/index.rst @@ -166,12 +166,30 @@ cookie named ``_token`` before redirecting to the Airflow UI. The Airflow UI wil response = RedirectResponse(url="/") secure = request.base_url.scheme == "https" or bool(conf.get("api", "ssl_cert", fallback="")) - response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=secure) + response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=secure, httponly=True) return response .. note:: - Do not set the cookie parameter ``httponly`` to ``True``. Airflow UI needs to access the JWT token from the cookie. + Ensure that the cookie parameter ``httponly`` is set to ``True``. The UI does not manage the token. +Refreshing JWT Token +'''''''''''''''''''' +Refreshing token is optional feature and its availability depends on the specific implementation of the auth manager. +The auth manager is responsible for refreshing the JWT token when it expires. +The Airflow API uses middleware that intercepts every request and checks the validity of the JWT token. +Token communication is handled through ``httponly`` cookies to improve security. +When the token expires, the `JWTRefreshMiddleware `_ middleware calls the auth manager's ``refresh_user`` method to obtain a new token. + + +To support token refresh operations, the auth manager must implement the ``refresh_user`` method. +This method receives an expired token and must return a new valid token. +User information is extracted from the expired token and used to generate a fresh token. + +An example implementation of ``refresh_user`` could be: +`KeycloakAuthManager::refresh_user `_ +User information is derived from the ``BaseUser`` instance. +It is important that the user object contains all the fields required to refresh the token. An example user class could be: +`KeycloakAuthManagerUser(BaseUser) `_. Optional methods recommended to override for optimization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -183,6 +201,7 @@ The following methods aren't required to override to have a functional Airflow a * ``batch_is_authorized_pool``: Batch version of ``is_authorized_pool``. If not overridden, it will call ``is_authorized_pool`` for every single item. * ``batch_is_authorized_variable``: Batch version of ``is_authorized_variable``. If not overridden, it will call ``is_authorized_variable`` for every single item. * ``get_authorized_dag_ids``: Return the list of Dag IDs the user has access to. If not overridden, it will call ``is_authorized_dag`` for every single Dag available in the environment. +* ``is_authorized_hitl_task``: Return whether the user is authorized to approve or reject a Human-in-the-loop (HITL) task. Override this method to implement custom authorization logic for HITL tasks. If not overridden, it checks if the user's ID is in the assigned users list. * Note: To filter the results of ``get_authorized_dag_ids``, it is recommended that you define the filtering logic in your ``filter_authorized_dag_ids`` method. For example, this may be useful if you rely on per-Dag access controls derived from one or more fields on a given Dag (e.g. Dag tags). * This method requires an active session with the Airflow metadata database. As such, overriding the ``get_authorized_dag_ids`` method is an advanced use case, which should be considered carefully -- it is recommended you refer to the :doc:`../../database-erd-ref`. diff --git a/airflow-core/docs/core-concepts/dag-run.rst b/airflow-core/docs/core-concepts/dag-run.rst index f524515e3b179..e1e54d8aa7bfe 100644 --- a/airflow-core/docs/core-concepts/dag-run.rst +++ b/airflow-core/docs/core-concepts/dag-run.rst @@ -278,6 +278,28 @@ Example of a parameterized Dag: **Note**: The parameters from ``dag_run.conf`` can only be used in a template field of an operator. +Wait for a Dag Run +------------------ + +Airflow provides an experimental API to **wait for a Dag run to complete**. This is particularly useful when integrating Airflow into external systems or automation pipelines that need to pause execution until a Dag finishes. + +The endpoint blocks (by polling) until the specified Dag run reaches a terminal state: ``success``, ``failed``, or ``canceled``. + +This endpoint streams responses using the **NDJSON (Newline-Delimited JSON)** format. Each line in the response is a JSON object representing the state of the Dag run at that moment. + +For example: + +.. code-block:: none + + {"state": "running"} + {"state": "success", "results": {"op": 42}} + +This allows clients to monitor the run in real time and optionally collect XCom results from specific tasks. + +.. note:: + + This feature is **experimental** and may change or be removed in future Airflow versions. + Using CLI ^^^^^^^^^^^ diff --git a/airflow-core/docs/core-concepts/dags.rst b/airflow-core/docs/core-concepts/dags.rst index 04551838bed44..b1bb0c8bec294 100644 --- a/airflow-core/docs/core-concepts/dags.rst +++ b/airflow-core/docs/core-concepts/dags.rst @@ -551,6 +551,9 @@ A TaskGroup can be used to organize tasks into hierarchical groups in Graph view Tasks in TaskGroups live on the same original Dag, and honor all the Dag settings and pool configurations. +.. seealso:: + API reference for :class:`~airflow.sdk.TaskGroup` and :class:`~airflow.sdk.task_group` + .. image:: /img/ui-light/task_group.gif Dependency relationships can be applied across all tasks in a TaskGroup with the ``>>`` and ``<<`` operators. For example, the following code puts ``task1`` and ``task2`` in TaskGroup ``group1`` and then puts both tasks upstream of ``task3``: @@ -857,8 +860,8 @@ Here's a simple example using the existing email Notifier: interval=timedelta(minutes=30), callback=SmtpNotifier( to="team@example.com", - subject="[Alert] Dag {{ dag.dag_id }} exceeded time threshold", - html_content="The Dag has been running for more than 30 minutes since being queued.", + subject="🚨 Dag {{ dag_run.dag_id }} missed deadline at {{ deadline.deadline_time }}", + html_content="The Dag Run {{ dag_run.dag_run_id }} has been running for more than 30 minutes since being queued.", ), ), ): diff --git a/airflow-core/docs/core-concepts/debug.rst b/airflow-core/docs/core-concepts/debug.rst index dd627bb355858..0bed4ec3f2ea2 100644 --- a/airflow-core/docs/core-concepts/debug.rst +++ b/airflow-core/docs/core-concepts/debug.rst @@ -78,7 +78,7 @@ Run ``python -m pdb .py`` for an interactive debugging experie .. code-block:: bash - root@ef2c84ad4856:/opt/airflow# python -m pdb providers/standard/src/airflow/providers/standard/example_dags/example_bash_operator.py + [Breeze:3.10.19] root@ef2c84ad4856:/opt/airflow# python -m pdb providers/standard/src/airflow/providers/standard/example_dags/example_bash_operator.py > /opt/airflow/providers/standard/src/airflow/providers/standard/example_dags/example_bash_operator.py(18)() -> """Example Dag demonstrating the usage of the BashOperator.""" (Pdb) b 45 diff --git a/airflow-core/docs/core-concepts/executor/index.rst b/airflow-core/docs/core-concepts/executor/index.rst index 7a4b5e88fe353..469bbf0847e27 100644 --- a/airflow-core/docs/core-concepts/executor/index.rst +++ b/airflow-core/docs/core-concepts/executor/index.rst @@ -92,7 +92,7 @@ Airflow tasks are sent to a central queue where remote workers pull tasks to exe * :doc:`CeleryExecutor ` * :doc:`BatchExecutor ` -* :doc:`EdgeExecutor ` (Experimental Pre-Release) +* :doc:`EdgeExecutor ` *Containerized Executors* diff --git a/airflow-core/docs/core-concepts/executor/local.rst b/airflow-core/docs/core-concepts/executor/local.rst index 65dd1ba471dbe..dc7a70fd87361 100644 --- a/airflow-core/docs/core-concepts/executor/local.rst +++ b/airflow-core/docs/core-concepts/executor/local.rst @@ -21,32 +21,48 @@ Local Executor ============== -:class:`~airflow.executors.local_executor.LocalExecutor` runs tasks by spawning processes in a controlled fashion in different modes. +:class:`~airflow.executors.local_executor.LocalExecutor` runs tasks by spawning processes in a controlled fashion on the scheduler node. -Given that BaseExecutor has the option to receive a ``parallelism`` parameter to limit the number of process spawned, -when this parameter is ``0`` the number of processes that LocalExecutor can spawn is unlimited. +The parameter ``parallelism`` limits the number of process spawned not to overwhelm the node. +This parameter must be greater than ``0``. -The following strategies are implemented: +The :class:`~airflow.executors.local_executor.LocalExecutor` spawns the number of processes equal to the value of ``self.parallelism`` at +``start`` time, using a ``task_queue`` to coordinate the ingestion of tasks and the work distribution among the workers, which will take +a task as soon as they are ready. During the lifecycle of the LocalExecutor, the worker processes are running waiting for tasks, once the +LocalExecutor receives the call to shutdown the executor a poison token is sent to the workers to terminate them. -- | **Unlimited Parallelism** (``self.parallelism == 0``): In this strategy, LocalExecutor will - | spawn a process every time ``execute_async`` is called, that is, every task submitted to the - | :class:`~airflow.executors.local_executor.LocalExecutor` will be executed in its own process. Once the task is executed and the - | result stored in the ``result_queue``, the process terminates. There is no need for a - | ``task_queue`` in this approach, since as soon as a task is received a new process will be - | allocated to the task. Processes used in this strategy are of class :class:`~airflow.executors.local_executor.LocalWorker`. +The worker spawning behavior differs based on the multiprocessing start method: -- | **Limited Parallelism** (``self.parallelism > 0``): In this strategy, the :class:`~airflow.executors.local_executor.LocalExecutor` spawns - | the number of processes equal to the value of ``self.parallelism`` at ``start`` time, - | using a ``task_queue`` to coordinate the ingestion of tasks and the work distribution among - | the workers, which will take a task as soon as they are ready. During the lifecycle of - | the LocalExecutor, the worker processes are running waiting for tasks, once the - | LocalExecutor receives the call to shutdown the executor a poison token is sent to the - | workers to terminate them. Processes used in this strategy are of class :class:`~airflow.executors.local_executor.QueuedLocalWorker`. +- **Fork mode** (default on Linux): Workers are spawned all at once up to ``parallelism`` to prevent memory spikes + caused by Copy-on-Write (COW). See `Discussion `_ + for details. +- **Spawn mode** (default on macOS and Windows): Workers are spawned one at a time as needed to prevent + the overhead of spawning many processes simultaneously. .. note:: - When multiple Schedulers are configured with ``executor = LocalExecutor`` in the ``[core]`` section of your ``airflow.cfg``, each Scheduler will run a LocalExecutor. This means tasks would be processed in a distributed fashion across the machines running the Schedulers. + The ``parallelism`` parameter can be configured via the ``[core] parallelism`` option in ``airflow.cfg``. + The default value is ``32``. + +.. warning:: + + Since LocalExecutor workers are spawned as sub-processes of the scheduler, in containerized environments + this may appear as excessive memory consumption by the scheduler process. This can potentially trigger + container restarts due to OOM (Out of Memory). Consider adjusting the ``parallelism`` value based on + your container's resource limits. + +.. note:: + + When multiple Schedulers are configured with ``executor=LocalExecutor`` in the ``[core]`` section of your ``airflow.cfg``, each + Scheduler will run a LocalExecutor. This means tasks would be processed in a distributed fashion across the machines running the + Schedulers. One consideration should be taken into account: - - Restarting a Scheduler: If a Scheduler is restarted, it may take some time for other Schedulers to recognize the orphaned tasks and restart or fail them. + - Restarting a Scheduler: If a Scheduler is restarted, it may take some time for other Schedulers to recognize the orphaned tasks + and restart or fail them. + +.. note:: + + Previous versions of Airflow had the option to configure the LocalExecutor with unlimited parallelism + (``self.parallelism = 0``). This option has been removed in Airflow 3.0.0 to avoid overwhelming the scheduler node. diff --git a/airflow-core/docs/core-concepts/operators.rst b/airflow-core/docs/core-concepts/operators.rst index 256613537b4c7..2e8b82713d3d1 100644 --- a/airflow-core/docs/core-concepts/operators.rst +++ b/airflow-core/docs/core-concepts/operators.rst @@ -84,11 +84,25 @@ Here, ``{{ ds }}`` is a templated variable, and because the ``env`` parameter of You can also pass in a callable instead when Python is more readable than a Jinja template. The callable must accept two named arguments ``context`` and ``jinja_env``: +The ``context`` parameter is an Airflow's ``Context`` object that provides runtime information for the current task execution. Its contents can be accessed with Python's standard `dict syntax `_. It includes all variables available in Jinja templates and is read-only from the perspective of template rendering - while you can access and use its values, modifications won't affect the task execution environment. + +For a complete list of available context variables see :ref:`Templates reference `. + .. code-block:: python - def build_complex_command(context, jinja_env): + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + import jinja2 + from airflow.sdk import Context + + + def build_complex_command(context: Context, jinja_env: jinja2.Environment) -> str: + # Access runtime information from the context dictionary + task_id = context["ti"].task_id + execution_date = context["ds"] with open("file.csv") as f: - return do_complex_things(f) + return do_complex_things(f, task_id, execution_date) t = BashOperator( @@ -101,7 +115,7 @@ Since each template field is only rendered once, the callable's return value wil .. code-block:: python - def build_complex_command(context, jinja_env): + def build_complex_command(context: Context, jinja_env: jinja2.Environment) -> str: with open("file.csv") as f: data = do_complex_things(f) return context["task"].render_template(data, context, jinja_env) diff --git a/airflow-core/docs/core-concepts/overview.rst b/airflow-core/docs/core-concepts/overview.rst index 9b80ff331b483..dc51476a2639c 100644 --- a/airflow-core/docs/core-concepts/overview.rst +++ b/airflow-core/docs/core-concepts/overview.rst @@ -59,8 +59,8 @@ A minimal Airflow installation consists of the following components: * A folder of *Dag files*, which is read by the *scheduler* to figure out what tasks to run and when to run them. -* A *metadata database*, usually PostgreSQL or MySQL, which stores the state of tasks, Dags, variables, - Dags and tasks. +* A *metadata database*, usually PostgreSQL or MySQL, which stores the state of tasks, Dags and variables. + Setting up a metadata database is described in :doc:`/howto/set-up-database` and is required for Airflow to work. @@ -244,6 +244,6 @@ User interface Airflow comes with a user interface that lets you see what Dags and their tasks are doing, trigger runs of Dags, view logs, and do some limited debugging and resolution of problems with your Dags. -.. image:: ../img/ui-dark/dags.png +.. image:: ../img/ui-light/dags.png It's generally the best way to see the status of your Airflow installation as a whole, as well as diving into individual Dags to see their layout, the status of each task, and the logs from each task. diff --git a/airflow-core/docs/faq.rst b/airflow-core/docs/faq.rst index 00673a8e5f7f3..f57f2ddebf22c 100644 --- a/airflow-core/docs/faq.rst +++ b/airflow-core/docs/faq.rst @@ -227,7 +227,6 @@ There are several reasons why Dags might disappear from the UI. Common causes in * :ref:`config:dag_processor__file_parsing_sort_mode` - Ensure sorting method matches your sync strategy * :ref:`config:dag_processor__parsing_processes` - Number of parallel parsers * :ref:`config:scheduler__parsing_cleanup_interval` - Controls stale Dag cleanup frequency - * :ref:`config:scheduler__dag_stale_not_seen_duration` - Time threshold for marking Dags as stale * **File synchronization problems** - Common with git-sync setups: diff --git a/airflow-core/docs/howto/custom-view-plugin.rst b/airflow-core/docs/howto/custom-view-plugin.rst index 1250475fcba4e..3520de1a652ec 100644 --- a/airflow-core/docs/howto/custom-view-plugin.rst +++ b/airflow-core/docs/howto/custom-view-plugin.rst @@ -54,6 +54,13 @@ available in :doc:`plugin `. Developing React Applications with the Bootstrap Tool ===================================================== +.. warning:: + React applications are new in Airflow 3.1 and should be considered experimental. The feature may be + subject to changes in future versions without warning based on user feedback and errors reported. + Dependency and state interactions between the UI and plugins may need to be refactored, which will also change the bootstrapped example project provided. + +|experimental| + Airflow provides a React plugin bootstrap tool to help developers quickly create, develop, and integrate external React applications into the core UI. This is the most flexible and recommended way to customize the Airflow UI. This tool generates a complete React project structure that builds as a library compatible with dynamic imports and shares React instances with the host Airflow application. diff --git a/airflow-core/docs/howto/customize-ui.rst b/airflow-core/docs/howto/customize-ui.rst index 1eb393096100c..7d037d2485ff3 100644 --- a/airflow-core/docs/howto/customize-ui.rst +++ b/airflow-core/docs/howto/customize-ui.rst @@ -21,7 +21,7 @@ Customizing the UI .. _customizing-the-ui: Customizing Dag UI Header and Airflow Page Titles -================================================== +------------------------------------------------- Airflow now allows you to customize the Dag home page header and page title. This will help distinguish between various installations of Airflow or simply amend the page text. @@ -32,11 +32,11 @@ distinguish between various installations of Airflow or simply amend the page te To make this change, simply: -1. Add the configuration option of ``instance_name`` under the ``[webserver]`` section inside ``airflow.cfg``: +1. Add the configuration option of ``instance_name`` under the ``[api]`` section inside ``airflow.cfg``: .. code-block:: - [webserver] + [api] instance_name = "DevEnv" @@ -45,7 +45,7 @@ To make this change, simply: .. code-block:: - AIRFLOW__WEBSERVER__INSTANCE_NAME = "DevEnv" + AIRFLOW__API__INSTANCE_NAME = "DevEnv" Screenshots @@ -61,17 +61,47 @@ After .. image:: ../img/change-site-title/example_instance_name_configuration.png +| -Add custom alert messages on the dashboard ------------------------------------------- +Adding Dashboard Alert Messages +=============================== -Extra alert messages can be shown on the UI dashboard. This can be useful for warning about setup issues -or announcing changes to end users. The following example shows how to add alert messages: +Extra alert messages can be shown on the Airflow dashboard. This can be useful for warning about setup issues, announcing changes +to end users, or providing real-time status information. Dashboard alerts support both static and dynamic content. -1. Add the following contents to ``airflow_local_settings.py`` file under ``$AIRFLOW_HOME/config``. - Each alert message should specify a severity level (``info``, ``warning``, ``error``) using ``category``. +Basic Static Alerts +------------------- - .. code-block:: python +To add static alert messages that remain constant until the webserver is restarted: + +1. Create an ``airflow_local_settings.py`` file and place it in ``$PYTHONPATH`` or in the ``$AIRFLOW_HOME/config`` folder. + (Airflow adds ``$AIRFLOW_HOME/config`` to ``PYTHONPATH`` when Airflow is initialized) + +2. Add the following contents to ``airflow_local_settings.py``: + + .. note:: + See :ref:`Configuring local settings ` for details on how to configure local settings. + + .. code-block:: python + + from airflow.api_fastapi.common.types import UIAlert + + DASHBOARD_UIALERTS = [ + UIAlert("Welcome to Airflow", category="info"), + ] + +3. Restart the Airflow webserver, and you should now see the alert message displayed on the dashboard. + +Alert Categories +---------------- + +You can control the category of the alert message. Available categories include: + +- ``"info"`` (default) - Blue informational alerts +- ``"warning"`` - Yellow warning alerts +- ``"error"`` - Red error alerts + +.. code-block:: python from airflow.api_fastapi.common.types import UIAlert @@ -81,19 +111,70 @@ or announcing changes to end users. The following example shows how to add alert UIAlert(text="Critical error detected!", category="error"), ] - See :ref:`Configuring local settings ` for details on how to - configure local settings. +.. image:: ../img/ui-alert-message.png -2. Restart Airflow Webserver, and you should now see: +Markdown Content in Alerts +-------------------------- -.. image:: ../img/ui-alert-message.png +Markdown can be included in alert messages for richer formatting. In the following example, we show an alert +message of heading 2 with a link included: -Alert messages also support Markdown. In the following example, we show an alert message of heading 2 with a link included. +.. code-block:: python - .. code-block:: python + from airflow.api_fastapi.common.types import UIAlert - DASHBOARD_UIALERTS = [ - UIAlert(text="## Visit [airflow.apache.org](https://airflow.apache.org)", category="info"), - ] + DASHBOARD_UIALERTS = [ + UIAlert(text="## Visit [airflow.apache.org](https://airflow.apache.org)", category="info"), + ] .. image:: ../img/ui-alert-message-markdown.png + +Dynamic Dashboard Alerts +------------------------ + +Dashboard alerts support dynamic content that updates each time the dashboard page is refreshed. This allows for real-time +status updates without requiring webserver restarts. Dynamic alerts must be defined as an instance of an iterable object. +The recommended approach is to create a class that subclasses ``list`` and implements a custom ``__iter__`` method that +yields fresh alerts each time Airflow iterates over the alerts. + +.. note:: + When implementing dynamic alerts it is important to keep alert generation logic lightweight to avoid + impacting dashboard load times. Consider caching results for expensive operations and handle exceptions + gracefully to prevent alert generation from breaking the UI. + +Dynamic alerts are particularly useful for: + +- **Real-time notifications**: Display current status updates or announcements +- **Deployment notifications**: Show current deployment status, build progress, or GitOps state +- **Temporary maintenance alerts**: Provide time-sensitive information about ongoing maintenance or issues +- **Environment-specific warnings**: Display different alerts based on current environment conditions +- **External service status**: Show the availability of dependent services or APIs + +Creating Dynamic Alerts +^^^^^^^^^^^^^^^^^^^^^^^ + +To create dynamic alerts, define ``DASHBOARD_UIALERTS`` as an instance of a class that subclasses ``list`` +and implements the ``__iter__`` method. The UI will iterate over any number ``UIAlert`` instances yielded by +this method and expose them as alerts on the dashboard page. + +The example below demonstrates how logic can be applied to yield alerts dynamically. More practical use +cases might include alerts yielded from APIs, database queries or files. + +.. code-block:: python + + import random + from airflow.api_fastapi.common.types import UIAlert + + + class DynamicAlerts(list): + def __iter__(self): + # This method is called each time Airflow iterates over DASHBOARD_UIALERTS + # Example: Flip a coin + if random.choice([True, False]): + yield UIAlert("Heads!", category="info") + else: + yield UIAlert("Tails!", category="warning") + + + # Create an instance of the class + DASHBOARD_UIALERTS = DynamicAlerts() diff --git a/airflow-core/docs/howto/deadline-alerts.rst b/airflow-core/docs/howto/deadline-alerts.rst index 19b4ea58e786a..e52fdd6aeb1d7 100644 --- a/airflow-core/docs/howto/deadline-alerts.rst +++ b/airflow-core/docs/howto/deadline-alerts.rst @@ -29,6 +29,11 @@ Deadline Alerts allow you to set time thresholds for your Dag runs and automatic thresholds are exceeded. You can set up Deadline Alerts by choosing a built-in reference point, setting an interval, and defining a response using either Airflow's Notifiers or a custom callback function. +Migrating from SLA +------------------ + +For help migrating from SLA to Deadlines, see the :doc:`migration guide ` + Creating a Deadline Alert ------------------------- @@ -64,7 +69,9 @@ Below is an example Dag implementation. If the Dag has not finished 15 minutes a interval=timedelta(minutes=15), callback=AsyncCallback( SlackWebhookNotifier, - kwargs={"text": "Dag 'slack_deadline_alert' still running after 30 minutes."}, + kwargs={ + "text": "🚨 Dag {{ dag_run.dag_id }} missed deadline at {{ deadline.deadline_time }}. DagRun: {{ dag_run }}" + }, ), ), ): @@ -78,6 +85,8 @@ The timeline for this example would look like this: Scheduled Queued Started Deadline 00:00 00:03 00:05 00:18 +.. _built-in-deadline-references: + Using Built-in References ------------------------- @@ -108,7 +117,9 @@ Here's an example using a fixed datetime: interval=timedelta(minutes=-30), # Alert 30 minutes before the reference. callback=AsyncCallback( SlackWebhookNotifier, - kwargs={"text": "Dag 'slack_deadline_alert' still running after 30 minutes."}, + kwargs={ + "text": "🚨 Dag {{ dag_run.dag_id }} missed deadline at {{ deadline.deadline_time }}. DagRun: {{ dag_run }}" + }, ), ), ): @@ -128,9 +139,10 @@ The timeline for this example would look like this: Using Callbacks --------------- -When a deadline is exceeded, the callback is executed. You can use an existing :doc:`Notifier ` -or create a custom callback function. A callback must be an :class:`~airflow.sdk.definitions.deadline.AsyncCallback`, -with support coming soon for :class:`~airflow.sdk.definitions.deadline.SyncCallback`. +When a deadline is exceeded, the callback's callable is executed with the specified kwargs. You can use an +existing :doc:`Notifier ` or create a custom callable. A callback must be an +:class:`~airflow.sdk.definitions.deadline.AsyncCallback`, with support coming soon for +:class:`~airflow.sdk.definitions.deadline.SyncCallback`. Using Built-in Notifiers ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -146,7 +158,9 @@ Here's an example using the Slack Notifier if the Dag run has not finished withi interval=timedelta(minutes=30), callback=AsyncCallback( SlackWebhookNotifier, - kwargs={"text": "Dag 'slack_deadline_alert' still running after 30 minutes."}, + kwargs={ + "text": "🚨 Dag {{ dag_run.dag_id }} missed deadline at {{ deadline.deadline_time }}. DagRun: {{ dag_run }}" + }, ), ), ): @@ -155,7 +169,7 @@ Here's an example using the Slack Notifier if the Dag run has not finished withi Creating Custom Callbacks ^^^^^^^^^^^^^^^^^^^^^^^^^ -You can create custom callbacks for more complex handling. If ``kwargs`` are specified in the ``Callback``, +You can create custom callables for more complex handling. If ``kwargs`` are specified in the ``Callback``, they are passed to the callback function. **Asynchronous callbacks** must be defined somewhere in the Triggerer's system path. @@ -163,7 +177,8 @@ Triggerer's system path. Regarding Async Custom Deadline callbacks: * Async callbacks are executed by the Triggerer, so users must ensure they are importable by the Triggerer. - * One easy way to do this is to place the callback as a top-level method in a new file in the plugins folder. + * One easy way to do this is to place the callable as a top-level method in a new file in the plugins folder. + Nested callables are not currently supported. * The Triggerer will need to be restarted when a callback is added or changed in order to reload the file. @@ -175,7 +190,9 @@ A **custom asynchronous callback** might look like this: async def custom_async_callback(**kwargs): """Handle deadline violation with custom logic.""" - print(f"Deadline exceeded for Dag {kwargs.get("dag_id")}!") + context = kwargs.get("context", {}) + print(f"Deadline exceeded for Dag {context.get("dag_run", {}).get("dag_id")}!") + print(f"Context: {context}") print(f"Alert type: {kwargs.get("alert_type")}") # Additional custom handling here @@ -199,12 +216,21 @@ A **custom asynchronous callback** might look like this: interval=timedelta(minutes=15), callback=AsyncCallback( custom_async_callback, - kwargs={"alert_type": "time_exceeded", "dag_id": "custom_deadline_alert"}, + kwargs={"alert_type": "time_exceeded"}, ), ), ): EmptyOperator(task_id="example_task") +Templating and Context +^^^^^^^^^^^^^^^^^^^^^^ + +Currently, a relatively simple version of the Airflow context is passed to callables and Airflow does not run +:ref:`concepts:jinja-templating` on the kwargs. However, Notifiers already run templating with the +provided context as part of their execution. This means that templating can be used when using a Notifier +as long as the variables being templated are included in the simplified context. This currently includes the +ID and the calculated deadline time of the Deadline Alert as well as the data included in the ``GET`` REST API +response for Dag Run. Support for more comprehensive context and templating will be added in future versions. Deadline Calculation ^^^^^^^^^^^^^^^^^^^^ diff --git a/airflow-core/docs/howto/docker-compose/docker-compose.yaml b/airflow-core/docs/howto/docker-compose/docker-compose.yaml index 2c2a614c9ef72..3892e04414358 100644 --- a/airflow-core/docs/howto/docker-compose/docker-compose.yaml +++ b/airflow-core/docs/howto/docker-compose/docker-compose.yaml @@ -51,6 +51,8 @@ x-airflow-common: # and uncomment the "build" line below, Then run `docker-compose build` to build the images. image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:|version|} # build: . + env_file: + - ${ENV_FILE_PATH:-.env} environment: &airflow-common-env AIRFLOW__CORE__EXECUTOR: CeleryExecutor diff --git a/airflow-core/docs/howto/docker-compose/index.rst b/airflow-core/docs/howto/docker-compose/index.rst index 7e6d15a9e67a6..e1045f70ea087 100644 --- a/airflow-core/docs/howto/docker-compose/index.rst +++ b/airflow-core/docs/howto/docker-compose/index.rst @@ -154,6 +154,24 @@ If you want to initialize ``airflow.cfg`` with default values before launching t This will seed ``airflow.cfg`` with default values in ``config`` folder. +On systems with SELinux/AppArmor, you may run into permission issues. If this happens, edit your ``docker-compose.yaml`` file by added the suffix ``:z`` to all volumes: + +.. code-block:: yaml + + volumes: + - ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags:z + - ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs:z + - ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config:z + - ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins:z + +If, after this change, you are still experiencing permission issues when creating the ``airflow.cfg`` file, you can apply a very permissive setting to the ``config/`` folder: + +.. code-block:: bash + + sudo chmod -R 777 ./config + +Note that the above is a *work around* that should never be used in production. + Initialize the database ----------------------- @@ -163,14 +181,11 @@ On **all operating systems**, you need to run database migrations and create the docker compose up airflow-init -After initialization is complete, you should see a message like this: +After initialization is complete, you should see output related to files, folders, and plug-ins and finally a message like this: .. parsed-literal:: - airflow-init_1 | Upgrades done - airflow-init_1 | Admin user airflow created - airflow-init_1 | |version| - start_airflow-init_1 exited with code 0 + airflow-init-1 exited with code 0 The account created has the login ``airflow`` and the password ``airflow``. @@ -280,9 +295,14 @@ Here is a sample ``curl`` command, which sends a request to retrieve a pool list .. code-block:: bash ENDPOINT_URL="http://localhost:8080" - curl -X GET \ - --user "airflow:airflow" \ - "${ENDPOINT_URL}/api/v1/pools" + JWT_TOKEN=$(curl -s -X POST ${ENDPOINT_URL}/auth/token \ + -H "Content-Type: application/json" \ + -d '{"username": "airflow", "password": "airflow"}' |\ + jq -r '.access_token' \ + ) + curl -X GET \ + "${ENDPOINT_URL}/api/v2/pools" \ + -H "Authorization: Bearer ${JWT_TOKEN}" Cleaning up =========== diff --git a/airflow-core/docs/howto/index.rst b/airflow-core/docs/howto/index.rst index 28dd34a8058e3..0d4f79e74e547 100644 --- a/airflow-core/docs/howto/index.rst +++ b/airflow-core/docs/howto/index.rst @@ -34,6 +34,7 @@ configuring an Airflow environment. add-dag-tags add-owner-links notifications + sla-to-deadlines deadline-alerts set-config set-up-database @@ -55,3 +56,4 @@ configuring an Airflow environment. dynamic-dag-generation docker-compose/index run-with-self-signed-certificate + performance diff --git a/airflow-core/docs/howto/performance.rst b/airflow-core/docs/howto/performance.rst new file mode 100644 index 0000000000000..1f7d77d85dc58 --- /dev/null +++ b/airflow-core/docs/howto/performance.rst @@ -0,0 +1,55 @@ +.. 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. + +Performance tuning (API and UI) +=============================== + +This guide collects pragmatic tips that improve Airflow performance for API and UI workloads. + +Custom metadata indexes +----------------------- + +If you observe slowness in some API calls or specific UI views, you should inspect query plans and add indexes yourself +that match your workload. Listing endpoints and UI table views with specific ordering criteria are likely +to benefit from additional indexes if you have a large volume of metadata. + +When to use +^^^^^^^^^^^ + +- Slow API list/detail endpoints caused by frequent scans or lookups on columns like ``start_date``, timestamps (e.g. ``dttm``), or status fields. +- UI pages that load large lists or perform heavy filtering on metadata tables. + +Guidance +^^^^^^^^ + +- Inspect the query planner (e.g., ``EXPLAIN``/``EXPLAIN ANALYZE``) for slow endpoints and identify missing indexes. +- Prefer single or composite indexes that match your most common ordering logic, typically the ``order_by`` + query parameter used in API calls. Composite indexes can cover multi criteria ordering. +- Your optimal indexes depend on how you use the API and UI; there is no one-size-fits-all set we can ship by default. + +Upgrade considerations +^^^^^^^^^^^^^^^^^^^^^^ + +To avoid conflicts with Airflow database upgrades, delete your custom indexes before running an Airflow DB upgrade +and re-apply them after the upgrade succeeds. + +Notes +^^^^^ + +- Review query plans (e.g. via ``EXPLAIN``) to choose effective column sets and ordering for your workload. +- Composite indexes should list columns in selectivity order appropriate to your most common predicates. +- Indexes incur write overhead; add only those that materially improve your read paths. diff --git a/airflow-core/docs/howto/set-up-database.rst b/airflow-core/docs/howto/set-up-database.rst index 6846efc303af5..b9c8f1aae01f0 100644 --- a/airflow-core/docs/howto/set-up-database.rst +++ b/airflow-core/docs/howto/set-up-database.rst @@ -32,8 +32,8 @@ By default, Airflow uses **SQLite**, which is intended for development purposes Airflow supports the following database engine versions, so make sure which version you have. Old versions may not support all SQL statements. -* PostgreSQL: 12, 13, 14, 15, 16 -* MySQL: 8.0, `Innovation `_ +* PostgreSQL: 13, 14, 15, 16, 17 +* MySQL: 8.0, 8.4, `Innovation `_ * SQLite: 3.15.0+ If you plan on running more than one scheduler, you have to meet additional requirements. @@ -93,7 +93,7 @@ You can make sure which version is used by the interpreter by running this check .. code-block:: bash - root@b8a8e73caa2c:/opt/airflow# python + [Breeze:3.10.19] root@b8a8e73caa2c:/opt/airflow# python Python 3.8.10 (default, Mar 15 2022, 12:22:08) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. diff --git a/airflow-core/docs/howto/sla-to-deadlines.rst b/airflow-core/docs/howto/sla-to-deadlines.rst new file mode 100644 index 0000000000000..dd1e41592605a --- /dev/null +++ b/airflow-core/docs/howto/sla-to-deadlines.rst @@ -0,0 +1,89 @@ + .. 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. + +Migrating from SLA to Deadline Alerts +===================================== + +Two Different Paradigms +----------------------- + +While the goal of the **SLA** and **Deadline Alerts** features are very similar, they use two very different approaches. +This guide will lay out the major differences and help you decide on the best approach for your use case. + +To begin with, we'll start by explaining the two approaches then go into how to find the right Deadline for your use case. + +SLA +^^^ + +When the dag run **finishes**, check the current time. If the time is greater than (logical_date + sla) then +execute ``sla_miss_callback``. If the Dag run never finishes, the SLA is never checked. + +Deadline Alerts +^^^^^^^^^^^^^^^ + +When a Dag run **starts**, calculate and store (:ref:`DeadlineReference ` + interval). +The scheduler loop then checks periodically (default 5 seconds, set by ``scheduler_heartbeat_sec``) if any of those +times have passed then execute ``callback(**kwargs)``. + +The most direct migration path would be to use the ``DeadlineReference.DAGRUN_LOGICAL_DATE`` reference, but note that +the major change is that the Deadline's callback will execute "immediately" (within ``scheduler_heartbeat_sec`` of the +calculated expiration time) and not wait until the Dag finishes first. + +Equivalent Example Dags +----------------------- + +Below is a Dag using a 1-hour SLA, followed by an equivalent Dag using Deadline Alerts. + +SLA Example +^^^^^^^^^^^ + +.. code-block:: python + + with DAG( + "minimal_sla_example", + default_args={"sla": timedelta(hours=1)}, + sla_miss_callback=SlackWebhookNotifier( + text="SLA missed for {{ dag_run.dag_id }}", + ), + ): + BashOperator(task_id="long_task", bash_command="sleep 3600") + +Deadline Alerts Example +^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + with DAG( + "minimal_deadline_example", + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_LOGICAL_DATE, + interval=timedelta(hours=1), + callback=AsyncCallback( + SlackWebhookNotifier, + kwargs={ + text: "Deadline missed for {{ dag_run.dag_id }}", + }, + ), + ), + ): + BashOperator(task_id="long_task", bash_command="sleep 3600") + + +Further Reading +--------------- + +For more details on the Deadline Alerts feature, see the :doc:`how-to guide `. diff --git a/airflow-core/docs/img/airflow_erd.sha256 b/airflow-core/docs/img/airflow_erd.sha256 index ee840ec21f2d0..239ef51f09200 100644 --- a/airflow-core/docs/img/airflow_erd.sha256 +++ b/airflow-core/docs/img/airflow_erd.sha256 @@ -1 +1 @@ -c05cefbe080ed889ebe132a0285756db477ff28256bbc1e86da1e053873f6478 \ No newline at end of file +4827b81c01c9a7993d9796da4990f2a950c2069687db4b0e9cba0c0ae851bd9c \ No newline at end of file diff --git a/airflow-core/docs/img/airflow_erd.svg b/airflow-core/docs/img/airflow_erd.svg index a129862ad113f..43738fe19390d 100644 --- a/airflow-core/docs/img/airflow_erd.svg +++ b/airflow-core/docs/img/airflow_erd.svg @@ -4,2431 +4,2684 @@ - - + + %3 - - + + -log - -log - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - -dttm - - [TIMESTAMP] - -event - - [VARCHAR(60)] - -extra - - [TEXT] - -logical_date - - [TIMESTAMP] - -map_index - - [INTEGER] - -owner - - [VARCHAR(500)] - -owner_display_name - - [VARCHAR(500)] - -run_id - - [VARCHAR(250)] - -task_id - - [VARCHAR(250)] - -try_number - - [INTEGER] +alembic_version + +alembic_version + +version_num + + [VARCHAR(32)] + NOT NULL - + -dag_priority_parsing_request - -dag_priority_parsing_request - -id - - [VARCHAR(32)] - NOT NULL - -bundle_name - - [VARCHAR(250)] - NOT NULL - -relative_fileloc - - [VARCHAR(2000)] - NOT NULL +asset + +asset + +id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +extra + + [JSON] + NOT NULL + +group + + [VARCHAR(1500)] + NOT NULL + +name + + [VARCHAR(1500)] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + +uri + + [VARCHAR(1500)] + NOT NULL - + -job - -job - -id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - -end_date - - [TIMESTAMP] - -executor_class - - [VARCHAR(500)] - -hostname - - [VARCHAR(500)] - -job_type - - [VARCHAR(30)] - -latest_heartbeat - - [TIMESTAMP] - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -unixname - - [VARCHAR(1000)] +asset_active + +asset_active + +name + + [VARCHAR(1500)] + NOT NULL + +uri + + [VARCHAR(1500)] + NOT NULL - + + +asset:name--asset_active:name + +1 +1 + + + +asset:uri--asset_active:uri + +1 +1 + + -callback_request - -callback_request - -id - - [INTEGER] - NOT NULL - -callback_data - - [JSONB] - NOT NULL - -callback_type - - [VARCHAR(20)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -priority_weight - - [INTEGER] - NOT NULL +asset_alias_asset + +asset_alias_asset + +alias_id + + [INTEGER] + NOT NULL + +asset_id + + [INTEGER] + NOT NULL - + + +asset:id--asset_alias_asset:asset_id + +0..N +1 + + -import_error - -import_error - -id - - [INTEGER] - NOT NULL - -bundle_name - - [VARCHAR(250)] - -filename - - [VARCHAR(1024)] - -stacktrace - - [TEXT] - -timestamp - - [TIMESTAMP] +asset_dag_run_queue + +asset_dag_run_queue + +asset_id + + [INTEGER] + NOT NULL + +target_dag_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL - + + +asset:id--asset_dag_run_queue:asset_id + +0..N +1 + + -dag_bundle - -dag_bundle - -name - - [VARCHAR(250)] - NOT NULL - -active - - [BOOLEAN] - -last_refreshed - - [TIMESTAMP] - -signed_url_template - - [VARCHAR(200)] - -template_params - - [JSON] - -version - - [VARCHAR(200)] +asset_watcher + +asset_watcher + +asset_id + + [INTEGER] + NOT NULL + +trigger_id + + [INTEGER] + NOT NULL + +name + + [VARCHAR(1500)] + NOT NULL + + + +asset:id--asset_watcher:asset_id + +0..N +1 - + -dag_bundle_team - -dag_bundle_team - -dag_bundle_name - - [VARCHAR(250)] - NOT NULL - -team_id - - [UUID] - NOT NULL +dag_schedule_asset_reference + +dag_schedule_asset_reference + +asset_id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL - - -dag_bundle--dag_bundle_team - -0..N -1 + + +asset:id--dag_schedule_asset_reference:asset_id + +0..N +1 - + -dag - -dag - -dag_id - - [VARCHAR(250)] - NOT NULL - -asset_expression - - [JSON] - -bundle_name - - [VARCHAR(250)] - NOT NULL - -bundle_version - - [VARCHAR(200)] - -dag_display_name - - [VARCHAR(2000)] - -deadline - - [JSON] - -description - - [TEXT] - -fileloc - - [VARCHAR(2000)] - -has_import_errors - - [BOOLEAN] - -has_task_concurrency_limits - - [BOOLEAN] - NOT NULL - -is_paused - - [BOOLEAN] - -is_stale - - [BOOLEAN] - -last_expired - - [TIMESTAMP] - -last_parse_duration - - [DOUBLE_PRECISION] - -last_parsed_time - - [TIMESTAMP] - -max_active_runs - - [INTEGER] - -max_active_tasks - - [INTEGER] - NOT NULL - -max_consecutive_failed_dag_runs - - [INTEGER] - NOT NULL - -next_dagrun - - [TIMESTAMP] - -next_dagrun_create_after - - [TIMESTAMP] - -next_dagrun_data_interval_end - - [TIMESTAMP] - -next_dagrun_data_interval_start - - [TIMESTAMP] - -owners - - [VARCHAR(2000)] - -relative_fileloc - - [VARCHAR(2000)] - -timetable_description - - [VARCHAR(1000)] - -timetable_summary - - [TEXT] +task_inlet_asset_reference + +task_inlet_asset_reference + +asset_id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL - - -dag_bundle--dag - -0..N -1 + + +asset:id--task_inlet_asset_reference:asset_id + +0..N +1 + + + +task_outlet_asset_reference + +task_outlet_asset_reference + +asset_id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + + + +asset:id--task_outlet_asset_reference:asset_id + +0..N +1 + + + +asset_alias + +asset_alias + +id + + [INTEGER] + NOT NULL + +group + + [VARCHAR(1500)] + NOT NULL + +name + + [VARCHAR(1500)] + NOT NULL + + + +asset_alias:id--asset_alias_asset:alias_id + +0..N +1 + + + +asset_alias_asset_event + +asset_alias_asset_event + +alias_id + + [INTEGER] + NOT NULL + +event_id + + [INTEGER] + NOT NULL + + + +asset_alias:id--asset_alias_asset_event:alias_id + +0..N +1 - + dag_schedule_asset_alias_reference - -dag_schedule_asset_alias_reference - -alias_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + +dag_schedule_asset_alias_reference + +alias_id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL - - -dag--dag_schedule_asset_alias_reference - -0..N -1 + + +asset_alias:id--dag_schedule_asset_alias_reference:alias_id + +0..N +1 - - -dag_schedule_asset_reference - -dag_schedule_asset_reference - -asset_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + + +asset_event + +asset_event + +id + + [INTEGER] + NOT NULL + +asset_id + + [INTEGER] + NOT NULL + +extra + + [JSON] + NOT NULL + +partition_key + + [VARCHAR(250)] + +source_dag_id + + [VARCHAR(250)] + +source_map_index + + [INTEGER] + +source_run_id + + [VARCHAR(250)] + +source_task_id + + [VARCHAR(250)] + +timestamp + + [TIMESTAMP] + NOT NULL + + + +asset_event:id--asset_alias_asset_event:event_id + +0..N +1 + + + +dagrun_asset_event + +dagrun_asset_event + +dag_run_id + + [INTEGER] + NOT NULL + +event_id + + [INTEGER] + NOT NULL + + + +asset_event:id--dagrun_asset_event:event_id + +0..N +1 + + + +dag + +dag + +dag_id + + [VARCHAR(250)] + NOT NULL + +asset_expression + + [JSON] + +bundle_name + + [VARCHAR(250)] + NOT NULL + +bundle_version + + [VARCHAR(200)] + +dag_display_name + + [VARCHAR(2000)] + +deadline + + [JSON] + +description + + [TEXT] + +fail_fast + + [BOOLEAN] + NOT NULL + +fileloc + + [VARCHAR(2000)] + +has_import_errors + + [BOOLEAN] + NOT NULL + +has_task_concurrency_limits + + [BOOLEAN] + NOT NULL + +is_paused + + [BOOLEAN] + NOT NULL + +is_stale + + [BOOLEAN] + NOT NULL + +last_expired + + [TIMESTAMP] + +last_parse_duration + + [FLOAT] + +last_parsed_time + + [TIMESTAMP] + +max_active_runs + + [INTEGER] + +max_active_tasks + + [INTEGER] + NOT NULL + +max_consecutive_failed_dag_runs + + [INTEGER] + NOT NULL + +next_dagrun + + [TIMESTAMP] + +next_dagrun_create_after + + [TIMESTAMP] + +next_dagrun_data_interval_end + + [TIMESTAMP] + +next_dagrun_data_interval_start + + [TIMESTAMP] + +owners + + [VARCHAR(2000)] + +relative_fileloc + + [VARCHAR(2000)] + +timetable_description + + [VARCHAR(1000)] + +timetable_summary + + [TEXT] + + + +dag:dag_id--asset_dag_run_queue:target_dag_id + +0..N +1 - -dag--dag_schedule_asset_reference - -0..N -1 + +dag:dag_id--dag_schedule_asset_reference:dag_id + +0..N +1 - - -task_outlet_asset_reference - -task_outlet_asset_reference - -asset_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + + +dag:dag_id--task_inlet_asset_reference:dag_id + +0..N +1 - -dag--task_outlet_asset_reference - -0..N -1 + +dag:dag_id--task_outlet_asset_reference:dag_id + +0..N +1 - - -task_inlet_asset_reference - -task_inlet_asset_reference - -asset_id - - [INTEGER] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL + + +dag:dag_id--dag_schedule_asset_alias_reference:dag_id + +0..N +1 - - -dag--task_inlet_asset_reference - -0..N -1 + + +dag_version + +dag_version + +id + + [CHAR(32)] + NOT NULL + +bundle_name + + [VARCHAR(250)] + +bundle_version + + [VARCHAR(250)] + +created_at + + [TIMESTAMP] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +last_updated + + [TIMESTAMP] + NOT NULL + +version_number + + [INTEGER] + NOT NULL - - -asset_dag_run_queue - -asset_dag_run_queue - -asset_id - - [INTEGER] - NOT NULL - -target_dag_id - - [VARCHAR(250)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL + + +dag:dag_id--dag_version:dag_id + +0..N +1 - - -dag--asset_dag_run_queue - -0..N -1 + + +dag_favorite + +dag_favorite + +dag_id + + [VARCHAR(250)] + NOT NULL + +user_id + + [VARCHAR(250)] + NOT NULL + + + +dag:dag_id--dag_favorite:dag_id + +0..N +1 + + + +dag_owner_attributes + +dag_owner_attributes + +dag_id + + [VARCHAR(250)] + NOT NULL + +owner + + [VARCHAR(500)] + NOT NULL + +link + + [VARCHAR(500)] + NOT NULL + + + +dag:dag_id--dag_owner_attributes:dag_id + +0..N +1 - + dag_schedule_asset_name_reference - -dag_schedule_asset_name_reference - -dag_id - - [VARCHAR(250)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL + +dag_schedule_asset_name_reference + +dag_id + + [VARCHAR(250)] + NOT NULL + +name + + [VARCHAR(1500)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL - -dag--dag_schedule_asset_name_reference - -0..N -1 + +dag:dag_id--dag_schedule_asset_name_reference:dag_id + +0..N +1 - + dag_schedule_asset_uri_reference - -dag_schedule_asset_uri_reference - -dag_id - - [VARCHAR(250)] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL + +dag_schedule_asset_uri_reference + +dag_id + + [VARCHAR(250)] + NOT NULL + +uri + + [VARCHAR(1500)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL - -dag--dag_schedule_asset_uri_reference - -0..N -1 - - - -dag_version - -dag_version - -id - - [UUID] - NOT NULL - -bundle_name - - [VARCHAR(250)] - -bundle_version - - [VARCHAR(250)] - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -last_updated - - [TIMESTAMP] - NOT NULL - -version_number - - [INTEGER] - NOT NULL - - - -dag--dag_version - -0..N -1 + +dag:dag_id--dag_schedule_asset_uri_reference:dag_id + +0..N +1 - + dag_tag - -dag_tag - -dag_id - - [VARCHAR(250)] - NOT NULL - -name - - [VARCHAR(100)] - NOT NULL + +dag_tag + +dag_id + + [VARCHAR(250)] + NOT NULL + +name + + [VARCHAR(100)] + NOT NULL - -dag--dag_tag - -0..N -1 - - - -dag_owner_attributes - -dag_owner_attributes - -dag_id - - [VARCHAR(250)] - NOT NULL - -owner - - [VARCHAR(500)] - NOT NULL - -link - - [VARCHAR(500)] - NOT NULL - - - -dag--dag_owner_attributes - -0..N -1 + +dag:dag_id--dag_tag:dag_id + +0..N +1 - + dag_warning - -dag_warning - -dag_id - - [VARCHAR(250)] - NOT NULL - -warning_type - - [VARCHAR(50)] - NOT NULL - -message - - [TEXT] - NOT NULL - -timestamp - - [TIMESTAMP] - NOT NULL + +dag_warning + +dag_id + + [VARCHAR(250)] + NOT NULL + +warning_type + + [VARCHAR(50)] + NOT NULL + +message + + [TEXT] + NOT NULL + +timestamp + + [TIMESTAMP] + NOT NULL - -dag--dag_warning - -0..N -1 + +dag:dag_id--dag_warning:dag_id + +0..N +1 - - -dag_favorite - -dag_favorite - -dag_id - - [VARCHAR(250)] - NOT NULL - -user_id - - [VARCHAR(250)] - NOT NULL + + +dag_run + +dag_run + +id + + [INTEGER] + NOT NULL + +backfill_id + + [INTEGER] + +bundle_version + + [VARCHAR(250)] + +clear_number + + [INTEGER] + NOT NULL + +conf + + [JSON] + +context_carrier + + [JSON] + +created_dag_version_id + + [CHAR(32)] + +creating_job_id + + [INTEGER] + +dag_id + + [VARCHAR(250)] + NOT NULL + +data_interval_end + + [TIMESTAMP] + +data_interval_start + + [TIMESTAMP] + +end_date + + [TIMESTAMP] + +last_scheduling_decision + + [TIMESTAMP] + +log_template_id + + [INTEGER] + NOT NULL + +logical_date + + [TIMESTAMP] + +partition_key + + [VARCHAR(250)] + +queued_at + + [TIMESTAMP] + +run_after + + [TIMESTAMP] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +run_type + + [VARCHAR(50)] + NOT NULL + +scheduled_by_job_id + + [INTEGER] + +span_status + + [VARCHAR(250)] + NOT NULL + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(50)] + NOT NULL + +triggered_by + + [VARCHAR(50)] + +triggering_user_name + + [VARCHAR(512)] + +updated_at + + [TIMESTAMP] + NOT NULL - - -dag--dag_favorite - -0..N -1 + + +dag_version:id--dag_run:created_dag_version_id + +0..N +{0,1} - - -team - -team - -id - - [UUID] - NOT NULL - -name - - [VARCHAR(50)] - NOT NULL + + +task_instance + +task_instance + +id + + [VARCHAR(36)] + NOT NULL + +context_carrier + + [JSON] + +custom_operator_name + + [VARCHAR(1000)] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_version_id + + [CHAR(32)] + +duration + + [FLOAT] + +end_date + + [TIMESTAMP] + +executor + + [VARCHAR(1000)] + +executor_config + + [BLOB] + NOT NULL + +external_executor_id + + [VARCHAR(250)] + +hostname + + [VARCHAR(1000)] + NOT NULL + +last_heartbeat_at + + [TIMESTAMP] + +map_index + + [INTEGER] + NOT NULL + +max_tries + + [INTEGER] + NOT NULL + +next_kwargs + + [JSON] + +next_method + + [VARCHAR(1000)] + +operator + + [VARCHAR(1000)] + +pid + + [INTEGER] + +pool + + [VARCHAR(256)] + NOT NULL + +pool_slots + + [INTEGER] + NOT NULL + +priority_weight + + [INTEGER] + NOT NULL + +queue + + [VARCHAR(256)] + NOT NULL + +queued_by_job_id + + [INTEGER] + +queued_dttm + + [TIMESTAMP] + +rendered_map_index + + [VARCHAR(250)] + +run_id + + [VARCHAR(250)] + NOT NULL + +scheduled_dttm + + [TIMESTAMP] + +span_status + + [VARCHAR(250)] + NOT NULL + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(20)] + +task_display_name + + [VARCHAR(2000)] + +task_id + + [VARCHAR(250)] + NOT NULL + +trigger_id + + [INTEGER] + +trigger_timeout + + [TIMESTAMP] + +try_number + + [INTEGER] + NOT NULL + +unixname + + [VARCHAR(1000)] + NOT NULL + +updated_at + + [TIMESTAMP] - - -team--dag_bundle_team - -0..N -1 + + +dag_version:id--task_instance:dag_version_id + +0..N +{0,1} - - -connection - -connection - -id - - [INTEGER] - NOT NULL - -conn_id - - [VARCHAR(250)] - NOT NULL - -conn_type - - [VARCHAR(500)] - NOT NULL - -description - - [TEXT] - -extra - - [TEXT] - -host - - [VARCHAR(500)] - -is_encrypted - - [BOOLEAN] - -is_extra_encrypted - - [BOOLEAN] - -login - - [TEXT] - -password - - [TEXT] - -port - - [INTEGER] - -schema - - [VARCHAR(500)] - -team_id - - [UUID] + + +dag_code + +dag_code + +id + + [CHAR(32)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_version_id + + [CHAR(32)] + NOT NULL + +fileloc + + [VARCHAR(2000)] + NOT NULL + +last_updated + + [TIMESTAMP] + NOT NULL + +source_code + + [TEXT] + NOT NULL + +source_code_hash + + [VARCHAR(32)] + NOT NULL - - -team--connection - -0..N -{0,1} + + +dag_version:id--dag_code:dag_version_id + +0..N +1 - - -variable - -variable - -id - - [INTEGER] - NOT NULL - -description - - [TEXT] - -is_encrypted - - [BOOLEAN] - -key - - [VARCHAR(250)] - -team_id - - [UUID] - -val - - [TEXT] + + +serialized_dag + +serialized_dag + +id + + [CHAR(32)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +dag_hash + + [VARCHAR(32)] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_version_id + + [CHAR(32)] + NOT NULL + +data + + [JSON] + +data_compressed + + [BLOB] + +last_updated + + [TIMESTAMP] + NOT NULL - - -team--variable - -0..N -{0,1} + + +dag_version:id--serialized_dag:dag_version_id + +0..N +1 - - -slot_pool - -slot_pool - -id - - [INTEGER] - NOT NULL - -description - - [TEXT] - -include_deferred - - [BOOLEAN] - NOT NULL - -pool - - [VARCHAR(256)] - -slots - - [INTEGER] - -team_id - - [UUID] + + +dag_bundle + +dag_bundle + +name + + [VARCHAR(250)] + NOT NULL + +active + + [BOOLEAN] + +last_refreshed + + [TIMESTAMP] + +signed_url_template + + [VARCHAR(200)] + +template_params + + [TEXT] + +version + + [VARCHAR(200)] - - -team--slot_pool - -0..N -{0,1} + + +dag_bundle:name--dag:bundle_name + +0..N +1 - - -asset_alias - -asset_alias - -id - - [INTEGER] - NOT NULL - -group - - [VARCHAR(1500)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL + + +dag_bundle_team + +dag_bundle_team + +dag_bundle_name + + [VARCHAR(250)] + NOT NULL + +team_id + + [CHAR(32)] + NOT NULL - - -asset_alias_asset - -asset_alias_asset - -alias_id - - [INTEGER] - NOT NULL - -asset_id - - [INTEGER] - NOT NULL + + +dag_bundle:name--dag_bundle_team:dag_bundle_name + +0..N +1 - - -asset_alias--asset_alias_asset - -0..N -1 + + +asset_partition_dag_run + +asset_partition_dag_run + +id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +created_dag_run_id + + [INTEGER] + +partition_key + + [VARCHAR(250)] + NOT NULL + +target_dag_id + + [VARCHAR(250)] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL - - -asset_alias_asset_event - -asset_alias_asset_event - -alias_id - - [INTEGER] - NOT NULL - -event_id - - [INTEGER] - NOT NULL + + +dag_run:id--dagrun_asset_event:dag_run_id + +0..N +1 - - -asset_alias--asset_alias_asset_event - -0..N -1 + + +dag_run:id--asset_partition_dag_run:created_dag_run_id + +0..N +{0,1} - - -asset_alias--dag_schedule_asset_alias_reference - -0..N -1 + + +backfill_dag_run + +backfill_dag_run + +id + + [INTEGER] + NOT NULL + +backfill_id + + [INTEGER] + NOT NULL + +dag_run_id + + [INTEGER] + +exception_reason + + [VARCHAR(250)] + +logical_date + + [TIMESTAMP] + NOT NULL + +sort_ordinal + + [INTEGER] + NOT NULL - - -asset - -asset - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -extra - - [JSON] - NOT NULL - -group - - [VARCHAR(1500)] - NOT NULL - -name - - [VARCHAR(1500)] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL + + +dag_run:id--backfill_dag_run:dag_run_id + +0..N +{0,1} - - -asset--asset_alias_asset - -0..N -1 + + +dag_run_note + +dag_run_note + +dag_run_id + + [INTEGER] + NOT NULL + +content + + [VARCHAR(1000)] + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + +user_id + + [VARCHAR(128)] - - -asset_trigger - -asset_trigger - -asset_id - - [INTEGER] - NOT NULL - -trigger_id - - [INTEGER] - NOT NULL - - - -asset--asset_trigger - -0..N -1 + + +dag_run:id--dag_run_note:dag_run_id + +1 +1 - - -asset_active - -asset_active - -name - - [VARCHAR(1500)] - NOT NULL - -uri - - [VARCHAR(1500)] - NOT NULL + + +deadline + +deadline + +id + + [CHAR(32)] + NOT NULL + +callback_id + + [CHAR(32)] + NOT NULL + +dagrun_id + + [INTEGER] + +deadline_time + + [TIMESTAMP] + NOT NULL + +missed + + [BOOLEAN] + NOT NULL - - -asset--asset_active - -1 -1 + + +dag_run:id--deadline:dagrun_id + +0..N +{0,1} - - -asset--asset_active - -1 -1 + + +dag_run:run_id--task_instance:run_id + +0..N +1 - - -asset--dag_schedule_asset_reference - -0..N -1 + + +dag_run:dag_id--task_instance:dag_id + +0..N +1 - - -asset--task_outlet_asset_reference - -0..N -1 + + +hitl_detail + +hitl_detail + +ti_id + + [VARCHAR(36)] + NOT NULL + +assignees + + [JSON] + +body + + [TEXT] + +chosen_options + + [JSON] + +created_at + + [TIMESTAMP] + NOT NULL + +defaults + + [JSON] + +multiple + + [BOOLEAN] + +options + + [JSON] + NOT NULL + +params + + [JSON] + NOT NULL + +params_input + + [JSON] + NOT NULL + +responded_at + + [TIMESTAMP] + +responded_by + + [JSON] + +subject + + [TEXT] + NOT NULL - - -asset--task_inlet_asset_reference - -0..N -1 + + +task_instance:id--hitl_detail:ti_id + +1 +1 - - -asset--asset_dag_run_queue - -0..N -1 + + +task_instance_history + +task_instance_history + +task_instance_id + + [VARCHAR(36)] + NOT NULL + +context_carrier + + [JSON] + +custom_operator_name + + [VARCHAR(1000)] + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_version_id + + [CHAR(32)] + +duration + + [FLOAT] + +end_date + + [TIMESTAMP] + +executor + + [VARCHAR(1000)] + +executor_config + + [BLOB] + +external_executor_id + + [VARCHAR(250)] + +hostname + + [VARCHAR(1000)] + +map_index + + [INTEGER] + NOT NULL + +max_tries + + [INTEGER] + +next_kwargs + + [JSON] + +next_method + + [VARCHAR(1000)] + +operator + + [VARCHAR(1000)] + +pid + + [INTEGER] + +pool + + [VARCHAR(256)] + NOT NULL + +pool_slots + + [INTEGER] + NOT NULL + +priority_weight + + [INTEGER] + +queue + + [VARCHAR(256)] + +queued_by_job_id + + [INTEGER] + +queued_dttm + + [TIMESTAMP] + +rendered_map_index + + [VARCHAR(250)] + +run_id + + [VARCHAR(250)] + NOT NULL + +scheduled_dttm + + [TIMESTAMP] + +span_status + + [VARCHAR(250)] + NOT NULL + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(20)] + +task_display_name + + [VARCHAR(2000)] + +task_id + + [VARCHAR(250)] + NOT NULL + +trigger_id + + [INTEGER] + +trigger_timeout + + [DATETIME] + +try_number + + [INTEGER] + NOT NULL + +unixname + + [VARCHAR(1000)] + +updated_at + + [TIMESTAMP] - - -asset_event - -asset_event - -id - - [INTEGER] - NOT NULL - -asset_id - - [INTEGER] - NOT NULL - -extra - - [JSON] - NOT NULL - -source_dag_id - - [VARCHAR(250)] - -source_map_index - - [INTEGER] - -source_run_id - - [VARCHAR(250)] - -source_task_id - - [VARCHAR(250)] - -timestamp - - [TIMESTAMP] - NOT NULL + + +task_instance:run_id--task_instance_history:run_id + +0..N +1 - - -asset_event--asset_alias_asset_event - -0..N -1 + + +task_instance:task_id--task_instance_history:task_id + +0..N +1 - - -dagrun_asset_event - -dagrun_asset_event - -dag_run_id - - [INTEGER] - NOT NULL - -event_id - - [INTEGER] - NOT NULL + + +task_instance:map_index--task_instance_history:map_index + +0..N +1 - - -asset_event--dagrun_asset_event - -0..N -1 + + +task_instance:dag_id--task_instance_history:dag_id + +0..N +1 - - -trigger - -trigger - -id - - [INTEGER] - NOT NULL - -classpath - - [VARCHAR(1000)] - NOT NULL - -created_date - - [TIMESTAMP] - NOT NULL - -kwargs - - [TEXT] - NOT NULL - -triggerer_id - - [INTEGER] - - - -trigger--asset_trigger - -0..N -1 + + +rendered_task_instance_fields + +rendered_task_instance_fields + +dag_id + + [VARCHAR(250)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +k8s_pod_yaml + + [JSON] + +rendered_fields + + [JSON] + NOT NULL - - -task_instance - -task_instance - -id - - [UUID] - NOT NULL - -context_carrier - - [JSONB] - -custom_operator_name - - [VARCHAR(1000)] - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - -duration - - [DOUBLE_PRECISION] - -end_date - - [TIMESTAMP] - -executor - - [VARCHAR(1000)] - -executor_config - - [BYTEA] - -external_executor_id - - [VARCHAR(250)] - -hostname - - [VARCHAR(1000)] - -last_heartbeat_at - - [TIMESTAMP] - -map_index - - [INTEGER] - NOT NULL - -max_tries - - [INTEGER] - -next_kwargs - - [JSONB] - -next_method - - [VARCHAR(1000)] - -operator - - [VARCHAR(1000)] - -pid - - [INTEGER] - -pool - - [VARCHAR(256)] - NOT NULL - -pool_slots - - [INTEGER] - NOT NULL - -priority_weight - - [INTEGER] - -queue - - [VARCHAR(256)] - -queued_by_job_id - - [INTEGER] - -queued_dttm - - [TIMESTAMP] - -rendered_map_index - - [VARCHAR(250)] - -run_id - - [VARCHAR(250)] - NOT NULL - -scheduled_dttm - - [TIMESTAMP] - -span_status - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -task_display_name - - [VARCHAR(2000)] - -task_id - - [VARCHAR(250)] - NOT NULL - -trigger_id - - [INTEGER] - -trigger_timeout - - [TIMESTAMP] - -try_number - - [INTEGER] - -unixname - - [VARCHAR(1000)] - -updated_at - - [TIMESTAMP] + + +task_instance:dag_id--rendered_task_instance_fields:dag_id + +0..N +1 - - -trigger--task_instance - -0..N -{0,1} + + +task_instance:map_index--rendered_task_instance_fields:map_index + +0..N +1 - - -deadline - -deadline - -id - - [UUID] - NOT NULL - -callback - - [JSON] - NOT NULL - -callback_state - - [VARCHAR(20)] - -dagrun_id - - [INTEGER] - -deadline_time - - [TIMESTAMP] - NOT NULL - -trigger_id - - [INTEGER] - - - -trigger--deadline - -0..N -{0,1} + + +task_instance:run_id--rendered_task_instance_fields:run_id + +0..N +1 - - -hitl_detail - -hitl_detail - -ti_id - - [UUID] - NOT NULL - -body - - [TEXT] - -chosen_options - - [JSON] - -defaults - - [JSON] - -multiple - - [BOOLEAN] - -options - - [JSON] - NOT NULL - -params - - [JSON] - NOT NULL - -params_input - - [JSON] - NOT NULL - -responded_user_id - - [VARCHAR(128)] - -responded_user_name - - [VARCHAR(128)] - -respondents - - [JSON] - -response_at - - [TIMESTAMP] - -subject - - [TEXT] - NOT NULL + + +task_instance:task_id--rendered_task_instance_fields:task_id + +0..N +1 - - -task_instance--hitl_detail - -1 -1 + + +task_instance_note + +task_instance_note + +ti_id + + [VARCHAR(36)] + NOT NULL + +content + + [VARCHAR(1000)] + +created_at + + [TIMESTAMP] + NOT NULL + +updated_at + + [TIMESTAMP] + NOT NULL + +user_id + + [VARCHAR(128)] + + + +task_instance:id--task_instance_note:ti_id + +1 +1 - + task_map - -task_map - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -keys - - [JSONB] - -length - - [INTEGER] - NOT NULL + +task_map + +dag_id + + [VARCHAR(250)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +keys + + [JSON] + +length + + [INTEGER] + NOT NULL - -task_instance--task_map - -0..N -1 + +task_instance:dag_id--task_map:dag_id + +0..N +1 - -task_instance--task_map - -0..N -1 + +task_instance:run_id--task_map:run_id + +0..N +1 - -task_instance--task_map - -0..N -1 + +task_instance:task_id--task_map:task_id + +0..N +1 - -task_instance--task_map - -0..N -1 + +task_instance:map_index--task_map:map_index + +0..N +1 - + task_reschedule - -task_reschedule - -id - - [INTEGER] - NOT NULL - -duration - - [INTEGER] - NOT NULL - -end_date - - [TIMESTAMP] - NOT NULL - -reschedule_date - - [TIMESTAMP] - NOT NULL - -start_date - - [TIMESTAMP] - NOT NULL - -ti_id - - [UUID] - NOT NULL + +task_reschedule + +id + + [INTEGER] + NOT NULL + +duration + + [INTEGER] + NOT NULL + +end_date + + [TIMESTAMP] + NOT NULL + +reschedule_date + + [TIMESTAMP] + NOT NULL + +start_date + + [TIMESTAMP] + NOT NULL + +ti_id + + [VARCHAR(36)] + NOT NULL - -task_instance--task_reschedule - -0..N -1 + +task_instance:id--task_reschedule:ti_id + +0..N +1 - + xcom - -xcom - -dag_run_id - - [INTEGER] - NOT NULL - -key - - [VARCHAR(512)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -timestamp - - [TIMESTAMP] - NOT NULL - -value - - [JSONB] + +xcom + +dag_run_id + + [INTEGER] + NOT NULL + +key + + [VARCHAR(512)] + NOT NULL + +map_index + + [INTEGER] + NOT NULL + +task_id + + [VARCHAR(250)] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +run_id + + [VARCHAR(250)] + NOT NULL + +timestamp + + [TIMESTAMP] + NOT NULL + +value + + [JSON] - -task_instance--xcom - -0..N -1 + +task_instance:dag_id--xcom:dag_id + +0..N +1 - -task_instance--xcom - -0..N -1 + +task_instance:run_id--xcom:run_id + +0..N +1 - -task_instance--xcom - -0..N -1 + +task_instance:task_id--xcom:task_id + +0..N +1 - -task_instance--xcom - -0..N -1 + +task_instance:map_index--xcom:map_index + +0..N +1 - - -task_instance_note - -task_instance_note - -ti_id - - [UUID] - NOT NULL - -content - - [VARCHAR(1000)] - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -user_id - - [VARCHAR(128)] - - - -task_instance--task_instance_note - -1 -1 - - - -task_instance_history - -task_instance_history - -task_instance_id - - [UUID] - NOT NULL - -context_carrier - - [JSONB] - -custom_operator_name - - [VARCHAR(1000)] - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - -duration - - [DOUBLE_PRECISION] - -end_date - - [TIMESTAMP] - -executor - - [VARCHAR(1000)] - -executor_config - - [BYTEA] - -external_executor_id - - [VARCHAR(250)] - -hostname - - [VARCHAR(1000)] - -map_index - - [INTEGER] - NOT NULL - -max_tries - - [INTEGER] - -next_kwargs - - [JSONB] - -next_method - - [VARCHAR(1000)] - -operator - - [VARCHAR(1000)] - -pid - - [INTEGER] - -pool - - [VARCHAR(256)] - NOT NULL - -pool_slots - - [INTEGER] - NOT NULL - -priority_weight - - [INTEGER] - -queue - - [VARCHAR(256)] - -queued_by_job_id - - [INTEGER] - -queued_dttm - - [TIMESTAMP] - -rendered_map_index - - [VARCHAR(250)] - -run_id - - [VARCHAR(250)] - NOT NULL - -scheduled_dttm - - [TIMESTAMP] - -span_status - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(20)] - -task_display_name - - [VARCHAR(2000)] - -task_id - - [VARCHAR(250)] - NOT NULL - -trigger_id - - [INTEGER] - -trigger_timeout - - [TIMESTAMP] - -try_number - - [INTEGER] - NOT NULL - -unixname - - [VARCHAR(1000)] - -updated_at - - [TIMESTAMP] - - - -task_instance--task_instance_history - -0..N -1 - - - -task_instance--task_instance_history - -0..N -1 - - - -task_instance--task_instance_history - -0..N -1 - - - -task_instance--task_instance_history - -0..N -1 + + +log_template + +log_template + +id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +elasticsearch_id + + [TEXT] + NOT NULL + +filename + + [TEXT] + NOT NULL - - -rendered_task_instance_fields - -rendered_task_instance_fields - -dag_id - - [VARCHAR(250)] - NOT NULL - -map_index - - [INTEGER] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -task_id - - [VARCHAR(250)] - NOT NULL - -k8s_pod_yaml - - [JSON] - -rendered_fields - - [JSON] - NOT NULL + + +log_template:id--dag_run:log_template_id + +0..N +1 - - -task_instance--rendered_task_instance_fields - -0..N -1 + + +backfill + +backfill + +id + + [INTEGER] + NOT NULL + +completed_at + + [TIMESTAMP] + +created_at + + [TIMESTAMP] + NOT NULL + +dag_id + + [VARCHAR(250)] + NOT NULL + +dag_run_conf + + [JSON] + NOT NULL + +from_date + + [TIMESTAMP] + NOT NULL + +is_paused + + [BOOLEAN] + +max_active_runs + + [INTEGER] + NOT NULL + +reprocess_behavior + + [VARCHAR(250)] + NOT NULL + +to_date + + [TIMESTAMP] + NOT NULL + +triggering_user_name + + [VARCHAR(512)] + +updated_at + + [TIMESTAMP] + NOT NULL - - -task_instance--rendered_task_instance_fields - -0..N -1 + + +backfill:id--dag_run:backfill_id + +0..N +{0,1} - - -task_instance--rendered_task_instance_fields - -0..N -1 + + +backfill:id--backfill_dag_run:backfill_id + +0..N +1 - - -task_instance--rendered_task_instance_fields - -0..N -1 + + +trigger + +trigger + +id + + [INTEGER] + NOT NULL + +classpath + + [VARCHAR(1000)] + NOT NULL + +created_date + + [TIMESTAMP] + NOT NULL + +kwargs + + [TEXT] + NOT NULL + +triggerer_id + + [INTEGER] + + + +trigger:id--asset_watcher:trigger_id + +0..N +1 - - -dag_version--task_instance - -0..N -{0,1} + + +trigger:id--task_instance:trigger_id + +0..N +{0,1} - + -dag_run - -dag_run - -id - - [INTEGER] - NOT NULL - -backfill_id - - [INTEGER] - -bundle_version - - [VARCHAR(250)] - -clear_number - - [INTEGER] - NOT NULL - -conf - - [JSONB] - -context_carrier - - [JSONB] - -created_dag_version_id - - [UUID] - -creating_job_id - - [INTEGER] - -dag_id - - [VARCHAR(250)] - NOT NULL - -data_interval_end - - [TIMESTAMP] - -data_interval_start - - [TIMESTAMP] - -end_date - - [TIMESTAMP] - -last_scheduling_decision - - [TIMESTAMP] - -log_template_id - - [INTEGER] - -logical_date - - [TIMESTAMP] - -queued_at - - [TIMESTAMP] - -run_after - - [TIMESTAMP] - NOT NULL - -run_id - - [VARCHAR(250)] - NOT NULL - -run_type - - [VARCHAR(50)] - NOT NULL - -scheduled_by_job_id - - [INTEGER] - -span_status - - [VARCHAR(250)] - NOT NULL - -start_date - - [TIMESTAMP] - -state - - [VARCHAR(50)] - -triggered_by - - [VARCHAR(50)] - -triggering_user_name - - [VARCHAR(512)] - -updated_at - - [TIMESTAMP] +callback + +callback + +id + + [CHAR(32)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +data + + [JSON] + NOT NULL + +fetch_method + + [VARCHAR(20)] + NOT NULL + +output + + [TEXT] + +priority_weight + + [INTEGER] + NOT NULL + +state + + [VARCHAR(10)] + +trigger_id + + [INTEGER] + +type + + [VARCHAR(20)] + NOT NULL + + + +trigger:id--callback:trigger_id + +0..N +{0,1} - - -dag_version--dag_run - -0..N -{0,1} + + +callback:id--deadline:callback_id + +0..N +1 - + -dag_code - -dag_code - -id - - [UUID] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - NOT NULL - -fileloc - - [VARCHAR(2000)] - NOT NULL - -last_updated - - [TIMESTAMP] - NOT NULL - -source_code - - [TEXT] - NOT NULL - -source_code_hash - - [VARCHAR(32)] - NOT NULL - - - -dag_version--dag_code - -0..N -1 +callback_request + +callback_request + +id + + [INTEGER] + NOT NULL + +callback_data + + [JSON] + NOT NULL + +callback_type + + [VARCHAR(20)] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +priority_weight + + [INTEGER] + NOT NULL - + -serialized_dag - -serialized_dag - -id - - [UUID] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -dag_hash - - [VARCHAR(32)] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_version_id - - [UUID] - NOT NULL - -data - - [JSON] - -data_compressed - - [BYTEA] - -last_updated - - [TIMESTAMP] - NOT NULL - - - -dag_version--serialized_dag - -0..N -1 - - - -dag_run--dagrun_asset_event - -0..N -1 +connection + +connection + +id + + [INTEGER] + NOT NULL + +conn_id + + [VARCHAR(250)] + NOT NULL + +conn_type + + [VARCHAR(500)] + NOT NULL + +description + + [VARCHAR(5000)] + +extra + + [TEXT] + +host + + [VARCHAR(500)] + +is_encrypted + + [BOOLEAN] + NOT NULL + +is_extra_encrypted + + [BOOLEAN] + NOT NULL + +login + + [TEXT] + +password + + [TEXT] + +port + + [INTEGER] + +schema + + [VARCHAR(500)] + +team_id + + [CHAR(32)] - - -dag_run--task_instance - -0..N -1 + + +team + +team + +id + + [CHAR(32)] + NOT NULL + +name + + [VARCHAR(50)] + NOT NULL - - -dag_run--task_instance - -0..N -1 + + +team:id--dag_bundle_team:team_id + +0..N +1 - - -dag_run--deadline - -0..N -{0,1} + + +team:id--connection:team_id + +0..N +{0,1} - + -backfill_dag_run - -backfill_dag_run - -id - - [INTEGER] - NOT NULL - -backfill_id - - [INTEGER] - NOT NULL - -dag_run_id - - [INTEGER] - -exception_reason - - [VARCHAR(250)] - -logical_date - - [TIMESTAMP] - NOT NULL - -sort_ordinal - - [INTEGER] - NOT NULL +slot_pool + +slot_pool + +id + + [INTEGER] + NOT NULL + +description + + [TEXT] + +include_deferred + + [BOOLEAN] + NOT NULL + +pool + + [VARCHAR(256)] + NOT NULL + +slots + + [INTEGER] + NOT NULL + +team_id + + [CHAR(32)] - - -dag_run--backfill_dag_run - -0..N -{0,1} + + +team:id--slot_pool:team_id + +0..N +{0,1} - + -dag_run_note - -dag_run_note - -dag_run_id - - [INTEGER] - NOT NULL - -content - - [VARCHAR(1000)] - -created_at - - [TIMESTAMP] - NOT NULL - -updated_at - - [TIMESTAMP] - NOT NULL - -user_id - - [VARCHAR(128)] - - - -dag_run--dag_run_note - -1 -1 - - - -log_template - -log_template - -id - - [INTEGER] - NOT NULL - -created_at - - [TIMESTAMP] - NOT NULL - -elasticsearch_id - - [TEXT] - NOT NULL - -filename - - [TEXT] - NOT NULL +variable + +variable + +id + + [INTEGER] + NOT NULL + +description + + [TEXT] + +is_encrypted + + [BOOLEAN] + NOT NULL + +key + + [VARCHAR(250)] + NOT NULL + +team_id + + [CHAR(32)] + +val + + [TEXT] + NOT NULL - - -log_template--dag_run - -0..N -{0,1} + + +team:id--variable:team_id + +0..N +{0,1} - + -backfill - -backfill - -id - - [INTEGER] - NOT NULL - -completed_at - - [TIMESTAMP] - -created_at - - [TIMESTAMP] - NOT NULL - -dag_id - - [VARCHAR(250)] - NOT NULL - -dag_run_conf - - [JSON] - NOT NULL - -from_date - - [TIMESTAMP] - NOT NULL - -is_paused - - [BOOLEAN] - -max_active_runs - - [INTEGER] - NOT NULL - -reprocess_behavior - - [VARCHAR(250)] - NOT NULL - -to_date - - [TIMESTAMP] - NOT NULL - -triggering_user_name - - [VARCHAR(512)] - -updated_at - - [TIMESTAMP] - NOT NULL +dag_priority_parsing_request + +dag_priority_parsing_request + +id + + [VARCHAR(32)] + NOT NULL + +bundle_name + + [VARCHAR(250)] + NOT NULL + +relative_fileloc + + [VARCHAR(2000)] + NOT NULL + + + +hitl_detail_history + +hitl_detail_history + +ti_history_id + + [VARCHAR(36)] + NOT NULL + +assignees + + [JSON] + +body + + [TEXT] + +chosen_options + + [JSON] + +created_at + + [TIMESTAMP] + NOT NULL + +defaults + + [JSON] + +multiple + + [BOOLEAN] + +options + + [JSON] + NOT NULL + +params + + [JSON] + NOT NULL + +params_input + + [JSON] + NOT NULL + +responded_at + + [TIMESTAMP] + +responded_by + + [JSON] + +subject + + [TEXT] + NOT NULL + + + +task_instance_history:task_instance_id--hitl_detail_history:ti_history_id + +1 +1 - - -backfill--dag_run - -0..N -{0,1} + + +import_error + +import_error + +id + + [INTEGER] + NOT NULL + +bundle_name + + [VARCHAR(250)] + +filename + + [VARCHAR(1024)] + +stacktrace + + [TEXT] + +timestamp + + [TIMESTAMP] - - -backfill--backfill_dag_run - -0..N -1 + + +job + +job + +id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + +end_date + + [TIMESTAMP] + +executor_class + + [VARCHAR(500)] + +hostname + + [VARCHAR(500)] + +job_type + + [VARCHAR(30)] + +latest_heartbeat + + [TIMESTAMP] + +start_date + + [TIMESTAMP] + +state + + [VARCHAR(20)] + +unixname + + [VARCHAR(1000)] - - -alembic_version - -alembic_version - -version_num - - [VARCHAR(32)] - NOT NULL + + +log + +log + +id + + [INTEGER] + NOT NULL + +dag_id + + [VARCHAR(250)] + +dttm + + [TIMESTAMP] + NOT NULL + +event + + [VARCHAR(60)] + NOT NULL + +extra + + [TEXT] + +logical_date + + [TIMESTAMP] + +map_index + + [INTEGER] + +owner + + [VARCHAR(500)] + +owner_display_name + + [VARCHAR(500)] + +run_id + + [VARCHAR(250)] + +task_id + + [VARCHAR(250)] + +try_number + + [INTEGER] + + + +partitioned_asset_key_log + +partitioned_asset_key_log + +id + + [INTEGER] + NOT NULL + +asset_event_id + + [INTEGER] + NOT NULL + +asset_id + + [INTEGER] + NOT NULL + +asset_partition_dag_run_id + + [INTEGER] + NOT NULL + +created_at + + [TIMESTAMP] + NOT NULL + +source_partition_key + + [VARCHAR(250)] + NOT NULL + +target_dag_id + + [VARCHAR(250)] + NOT NULL + +target_partition_key + + [VARCHAR(250)] + NOT NULL diff --git a/airflow-core/docs/img/apache.jpg b/airflow-core/docs/img/apache.jpg deleted file mode 100644 index 312251f8d22cd..0000000000000 Binary files a/airflow-core/docs/img/apache.jpg and /dev/null differ diff --git a/airflow-core/docs/img/asf_logo_wide.png b/airflow-core/docs/img/asf_logo_wide.png new file mode 100644 index 0000000000000..cef985efb0219 Binary files /dev/null and b/airflow-core/docs/img/asf_logo_wide.png differ diff --git a/airflow-core/docs/img/change-site-title/default_instance_name_configuration.png b/airflow-core/docs/img/change-site-title/default_instance_name_configuration.png index 70fb653a510d0..3a814111c0e0c 100644 Binary files a/airflow-core/docs/img/change-site-title/default_instance_name_configuration.png and b/airflow-core/docs/img/change-site-title/default_instance_name_configuration.png differ diff --git a/airflow-core/docs/img/change-site-title/example_instance_name_configuration.png b/airflow-core/docs/img/change-site-title/example_instance_name_configuration.png index 45ec5e51d5a2a..5084842ad1015 100644 Binary files a/airflow-core/docs/img/change-site-title/example_instance_name_configuration.png and b/airflow-core/docs/img/change-site-title/example_instance_name_configuration.png differ diff --git a/airflow-core/docs/img/hitl_approve_reject.png b/airflow-core/docs/img/hitl_approve_reject.png deleted file mode 100644 index 94963dbcc8e12..0000000000000 Binary files a/airflow-core/docs/img/hitl_approve_reject.png and /dev/null differ diff --git a/airflow-core/docs/img/hitl_branch_selected.png b/airflow-core/docs/img/hitl_branch_selected.png deleted file mode 100644 index 342a13c937628..0000000000000 Binary files a/airflow-core/docs/img/hitl_branch_selected.png and /dev/null differ diff --git a/airflow-core/docs/img/hitl_branch_selection.png b/airflow-core/docs/img/hitl_branch_selection.png deleted file mode 100644 index fb3fc472bcc2c..0000000000000 Binary files a/airflow-core/docs/img/hitl_branch_selection.png and /dev/null differ diff --git a/airflow-core/docs/img/hitl_wait_for_input.png b/airflow-core/docs/img/hitl_wait_for_input.png deleted file mode 100644 index 3d0ba69ddb42f..0000000000000 Binary files a/airflow-core/docs/img/hitl_wait_for_input.png and /dev/null differ diff --git a/airflow-core/docs/img/hitl_wait_for_multiple_options.png b/airflow-core/docs/img/hitl_wait_for_multiple_options.png deleted file mode 100644 index 549b168318e69..0000000000000 Binary files a/airflow-core/docs/img/hitl_wait_for_multiple_options.png and /dev/null differ diff --git a/airflow-core/docs/img/hitl_wait_for_option.png b/airflow-core/docs/img/hitl_wait_for_option.png deleted file mode 100644 index af348e5054cf9..0000000000000 Binary files a/airflow-core/docs/img/hitl_wait_for_option.png and /dev/null differ diff --git a/airflow-core/docs/img/operator_extra_link.png b/airflow-core/docs/img/operator_extra_link.png index 30302de841dd0..e4b0624a51ff5 100644 Binary files a/airflow-core/docs/img/operator_extra_link.png and b/airflow-core/docs/img/operator_extra_link.png differ diff --git a/airflow-core/docs/img/ui-dark/add-dag-tags.png b/airflow-core/docs/img/ui-dark/add-dag-tags.png index 640fc56597a76..b6740e3998041 100644 Binary files a/airflow-core/docs/img/ui-dark/add-dag-tags.png and b/airflow-core/docs/img/ui-dark/add-dag-tags.png differ diff --git a/airflow-core/docs/img/ui-dark/asset_view.png b/airflow-core/docs/img/ui-dark/asset_view.png index a42d0eca5d144..434bb15bc5d59 100644 Binary files a/airflow-core/docs/img/ui-dark/asset_view.png and b/airflow-core/docs/img/ui-dark/asset_view.png differ diff --git a/airflow-core/docs/img/ui-dark/backfill.png b/airflow-core/docs/img/ui-dark/backfill.png index 8b9a41c698d8d..53950b9698536 100644 Binary files a/airflow-core/docs/img/ui-dark/backfill.png and b/airflow-core/docs/img/ui-dark/backfill.png differ diff --git a/airflow-core/docs/img/ui-dark/basic_dag.png b/airflow-core/docs/img/ui-dark/basic_dag.png index f62203ea869b1..ad8d05899e89a 100644 Binary files a/airflow-core/docs/img/ui-dark/basic_dag.png and b/airflow-core/docs/img/ui-dark/basic_dag.png differ diff --git a/airflow-core/docs/img/ui-dark/branch_note.png b/airflow-core/docs/img/ui-dark/branch_note.png index 6e27b68a86b04..1faf58d7d432b 100644 Binary files a/airflow-core/docs/img/ui-dark/branch_note.png and b/airflow-core/docs/img/ui-dark/branch_note.png differ diff --git a/airflow-core/docs/img/ui-dark/branch_with_trigger.png b/airflow-core/docs/img/ui-dark/branch_with_trigger.png index 04467635005c8..3befb712cfe47 100644 Binary files a/airflow-core/docs/img/ui-dark/branch_with_trigger.png and b/airflow-core/docs/img/ui-dark/branch_with_trigger.png differ diff --git a/airflow-core/docs/img/ui-dark/branch_without_trigger.png b/airflow-core/docs/img/ui-dark/branch_without_trigger.png index 6c30d728ca892..c76153962190d 100644 Binary files a/airflow-core/docs/img/ui-dark/branch_without_trigger.png and b/airflow-core/docs/img/ui-dark/branch_without_trigger.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_doc.png b/airflow-core/docs/img/ui-dark/dag_doc.png index 0f048cd6ca6b7..f4bf08801f0e5 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_doc.png and b/airflow-core/docs/img/ui-dark/dag_doc.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_graph_external_conditions.png b/airflow-core/docs/img/ui-dark/dag_graph_external_conditions.png index 38537e1850bb7..55d3a3b135998 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_graph_external_conditions.png and b/airflow-core/docs/img/ui-dark/dag_graph_external_conditions.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_list.png b/airflow-core/docs/img/ui-dark/dag_list.png index c7b378a459470..a9de2f8082f4a 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_list.png and b/airflow-core/docs/img/ui-dark/dag_list.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_overview_details.png b/airflow-core/docs/img/ui-dark/dag_overview_details.png index c3c7222060011..0513d025c3b63 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_overview_details.png and b/airflow-core/docs/img/ui-dark/dag_overview_details.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_run_details.png b/airflow-core/docs/img/ui-dark/dag_run_details.png index fe0a1648d035a..2a3b30dc36d02 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_run_details.png and b/airflow-core/docs/img/ui-dark/dag_run_details.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_run_graph.png b/airflow-core/docs/img/ui-dark/dag_run_graph.png index 09b4b897458a3..954f1ced30c0e 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_run_graph.png and b/airflow-core/docs/img/ui-dark/dag_run_graph.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_run_task_instances.png b/airflow-core/docs/img/ui-dark/dag_run_task_instances.png index cebbae11c8b95..a33fed1ab5a0c 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_run_task_instances.png and b/airflow-core/docs/img/ui-dark/dag_run_task_instances.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_task_instance_details.png b/airflow-core/docs/img/ui-dark/dag_task_instance_details.png index d6dd08f9ec51e..28c17a41e1f8d 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_task_instance_details.png and b/airflow-core/docs/img/ui-dark/dag_task_instance_details.png differ diff --git a/airflow-core/docs/img/ui-dark/dag_trigger_window_single_run.png b/airflow-core/docs/img/ui-dark/dag_trigger_window_single_run.png index f5eeab007846c..2495730e8be8d 100644 Binary files a/airflow-core/docs/img/ui-dark/dag_trigger_window_single_run.png and b/airflow-core/docs/img/ui-dark/dag_trigger_window_single_run.png differ diff --git a/airflow-core/docs/img/ui-dark/dags.png b/airflow-core/docs/img/ui-dark/dags.png index 56458caedef62..75f28fd7281b0 100644 Binary files a/airflow-core/docs/img/ui-dark/dags.png and b/airflow-core/docs/img/ui-dark/dags.png differ diff --git a/airflow-core/docs/img/ui-dark/demo_complex_dag_overview_with_failed_tasks.png b/airflow-core/docs/img/ui-dark/demo_complex_dag_overview_with_failed_tasks.png new file mode 100644 index 0000000000000..67b5053d37380 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/demo_complex_dag_overview_with_failed_tasks.png differ diff --git a/airflow-core/docs/img/ui-dark/demo_dag_overview_with_failed_tasks.png b/airflow-core/docs/img/ui-dark/demo_dag_overview_with_failed_tasks.png deleted file mode 100644 index 86269a9103c1a..0000000000000 Binary files a/airflow-core/docs/img/ui-dark/demo_dag_overview_with_failed_tasks.png and /dev/null differ diff --git a/airflow-core/docs/img/ui-dark/demo_graph_and_code_view.png b/airflow-core/docs/img/ui-dark/demo_graph_and_code_view.png index c35615a2d7122..e69c59e0d0a15 100644 Binary files a/airflow-core/docs/img/ui-dark/demo_graph_and_code_view.png and b/airflow-core/docs/img/ui-dark/demo_graph_and_code_view.png differ diff --git a/airflow-core/docs/img/ui-dark/demo_grid_view_with_task_logs.png b/airflow-core/docs/img/ui-dark/demo_grid_view_with_task_logs.png index 36c6d28d59fda..e85e8f3664a66 100644 Binary files a/airflow-core/docs/img/ui-dark/demo_grid_view_with_task_logs.png and b/airflow-core/docs/img/ui-dark/demo_grid_view_with_task_logs.png differ diff --git a/airflow-core/docs/img/ui-dark/hitl_approve_reject.png b/airflow-core/docs/img/ui-dark/hitl_approve_reject.png new file mode 100644 index 0000000000000..97827d126f126 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/hitl_approve_reject.png differ diff --git a/airflow-core/docs/img/ui-dark/hitl_branch_selected.png b/airflow-core/docs/img/ui-dark/hitl_branch_selected.png new file mode 100644 index 0000000000000..d0da408e90429 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/hitl_branch_selected.png differ diff --git a/airflow-core/docs/img/ui-dark/hitl_branch_selection.png b/airflow-core/docs/img/ui-dark/hitl_branch_selection.png new file mode 100644 index 0000000000000..c194bb6bb83ad Binary files /dev/null and b/airflow-core/docs/img/ui-dark/hitl_branch_selection.png differ diff --git a/airflow-core/docs/img/ui-dark/hitl_wait_for_input.png b/airflow-core/docs/img/ui-dark/hitl_wait_for_input.png new file mode 100644 index 0000000000000..1cc50013c48ba Binary files /dev/null and b/airflow-core/docs/img/ui-dark/hitl_wait_for_input.png differ diff --git a/airflow-core/docs/img/ui-dark/hitl_wait_for_multiple_options.png b/airflow-core/docs/img/ui-dark/hitl_wait_for_multiple_options.png new file mode 100644 index 0000000000000..982de55eb5c44 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/hitl_wait_for_multiple_options.png differ diff --git a/airflow-core/docs/img/ui-dark/hitl_wait_for_option.png b/airflow-core/docs/img/ui-dark/hitl_wait_for_option.png new file mode 100644 index 0000000000000..64792bd3802e9 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/hitl_wait_for_option.png differ diff --git a/airflow-core/docs/img/ui-dark/task_doc.png b/airflow-core/docs/img/ui-dark/task_doc.png index bd9e7e1038df3..f2e7170ea403a 100644 Binary files a/airflow-core/docs/img/ui-dark/task_doc.png and b/airflow-core/docs/img/ui-dark/task_doc.png differ diff --git a/airflow-core/docs/img/ui-dark/task_instance_history.png b/airflow-core/docs/img/ui-dark/task_instance_history.png index 4755ac4038cf0..35d8c00a17ccf 100644 Binary files a/airflow-core/docs/img/ui-dark/task_instance_history.png and b/airflow-core/docs/img/ui-dark/task_instance_history.png differ diff --git a/airflow-core/docs/img/ui-dark/task_instance_history_log.png b/airflow-core/docs/img/ui-dark/task_instance_history_log.png index 36a45b06bdd6b..3d978c7919224 100644 Binary files a/airflow-core/docs/img/ui-dark/task_instance_history_log.png and b/airflow-core/docs/img/ui-dark/task_instance_history_log.png differ diff --git a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-1.png b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-1.png index abc253fa94cba..b4c093d0548c4 100644 Binary files a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-1.png and b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-1.png differ diff --git a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-2.png b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-2.png index c7551ff142140..2493fa57c009d 100644 Binary files a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-2.png and b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-2.png differ diff --git a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-3.png b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-3.png index 9c6c33ce9c2db..3e928ae0a1961 100644 Binary files a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-3.png and b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-3.png differ diff --git a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-4.png b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-4.png index f415de98148a4..6efae446e06b8 100644 Binary files a/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-4.png and b/airflow-core/docs/img/ui-dark/trigger-dag-tutorial-form-4.png differ diff --git a/airflow-core/docs/img/ui-dark/tutorial_pipeline_add_connection.png b/airflow-core/docs/img/ui-dark/tutorial_pipeline_add_connection.png index 51506890ac288..480f0ade8d6cb 100644 Binary files a/airflow-core/docs/img/ui-dark/tutorial_pipeline_add_connection.png and b/airflow-core/docs/img/ui-dark/tutorial_pipeline_add_connection.png differ diff --git a/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_list.png b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_list.png deleted file mode 100644 index 27311e1fd7443..0000000000000 Binary files a/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_list.png and /dev/null differ diff --git a/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_list_trigger.png b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_list_trigger.png new file mode 100644 index 0000000000000..6f89b1602d34c Binary files /dev/null and b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_list_trigger.png differ diff --git a/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_overview_processed.png b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_overview_processed.png index 5054139b3526f..dc7a137a4a088 100644 Binary files a/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_overview_processed.png and b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_overview_processed.png differ diff --git a/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_task_instance_logs.png b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_task_instance_logs.png new file mode 100644 index 0000000000000..559fa029ea7d8 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/tutorial_pipeline_dag_task_instance_logs.png differ diff --git a/airflow-core/docs/img/ui-dark/variable_hidden.png b/airflow-core/docs/img/ui-dark/variable_hidden.png index 44c8bcca227be..a1d92f72ddf48 100644 Binary files a/airflow-core/docs/img/ui-dark/variable_hidden.png and b/airflow-core/docs/img/ui-dark/variable_hidden.png differ diff --git a/airflow-core/docs/img/ui-light/asset_view.png b/airflow-core/docs/img/ui-light/asset_view.png index 69cc8ff5089ec..2f2d1dcde10aa 100644 Binary files a/airflow-core/docs/img/ui-light/asset_view.png and b/airflow-core/docs/img/ui-light/asset_view.png differ diff --git a/airflow-core/docs/img/ui-light/backfill.png b/airflow-core/docs/img/ui-light/backfill.png index 0eab89ae7b800..4d8dc84f2bd90 100644 Binary files a/airflow-core/docs/img/ui-light/backfill.png and b/airflow-core/docs/img/ui-light/backfill.png differ diff --git a/airflow-core/docs/img/ui-light/basic_dag.png b/airflow-core/docs/img/ui-light/basic_dag.png index 458ee61544bba..1cdaa57482d57 100644 Binary files a/airflow-core/docs/img/ui-light/basic_dag.png and b/airflow-core/docs/img/ui-light/basic_dag.png differ diff --git a/airflow-core/docs/img/ui-light/branch_note.png b/airflow-core/docs/img/ui-light/branch_note.png index 0a3d923d1a906..a7efb2c1b620d 100644 Binary files a/airflow-core/docs/img/ui-light/branch_note.png and b/airflow-core/docs/img/ui-light/branch_note.png differ diff --git a/airflow-core/docs/img/ui-light/branch_with_trigger.png b/airflow-core/docs/img/ui-light/branch_with_trigger.png index eb171565321ad..80124a4c3041f 100644 Binary files a/airflow-core/docs/img/ui-light/branch_with_trigger.png and b/airflow-core/docs/img/ui-light/branch_with_trigger.png differ diff --git a/airflow-core/docs/img/ui-light/branch_without_trigger.png b/airflow-core/docs/img/ui-light/branch_without_trigger.png index c2d726506c50a..48b9d2e468eb1 100644 Binary files a/airflow-core/docs/img/ui-light/branch_without_trigger.png and b/airflow-core/docs/img/ui-light/branch_without_trigger.png differ diff --git a/airflow-core/docs/img/ui-light/dag_doc.png b/airflow-core/docs/img/ui-light/dag_doc.png index 3b94e97d3f270..478cb6e310c58 100644 Binary files a/airflow-core/docs/img/ui-light/dag_doc.png and b/airflow-core/docs/img/ui-light/dag_doc.png differ diff --git a/airflow-core/docs/img/ui-light/dag_graph_external_conditions.png b/airflow-core/docs/img/ui-light/dag_graph_external_conditions.png index 1c4b2e0d69ecc..c274b0ef65f32 100644 Binary files a/airflow-core/docs/img/ui-light/dag_graph_external_conditions.png and b/airflow-core/docs/img/ui-light/dag_graph_external_conditions.png differ diff --git a/airflow-core/docs/img/ui-light/dag_list.png b/airflow-core/docs/img/ui-light/dag_list.png index ceb1065dcc468..3be956f3e57a6 100644 Binary files a/airflow-core/docs/img/ui-light/dag_list.png and b/airflow-core/docs/img/ui-light/dag_list.png differ diff --git a/airflow-core/docs/img/ui-light/dag_overview_details.png b/airflow-core/docs/img/ui-light/dag_overview_details.png index cfb18a3cfca23..5bc67c283cbb9 100644 Binary files a/airflow-core/docs/img/ui-light/dag_overview_details.png and b/airflow-core/docs/img/ui-light/dag_overview_details.png differ diff --git a/airflow-core/docs/img/ui-light/dag_run_details.png b/airflow-core/docs/img/ui-light/dag_run_details.png index b0a271e2342dc..00fbd5b219acf 100644 Binary files a/airflow-core/docs/img/ui-light/dag_run_details.png and b/airflow-core/docs/img/ui-light/dag_run_details.png differ diff --git a/airflow-core/docs/img/ui-light/dag_run_graph.png b/airflow-core/docs/img/ui-light/dag_run_graph.png index 31dccef7612ee..4ca25568b78c7 100644 Binary files a/airflow-core/docs/img/ui-light/dag_run_graph.png and b/airflow-core/docs/img/ui-light/dag_run_graph.png differ diff --git a/airflow-core/docs/img/ui-light/dag_run_task_instances.png b/airflow-core/docs/img/ui-light/dag_run_task_instances.png index 42c71c958dec1..4576972351ff2 100644 Binary files a/airflow-core/docs/img/ui-light/dag_run_task_instances.png and b/airflow-core/docs/img/ui-light/dag_run_task_instances.png differ diff --git a/airflow-core/docs/img/ui-light/dag_task_instance_details.png b/airflow-core/docs/img/ui-light/dag_task_instance_details.png index e2a3be93226ba..86e42f2ec3a77 100644 Binary files a/airflow-core/docs/img/ui-light/dag_task_instance_details.png and b/airflow-core/docs/img/ui-light/dag_task_instance_details.png differ diff --git a/airflow-core/docs/img/ui-light/dag_trigger_window_single_run.png b/airflow-core/docs/img/ui-light/dag_trigger_window_single_run.png index 60062d5960aaa..a0fe4eec40c85 100644 Binary files a/airflow-core/docs/img/ui-light/dag_trigger_window_single_run.png and b/airflow-core/docs/img/ui-light/dag_trigger_window_single_run.png differ diff --git a/airflow-core/docs/img/ui-light/dags.png b/airflow-core/docs/img/ui-light/dags.png index 0c408515fb172..1d2e168a8e17f 100644 Binary files a/airflow-core/docs/img/ui-light/dags.png and b/airflow-core/docs/img/ui-light/dags.png differ diff --git a/airflow-core/docs/img/ui-light/demo_complex_dag_overview_with_failed_tasks.png b/airflow-core/docs/img/ui-light/demo_complex_dag_overview_with_failed_tasks.png new file mode 100644 index 0000000000000..bb459b5c6c92d Binary files /dev/null and b/airflow-core/docs/img/ui-light/demo_complex_dag_overview_with_failed_tasks.png differ diff --git a/airflow-core/docs/img/ui-light/demo_dag_overview_with_failed_tasks.png b/airflow-core/docs/img/ui-light/demo_dag_overview_with_failed_tasks.png deleted file mode 100644 index 18ccd3c11c912..0000000000000 Binary files a/airflow-core/docs/img/ui-light/demo_dag_overview_with_failed_tasks.png and /dev/null differ diff --git a/airflow-core/docs/img/ui-light/demo_graph_and_code_view.png b/airflow-core/docs/img/ui-light/demo_graph_and_code_view.png index d17d9bda00742..613cbdc7063a6 100644 Binary files a/airflow-core/docs/img/ui-light/demo_graph_and_code_view.png and b/airflow-core/docs/img/ui-light/demo_graph_and_code_view.png differ diff --git a/airflow-core/docs/img/ui-light/demo_grid_view_with_task_logs.png b/airflow-core/docs/img/ui-light/demo_grid_view_with_task_logs.png index a00ed336a936a..c28729cc9b6ce 100644 Binary files a/airflow-core/docs/img/ui-light/demo_grid_view_with_task_logs.png and b/airflow-core/docs/img/ui-light/demo_grid_view_with_task_logs.png differ diff --git a/airflow-core/docs/img/ui-light/hitl_approve_reject.png b/airflow-core/docs/img/ui-light/hitl_approve_reject.png new file mode 100644 index 0000000000000..0ae6161854464 Binary files /dev/null and b/airflow-core/docs/img/ui-light/hitl_approve_reject.png differ diff --git a/airflow-core/docs/img/ui-light/hitl_branch_selected.png b/airflow-core/docs/img/ui-light/hitl_branch_selected.png new file mode 100644 index 0000000000000..f1e6b5176a21d Binary files /dev/null and b/airflow-core/docs/img/ui-light/hitl_branch_selected.png differ diff --git a/airflow-core/docs/img/ui-light/hitl_branch_selection.png b/airflow-core/docs/img/ui-light/hitl_branch_selection.png new file mode 100644 index 0000000000000..45d9c71dce73a Binary files /dev/null and b/airflow-core/docs/img/ui-light/hitl_branch_selection.png differ diff --git a/airflow-core/docs/img/ui-light/hitl_wait_for_input.png b/airflow-core/docs/img/ui-light/hitl_wait_for_input.png new file mode 100644 index 0000000000000..9d74d32edc055 Binary files /dev/null and b/airflow-core/docs/img/ui-light/hitl_wait_for_input.png differ diff --git a/airflow-core/docs/img/ui-light/hitl_wait_for_multiple_options.png b/airflow-core/docs/img/ui-light/hitl_wait_for_multiple_options.png new file mode 100644 index 0000000000000..8888a0b68e3fe Binary files /dev/null and b/airflow-core/docs/img/ui-light/hitl_wait_for_multiple_options.png differ diff --git a/airflow-core/docs/img/ui-light/hitl_wait_for_option.png b/airflow-core/docs/img/ui-light/hitl_wait_for_option.png new file mode 100644 index 0000000000000..8dd32cfcb3de0 Binary files /dev/null and b/airflow-core/docs/img/ui-light/hitl_wait_for_option.png differ diff --git a/airflow-core/docs/img/ui-light/task_doc.png b/airflow-core/docs/img/ui-light/task_doc.png index 887332b71bb4b..9b2a6fd56d77d 100644 Binary files a/airflow-core/docs/img/ui-light/task_doc.png and b/airflow-core/docs/img/ui-light/task_doc.png differ diff --git a/airflow-core/docs/img/ui-light/task_instance_history.png b/airflow-core/docs/img/ui-light/task_instance_history.png index 27835950e7cca..f7a61689f7096 100644 Binary files a/airflow-core/docs/img/ui-light/task_instance_history.png and b/airflow-core/docs/img/ui-light/task_instance_history.png differ diff --git a/airflow-core/docs/img/ui-light/task_instance_history_log.png b/airflow-core/docs/img/ui-light/task_instance_history_log.png index afcccd0f2898e..6bc1843b6d2eb 100644 Binary files a/airflow-core/docs/img/ui-light/task_instance_history_log.png and b/airflow-core/docs/img/ui-light/task_instance_history_log.png differ diff --git a/airflow-core/docs/img/ui-light/tutorial_pipeline_add_connection.png b/airflow-core/docs/img/ui-light/tutorial_pipeline_add_connection.png index c455897a6e6b2..3810f56567ce6 100644 Binary files a/airflow-core/docs/img/ui-light/tutorial_pipeline_add_connection.png and b/airflow-core/docs/img/ui-light/tutorial_pipeline_add_connection.png differ diff --git a/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_list.png b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_list.png deleted file mode 100644 index 678634214d5c1..0000000000000 Binary files a/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_list.png and /dev/null differ diff --git a/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_list_trigger.png b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_list_trigger.png new file mode 100644 index 0000000000000..78f561ddadcbb Binary files /dev/null and b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_list_trigger.png differ diff --git a/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_overview_processed.png b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_overview_processed.png index 1de592d0769f8..503ee59620219 100644 Binary files a/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_overview_processed.png and b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_overview_processed.png differ diff --git a/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_task_instance_logs.png b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_task_instance_logs.png new file mode 100644 index 0000000000000..9076318b2af28 Binary files /dev/null and b/airflow-core/docs/img/ui-light/tutorial_pipeline_dag_task_instance_logs.png differ diff --git a/airflow-core/docs/img/ui-light/variable_hidden.png b/airflow-core/docs/img/ui-light/variable_hidden.png index 51d1331e99af3..9f03ba3152c7b 100644 Binary files a/airflow-core/docs/img/ui-light/variable_hidden.png and b/airflow-core/docs/img/ui-light/variable_hidden.png differ diff --git a/airflow-core/docs/index.rst b/airflow-core/docs/index.rst index d3788b024c4b0..fc2d641d306f8 100644 --- a/airflow-core/docs/index.rst +++ b/airflow-core/docs/index.rst @@ -79,7 +79,7 @@ Here you see: Airflow parses the script, schedules the tasks, and executes them in the defined order. The status of the ``"demo"`` Dag is displayed in the web interface: -.. image:: /img/ui-dark/demo_graph_and_code_view.png +.. image:: /img/ui-light/demo_graph_and_code_view.png :alt: Demo Dag in the Graph View, showing the status of one Dag run along with Dag code. | @@ -88,7 +88,7 @@ This example uses a simple Bash command and Python function, but Airflow tasks c tasks to run a Spark job, move files between storage buckets, or send a notification email. Here's what that same Dag looks like over time, with multiple runs: -.. image:: /img/ui-dark/demo_grid_view_with_task_logs.png +.. image:: /img/ui-light/demo_grid_view_with_task_logs.png :alt: Demo Dag in the Grid View, showing the status of all Dag runs, as well as logs for a task instance | @@ -96,7 +96,7 @@ like over time, with multiple runs: Each column in the grid represents a single Dag run. While the graph and grid views are most commonly used, Airflow provides several other views to help you monitor and troubleshoot workflows — such as the ``Dag Overview`` view: -.. image:: /img/ui-dark/demo_dag_overview_with_failed_tasks.png +.. image:: /img/ui-light/demo_complex_dag_overview_with_failed_tasks.png :alt: Overview of a complex Dag in the Grid View, showing the status of all Dag runs, as well as quick links to recently failed task logs | @@ -144,8 +144,6 @@ experience is continuously improving, but defining workflows as code is central .. toctree:: :hidden: :caption: Content - :titlesonly: - :maxdepth: 1 Overview start diff --git a/airflow-core/docs/installation/dependencies.rst b/airflow-core/docs/installation/dependencies.rst index e41e03ab46a41..8e209869abbae 100644 --- a/airflow-core/docs/installation/dependencies.rst +++ b/airflow-core/docs/installation/dependencies.rst @@ -38,9 +38,11 @@ For the list of the extras and what they enable, see: :doc:`/extra-packages-ref` Provider distributions '''''''''''''''''''''' -Unlike Apache Airflow 1.10, the Airflow 2.0 is delivered in multiple, separate, but connected packages. -The core of Airflow scheduling system is delivered as ``apache-airflow`` package and there are around -60 providers which can be installed separately as so called ``Airflow providers``. +Airflow is delivered in multiple, separate, but connected packages. There is the main ``apache-airflow`` +package, ``airflow-core`` package (which is a dependency of ``apache-airflow``) which implements +main airflow functionality, ``airflow-task-sdk`` package that is used by Dag authors to implement Dags, +and multiple so called ``Airflow providers`` packages. + The default Airflow installation doesn't have many integrations and you have to install them yourself. You can even develop and install your own providers for Airflow. For more information, @@ -59,13 +61,13 @@ optional features to "core" Apache Airflow. One type of such optional features i packages, but not all optional features of Apache Airflow have corresponding providers. We are using the ``extras`` setuptools features to also install providers. -Most of the extras are also linked (same name) with providers - for example adding ``[google]`` -extra also adds ``apache-airflow-providers-google`` as dependency. However, there are some extras that do -not install providers (examples ``github_enterprise``, ``kerberos``, ``async`` - they add some extra -dependencies which are needed for those ``extra`` features of Airflow mentioned. The three examples -above add respectively GitHub Enterprise OAuth authentication, Kerberos integration or -asynchronous workers for Gunicorn. None of those have providers, they are just extending Apache Airflow -"core" package with new functionalities. +Most of the extras are also linked (same name) with providers - for example using ``apache-airflow[google]`` +extra installs ``airflow-core`` and adds ``apache-airflow-providers-google`` as dependency. +However, there are some extras that do not install providers (examples ``github_enterprise``, ``kerberos`` - +they add some extra dependencies which are needed for those ``extra`` features of +Airflow mentioned. The three examples above add respectively GitHub Enterprise OAuth authentication, +Kerberos integration . None of those have providers, they are just extending Apache Airflow +``airflow-core`` package with new functionalities. System dependencies ''''''''''''''''''' diff --git a/airflow-core/docs/installation/installing-from-pypi.rst b/airflow-core/docs/installation/installing-from-pypi.rst index d2d9d6507d019..1815cd246a3ac 100644 --- a/airflow-core/docs/installation/installing-from-pypi.rst +++ b/airflow-core/docs/installation/installing-from-pypi.rst @@ -24,7 +24,7 @@ PyPI `__. Installation tools '''''''''''''''''' -Only ``pip`` installation is currently officially supported. +Only ``pip`` and ``uv`` installation is currently officially supported. .. note:: @@ -33,7 +33,8 @@ Only ``pip`` installation is currently officially supported. ``pip`` - especially when it comes to constraint vs. requirements management. Installing via ``Poetry`` or ``pip-tools`` is not currently supported. If you wish to install Airflow using those tools you should use the constraints and convert them to appropriate - format and workflow that your tool requires. + format and workflow that your tool requires. Uv follows ``pip`` approach + with ``uv pip`` so it should work similarly. Typical command to install Airflow from scratch in a reproducible way from PyPI looks like below: diff --git a/airflow-core/docs/installation/installing-from-sources.rst b/airflow-core/docs/installation/installing-from-sources.rst index bafd3df9b338a..71272e81b967e 100644 --- a/airflow-core/docs/installation/installing-from-sources.rst +++ b/airflow-core/docs/installation/installing-from-sources.rst @@ -23,8 +23,7 @@ Released packages .. jinja:: official_download_page - This page describes downloading and verifying Airflow® version - ``{{ airflow_version }}`` using officially released packages. + This page describes downloading and verifying Airflow® version ``|version|`` using officially released packages. You can also install ``Apache Airflow`` - as most Python packages - via :doc:`PyPI `. You can choose different version of Airflow by selecting a different version from the drop-down at the top-left of the page. @@ -45,11 +44,13 @@ The |version| downloads of Airflow® are available at: .. jinja:: official_download_page - * `Sources package for airflow <{{ closer_lua_url }}/apache-airflow-{{ airflow_version }}-source.tar.gz>`__ (`asc <{{ base_url }}/apache-airflow-{{ airflow_version }}-source.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache-airflow-{{ airflow_version }}-source.tar.gz.sha512>`__) - * `Sdist package for airflow meta package <{{ closer_lua_url }}/apache-airflow-{{ airflow_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache-airflow-{{ airflow_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache-airflow-{{ airflow_version }}.tar.gz.sha512>`__) - * `Whl package for airflow meta package <{{ closer_lua_url }}/apache_airflow-{{ airflow_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow-{{ airflow_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow-{{ airflow_version }}-py3-none-any.whl.sha512>`__) - * `Sdist package for airflow core package <{{ closer_lua_url }}/apache-airflow_core-{{ airflow_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache-airflow_core-{{ airflow_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache-airflow_core-{{ airflow_version }}.tar.gz.sha512>`__) - * `Whl package for airflow core package <{{ closer_lua_url }}/apache_airflow_core-{{ airflow_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow_core-{{ airflow_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_core-{{ airflow_version }}-py3-none-any.whl.sha512>`__) + * `Sources package for airflow <{{ closer_lua_url }}/apache_airflow-{{ airflow_version }}-source.tar.gz>`__ (`asc <{{ base_url }}/apache_airflow-{{ airflow_version }}-source.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache_airflow-{{ airflow_version }}-source.tar.gz.sha512>`__) + * `Sdist package for airflow meta distribution <{{ closer_lua_url }}/apache_airflow-{{ airflow_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache_airflow-{{ airflow_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache_airflow-{{ airflow_version }}.tar.gz.sha512>`__) + * `Whl package for airflow meta distributio <{{ closer_lua_url }}/apache_airflow-{{ airflow_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow-{{ airflow_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow-{{ airflow_version }}-py3-none-any.whl.sha512>`__) + * `Sdist package for airflow core distribution <{{ closer_lua_url }}/apache_airflow_core-{{ airflow_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache_airflow_core-{{ airflow_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache_airflow_core-{{ airflow_version }}.tar.gz.sha512>`__) + * `Whl package for airflow core distribution <{{ closer_lua_url }}/apache_airflow_core-{{ airflow_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow_core-{{ airflow_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_core-{{ airflow_version }}-py3-none-any.whl.sha512>`__) + * `Sdist package for airflow task-sdk distribution <{{ closer_lua_url_task_sdk }}/apache_airflow_task_sdk-{{ task_sdk_version }}.tar.gz>`__ (`asc <{{ base_url_task_sdk }}/apache_airflow_task_sdk-{{ task_sdk_version }}.tar.gz.asc>`__, `sha512 <{{ base_url_task_sdk }}/apache_airflow_task_sdk-{{ task_sdk_version }}.tar.gz.sha512>`__) + * `Whl package for airflow task-sdk distribution <{{ closer_lua_url_task_sdk }}/apache_airflow_task_sdk-{{ task_sdk_version }}-py3-none-any.whl>`__ (`asc <{{ base_url_task_sdk }}/apache_airflow_task_sdk-{{ task_sdk_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url_task_sdk }}/apache_airflow_task_sdk-{{ task_sdk_version }}-py3-none-any.whl.sha512>`__) If you want to install from the source code, you can download from the sources link above, it will contain a ``INSTALL`` file containing details on how you can build and install Airflow. diff --git a/airflow-core/docs/installation/supported-versions.rst b/airflow-core/docs/installation/supported-versions.rst index 8193b1d62237d..da94acfbda065 100644 --- a/airflow-core/docs/installation/supported-versions.rst +++ b/airflow-core/docs/installation/supported-versions.rst @@ -29,7 +29,7 @@ Apache Airflow® version life cycle: ========= ===================== ========= =============== ===================== ================ Version Current Patch/Minor State First Release Limited Maintenance EOL/Terminated ========= ===================== ========= =============== ===================== ================ -3 3.0.6 Supported Apr 22, 2025 TBD TBD +3 3.1.6 Supported Apr 22, 2025 TBD TBD 2 2.11.0 Supported Dec 17, 2020 Oct 22, 2025 Apr 22, 2026 1.10 1.10.15 EOL Aug 27, 2018 Dec 17, 2020 June 17, 2021 1.9 1.9.0 EOL Jan 03, 2018 Aug 27, 2018 Aug 27, 2018 diff --git a/airflow-core/docs/installation/upgrading_to_airflow3.rst b/airflow-core/docs/installation/upgrading_to_airflow3.rst index c0aa1b8e33ffd..4f7f8d5ce5650 100644 --- a/airflow-core/docs/installation/upgrading_to_airflow3.rst +++ b/airflow-core/docs/installation/upgrading_to_airflow3.rst @@ -61,7 +61,7 @@ Step 1: Take care of prerequisites ---------------------------------- - Make sure that you are on Airflow 2.7 or later. It is recommended to upgrade to latest 2.x and then to Airflow 3. -- Make sure that your Python version is in the supported list. Airflow 3.0.0 supports the following Python versions: Python 3.9, 3.10, 3.11 and 3.12. +- Make sure that your Python version is in the supported list. - Ensure that you are not using any features or functionality that have been :ref:`removed in Airflow 3`. @@ -89,24 +89,24 @@ Step 3: Dag authors - Check your Airflow Dags for compatibility To minimize friction for users upgrading from prior versions of Airflow, we have created a Dag upgrade check utility using `Ruff `_ combined with `AIR `_ rules. The rules AIR301 and AIR302 indicate breaking changes in Airflow 3, while AIR311 and AIR312 highlight changes that are not currently breaking but are strongly recommended for updates. -The latest available ``ruff`` version will have the most up-to-date rules, but be sure to use at least version ``0.11.13``. The below example demonstrates how to check +The latest available ``ruff`` version will have the most up-to-date rules, but be sure to use at least version ``0.13.1``. The below example demonstrates how to check for Dag incompatibilities that will need to be fixed before they will work as expected on Airflow 3. .. code-block:: bash - ruff check dags/ --select AIR301 --preview + ruff check dags/ --select AIR301 To preview the recommended fixes, run the following command: .. code-block:: bash - ruff check dags/ --select AIR301 --show-fixes --preview + ruff check dags/ --select AIR301 --show-fixes Some changes can be automatically fixed. To do so, run the following command: .. code-block:: bash - ruff check dags/ --select AIR301 --fix --preview + ruff check dags/ --select AIR301 --fix Some of the fixes are marked as unsafe. Unsafe fixes usually do not break Dag code. They're marked as unsafe as they may change some runtime behavior. For more information, see `Fix Safety `_. @@ -114,12 +114,7 @@ To trigger these fixes, run the following command: .. code-block:: bash - ruff check dags/ --select AIR301 --fix --unsafe-fixes --preview - -.. note:: - Ruff has strict policy about when a rule becomes stable. Till it does you must use --preview flag. - The progress of Airflow Ruff rule become stable can be tracked in https://github.com/astral-sh/ruff/issues/17749 - That said, from Airflow side the rules are perfectly fine to be used. + ruff check dags/ --select AIR301 --fix --unsafe-fixes .. note:: @@ -127,6 +122,68 @@ To trigger these fixes, run the following command: You can also configure these flags through configuration files. See `Configuring Ruff `_ for details. +Key Import Updates +^^^^^^^^^^^^^^^^^^ + +While ruff can automatically fix many import issues, here are the key import changes you'll need to make to ensure your DAGs and other +code import Airflow components correctly in Airflow 3. The older paths are deprecated and will be removed in a future Airflow version. + +.. list-table:: + :header-rows: 1 + :widths: 50, 50 + + * - **Old Import Path (Deprecated)** + - **New Import Path (airflow.sdk)** + * - ``airflow.decorators.dag`` + - ``airflow.sdk.dag`` + * - ``airflow.decorators.task`` + - ``airflow.sdk.task`` + * - ``airflow.decorators.task_group`` + - ``airflow.sdk.task_group`` + * - ``airflow.decorators.setup`` + - ``airflow.sdk.setup`` + * - ``airflow.decorators.teardown`` + - ``airflow.sdk.teardown`` + * - ``airflow.models.dag.DAG`` + - ``airflow.sdk.DAG`` + * - ``airflow.models.baseoperator.BaseOperator`` + - ``airflow.sdk.BaseOperator`` + * - ``airflow.models.param.Param`` + - ``airflow.sdk.Param`` + * - ``airflow.models.param.ParamsDict`` + - ``airflow.sdk.ParamsDict`` + * - ``airflow.models.baseoperatorlink.BaseOperatorLink`` + - ``airflow.sdk.BaseOperatorLink`` + * - ``airflow.sensors.base.BaseSensorOperator`` + - ``airflow.sdk.BaseSensorOperator`` + * - ``airflow.hooks.base.BaseHook`` + - ``airflow.sdk.BaseHook`` + * - ``airflow.notifications.basenotifier.BaseNotifier`` + - ``airflow.sdk.BaseNotifier`` + * - ``airflow.utils.task_group.TaskGroup`` + - ``airflow.sdk.TaskGroup`` + * - ``airflow.datasets.Dataset`` + - ``airflow.sdk.Asset`` + * - ``airflow.datasets.DatasetAlias`` + - ``airflow.sdk.AssetAlias`` + * - ``airflow.datasets.DatasetAll`` + - ``airflow.sdk.AssetAll`` + * - ``airflow.datasets.DatasetAny`` + - ``airflow.sdk.AssetAny`` + * - ``airflow.models.connection.Connection`` + - ``airflow.sdk.Connection`` + * - ``airflow.models.context.Context`` + - ``airflow.sdk.Context`` + * - ``airflow.models.variable.Variable`` + - ``airflow.sdk.Variable`` + * - ``airflow.io.*`` + - ``airflow.sdk.io.*`` + +**Migration Timeline** + +- **Airflow 3.1**: Legacy imports show deprecation warnings but continue to work +- **Future Airflow version**: Legacy imports will be **removed** + Step 4: Install the Standard Provider -------------------------------------- @@ -135,12 +192,97 @@ Step 4: Install the Standard Provider - For convenience, this package can also be installed on Airflow 2.x versions, so that Dags can be modified to reference these Operators from the standard provider package instead of Airflow Core. -Step 5: Review custom operators for direct db access ----------------------------------------------------- +Step 5: Review custom written tasks for direct DB access +-------------------------------------------------------- + +In Airflow 3, operators cannot access the Airflow metadata database directly using database sessions. +If you have custom operators, review your code to ensure there are no direct database access calls. +You can follow examples in https://github.com/apache/airflow/issues/49187 to learn how to modify your code if needed. + +If you have custom operators or task code that previously accessed the metadata database directly, you must migrate to one of the following approaches: + +Recommended Approach: Use Airflow Python Client +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use the official `Airflow Python Client `_ to interact with +Airflow metadata database via REST API. The Python Client has APIs defined for most use cases, including DagRuns, +TaskInstances, Variables, Connections, XComs, and more. + +**Pros:** +- No direct database network access required from workers +- Most aligned with Airflow 3's API-first architecture +- No database credentials needed in worker environment (uses API tokens) +- Workers don't need database drivers installed +- Centralized access control and authentication via API server + +**Cons:** +- Requires installing ``apache-airflow-client`` package +- Requires acquisition of access tokens by performing API call to ``/auth/token`` and rotating them as needed +- Requires API server availability and network access to API server +- Not all database operations may be exposed via API endpoints + +.. note:: + If you need functionality that is not available via the Airflow Python Client, consider requesting new API endpoints or Task SDK features. The Airflow community prioritizes adding missing API capabilities over enabling direct database access. + +Known Workaround: Use DbApiHook (PostgresHook or MySqlHook) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. warning:: + This approach is **NOT recommended** and is documented only as a known workaround for users who cannot use the Airflow Python Client. This approach has significant limitations and **will** break in future Airflow versions. + + **Important considerations:** + + - **Will break in future versions**: This approach will break in Airflow 3.2+ and beyond. You are responsible for adapting your code when schema changes occur. + - **Database schema is NOT a public API**: The Airflow metadata database schema can change at any time without notice. Schema changes will break your queries without warning. + - **Breaks task isolation**: This contradicts one of Airflow 3's core features - task isolation. Tasks should not directly access the metadata database. + - **Performance implications**: This reintroduces Airflow 2 behavior where each task opens separate database connections, dramatically changing performance characteristics and scalability. + +If your use case cannot be addressed using the Python Client and you understand the risks above, you ma use database hooks to query your metadata database directly. Create a database +connection (PostgreSQL or MySQL, matching your metadata database type) pointing to your metadata database +and use Database Hooks in Airflow. + +**Note:** These hooks connect directly to the database (not via +the API server) using database drivers like psycopg2 or mysqlclient. -- In Airflow 3 operators can not access the Airflow metadata database directly using database sessions. - If you have custom operators, review the code to make sure there are no direct db access. - You can follow examples in https://github.com/apache/airflow/issues/49187 to find how to modify your code if needed. +**Example using PostgresHook (MySql has similar interface too)** + +.. code-block:: python + + from airflow.sdk import task + from airflow.providers.postgres.hooks.postgres import PostgresHook + + + @task + def get_connections_from_db(): + hook = PostgresHook(postgres_conn_id="metadata_postgres") + records = hook.get_records( + sql=""" + SELECT conn_id, conn_type, host, schema, login + FROM connection + WHERE conn_type = 'postgres' + LIMIT 10; + """ + ) + + return records + +**Example using SQLExecuteQueryOperator** + +You can also use ``SQLExecuteQueryOperator`` if you prefer to use operators instead of hooks: + +.. code-block:: python + + from airflow.providers.common.sql.operators.sql import SQLExecuteQueryOperator + + query_task = SQLExecuteQueryOperator( + task_id="query_metadata", + conn_id="metadata_postgres", + sql="SELECT conn_id, conn_type FROM connection WHERE conn_type = 'postgres'", + do_xcom_push=True, + ) + +.. note:: + Always use **read-only database credentials** for metadata database connections and it is recommended to use temporary credentials. Step 6: Deployment Managers - Upgrade your Airflow Instance ------------------------------------------------------------ @@ -174,6 +316,9 @@ them to FastAPI apps or ensure you install the FAB provider which provides a bac Ideally, you should convert your plugins to the Airflow 3 Plugin interface i.e External Views (``external_views``), Fast API apps (``fastapi_apps``) and FastAPI middlewares (``fastapi_root_middlewares``). +If you use the Airflow Helm Chart to deploy Airflow, please check your defined values against configuration options available in Airflow 3. +All configuration options below ``webserver`` need to be changed to ``apiServer``. Consider that many parameters have been renamed or removed. + Step 7: Changes to your startup scripts --------------------------------------- @@ -191,6 +336,15 @@ The Dag processor must now be started independently, even for local or developme You should now be able to start up your Airflow 3 instance. +Step 8: Things to check +----------------------- + +Please consider checking the following things after upgrading your Airflow instance: + +- If you configured Single-Sign-On (SSO) using OAuth, OIDC, or LDAP, make sure that the authentication is working as expected. + If you use a custom ``webserver_config.py`` you need to replace ``from airflow.www.security import AirflowSecurityManager`` + with ``from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride``. + .. _breaking-changes: Breaking Changes @@ -202,7 +356,7 @@ These include: - **SubDAGs**: Replaced by TaskGroups, Assets, and Data Aware Scheduling. - **Sequential Executor**: Replaced by LocalExecutor, which can be used with SQLite for local development use cases. - **CeleryKubernetesExecutor and LocalKubernetesExecutor**: Replaced by `Multiple Executor Configuration `_ -- **SLAs**: Deprecated and removed; Will be replaced by forthcoming `Deadline Alerts `_. +- **SLAs**: Deprecated and removed; replaced with :doc:`Deadline Alerts `. - **Subdir**: Used as an argument on many CLI commands, ``--subdir`` or ``-S`` has been superseded by :doc:`Dag bundles `. - **REST API** (``/api/v1``) replaced: Use the modern FastAPI-based stable ``/api/v2`` instead; see :doc:`Airflow API v2 ` for details. - **Some Airflow context variables**: The following keys are no longer available in a :ref:`task instance's context `. If not replaced, will cause Dag errors: diff --git a/airflow-core/docs/license.rst b/airflow-core/docs/license.rst index 3ef9a61927834..a5b85d9da238d 100644 --- a/airflow-core/docs/license.rst +++ b/airflow-core/docs/license.rst @@ -20,8 +20,10 @@ License ======== -.. image:: img/apache.jpg +.. image:: img/asf_logo_wide.png :width: 150 + :align: center + .. code-block:: text diff --git a/airflow-core/docs/migrations-ref.rst b/airflow-core/docs/migrations-ref.rst index 1e0c886097e00..c8569e667be75 100644 --- a/airflow-core/docs/migrations-ref.rst +++ b/airflow-core/docs/migrations-ref.rst @@ -39,7 +39,10 @@ Here's the list of all the Database Migrations that are executed via when you ru +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | Revision ID | Revises ID | Airflow Version | Description | +=========================+==================+===================+==============================================================+ -| ``eaf332f43c7c`` (head) | ``a3c7f2b18d4e`` | ``3.1.0`` | add last_parse_duration to dag model. | +| ``cc92b33c6709`` (head) | ``eaf332f43c7c`` | ``3.1.0`` | Add backward compatibility for serialized DAG format v3 to | +| | | | v2. | ++-------------------------+------------------+-------------------+--------------------------------------------------------------+ +| ``eaf332f43c7c`` | ``a3c7f2b18d4e`` | ``3.1.0`` | add last_parse_duration to dag model. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ | ``a3c7f2b18d4e`` | ``7582ea3f3dd5`` | ``3.1.0`` | Add tables to store teams and associations with dag bundles. | +-------------------------+------------------+-------------------+--------------------------------------------------------------+ diff --git a/airflow-core/docs/project.rst b/airflow-core/docs/project.rst index ade05e1321e81..6f94282ca4644 100644 --- a/airflow-core/docs/project.rst +++ b/airflow-core/docs/project.rst @@ -59,6 +59,7 @@ Committers - Felix Uellendall (@feluelle) - Fokko Driesprong (@fokko) - Gopal Dirisala (@dirrao) +- Guan-Ming (Wesley) Chiu (@guan404ming) - Hitesh Shah (@hiteshs) - Hussein Awala (@hussein-awala) - Jakob Homan (@jghoman) @@ -98,6 +99,7 @@ Committers - Shahar Epstein (@shahar1) - Shubham Raj (@shubhamraj-git) - Siddharth "Sid" Anand (@r39132) +- Sriraj Dheeraj Turaga (@dheerajturaga) - Sumit Maheshwari (@msumit) - Tao Feng (@feng-tao) - Tomasz Urbaszek (@turbaszek) diff --git a/airflow-core/docs/public-airflow-interface.rst b/airflow-core/docs/public-airflow-interface.rst index 9abe6cbd5cb44..1d986afccb779 100644 --- a/airflow-core/docs/public-airflow-interface.rst +++ b/airflow-core/docs/public-airflow-interface.rst @@ -15,9 +15,8 @@ specific language governing permissions and limitations under the License. -===================================== Public Interface for Airflow 3.0+ -===================================== +================================= .. warning:: @@ -27,9 +26,6 @@ Public Interface for Airflow 3.0+ `Airflow 2.11 Public Interface Documentation `_ for the legacy interface. -Public Interface of Airflow -........................... - The Public Interface of Apache Airflow is the collection of interfaces and behaviors in Apache Airflow whose changes are governed by semantic versioning. A user interacts with Airflow's public interface by creating and managing Dags, managing tasks and dependencies, @@ -46,7 +42,7 @@ from task code is no longer allowed. Instead, use the :doc:`Stable REST API `_. Using Airflow Public Interfaces -=============================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. note:: @@ -89,7 +85,7 @@ way, the Stable REST API is recommended. Using the Public Interface for Dag authors -========================================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The primary interface for Dag authors is the :doc:`airflow.sdk namespace `. This provides a stable, well-defined interface for creating Dags and tasks that is not subject to internal @@ -130,6 +126,9 @@ API Server, etc.), providing a version-agnostic, stable interface for writing an * ``get_current_context`` * ``get_parsing_context`` +.. seealso:: + API reference for :class:`~airflow.sdk.TaskGroup`, :class:`~airflow.sdk.DAG`, and :class:`~airflow.sdk.task_group` + **Migration from Airflow 2.x:** For detailed migration instructions from Airflow 2.x to 3.x, including import changes and other breaking changes, @@ -142,7 +141,7 @@ Legacy import paths (e.g., ``airflow.models.dag.DAG``, ``airflow.decorator.task` removed in a future Airflow version. Dags -==== +---- The Dag is Airflow's core entity that represents a recurring workflow. You can create a Dag by instantiating the :class:`~airflow.sdk.DAG` class in your Dag file. Dags can also have parameters @@ -156,7 +155,6 @@ Airflow has a set of example Dags that you can use to learn how to write Dags .. toctree:: :includehidden: :glob: - :hidden: :maxdepth: 1 _api/airflow/example_dags/index @@ -183,7 +181,7 @@ References for the modules used in Dags are here: .. _pythonapi:operators: Operators -========= +--------- The base classes :class:`~airflow.sdk.BaseOperator` and :class:`~airflow.sdk.BaseSensorOperator` are public and may be extended to make new operators. @@ -193,7 +191,7 @@ from the airflow.sdk namespace. Subclasses of BaseOperator which are published in Apache Airflow are public in *behavior* but not in *structure*. That is to say, the Operator's parameters and behavior is governed by semver but the methods are subject to change at any time. Task Instances -============== +-------------- Task instances are the individual runs of a single task in a Dag (in a Dag Run). Task instances are accessed through the Task Context via :func:`~airflow.sdk.get_current_context`. Direct database access is not possible. @@ -203,7 +201,7 @@ the Task Context via :func:`~airflow.sdk.get_current_context`. Direct database a For detailed API documentation, see the `Task SDK Reference `_. Task Instance Keys -================== +------------------ Task instance keys are unique identifiers of task instances in a Dag (in a Dag Run). A key is a tuple that consists of ``dag_id``, ``task_id``, ``run_id``, ``try_number``, and ``map_index``. @@ -240,7 +238,7 @@ Example of accessing task instance information through Task Context: .. _pythonapi:hooks: Hooks -===== +----- Hooks are interfaces to external platforms and databases, implementing a common interface when possible and acting as building blocks for operators. All hooks @@ -252,13 +250,12 @@ by extending them: .. toctree:: :includehidden: :glob: - :hidden: :maxdepth: 1 _api/airflow/hooks/index Public Airflow utilities -======================== +^^^^^^^^^^^^^^^^^^^^^^^^ When writing or extending Hooks and Operators, Dag authors and developers can use the following classes: @@ -309,7 +306,7 @@ Reference for classes used for the utilities are here: Public Exceptions -================= +^^^^^^^^^^^^^^^^^ When writing the custom Operators and Hooks, you can handle and raise public Exceptions that Airflow exposes: @@ -317,25 +314,23 @@ exposes: .. toctree:: :includehidden: :glob: - :hidden: :maxdepth: 1 _api/airflow/exceptions/index Public Utility classes -====================== +^^^^^^^^^^^^^^^^^^^^^^ .. toctree:: :includehidden: :glob: - :hidden: :maxdepth: 1 _api/airflow/utils/state/index Using Public Interface to extend Airflow capabilities -===================================================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Airflow uses Plugin mechanism to extend Airflow platform capabilities. They allow to extend Airflow UI but also they are the way to expose the below customizations (Triggers, Timetables, Listeners, etc.). @@ -348,7 +343,7 @@ that do not require plugins - you can read more about them in :doc:`howto/custom Here are the ways how Plugins can be used to extend Airflow: Triggers -======== +-------- Airflow uses Triggers to implement ``asyncio`` compatible Deferrable Operators. All Triggers derive from :class:`~airflow.triggers.base.BaseTrigger`. @@ -359,7 +354,6 @@ by extending them: .. toctree:: :includehidden: :glob: - :hidden: :maxdepth: 1 _api/airflow/triggers/index @@ -367,7 +361,7 @@ by extending them: You can read more about Triggers in :doc:`authoring-and-scheduling/deferring`. Timetables -========== +---------- Custom timetable implementations provide Airflow's scheduler additional logic to schedule Dag runs in ways not possible with built-in schedule expressions. @@ -378,7 +372,6 @@ by extending them: .. toctree:: :includehidden: - :hidden: :maxdepth: 1 _api/airflow/timetables/index @@ -386,7 +379,7 @@ by extending them: You can read more about Timetables in :doc:`howto/timetable`. Listeners -========= +--------- Listeners enable you to respond to Dag/Task lifecycle events. @@ -400,7 +393,7 @@ can be implemented to respond to Dag/Task lifecycle events. You can read more about Listeners in :doc:`administration-and-deployment/listeners`. Extra Links -=========== +----------- Extra links are dynamic links that could be added to Airflow independently from custom Operators. Normally they can be defined by the Operators, but plugins allow you to override the links on a global level. @@ -408,7 +401,7 @@ they can be defined by the Operators, but plugins allow you to override the link You can read more about the Extra Links in :doc:`/howto/define-extra-link`. Using Public Interface to integrate with external services and applications -=========================================================================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Tasks in Airflow can orchestrate external services via Hooks and Operators. The core functionality of @@ -417,7 +410,7 @@ You can read more about providers :doc:`providers `. Executors -========= +--------- Executors are the mechanism by which task instances get run. All executors are derived from :class:`~airflow.executors.base_executor.BaseExecutor`. There are several @@ -437,7 +430,7 @@ You can read more about executors and how to write your own in :doc:`core-concep executors, and custom executors could not provide full functionality that built-in executors had. Secrets Backends -================ +---------------- Airflow can be configured to rely on secrets backends to retrieve :class:`~airflow.sdk.Connection` and :class:`~airflow.sdk.Variable`. @@ -448,7 +441,6 @@ All Secrets Backend implementations are public. You can extend their functionali .. toctree:: :includehidden: :glob: - :hidden: :maxdepth: 1 _api/airflow/secrets/index @@ -458,7 +450,7 @@ You can also find all the available Secrets Backends implemented in community pr in :doc:`apache-airflow-providers:core-extensions/secrets-backends`. Auth managers -============= +------------- Auth managers are responsible of user authentication and user authorization in Airflow. All auth managers are derived from :class:`~airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager`. @@ -469,21 +461,21 @@ public, but the different implementations of auth managers are not (i.e. FabAuth You can read more about auth managers and how to write your own in :doc:`core-concepts/auth-manager/index`. Connections -=========== +----------- When creating Hooks, you can add custom Connections. You can read more about connections in :doc:`apache-airflow-providers:core-extensions/connections` for available Connections implemented in the community providers. Extra Links -=========== +----------- When creating Hooks, you can add custom Extra Links that are displayed when the tasks are run. You can find out more about extra links in :doc:`apache-airflow-providers:core-extensions/extra-links` that also shows available extra links implemented in the community providers. Logging and Monitoring -====================== +---------------------- You can extend the way how logs are written by Airflow. You can find out more about log writing in :doc:`administration-and-deployment/logging-monitoring/index`. @@ -492,7 +484,7 @@ The :doc:`apache-airflow-providers:core-extensions/logging` that also shows avai implemented in the community providers. Decorators -========== +---------- Dag authors can use decorators to author Dags using the :doc:`TaskFlow ` concept. All Decorators derive from :class:`~airflow.sdk.bases.decorator.TaskDecorator`. @@ -512,24 +504,24 @@ by extending them: You can read more about creating custom Decorators in :doc:`howto/create-custom-decorator`. Email notifications -=================== +------------------- Airflow has a built-in way of sending email notifications and it allows to extend it by adding custom email notification classes. You can read more about email notifications in :doc:`howto/email-config`. Notifications -============= +------------- Airflow has a built-in extensible way of sending notifications using the various ``on_*_callback``. You can read more about notifications in :doc:`howto/notifications`. Cluster Policies -================ +---------------- Cluster Policies are the way to dynamically apply cluster-wide policies to the Dags being parsed or tasks being executed. You can read more about Cluster Policies in :doc:`administration-and-deployment/cluster-policies`. Lineage -======= +------- Airflow can help track origins of data, what happens to it and where it moves over time. You can read more about lineage in :doc:`administration-and-deployment/lineage`. @@ -538,7 +530,7 @@ about lineage in :doc:`administration-and-deployment/lineage`. What is not part of the Public Interface of Apache Airflow? -=========================================================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Everything not mentioned in this document should be considered as non-Public Interface. diff --git a/airflow-core/docs/security/audit_logs.rst b/airflow-core/docs/security/audit_logs.rst index 8c86a224736ef..c01c99b3fcd98 100644 --- a/airflow-core/docs/security/audit_logs.rst +++ b/airflow-core/docs/security/audit_logs.rst @@ -18,50 +18,607 @@ Audit Logs in Airflow ===================== +Understanding Audit Logs +------------------------- -Overview ---------- +Audit logs serve as the historical record of an Airflow system, documenting who performed what actions and when they occurred. These logs are essential for maintaining system integrity, meeting compliance requirements, and conducting forensic analysis when issues arise. -Audit logs are a critical component of any system that needs to maintain a high level of security and compliance. -They provide a way to track user actions and system events, which can be used to troubleshoot issues, detect security breaches, and ensure regulatory compliance. +In essence, audit logs answer three fundamental questions: -In Airflow, audit logs are used to track user actions and system events that occur during the execution of Dags and tasks. -They are stored in a database and can be accessed through the Airflow UI. +- **Who**: Which user or system component initiated an action +- **What**: The specific operation that was performed +- **When**: The precise timestamp of the event -To be able to see audit logs, a user needs to have the ``Audit Logs.can_read`` permission. Such user will be able to see all audit logs, independently of the Dags permissions applied. +The primary purposes of audit logs include: +- **Regulatory Compliance**: Meeting requirements for data governance and audit trails +- **Security Monitoring**: Detecting unauthorized access or suspicious activities +- **Operational Troubleshooting**: Understanding the sequence of events leading to system issues +- **Change Management**: Tracking modifications to critical system components -Level of Audit Logs +.. note:: + Access to audit logs requires the ``Audit Logs.can_read`` permission. Users with this permission can view all audit entries regardless of their DAG-specific access rights. + + +Understanding Event Logs +------------------------- + +Event logs represent the operational heartbeat of an Airflow system. Unlike audit logs, which focus on accountability and compliance, event logs capture the technical details of system behavior, application performance, and operational metrics. + +Event logs serve several critical functions: + +- **Debugging and Troubleshooting**: Providing detailed error messages and stack traces +- **Performance Monitoring**: Recording execution times, resource usage, and system metrics +- **Operational Insights**: Tracking system health, component interactions, and workflow execution +- **Development Support**: Offering detailed information for code debugging and optimization + +Event logs are typically stored in log files or external logging systems and include information such as: + +- Task execution details and output +- System errors and warnings +- Performance metrics and timing information +- Component startup and shutdown events +- Resource utilization data + +Audit Logs vs Event Logs +------------------------------------------ + +While both logging systems are crucial for system management, they serve distinct purposes and audiences: + +.. list-table:: + :header-rows: 1 + :widths: 25 37 38 + + * - Characteristic + - Audit Logs + - Event Logs + * - **Primary Purpose** + - Accountability and compliance tracking + - Operational monitoring and system debugging + * - **Target Audience** + - Security teams, auditors, compliance officers + - Developers, system administrators, operations teams + * - **Content Focus** + - User actions and administrative changes + - System behavior, errors, and performance data + * - **Storage Location** + - Structured database table (``log``) + - Log files, external logging systems + * - **Retention Requirements** + - Long-term (months to years for compliance) + - Short to medium-term (days to weeks) + * - **Query Patterns** + - "Who modified this configuration?" + - "Why did this task execution fail?" + + +Accessing Audit Logs -------------------- -Audit logs exist at the task level and the user level. +Airflow provides multiple interfaces for accessing audit log data, each suited to different use cases and technical requirements: -- Task Level: At the task level, audit logs capture information related to the execution of a task, such as the start time, end time, and status of the task. +**Web User Interface** + The Airflow web interface provides the most accessible method for viewing audit logs. Navigate to **Browse → Audit Logs** to access an interface with built-in filtering, sorting, and search capabilities. This interface is ideal for ad-hoc investigations and routine monitoring. -- User Level: At the user level, audit logs capture information related to user actions, such as creating, modifying, or deleting a Dag or task. +**REST API Integration** + For programmatic access and system integration, use the ``/eventLogs`` REST API endpoint. This approach enables automated monitoring, integration with external security tools, and custom reporting applications. +**Direct Database Access** + Advanced users can query the ``log`` table directly using SQL. This method provides maximum flexibility for complex queries, custom reporting, and integration with business intelligence tools. -Location of Audit Logs +Scope of Audit Logging ---------------------- -Audit logs can be accessed through the Airflow UI. They are located under the "Browse" tab, and can be viewed by selecting "Audit Logs" from the dropdown menu. +Airflow's audit logging system captures events across three distinct operational domains: + +**User-Initiated Actions** + These events occur when users interact with Airflow through any interface (web UI, REST API, or command-line tools). Examples include: + + - Manual DAG run triggers and modifications + - Configuration changes to variables, connections, and pools + - Task instance state modifications (clearing, marking as success/failed) + - Administrative operations and user management activities + +**System-Generated Events** + These events are automatically created by Airflow's internal processes during normal operation: + + - Task lifecycle state transitions (queued, running, success, failed) + - System monitoring events (heartbeat timeouts, external state changes) + - Automatic recovery operations (task rescheduling, retry attempts) + - Resource management activities + +**Command-Line Interface Operations** + These events capture activities performed through Airflow's CLI tools: + + - Direct task execution commands + - DAG management operations + - System administration and maintenance tasks + - Automated script executions + + +Common Audit Log Scenarios +--------------------------- + +To facilitate audit log analysis, here are some frequently encountered scenarios and their corresponding queries: + +**"Who triggered this DAG?"** + +.. code-block:: sql + + SELECT dttm, owner, extra + FROM log + WHERE event = 'trigger_dag_run' AND dag_id = 'example_dag' + ORDER BY dttm DESC; + +**"What happened to this failed task?"** + +.. code-block:: sql + + SELECT dttm, event, owner, extra + FROM log + WHERE dag_id = 'example_dag' AND task_id = 'example_task' + ORDER BY dttm DESC; + +**"Who changed variables recently?"** + +.. code-block:: sql + + SELECT dttm, event, owner, extra + FROM log + WHERE event LIKE '%variable%' + ORDER BY dttm DESC LIMIT 20; + +Event Catalog +-------------- + +The following section provides a complete reference of all events tracked by Airflow's audit logging system. Understanding these event types will help interpret audit logs and construct effective queries for specific use cases. + +Task Instance Events +~~~~~~~~~~~~~~~~~~~~ + +**System-generated task events**: + +- ``running``: Task instance started execution +- ``success``: Task instance completed successfully +- ``failed``: Task instance failed during execution +- ``skipped``: Task instance was skipped +- ``upstream_failed``: Task instance failed due to upstream failure +- ``up_for_retry``: Task instance is scheduled for retry +- ``up_for_reschedule``: Task instance is rescheduled +- ``queued``: Task instance is queued for execution +- ``scheduled``: Task instance is scheduled +- ``deferred``: Task instance is deferred (waiting for trigger) +- ``restarting``: Task instance is restarting +- ``removed``: Task instance was removed + +**System monitoring events**: + +- ``heartbeat timeout``: Task instance stopped sending heartbeats and will be terminated +- ``state mismatch``: Task instance state changed externally (outside of Airflow) +- ``stuck in queued reschedule``: Task instance was stuck in queued state and rescheduled +- ``stuck in queued tries exceeded``: Task instance exceeded maximum requeue attempts + +**User-initiated task events**: + +- ``fail task``: User manually marked task as failed +- ``skip task``: User manually marked task as skipped +- ``action_set_failed``: User set task instance as failed through UI/API +- ``action_set_success``: User set task instance as successful through UI/API +- ``action_set_retry``: User set task instance to retry +- ``action_set_skipped``: User set task instance as skipped +- ``action_set_running``: User set task instance as running +- ``action_clear``: User cleared task instance state + +User Action Events +~~~~~~~~~~~~~~~~~~ + +**DAG operations**: + +- ``trigger_dag_run``: User triggered a DAG run +- ``delete_dag_run``: User deleted a DAG run +- ``patch_dag_run``: User modified a DAG run +- ``clear_dag_run``: User cleared a DAG run +- ``get_dag_run``: User retrieved DAG run information +- ``get_dag_runs_batch``: User retrieved multiple DAG runs +- ``post_dag_run``: User created a DAG run +- ``patch_dag``: User modified DAG configuration +- ``get_dag``: User retrieved DAG information +- ``get_dags``: User retrieved multiple DAGs +- ``delete_dag``: User deleted a DAG + +**Task instance operations**: + +- ``post_clear_task_instances``: User cleared task instances +- ``patch_task_instance``: User modified a task instance +- ``get_task_instances_batch``: User retrieved task instance information +- ``delete_task_instance``: User deleted a task instance +- ``get_task_instance``: User retrieved single task instance information +- ``get_task_instance_tries``: User retrieved task instance retry information +- ``patch_task_instances_batch``: User modified multiple task instances + +**Variable operations**: + +- ``delete_variable``: User deleted a variable +- ``patch_variable``: User modified a variable +- ``post_variable``: User created a variable +- ``bulk_variables``: User performed bulk variable operations + +**Connection operations**: + +- ``delete_connection``: User deleted a connection +- ``post_connection``: User created a connection +- ``patch_connection``: User modified a connection +- ``bulk_connections``: User performed bulk connection operations +- ``create_default_connections``: User created default connections + +**Pool operations**: + +- ``get_pool``: User retrieved pool information +- ``get_pools``: User retrieved multiple pools +- ``post_pool``: User created a pool +- ``patch_pool``: User modified a pool +- ``delete_pool``: User deleted a pool +- ``bulk_pools``: User performed bulk pool operations + +**Asset operations**: + +- ``get_asset``: User retrieved asset information +- ``get_assets``: User retrieved multiple assets +- ``get_asset_alias``: User retrieved asset alias information +- ``get_asset_aliases``: User retrieved multiple asset aliases +- ``post_asset_events``: User created asset events +- ``get_asset_events``: User retrieved asset events +- ``materialize_asset``: User triggered asset materialization +- ``get_asset_queued_events``: User retrieved queued asset events +- ``delete_asset_queued_events``: User deleted queued asset events +- ``get_dag_asset_queued_events``: User retrieved DAG asset queued events +- ``delete_dag_asset_queued_events``: User deleted DAG asset queued events +- ``get_dag_asset_queued_event``: User retrieved specific DAG asset queued event +- ``delete_dag_asset_queued_event``: User deleted specific DAG asset queued event + +**Backfill operations**: + +- ``get_backfill``: User retrieved backfill information +- ``get_backfills``: User retrieved multiple backfills +- ``post_backfill``: User created a backfill +- ``pause_backfill``: User paused a backfill +- ``unpause_backfill``: User unpaused a backfill +- ``cancel_backfill``: User cancelled a backfill +- ``create_backfill_dry_run``: User performed backfill dry run + +**User and Role Management**: + +- ``get_user``: User retrieved user information +- ``get_users``: User retrieved multiple users +- ``post_user``: User created a user account +- ``patch_user``: User modified a user account +- ``delete_user``: User deleted a user account +- ``get_role``: User retrieved role information +- ``get_roles``: User retrieved multiple roles +- ``post_role``: User created a role +- ``patch_role``: User modified a role +- ``delete_role``: User deleted a role + +CLI Events +~~~~~~~~~~ + +**DAG Management Commands**: + +- ``cli_dags_list``: List all DAGs in the system +- ``cli_dags_show``: Display DAG information and structure +- ``cli_dags_state``: Check the state of a DAG run +- ``cli_dags_next_execution``: Show next execution time for a DAG +- ``cli_dags_trigger``: Trigger a DAG run from command line +- ``cli_dags_delete``: Delete a DAG and its metadata +- ``cli_dags_pause``: Pause a DAG +- ``cli_dags_unpause``: Unpause a DAG +- ``cli_dags_backfill``: Backfill DAG runs for a date range +- ``cli_dags_test``: Test a DAG without affecting the database + +**Task Management Commands**: + +- ``cli_tasks_list``: List tasks for a specific DAG +- ``cli_tasks_run``: Execute a specific task instance +- ``cli_tasks_test``: Test a task without affecting the database +- ``cli_tasks_state``: Check the state of a task instance +- ``cli_tasks_failed_deps``: Show failed dependencies for a task +- ``cli_tasks_render``: Render task templates +- ``cli_tasks_clear``: Clear task instance state + +**Database and System Commands**: + +- ``cli_db_init``: Initialize the Airflow database +- ``cli_db_upgrade``: Upgrade the database schema +- ``cli_db_reset``: Reset the database (dangerous operation) +- ``cli_db_shell``: Open database shell +- ``cli_db_check``: Check database connectivity and schema +- ``cli_db_migrate``: Migrate database schema (legacy command) +- ``cli_migratedb``: Legacy database migration command +- ``cli_initdb``: Legacy database initialization command +- ``cli_resetdb``: Legacy database reset command +- ``cli_upgradedb``: Legacy database upgrade command + +**User and Security Commands**: + +- ``cli_users_create``: Create a new user account +- ``cli_users_delete``: Delete a user account +- ``cli_users_list``: List all users in the system +- ``cli_users_add_role``: Add role to a user +- ``cli_users_remove_role``: Remove role from a user + +**Configuration and Variable Commands**: + +- ``cli_variables_get``: Retrieve variable value +- ``cli_variables_set``: Set variable value +- ``cli_variables_delete``: Delete a variable +- ``cli_variables_list``: List all variables +- ``cli_variables_import``: Import variables from file +- ``cli_variables_export``: Export variables to file + +**Connection Management Commands**: + +- ``cli_connections_get``: Retrieve connection details +- ``cli_connections_add``: Add a new connection +- ``cli_connections_delete``: Delete a connection +- ``cli_connections_list``: List all connections +- ``cli_connections_import``: Import connections from file +- ``cli_connections_export``: Export connections to file + +**Pool Management Commands**: + +- ``cli_pools_get``: Get pool information +- ``cli_pools_set``: Create or update a pool +- ``cli_pools_delete``: Delete a pool +- ``cli_pools_list``: List all pools +- ``cli_pools_import``: Import pools from file +- ``cli_pools_export``: Export pools to file + +**Service and Process Commands**: + +- ``cli_webserver``: Start the Airflow webserver +- ``cli_scheduler``: Start the Airflow scheduler +- ``cli_worker``: Start a Celery worker +- ``cli_flower``: Start Flower monitoring tool +- ``cli_triggerer``: Start the triggerer process +- ``cli_standalone``: Start Airflow in standalone mode +- ``cli_api_server``: Start the Airflow API server +- ``cli_dag_processor``: Start the DAG processor service +- ``cli_celery_worker``: Start Celery worker (alternative command) +- ``cli_celery_flower``: Start Celery Flower (alternative command) + +**Maintenance and Utility Commands**: + +- ``cli_cheat_sheet``: Display CLI command reference +- ``cli_version``: Show Airflow version information +- ``cli_info``: Display system information +- ``cli_config_get_value``: Get configuration value +- ``cli_config_list``: List configuration options +- ``cli_plugins``: List installed plugins +- ``cli_rotate_fernet_key``: Rotate Fernet encryption key +- ``cli_sync_perm``: Synchronize permissions +- ``cli_shell``: Start interactive Python shell +- ``cli_kerberos``: Start Kerberos ticket renewer + +**Testing and Development Commands**: + +- ``cli_test``: Run tests +- ``cli_render``: Render templates +- ``cli_dag_deps``: Show DAG dependencies +- ``cli_task_deps``: Show task dependencies + +**Legacy Commands**: + +- ``cli_run``: Legacy task run command +- ``cli_backfill``: Legacy backfill command +- ``cli_clear``: Legacy clear command +- ``cli_list_dags``: Legacy DAG list command +- ``cli_list_tasks``: Legacy task list command +- ``cli_pause``: Legacy pause command +- ``cli_unpause``: Legacy unpause command +- ``cli_trigger_dag``: Legacy DAG trigger command + +Each CLI command audit log entry includes: + +- **User identification**: Who executed the command +- **Command details**: Full command with arguments +- **Execution context**: Working directory, environment variables +- **Timestamp**: When the command was executed +- **Exit status**: Success or failure indication + +Custom Events +~~~~~~~~~~~~~ + +Airflow allows creating custom audit log entries programmatically: + +.. code-block:: python + + from airflow.models.log import Log + from airflow.utils.session import provide_session + + + @provide_session + def log_custom_event(session=None): + log_entry = Log(event="custom_event", owner="username", extra="Additional context information") + session.add(log_entry) + session.commit() + + +Anatomy of an Audit Log Entry +------------------------------ + +Each audit log record contains structured information that provides a complete picture of the logged event. Understanding these fields is essential for effective log analysis: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Field Name + - Description and Usage + * - ``dttm`` + - Timestamp indicating when the event occurred (UTC timezone) + * - ``event`` + - Descriptive name of the action or event (e.g., ``trigger_dag_run``, ``failed``) + * - ``owner`` + - Identity of the actor: username for user actions, "airflow" for system events + * - ``dag_id`` + - Identifier of the affected DAG (when applicable) + * - ``task_id`` + - Identifier of the affected task (when applicable) + * - ``run_id`` + - Specific DAG run identifier for tracking execution instances + * - ``try_number`` + - Attempt number for task retries and re-executions + * - ``map_index`` + - Index for dynamically mapped tasks + * - ``logical_date`` + - Logical execution date of the DAG run + * - ``extra`` + - JSON-formatted additional context (parameters, error details, etc.) + + +Audit Log Query Methods +----------------------- + +Effective audit log analysis requires understanding the various methods available for querying and retrieving log data. Each method has its strengths and is suited to different scenarios: + +**REST API Examples**: + +.. code-block:: bash + + # Get all audit logs + curl -X GET "http://localhost:8080/api/v1/eventLogs" + + # Filter by event type + curl -X GET "http://localhost:8080/api/v1/eventLogs?event=trigger_dag_run" + + # Filter by DAG + curl -X GET "http://localhost:8080/api/v1/eventLogs?dag_id=example_dag" + + # Filter by date range + curl -X GET "http://localhost:8080/api/v1/eventLogs?after=2024-01-01T00:00:00Z&before=2024-12-31T23:59:59Z" + +**Database Query Examples**: + +.. code-block:: sql + + -- Get recent user actions + SELECT dttm, event, owner, dag_id, task_id, extra + FROM log + WHERE owner IS NOT NULL + ORDER BY dttm DESC + LIMIT 100; + + -- Get task failure events + SELECT dttm, dag_id, task_id, run_id, extra + FROM log + WHERE event = 'failed' + ORDER BY dttm DESC; + + -- Get user actions on specific DAG + SELECT dttm, event, owner, extra + FROM log + WHERE dag_id = 'example_dag' AND owner IS NOT NULL + ORDER BY dttm DESC; + + +Querying Event Logs +------------------- + +Event logs (operational logs) are typically accessed through different methods depending on the logging configuration: + +**Log Files**: + +.. code-block:: bash + + # View scheduler logs + tail -f $AIRFLOW_HOME/logs/scheduler/latest/*.log + + # View webserver logs + tail -f $AIRFLOW_HOME/logs/webserver/webserver.log + + # View task logs for specific DAG run + cat $AIRFLOW_HOME/logs/dag_id/task_id/2024-01-01T00:00:00+00:00/1.log + +**REST API for Task Logs**: + +.. code-block:: bash + + # Get task instance logs + curl -X GET "http://localhost:8080/api/v1/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/logs/{try_number}" + + # Get task logs with metadata + curl -X GET "http://localhost:8080/api/v1/dags/example_dag/dagRuns/2024-01-01T00:00:00+00:00/taskInstances/example_task/logs/1?full_content=true" + +**Python Logging Integration**: + +.. code-block:: python + + import logging + from airflow.utils.log.logging_mixin import LoggingMixin + + + class MyOperator(BaseOperator, LoggingMixin): + def execute(self, context): + # These will appear in event logs + self.log.info("Task started") + self.log.warning("Warning message") + self.log.error("Error occurred") + +**External Logging Systems**: + +When using external logging systems (e.g., ELK stack, Splunk, CloudWatch): + +.. code-block:: bash + + # Example Elasticsearch query + curl -X GET "elasticsearch:9200/airflow-*/_search" -H 'Content-Type: application/json' -d' + { + "query": { + "bool": { + "must": [ + {"match": {"dag_id": "example_dag"}}, + {"range": {"@timestamp": {"gte": "2024-01-01", "lte": "2024-01-31"}}} + ] + } + } + }' + + +Practical Query Examples +------------------------------------- + +The following examples demonstrate practical applications of audit log queries for common operational and security scenarios. These queries serve as templates that can be adapted for specific requirements: + +**Security Investigation** + +.. code-block:: sql + + -- Find all actions by a specific user in the last 24 hours + SELECT dttm, event, dag_id, task_id, extra + FROM log + WHERE owner = 'suspicious_user' + AND dttm > NOW() - INTERVAL '24 hours' + ORDER BY dttm DESC; + +**Compliance Reporting** +.. code-block:: sql -Types of Events ---------------- + -- Get all variable and connection changes for audit report + SELECT dttm, event, owner, extra + FROM log + WHERE event IN ('post_variable', 'patch_variable', 'delete_variable', + 'post_connection', 'patch_connection', 'delete_connection') + AND dttm BETWEEN '2024-01-01' AND '2024-01-31' + ORDER BY dttm; -Airflow provides a set of predefined events that can be tracked in audit logs. These events include, but aren't limited to: +**Troubleshooting DAG Issues** -- ``trigger``: Triggering a Dag -- ``[variable,connection].create``: A user created a Connection or Variable -- ``[variable,connection].edit``: A user modified a Connection or Variable -- ``[variable,connection].delete``: A user deleted a Connection or Variable -- ``delete``: A user deleted a Dag or task -- ``failed``: Airflow or a user set a task as failed -- ``success``: Airflow or a user set a task as success -- ``retry``: Airflow or a user retried a task instance -- ``clear``: A user cleared a task's state -- ``cli_task_run``: Airflow triggered a task instance +.. code-block:: sql -In addition to these predefined events, Airflow allows you to define custom events that can be tracked in audit logs. -This can be done by calling the ``log`` method of the ``TaskInstance`` object. + -- See all events for a problematic DAG run + SELECT dttm, event, task_id, owner, extra + FROM log + WHERE dag_id = 'example_dag' + AND run_id = '2024-01-15T10:00:00+00:00' + ORDER BY dttm; diff --git a/airflow-core/docs/security/index.rst b/airflow-core/docs/security/index.rst index b9f79e2ee7063..28c7297425155 100644 --- a/airflow-core/docs/security/index.rst +++ b/airflow-core/docs/security/index.rst @@ -26,6 +26,13 @@ the different user types of Apache Airflow®, what they have access to, and the Also, if you want to understand how Airflow releases security patches and what to expect from them, head over to :doc:`Releasing security patches `. +There are number of services where you can track security issues reported and announced by Airflow same as for +any of the projects following the standard CVE databases. + +One such database is run by the MITRE corporation and you can search +for `Airflow CVEs `_ +there, however you might use whatever database you and your organization prefers to track security issues and CVEs. + Follow the below topics as well to understand other aspects of Airflow security: .. toctree:: diff --git a/airflow-core/docs/security/secrets/mask-sensitive-values.rst b/airflow-core/docs/security/secrets/mask-sensitive-values.rst index 1663883b2ca75..39ec58651abfb 100644 --- a/airflow-core/docs/security/secrets/mask-sensitive-values.rst +++ b/airflow-core/docs/security/secrets/mask-sensitive-values.rst @@ -20,8 +20,10 @@ Masking sensitive data ---------------------- -Airflow will by default mask Connection passwords and sensitive Variables and keys from a Connection's -extra (JSON) field when they appear in Task logs, in the Variable and in the Rendered fields views of the UI. +Airflow will by default mask Connection passwords, sensitive Variables, and keys from a Connection's +extra (JSON) field whose names contain one or more of the sensitive keywords when they appear in Task logs, +in the Variables UI, and in the Rendered fields views of the UI. Keys in the extra JSON that do not include +any of these sensitive keywords will not be redacted automatically. It does this by looking for the specific *value* appearing anywhere in your output. This means that if you have a connection with a password of ``a``, then every instance of the letter a in your logs will be replaced @@ -58,7 +60,7 @@ your Dag file or operator's ``execute`` function using the ``mask_secret`` funct @task def my_func(): - from airflow.sdk.execution_time.secrets_masker import mask_secret + from airflow.sdk.log import mask_secret mask_secret("custom_value") @@ -71,7 +73,7 @@ or class MyOperator(BaseOperator): def execute(self, context): - from airflow.sdk.execution_time.secrets_masker import mask_secret + from airflow.sdk.log import mask_secret mask_secret("custom_value") diff --git a/airflow-core/docs/start.rst b/airflow-core/docs/start.rst index 3ad574bdeefab..4175343389a8f 100644 --- a/airflow-core/docs/start.rst +++ b/airflow-core/docs/start.rst @@ -68,7 +68,7 @@ This quick start guide will help you bootstrap an Airflow standalone instance on :substitutions: - AIRFLOW_VERSION=3.0.3 + AIRFLOW_VERSION=3.1.3 # Extract the version of Python you have installed. If you're currently using a Python version that is not supported by Airflow, you may want to set this manually. # See above for supported versions. diff --git a/airflow-core/docs/templates-ref.rst b/airflow-core/docs/templates-ref.rst index 1ce0b1852e752..0cbd8bf29c8ba 100644 --- a/airflow-core/docs/templates-ref.rst +++ b/airflow-core/docs/templates-ref.rst @@ -188,7 +188,7 @@ Variable Description Some Airflow specific macros are also defined: -.. automodule:: airflow.macros +.. automodule:: airflow.sdk.execution_time.macros :members: .. _pendulum.DateTime: https://pendulum.eustace.io/docs/#introduction diff --git a/airflow-core/docs/troubleshooting.rst b/airflow-core/docs/troubleshooting.rst index 6756ab87e1051..b0f786469e420 100644 --- a/airflow-core/docs/troubleshooting.rst +++ b/airflow-core/docs/troubleshooting.rst @@ -36,16 +36,26 @@ Below are some example scenarios that could cause a task's state to change by a - A user marked the task as successful or failed in the Airflow UI. - An external script or process used the :doc:`Airflow REST API ` to change the state of a task. -TaskRunner killed ------------------ +Process terminated by signal +---------------------------- Sometimes, Airflow or some adjacent system will kill a task instance's ``TaskRunner``, causing the task instance to fail. -Here are some examples that could cause such an event: +Below we discuss a few common cases. -- A Dag run timeout, specified by ``dagrun_timeout`` in the Dag's definition. -- An Airflow worker running out of memory - - Usually, Airflow workers that run out of memory receive a SIGKILL, and the scheduler will fail the corresponding task instance for not having a heartbeat. However, in some scenarios, Airflow kills the task before that happens. +Dag run timeout +""""""""""""""" + +A dag run timeout can be specified by ``dagrun_timeout`` in the dag's definition. +The task process would likely be killed with SIGTERM (exit code -15). + +Out of memory error (OOM) +""""""""""""""""""""""""" + +When a task process consumes too much memory for a worker, the best case scenario is it is killed +with SIGKILL (exit code -9). Depending on configuration and infrastructure, it is also +possible that the whole worker will be killed due to OOM and then the tasks would be marked as +failed after failing to heartbeat. Lingering task supervisor processes ----------------------------------- diff --git a/airflow-core/docs/tutorial/fundamentals.rst b/airflow-core/docs/tutorial/fundamentals.rst index 0017860e343dc..02e60b5e8d153 100644 --- a/airflow-core/docs/tutorial/fundamentals.rst +++ b/airflow-core/docs/tutorial/fundamentals.rst @@ -165,11 +165,11 @@ documentation at the start of your Dag file. | -.. image:: ../img/ui-dark/task_doc.png +.. image:: ../img/ui-light/task_doc.png | -.. image:: ../img/ui-dark/dag_doc.png +.. image:: ../img/ui-light/dag_doc.png Setting up Dependencies ----------------------- diff --git a/airflow-core/docs/tutorial/hitl.rst b/airflow-core/docs/tutorial/hitl.rst index b104bc7d03366..cc6a76b5ef1ec 100644 --- a/airflow-core/docs/tutorial/hitl.rst +++ b/airflow-core/docs/tutorial/hitl.rst @@ -52,7 +52,7 @@ This is useful for workflows involving human guidance within large language mode You can click the task and find the Required Actions tab in the details panel. -.. image:: /img/hitl_wait_for_input.png +.. image:: /img/ui-light/hitl_wait_for_input.png :alt: Demo HITL task instance waiting for input | @@ -71,7 +71,7 @@ Users can select one of the available options, which can be used to direct the w | -.. image:: /img/hitl_wait_for_option.png +.. image:: /img/ui-light/hitl_wait_for_option.png :alt: Demo HITL task instance waiting for an option | @@ -86,7 +86,7 @@ Multiple options are also allowed. | -.. image:: /img/hitl_wait_for_multiple_options.png +.. image:: /img/ui-light/hitl_wait_for_multiple_options.png :alt: Demo HITL task instance waiting for multiple options | @@ -95,6 +95,9 @@ Approval or Rejection --------------------- A specialized form of option selection, which has only 'Approval' and 'Rejection' as options. +You can also set the ``assigned_users`` to restrict the users allowed to respond for a HITL operator. +It should be a list of user ids and user names (both needed) (e.g., ``[{"id": "1", "name": "user1"}, {"id": "2", "name": "user2"}]``. +ONLY the users within this list will be allowed to respond. .. exampleinclude:: /../../providers/standard/src/airflow/providers/standard/example_dags/example_hitl_operator.py :language: python @@ -106,7 +109,7 @@ A specialized form of option selection, which has only 'Approval' and 'Rejection As you can see in the body of this code snippet, you can use XComs to get information provided by the user. -.. image:: /img/hitl_approve_reject.png +.. image:: /img/ui-light/hitl_approve_reject.png :alt: Demo HITL task instance waiting for approval or rejection | @@ -136,14 +139,14 @@ And remember to specify their relationship in the workflow. | -.. image:: /img/hitl_branch_selection.png +.. image:: /img/ui-light/hitl_branch_selection.png :alt: Demo HITL task instance waiting for branch selection | After the branch is chosen, the workflow will proceed along the selected path. -.. image:: /img/hitl_branch_selected.png +.. image:: /img/ui-light/hitl_branch_selected.png :alt: Demo HITL task instance after branch selection Notifiers diff --git a/airflow-core/docs/tutorial/pipeline.rst b/airflow-core/docs/tutorial/pipeline.rst index c1b956ae29960..196ee7857bbb2 100644 --- a/airflow-core/docs/tutorial/pipeline.rst +++ b/airflow-core/docs/tutorial/pipeline.rst @@ -95,7 +95,7 @@ Fill in the following details: - Password: ``airflow`` - Port: ``5432`` -.. image:: ../img/ui-dark/tutorial_pipeline_add_connection.png +.. image:: ../img/ui-light/tutorial_pipeline_add_connection.png :alt: Add Connection form in Airflow's web UI with Postgres details filled in. | @@ -341,16 +341,21 @@ run using the play button. You can watch each task as it runs in the **Grid** view, and explore logs for each step. -.. image:: ../img/ui-dark/tutorial_pipeline_dag_list.png +.. image:: ../img/ui-light/tutorial_pipeline_dag_list_trigger.png :alt: Dag List view showing the ``process_employees`` Dag | -.. image:: ../img/ui-dark/tutorial_pipeline_dag_overview_processed.png +.. image:: ../img/ui-light/tutorial_pipeline_dag_overview_processed.png :alt: Dag Overview page for ``process_employees`` Dag showing the Dag run | +.. image:: ../img/ui-light/tutorial_pipeline_dag_task_instance_logs.png + :alt: Task instance page for a task in ``process_employees`` Dag showing the task instance logs + +| + Once it succeeds, you'll have a fully working pipeline that integrates data from the outside world, loads it into Postgres, and keeps it clean. diff --git a/airflow-core/newsfragments/46929.bugfix.rst b/airflow-core/newsfragments/46929.bugfix.rst deleted file mode 100644 index bff83a3807dae..0000000000000 --- a/airflow-core/newsfragments/46929.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Added validation in XCom.set() to disallow None or empty string keys; XCom.get() still allows None as key but disallows empty strings. diff --git a/airflow-core/newsfragments/49779.significant.rst b/airflow-core/newsfragments/49779.significant.rst deleted file mode 100644 index 187f6ac199198..0000000000000 --- a/airflow-core/newsfragments/49779.significant.rst +++ /dev/null @@ -1,20 +0,0 @@ -SecretCache class has been moved to ``airflow.sdk.execution_time.cache`` from ``airflow.secrets.cache`` - -* Types of change - - * [ ] Dag changes - * [ ] Config changes - * [ ] API changes - * [ ] CLI changes - * [ ] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [x] Code interface changes - -* Migration rules needed - - * ruff - - * AIR301 - - * [ ] ``airflow.secrets.cache.SecretCache`` → ``airflow.sdk.execution_time.cache.SecretCache`` diff --git a/airflow-core/newsfragments/50374.feature.rst b/airflow-core/newsfragments/50374.feature.rst deleted file mode 100644 index 85e4dc165e9c7..0000000000000 --- a/airflow-core/newsfragments/50374.feature.rst +++ /dev/null @@ -1 +0,0 @@ -When a Dag specifies ``schedule="@once"`` without an explicit ``start_date``, run it as soon as convenient. (Previously, the Dag would never run.) diff --git a/airflow-core/newsfragments/50693.significant.rst b/airflow-core/newsfragments/50693.significant.rst deleted file mode 100644 index 6368ddf0ee56c..0000000000000 --- a/airflow-core/newsfragments/50693.significant.rst +++ /dev/null @@ -1,41 +0,0 @@ -Unused webserver configuration options have been removed - -The following webserver options were moved into the ``api`` section: - -* ``[webserver] log_fetch_timeout_sec`` → ``[api] log_fetch_timeout_sec`` -* ``[webserver] hide_paused_dags_by_default`` → ``[api] hide_paused_dags_by_default`` -* ``[webserver] page_size`` → ``[api] page_size`` -* ``[webserver] default_wrap`` → ``[api] default_wrap`` -* ``[webserver] require_confirmation_dag_change`` → ``[api] require_confirmation_dag_change`` -* ``[webserver] auto_refresh_interval`` → ``[api] auto_refresh_interval`` - -The following configuration options are now unused and have been removed: - -- ``[webserver] instance_name_has_markup`` -- ``[webserver] warn_deployment_exposure`` - -* Types of change - - * [ ] Dag changes - * [x] Config changes - * [ ] API changes - * [ ] CLI changes - * [ ] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [ ] Code interface changes - -.. List the migration rules needed for this change (see https://github.com/apache/airflow/issues/41641) - -* Migration rules needed - - * ``airflow config lint`` - - * [ ] Remove configuration option ``[webserver] instance_name_has_markup`` - * [ ] Remove configuration option ``[webserver] warn_deployment_exposure`` - * [ ] [webserver] log_fetch_timeout_sec`` → ``[api] log_fetch_timeout_sec`` - * [ ] [webserver] hide_paused_dags_by_default`` → ``[api] hide_paused_dags_by_default`` - * [ ] [webserver] page_size`` → ``[api] page_size`` - * [ ] [webserver] default_wrap`` → ``[api] default_wrap`` - * [ ] [webserver] require_confirmation_dag_change`` → ``[api] require_confirmation_dag_change`` - * [ ] [webserver] auto_refresh_interval`` → ``[api] auto_refresh_interval`` diff --git a/airflow-core/newsfragments/51424.significant.rst b/airflow-core/newsfragments/51424.significant.rst deleted file mode 100644 index a455a28eb1c9b..0000000000000 --- a/airflow-core/newsfragments/51424.significant.rst +++ /dev/null @@ -1,17 +0,0 @@ -The ``consuming_dags`` key in asset API has been renamed to ``scheduled_dags``. - -The previous name caused confusion to users since the list does not contain all -Dags that technically *use* the asset, but only those that use it in their -``schedule`` argument. As a bug fix, the key has been renamed to clarify its -intention. - -* Types of change - - * [ ] Dag changes - * [ ] Config changes - * [x] API changes - * [ ] CLI changes - * [ ] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [ ] Code interface changes diff --git a/airflow-core/newsfragments/51639.significant.rst b/airflow-core/newsfragments/51639.significant.rst deleted file mode 100644 index 78708b4eb1913..0000000000000 --- a/airflow-core/newsfragments/51639.significant.rst +++ /dev/null @@ -1,17 +0,0 @@ -``enable_xcom_deserialize_support`` configuration option has been removed. - -This configuration was previously marked as a security risk due to potential remote code execution vulnerabilities -when deserializing arbitrary Python objects that came in from XComs. The removal is a security improvement since -all custom XCom serialization/deserialization is now handled safely at the worker level, making this configuration -unnecessary in core. Users should migrate to not setting this configuration. - -* Types of change - - * [ ] Dag changes - * [x] Config changes - * [ ] API changes - * [ ] CLI changes - * [ ] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [ ] Code interface changes diff --git a/airflow-core/newsfragments/52860.significant.rst b/airflow-core/newsfragments/52860.significant.rst deleted file mode 100644 index 5962897ec206d..0000000000000 --- a/airflow-core/newsfragments/52860.significant.rst +++ /dev/null @@ -1,17 +0,0 @@ -Replace API server ``access_logfile`` configuration with ``log_config`` - -The API server configuration option ``[api] access_logfile`` has been replaced with ``[api] log_config`` to align with uvicorn's logging configuration instead of the legacy gunicorn approach. -The new ``log_config`` option accepts a path to a logging configuration file compatible with ``logging.config.fileConfig``, providing more flexible logging configuration for the API server. - -This change also removes the dependency on gunicorn for daemonization, making the API server ``--daemon`` option consistent with other Airflow components like scheduler and triggerer. - -* Types of change - - * [ ] Dag changes - * [x] Config changes - * [ ] API changes - * [ ] CLI changes - * [ ] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [ ] Code interface changes diff --git a/airflow-core/newsfragments/53631.misc.rst b/airflow-core/newsfragments/53631.misc.rst deleted file mode 100644 index a6cef457b5844..0000000000000 --- a/airflow-core/newsfragments/53631.misc.rst +++ /dev/null @@ -1 +0,0 @@ -The constraint do not contain developer dependencies as of Airflow 3.1.0 diff --git a/airflow-core/newsfragments/53727.feature.rst b/airflow-core/newsfragments/53727.feature.rst deleted file mode 100644 index 90272000d1d1c..0000000000000 --- a/airflow-core/newsfragments/53727.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add Airflow Deadlines feature in Airflow 3.1. See https://airflow.apache.org/docs/apache-airflow/stable/howto/deadline-alerts.html. diff --git a/airflow-core/newsfragments/53796.misc.rst b/airflow-core/newsfragments/53796.misc.rst deleted file mode 100644 index a582154dc3ae6..0000000000000 --- a/airflow-core/newsfragments/53796.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``RemovedInAirflow4Warning`` warnings for ``airflow.security.permissions`` imports and ``access_control`` Dag attribute usage. diff --git a/airflow-core/newsfragments/54857.significant.rst b/airflow-core/newsfragments/54857.significant.rst deleted file mode 100644 index 84ba878db595f..0000000000000 --- a/airflow-core/newsfragments/54857.significant.rst +++ /dev/null @@ -1,14 +0,0 @@ -Remove ``get_task_group_children_getter`` and ``task_group_to_dict`` from task-sdk - -The ``get_task_group_children_getter`` and ``task_group_to_dict`` functions have been removed from the task-sdk (``airflow.sdk.definitions.taskgroup``) and moved to server-side API services. These functions are now internal to Airflow's API layer and should not be imported directly by users. - -* Types of change - - * [ ] Dag changes - * [ ] Config changes - * [ ] API changes - * [ ] CLI changes - * [ ] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [x] Code interface changes diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml index 7348391d7cb18..41806e70ff36d 100644 --- a/airflow-core/pyproject.toml +++ b/airflow-core/pyproject.toml @@ -25,7 +25,7 @@ requires = [ "pluggy==1.6.0", "smmap==5.0.2", "tomli==2.2.1; python_version < '3.11'", - "trove-classifiers==2025.9.8.13", + "trove-classifiers==2025.9.11.17", ] build-backend = "hatchling.build" @@ -34,7 +34,8 @@ build-backend = "hatchling.build" name = "apache-airflow-core" description = "Core packages for Apache Airflow, schedule and API server" readme = { file = "README.md", content-type = "text/markdown" } -license-files.globs = ["LICENSE", "3rd-party-licenses/*.txt", "NOTICE"] +license = "Apache-2.0" +license-files = ["LICENSE", "NOTICE"] # We know that it will take a while before we can support Python 3.14 because of all our dependencies # It takes about 4-7 months after Python release before we can support it, so we limit it to <3.14 # proactively. This way we also have a chance to test it with Python 3.14 and bump the upper binding @@ -55,7 +56,6 @@ classifiers = [ "Framework :: Apache Airflow", "Intended Audience :: Developers", "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -63,11 +63,17 @@ classifiers = [ ] # Version is defined in src/airflow/__init__.py and it is automatically synchronized by prek hook -version = "3.1.0" +version = "3.1.6" dependencies = [ "a2wsgi>=1.10.8", - "aiosqlite>=0.20.0", + # aiosqlite 0.22.0 has a problem with hanging pytest sessions and we excluded it + # See https://github.com/omnilib/aiosqlite/issues/369 + # It seems that while our test issues are fixed in 0.22.1, sqlalchemy 2 itself + # is not compatible with it and leaves thread hanging This is already fixed in main of sqlalchemy + # But not released yet - and we will likely have to add >=2.0.46+ for sqlalchemy when released to + # protect against it https://github.com/sqlalchemy/sqlalchemy/issues/13039 + "aiosqlite>=0.20.0,<0.22.0", # Alembic is important to handle our migrations in predictable and performant way. It is developed # together with SQLAlchemy. Our experience with Alembic is that it very stable in minor version # The 1.13.0 of alembic marked some migration code as SQLAlchemy 2+ only so we limit it to 1.13.1 @@ -82,7 +88,8 @@ dependencies = [ "cryptography>=41.0.0", "deprecated>=1.2.13", "dill>=0.2.2", - "fastapi[standard-no-fastapi-cloud-cli]>=0.116.0", + "fastapi[standard-no-fastapi-cloud-cli]>=0.116.0,<0.118.0", + "uvicorn>=0.37.0", "starlette>=0.45.0", "httpx>=0.25.0", 'importlib_metadata>=6.5;python_version<"3.12"', @@ -95,6 +102,7 @@ dependencies = [ "linkify-it-py>=2.0.0", "lockfile>=0.12.2", "methodtools>=0.4.7", + "natsort>=8.4.0", "opentelemetry-api>=1.27.0", "opentelemetry-exporter-otlp>=1.27.0", # opentelemetry-proto is a transitive dependency of @@ -134,17 +142,21 @@ dependencies = [ "tenacity>=8.3.0", "termcolor>=3.0.0", "typing-extensions>=4.14.1", - # Universal Pathlib 0.2.4 adds extra validation for Paths and our integration with local file paths - # Does not work with it Tracked in https://github.com/fsspec/universal_pathlib/issues/276 - "universal-pathlib>=0.2.2,!=0.2.4", + # https://github.com/apache/airflow/issues/56369 , rework universal-pathlib usage + "universal-pathlib>=0.2.6,<0.3.0", "uuid6>=2024.7.10", - "apache-airflow-task-sdk<1.2.0,>=1.1.0", + "apache-airflow-task-sdk<1.2.0,==1.1.6", # pre-installed providers - "apache-airflow-providers-common-compat>=1.6.0", - "apache-airflow-providers-common-io>=1.5.3", - "apache-airflow-providers-common-sql>=1.26.0", - "apache-airflow-providers-smtp>=2.0.2", - "apache-airflow-providers-standard>=0.4.0", + "apache-airflow-providers-common-compat>=1.7.4", + "apache-airflow-providers-common-io>=1.6.3", + "apache-airflow-providers-common-sql>=1.28.1", + "apache-airflow-providers-smtp>=2.3.1", + "apache-airflow-providers-standard>=1.9.0", + # Start of shared logging dependencies + "msgspec>=0.19.0", + "pygtrie>=2.5.0", + "structlog>=25.4.0", + # End of shared logging dependencies ] @@ -153,6 +165,7 @@ dependencies = [ "eventlet>=0.37.0", "gevent>=25.4.1", "greenlet>=3.1.0", + "greenback>=1.2.1", ] "graphviz" = [ # The graphviz package creates friction when installing on MacOS as it needs graphviz system package to @@ -202,6 +215,7 @@ path = "src/airflow/__init__.py" [tool.hatch.build.targets.sdist] include = [ "src/airflow", + "LICENSE", "NOTICE" ] exclude = [ @@ -211,8 +225,9 @@ exclude = [ ] [tool.hatch.build.targets.sdist.force-include] -"../shared/timezones/src/airflow_shared/timezones" = "src/airflow/_shared/timezones" +"../shared/logging/src/airflow_shared/logging" = "src/airflow/_shared/logging" "../shared/secrets_masker/src/airflow_shared/secrets_masker" = "src/airflow/_shared/secrets_masker" +"../shared/timezones/src/airflow_shared/timezones" = "src/airflow/_shared/timezones" [tool.hatch.build.targets.custom] path = "./hatch_build.py" @@ -280,6 +295,7 @@ apache-airflow-devel-common = { workspace = true } [tool.airflow] shared_distributions = [ - "apache-airflow-shared-timezones", + "apache-airflow-shared-logging", "apache-airflow-shared-secrets-masker", + "apache-airflow-shared-timezones", ] diff --git a/airflow-core/src/airflow/__init__.py b/airflow-core/src/airflow/__init__.py index 5c2d08b41c924..c08a79852f627 100644 --- a/airflow-core/src/airflow/__init__.py +++ b/airflow-core/src/airflow/__init__.py @@ -25,7 +25,7 @@ # lib.) This is required by some IDEs to resolve the import paths. __path__ = __import__("pkgutil").extend_path(__path__, __name__) -__version__ = "3.1.0" +__version__ = "3.1.6" import os diff --git a/airflow-core/src/airflow/_shared/logging b/airflow-core/src/airflow/_shared/logging new file mode 120000 index 0000000000000..19c320ffcf9ca --- /dev/null +++ b/airflow-core/src/airflow/_shared/logging @@ -0,0 +1 @@ +../../../../shared/logging/src/airflow_shared/logging \ No newline at end of file diff --git a/airflow-core/src/airflow/api/common/mark_tasks.py b/airflow-core/src/airflow/api/common/mark_tasks.py index d424bab603a9b..5c0ed4b9f5fec 100644 --- a/airflow-core/src/airflow/api/common/mark_tasks.py +++ b/airflow-core/src/airflow/api/common/mark_tasks.py @@ -20,7 +20,7 @@ from __future__ import annotations from collections.abc import Collection, Iterable -from typing import TYPE_CHECKING, TypeAlias, cast +from typing import TYPE_CHECKING, TypeAlias from sqlalchemy import and_, or_, select from sqlalchemy.orm import lazyload @@ -228,9 +228,7 @@ def set_dag_run_state_to_success( if not run_id: raise ValueError(f"Invalid dag_run_id: {run_id}") - # TODO (GH-52141): 'tasks' in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - tasks = cast("list[Operator]", dag.tasks) + tasks = dag.tasks # Mark all task instances of the dag run to success - except for unfinished teardown as they need to complete work. teardown_tasks = [task for task in tasks if task.is_teardown] @@ -312,13 +310,7 @@ def _set_runing_task(task: Operator) -> Operator: task.dag = dag return task - # TODO (GH-52141): 'tasks' in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - running_tasks = [ - _set_runing_task(task) - for task in cast("list[Operator]", dag.tasks) - if task.task_id in task_ids_of_running_tis - ] + running_tasks = [_set_runing_task(task) for task in dag.tasks if task.task_id in task_ids_of_running_tis] # Mark non-finished tasks as SKIPPED. pending_tis: list[TaskInstance] = session.scalars( diff --git a/airflow-core/src/airflow/api/common/trigger_dag.py b/airflow-core/src/airflow/api/common/trigger_dag.py index fa793d221ec58..614140b58c4a9 100644 --- a/airflow-core/src/airflow/api/common/trigger_dag.py +++ b/airflow-core/src/airflow/api/common/trigger_dag.py @@ -92,10 +92,10 @@ def _trigger_dag( else: data_interval = None - run_id = run_id or DagRun.generate_run_id( + run_id = run_id or dag.timetable.generate_run_id( run_type=DagRunType.MANUAL, - logical_date=coerced_logical_date, run_after=timezone.coerce_datetime(run_after), + data_interval=data_interval, ) # This intentionally does not use 'session' in the current scope because it diff --git a/airflow-core/src/airflow/api_fastapi/app.py b/airflow-core/src/airflow/api_fastapi/app.py index f515be6992c60..7c05295807e62 100644 --- a/airflow-core/src/airflow/api_fastapi/app.py +++ b/airflow-core/src/airflow/api_fastapi/app.py @@ -49,6 +49,9 @@ # Define the full path on which the potential auth manager fastapi is mounted AUTH_MANAGER_FASTAPI_APP_PREFIX = f"{API_ROOT_PATH}auth" +# Fast API apps mounted under these prefixes are not allowed +RESERVED_URL_PREFIXES = ["/api/v2", "/ui", "/execution"] + log = logging.getLogger(__name__) app: FastAPI | None = None @@ -158,8 +161,6 @@ def init_auth_manager(app: FastAPI | None = None) -> BaseAuthManager: def get_auth_manager() -> BaseAuthManager: """Return the auth manager, provided it's been initialized before.""" - global auth_manager - if auth_manager is None: raise RuntimeError( "Auth Manager has not been initialized yet. " @@ -185,6 +186,12 @@ def init_plugins(app: FastAPI) -> None: if url_prefix is None: log.error("'url_prefix' key is missing for the fastapi app: %s", name) continue + if url_prefix == "": + log.error("'url_prefix' key is empty string for the fastapi app: %s", name) + continue + if any(url_prefix.startswith(prefix) for prefix in RESERVED_URL_PREFIXES): + log.error("Plugin %s attempted to use reserved url_prefix '%s'", name, url_prefix) + continue log.debug("Adding subapplication %s under prefix %s", name, url_prefix) app.mount(url_prefix, subapp) @@ -200,5 +207,9 @@ def init_plugins(app: FastAPI) -> None: log.error("'middleware' key is missing for the fastapi middleware: %s", name) continue + if not callable(middleware): + log.error("'middleware' value for %s is should be callable: %s", name, middleware) + continue + log.debug("Adding root middleware %s", name) app.add_middleware(middleware, *args, **kwargs) diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py index d57dd3cdc39f1..d7d2acabe593e 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py @@ -135,12 +135,15 @@ def get_url_logout(self) -> str | None: """ return None - def get_url_refresh(self) -> str | None: + def refresh_user(self, *, user: T) -> T | None: """ - Return the URL to refresh the authentication token. + Refresh the user if needed. - This is used to refresh the authentication token when it expires. - The default implementation returns None, which means that the auth manager does not support refresh token. + By default, does nothing. Some auth managers might need to refresh the user to, for instance, + refresh some tokens that are needed to communicate with a service/tool. + + This method is called by every single request, it must be lightweight otherwise the overall API + server latency will increase. """ return None @@ -315,6 +318,18 @@ def filter_authorized_menu_items(self, menu_items: list[MenuItem], *, user: T) - :param user: the user """ + def is_authorized_hitl_task(self, *, assigned_users: set[str], user: T) -> bool: + """ + Check if a user is allowed to approve/reject a HITL task. + + By default, checks if the user's ID is in the assigned_users set. + Auth managers can override this method to implement custom logic. + + :param assigned_users: set of user IDs assigned to the task + :param user: the user to check authorization for + """ + return user.get_id() in assigned_users + def batch_is_authorized_connection( self, requests: Sequence[IsAuthorizedConnectionRequest], diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/exceptions.py b/airflow-core/src/airflow/api_fastapi/auth/managers/exceptions.py new file mode 100644 index 0000000000000..711b2a6334db0 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/exceptions.py @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + + +class AuthManagerRefreshTokenExpiredException(Exception): + """Exception to throw when the user refresh token is expired.""" diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py index f46674301ead1..372aecf603525 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py @@ -94,6 +94,7 @@ def login_all_admins(request: Request) -> RedirectResponse: COOKIE_NAME_JWT_TOKEN, SimpleAuthManagerLogin.create_token_all_admins(), secure=secure, + httponly=True, ) return response diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py index b3ce04a94b8ea..09250168e14ed 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py @@ -272,6 +272,26 @@ def filter_authorized_menu_items( ) -> list[MenuItem]: return menu_items + def is_authorized_hitl_task(self, *, assigned_users: set[str], user: SimpleAuthManagerUser) -> bool: + """ + Check if a user is allowed to approve/reject a HITL task. + + When simple_auth_manager_all_admins=True, all authenticated users are allowed + to approve/reject any task. Otherwise, the user must be in the assigned_users set. + """ + is_simple_auth_manager_all_admins = conf.getboolean("core", "simple_auth_manager_all_admins") + + if is_simple_auth_manager_all_admins: + # In all-admin mode, everyone is allowed + return True + + # If no assigned_users specified, allow access + if not assigned_users: + return True + + # Delegate to parent class for the actual authorization check + return super().is_authorized_hitl_task(assigned_users=assigned_users, user=user) + def get_fastapi_app(self) -> FastAPI | None: """ Specify a sub FastAPI application specific to the auth manager. @@ -280,7 +300,7 @@ def get_fastapi_app(self) -> FastAPI | None: """ from airflow.api_fastapi.auth.managers.simple.routes.login import login_router - dev_mode = os.environ.get("DEV_MODE", False) == "true" + dev_mode = os.environ.get("DEV_MODE", str(False)) == "true" directory = Path(__file__).parent.joinpath("ui", "dev" if dev_mode else "dist") directory.mkdir(exist_ok=True) diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package-lock.json b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package-lock.json index 18780182ff402..a0cfe4bb4f70b 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package-lock.json +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@chakra-ui/react": "^3.25.0", "@tanstack/react-query": "^5.85.5", - "axios": "^1.11.0", + "axios": "^1.12.0", "next-themes": "^0.4.6", "react": "^19.1.1", "react-cookie": "^8.0.1", @@ -3669,9 +3669,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -5524,10 +5524,11 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "foreground-child": "^3.1.0", diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json index dd681473cfd04..c5eda536ecd0d 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/package.json @@ -17,7 +17,7 @@ "dependencies": { "@chakra-ui/react": "^3.25.0", "@tanstack/react-query": "^5.85.5", - "axios": "^1.11.0", + "axios": "^1.12.0", "next-themes": "^0.4.6", "react": "^19.1.1", "react-cookie": "^8.0.1", @@ -51,6 +51,15 @@ "typescript-eslint": "^8.41.0", "vite": "^7.1.3", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/utils": "^8.50.0", + "@typescript-eslint/parser": "8.50.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@swc/core", + "esbuild" + ] } } diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml index 7621169aa6a35..d4d4ce5907251 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^5.85.5 version: 5.85.5(react@19.1.1) axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.12.0 + version: 1.12.0 next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -38,7 +38,7 @@ importers: devDependencies: '@7nohe/openapi-react-query-codegen': specifier: ^1.6.2 - version: 1.6.2(commander@12.1.0)(glob@10.4.5)(magicast@0.3.5)(ts-morph@22.0.0)(typescript@5.8.3) + version: 1.6.2(commander@12.1.0)(glob@10.5.0)(magicast@0.3.5)(ts-morph@22.0.0)(typescript@5.8.3) '@eslint/compat': specifier: ^1.3.2 version: 1.3.2(eslint@9.34.0(jiti@1.21.7)) @@ -63,6 +63,15 @@ importers: '@types/react-dom': specifier: ^19.1.8 version: 19.1.8(@types/react@19.1.11) + '@typescript-eslint/eslint-plugin': + specifier: 8.50.0 + version: 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: 8.50.0 + version: 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': + specifier: ^8.50.0 + version: 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) '@vitejs/plugin-react-swc': specifier: ^4.0.1 version: 4.0.1(@swc/helpers@0.5.17)(vite@7.1.3(@types/node@20.19.1)(jiti@1.21.7)) @@ -173,8 +182,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true @@ -182,8 +191,8 @@ packages: resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.3': - resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} '@babel/template@7.27.1': @@ -198,16 +207,16 @@ packages: resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} '@babel/types@7.27.1': resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} '@chakra-ui/react@3.25.0': @@ -538,8 +547,8 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.30': - resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -847,6 +856,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/eslint-plugin@8.50.0': + resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.41.0': resolution: {integrity: sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -854,11 +871,12 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.34.1': - resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==} + '@typescript-eslint/parser@8.50.0': + resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/project-service@8.41.0': resolution: {integrity: sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==} @@ -866,19 +884,19 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.34.1': - resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==} + '@typescript-eslint/project-service@8.50.0': + resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/scope-manager@8.41.0': resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.34.1': - resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==} + '@typescript-eslint/scope-manager@8.50.0': + resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/tsconfig-utils@8.41.0': resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==} @@ -886,6 +904,18 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.48.0': + resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.50.0': + resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.41.0': resolution: {integrity: sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -893,19 +923,24 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.34.1': - resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==} + '@typescript-eslint/type-utils@8.50.0': + resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/types@8.41.0': resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.34.1': - resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==} + '@typescript-eslint/types@8.46.1': + resolution: {integrity: sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.50.0': + resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/typescript-estree@8.41.0': resolution: {integrity: sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==} @@ -913,12 +948,11 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.34.1': - resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==} + '@typescript-eslint/typescript-estree@8.50.0': + resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/utils@8.41.0': resolution: {integrity: sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==} @@ -927,14 +961,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.34.1': - resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==} + '@typescript-eslint/utils@8.50.0': + resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/visitor-keys@8.41.0': resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.50.0': + resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react-swc@4.0.1': resolution: {integrity: sha512-NQhPjysi5duItyrMd5JWZFf2vNOuSMyw+EoZyTBDzk+DkfYD8WNrsUs09sELV2cr1P15nufsN25hsUBt4CKF9Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1203,8 +1244,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.0: - resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -1215,8 +1256,8 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} anymatch@3.1.3: @@ -1283,8 +1324,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.11.0: - resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.12.0: + resolution: {integrity: sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1740,8 +1781,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -1804,8 +1845,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true globals@11.12.0: @@ -2594,8 +2635,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} strip-indent@3.0.0: @@ -2642,6 +2683,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2868,11 +2913,11 @@ packages: snapshots: - '@7nohe/openapi-react-query-codegen@1.6.2(commander@12.1.0)(glob@10.4.5)(magicast@0.3.5)(ts-morph@22.0.0)(typescript@5.8.3)': + '@7nohe/openapi-react-query-codegen@1.6.2(commander@12.1.0)(glob@10.5.0)(magicast@0.3.5)(ts-morph@22.0.0)(typescript@5.8.3)': dependencies: '@hey-api/openapi-ts': 0.52.0(magicast@0.3.5)(typescript@5.8.3) commander: 12.1.0 - glob: 10.4.5 + glob: 10.5.0 ts-morph: 22.0.0 typescript: 5.8.3 transitivePeerDependencies: @@ -2967,18 +3012,18 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-globals@7.28.0': {} '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -2990,13 +3035,13 @@ snapshots: dependencies: '@babel/types': 7.27.1 - '@babel/parser@7.28.3': + '@babel/parser@7.28.4': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 '@babel/runtime@7.27.1': {} - '@babel/runtime@7.28.3': {} + '@babel/runtime@7.28.4': {} '@babel/template@7.27.1': dependencies: @@ -3007,8 +3052,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@babel/traverse@7.27.1': dependencies: @@ -3022,14 +3067,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.3': + '@babel/traverse@7.28.4': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.4 debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -3039,7 +3084,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.28.2': + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -3061,7 +3106,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -3092,7 +3137,7 @@ snapshots: '@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1)': dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 @@ -3304,7 +3349,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -3312,7 +3357,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.3.8': dependencies: @@ -3333,7 +3378,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.30': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 @@ -3498,7 +3543,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -3594,6 +3639,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.50.0 + eslint: 9.34.0(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.41.0 @@ -3606,42 +3667,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.34.1(typescript@5.8.3)': + '@typescript-eslint/parser@8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) - '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.50.0 debug: 4.4.1 + eslint: 9.34.0(jiti@1.21.7) typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/project-service@8.41.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.8.3) - '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.8.3) + '@typescript-eslint/types': 8.50.0 debug: 4.4.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.34.1': + '@typescript-eslint/project-service@8.50.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.34.1 - '@typescript-eslint/visitor-keys': 8.34.1 + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.8.3) + '@typescript-eslint/types': 8.50.0 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/scope-manager@8.41.0': dependencies: '@typescript-eslint/types': 8.41.0 '@typescript-eslint/visitor-keys': 8.41.0 - '@typescript-eslint/tsconfig-utils@8.34.1(typescript@5.8.3)': + '@typescript-eslint/scope-manager@8.50.0': dependencies: - typescript: 5.8.3 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 '@typescript-eslint/tsconfig-utils@8.41.0(typescript@5.8.3)': dependencies: typescript: 5.8.3 + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + '@typescript-eslint/type-utils@8.41.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.41.0 @@ -3654,26 +3731,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.34.1': {} - - '@typescript-eslint/types@8.41.0': {} - - '@typescript-eslint/typescript-estree@8.34.1(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.34.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) - '@typescript-eslint/types': 8.34.1 - '@typescript-eslint/visitor-keys': 8.34.1 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 + eslint: 9.34.0(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/types@8.41.0': {} + + '@typescript-eslint/types@8.46.1': {} + + '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/typescript-estree@8.41.0(typescript@5.8.3)': dependencies: '@typescript-eslint/project-service': 8.41.0(typescript@5.8.3) @@ -3690,13 +3765,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.1(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.50.0(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.34.1 - '@typescript-eslint/types': 8.34.1 - '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) - eslint: 9.34.0(jiti@1.21.7) + '@typescript-eslint/project-service': 8.50.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.8.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.1 + minimatch: 9.0.5 + semver: 7.7.2 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3712,16 +3791,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.34.1': + '@typescript-eslint/utils@8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.34.1 - eslint-visitor-keys: 4.2.1 + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.8.3) + eslint: 9.34.0(jiti@1.21.7) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/visitor-keys@8.41.0': dependencies: '@typescript-eslint/types': 8.41.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + eslint-visitor-keys: 4.2.1 + '@vitejs/plugin-react-swc@4.0.1(@swc/helpers@0.5.17)(vite@7.1.3(@types/node@20.19.1)(jiti@1.21.7))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.32 @@ -4296,7 +4386,7 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.2.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: @@ -4304,7 +4394,7 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} anymatch@3.1.3: dependencies: @@ -4388,9 +4478,9 @@ snapshots: axe-core@4.10.3: {} - axios@1.11.0: + axios@1.12.0: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.11 form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -4400,7 +4490,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -4795,8 +4885,8 @@ snapshots: eslint-plugin-perfectionist@4.15.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@typescript-eslint/types': 8.34.1 - '@typescript-eslint/utils': 8.34.1(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/types': 8.46.1 + '@typescript-eslint/utils': 8.50.0(eslint@9.34.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.34.0(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -4989,7 +5079,7 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + follow-redirects@1.15.11: {} for-each@0.3.5: dependencies: @@ -5070,7 +5160,7 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 @@ -5364,8 +5454,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 source-map-js: 1.2.1 optional: true @@ -5834,7 +5924,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string.prototype.includes@2.0.1: dependencies: @@ -5890,9 +5980,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.2.0 + ansi-regex: 6.2.2 strip-indent@3.0.0: dependencies: @@ -5938,6 +6028,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} @@ -6183,9 +6278,9 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 yallist@4.0.0: {} diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx index 6aa65345361fa..7d3bb4733e076 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Flex, Alert, CloseButton, Container, Heading, Span, Text } from "@chakra-ui/react"; +import { Alert, CloseButton, Container, Heading, Span, Text, Box, HStack } from "@chakra-ui/react"; import type { LoginResponse } from "openapi-gen/requests/types.gen"; import { useState } from "react"; import { useCookies } from "react-cookie"; @@ -78,58 +78,78 @@ export const Login = () => { }; return ( - - - - - Sign into Airflow - - - - {Boolean(error) && } - - Enter your username and password below: - - {isBannerDisabled === null && ( - - - - Simple auth manager enabled - - The Simple auth manager is intended for development and testing. If you're using it in - production, ensure that access is controlled through other means. Please read{" "} - - - the documentation - - {" "} - to learn more about simple auth manager. - - - { - localStorage.setItem(LOCAL_STORAGE_DISABLE_BANNER_KEY, "1"); - setIsBannerDisabled("1"); - }} - pos="relative" - top="-2" - /> - - )} - + + + + + Sign into Airflow + + + + {Boolean(error) && ( + + + + )} + + + Enter your username and password below: + + + + + {isBannerDisabled === null && ( + + + + Simple auth manager enabled + + The Simple auth manager is intended for development and testing. If you're using it in + production, ensure that access is controlled through other means. Please read{" "} + + + the documentation + + {" "} + to learn more about simple auth manager. + + + { + localStorage.setItem(LOCAL_STORAGE_DISABLE_BANNER_KEY, "1"); + setIsBannerDisabled("1"); + }} + pos="relative" + top="-2" + /> + + )} + + ); }; diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/LoginForm.tsx b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/LoginForm.tsx index 69a6b4f14fc0a..0b466e02274e0 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/LoginForm.tsx +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/login/LoginForm.tsx @@ -54,7 +54,7 @@ export const LoginForm = ({ isPending, onLogin }: LoginFormProps) => { Username {/* eslint-disable-next-line jsx-a11y/no-autofocus */} - + )} rules={{ required: true }} @@ -66,13 +66,13 @@ export const LoginForm = ({ isPending, onLogin }: LoginFormProps) => { render={({ field, fieldState }) => ( Password - + )} rules={{ required: true }} /> - diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/main.tsx b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/main.tsx index 4356f0fb6040a..43e48541704d2 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/main.tsx +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/main.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { ChakraProvider, defaultSystem } from "@chakra-ui/react"; +import { ChakraProvider } from "@chakra-ui/react"; import { QueryClientProvider } from "@tanstack/react-query"; import { ThemeProvider } from "next-themes"; import { CookiesProvider } from "react-cookie"; @@ -24,11 +24,12 @@ import { createRoot } from "react-dom/client"; import { RouterProvider } from "react-router-dom"; import { router } from "src/router"; +import { system } from "src/theme"; import { queryClient } from "./queryClient"; createRoot(document.querySelector("#root") as HTMLDivElement).render( - + diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/theme.ts b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/theme.ts new file mode 100644 index 0000000000000..7d40d21ef8093 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui/src/theme.ts @@ -0,0 +1,407 @@ +/*! + * 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. + */ +/* THIS FILE IS A COPY OF THE AIRFLOW UI THEME FILE LOCATED IN + airflow-core/src/airflow/ui/src/theme.ts +*/ +/* eslint-disable perfectionist/sort-objects */ +/* eslint-disable max-lines */ +import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react"; + +const generateSemanticTokens = (color: string, darkContrast: string = "white") => ({ + solid: { value: `{colors.${color}.600}` }, + contrast: { value: { _light: "white", _dark: darkContrast } }, + fg: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } }, + muted: { value: { _light: `{colors.${color}.200}`, _dark: `{colors.${color}.800}` } }, + subtle: { value: { _light: `{colors.${color}.100}`, _dark: `{colors.${color}.900}` } }, + emphasized: { value: { _light: `{colors.${color}.300}`, _dark: `{colors.${color}.700}` } }, + focusRing: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } }, +}); + +export const customConfig = defineConfig({ + // See https://chakra-ui.com/docs/theming/colors for more information on the colors used here. + theme: { + tokens: { + colors: { + black: { value: "oklch(0.23185 0.0323 266.44)" }, // Custom value for dark mode + brand: { + "50": { value: "oklch(0.98 0.006 248.717)" }, + "100": { value: "oklch(0.962 0.012 249.460)" }, + "200": { value: "oklch(0.923 0.023 255.082)" }, + "300": { value: "oklch(0.865 0.039 252.420)" }, + "400": { value: "oklch(0.705 0.066 256.378)" }, + "500": { value: "oklch(0.575 0.08 257.759)" }, + "600": { value: "oklch(0.469 0.084 257.657)" }, + "700": { value: "oklch(0.399 0.084 257.850)" }, + "800": { value: "oklch(0.324 0.072 260.329)" }, + "900": { value: "oklch(0.259 0.062 265.566)" }, + "950": { value: "oklch(0.179 0.05 265.487)" }, + }, + gray: { + // Values modified from original Tailwind to improve contrast in Chakra UI + "50": { value: "oklch(0.985 0.004 253)" }, // Original: oklch(0.985 0.002 247.839) + "100": { value: "oklch(0.955 0.006 253)" }, // Original: oklch(0.967 0.003 264.542) + "200": { value: "oklch(0.915 0.01 253)" }, // Original: oklch(0.928 0.006 264.531) + "300": { value: "oklch(0.85 0.016 253)" }, // Original: oklch(0.872 0.01 258.338) + "400": { value: "oklch(0.75 0.025 252)" }, // Original: oklch(0.707 0.022 261.325) + "500": { value: "oklch(0.63 0.042 252)" }, // Original: oklch(0.551 0.027 264.364) + "600": { value: "oklch(0.45 0.055 251)" }, // Original: oklch(0.446 0.03 256.802) + "700": { value: "oklch(0.35 0.045 251)" }, // Original: oklch(0.373 0.034 259.733) + "800": { value: "oklch(0.28 0.035 251)" }, // Original: oklch(0.278 0.033 256.848) + "900": { value: "oklch(0.18 0.03 251)" }, // Original: oklch(0.21 0.034 264.665) + "950": { value: "oklch(0.11 0.025 251)" }, // Original: oklch(0.13 0.028 261.692) + }, + // TAILWIND 4.0 COLORS + // See https://tailwindcss.com/docs/colors for more information on the colors used here. + red: { + "50": { value: "oklch(0.971 0.013 17.38)" }, + "100": { value: "oklch(0.936 0.032 17.717)" }, + "200": { value: "oklch(0.885 0.062 18.334)" }, + "300": { value: "oklch(0.808 0.114 19.571)" }, + "400": { value: "oklch(0.704 0.191 22.216)" }, + "500": { value: "oklch(0.637 0.237 25.331)" }, + "600": { value: "oklch(0.577 0.245 27.325)" }, + "700": { value: "oklch(0.505 0.213 27.518)" }, + "800": { value: "oklch(0.444 0.177 26.899)" }, + "900": { value: "oklch(0.396 0.141 25.723)" }, + "950": { value: "oklch(0.258 0.092 26.042)" }, + }, + // Values modified from original Tailwind to improve contrast in Chakra UI + orange: { + "50": { value: "oklch(0.982 0.013 83.915)" }, + "100": { value: "oklch(0.961 0.033 82.320)" }, + "200": { value: "oklch(0.918 0.065 79.975)" }, + "300": { value: "oklch(0.857 0.118 76.815)" }, + "400": { value: "oklch(0.7492 0.1439 62.081)" }, // Original: oklch(0.774 0.186 71.555) + "500": { value: "oklch(0.6462 0.1979 43.792)" }, // Original: oklch(0.705 0.213 47.604) + "600": { value: "oklch(0.5902 0.198 35.93)" }, // Original: oklch(0.632 0.214 41.185) + "700": { value: "oklch(0.553 0.184 41.777)" }, + "800": { value: "oklch(0.469 0.144 45.164)" }, + "900": { value: "oklch(0.414 0.110 48.717)" }, + "950": { value: "oklch(0.271 0.069 52.345)" }, + }, + amber: { + "50": { value: "oklch(0.987 0.022 95.277)" }, + "100": { value: "oklch(0.962 0.059 95.617)" }, + "200": { value: "oklch(0.924 0.12 95.746)" }, + "300": { value: "oklch(0.879 0.169 91.605)" }, + "400": { value: "oklch(0.828 0.189 84.429)" }, + "500": { value: "oklch(0.769 0.188 70.08)" }, + "600": { value: "oklch(0.666 0.179 58.318)" }, + "700": { value: "oklch(0.555 0.163 48.998)" }, + "800": { value: "oklch(0.473 0.137 46.201)" }, + "900": { value: "oklch(0.414 0.112 45.904)" }, + "950": { value: "oklch(0.279 0.077 45.635)" }, + }, + yellow: { + "50": { value: "oklch(0.987 0.026 102.212)" }, + "100": { value: "oklch(0.973 0.071 103.193)" }, + "200": { value: "oklch(0.945 0.129 101.54)" }, + "300": { value: "oklch(0.905 0.182 98.111)" }, + "400": { value: "oklch(0.852 0.199 91.936)" }, + "500": { value: "oklch(0.795 0.184 86.047)" }, + "600": { value: "oklch(0.681 0.162 75.834)" }, + "700": { value: "oklch(0.554 0.135 66.442)" }, + "800": { value: "oklch(0.476 0.114 61.907)" }, + "900": { value: "oklch(0.421 0.095 57.708)" }, + "950": { value: "oklch(0.286 0.066 53.813)" }, + }, + lime: { + "50": { value: "oklch(0.986 0.031 120.757)" }, + "100": { value: "oklch(0.967 0.067 122.328)" }, + "200": { value: "oklch(0.938 0.127 124.321)" }, + "300": { value: "oklch(0.897 0.196 126.665)" }, + "400": { value: "oklch(0.841 0.238 128.85)" }, + "500": { value: "oklch(0.768 0.233 130.85)" }, + "600": { value: "oklch(0.648 0.2 131.684)" }, + "700": { value: "oklch(0.532 0.157 131.589)" }, + "800": { value: "oklch(0.453 0.124 130.933)" }, + "900": { value: "oklch(0.405 0.101 131.063)" }, + "950": { value: "oklch(0.274 0.072 132.109)" }, + }, + green: { + // Values modified from original Tailwind to improve contrast in Chakra UI + "50": { value: "oklch(0.982 0.018 155.826)" }, + "100": { value: "oklch(0.962 0.044 156.743)" }, + "200": { value: "oklch(0.925 0.084 155.995)" }, + "300": { value: "oklch(0.75 0.18 153.0)" }, // Original: oklch(0.871 0.15 154.449) + "400": { value: "oklch(0.625 0.209 150.0)" }, // Original: oklch(0.792 0.209 151.711) + "500": { value: "oklch(0.528 0.219 149.579)" }, // Original: oklch(0.723 0.219 149.579) + "600": { value: "oklch(0.47 0.20 149.0)" }, // Original: oklch(0.627 0.194 149.214) + "700": { value: "oklch(0.40 0.16 149.5)" }, // Original: oklch(0.527 0.154 150.069) + "800": { value: "oklch(0.448 0.119 151.328)" }, + "900": { value: "oklch(0.393 0.095 152.535)" }, + "950": { value: "oklch(0.266 0.065 152.934)" }, + }, + emerald: { + "50": { value: "oklch(0.979 0.021 166.113)" }, + "100": { value: "oklch(0.95 0.052 163.051)" }, + "200": { value: "oklch(0.905 0.093 164.15)" }, + "300": { value: "oklch(0.845 0.143 164.978)" }, + "400": { value: "oklch(0.765 0.177 163.223)" }, + "500": { value: "oklch(0.696 0.17 162.48)" }, + "600": { value: "oklch(0.596 0.145 163.225)" }, + "700": { value: "oklch(0.508 0.118 165.612)" }, + "800": { value: "oklch(0.432 0.095 166.913)" }, + "900": { value: "oklch(0.378 0.077 168.94)" }, + "950": { value: "oklch(0.262 0.051 172.552)" }, + }, + teal: { + "50": { value: "oklch(0.984 0.014 180.72)" }, + "100": { value: "oklch(0.953 0.051 180.801)" }, + "200": { value: "oklch(0.91 0.096 180.426)" }, + "300": { value: "oklch(0.855 0.138 181.071)" }, + "400": { value: "oklch(0.777 0.152 181.912)" }, + "500": { value: "oklch(0.704 0.14 182.503)" }, + "600": { value: "oklch(0.6 0.118 184.704)" }, + "700": { value: "oklch(0.511 0.096 186.391)" }, + "800": { value: "oklch(0.437 0.078 188.216)" }, + "900": { value: "oklch(0.386 0.063 188.416)" }, + "950": { value: "oklch(0.277 0.046 192.524)" }, + }, + cyan: { + "50": { value: "oklch(0.984 0.019 200.873)" }, + "100": { value: "oklch(0.956 0.045 203.388)" }, + "200": { value: "oklch(0.917 0.08 205.041)" }, + "300": { value: "oklch(0.865 0.127 207.078)" }, + "400": { value: "oklch(0.789 0.154 211.53)" }, + "500": { value: "oklch(0.715 0.143 215.221)" }, + "600": { value: "oklch(0.609 0.126 221.723)" }, + "700": { value: "oklch(0.52 0.105 223.128)" }, + "800": { value: "oklch(0.45 0.085 224.283)" }, + "900": { value: "oklch(0.398 0.07 227.392)" }, + "950": { value: "oklch(0.302 0.056 229.695)" }, + }, + sky: { + "50": { value: "oklch(0.977 0.013 236.62)" }, + "100": { value: "oklch(0.951 0.026 236.824)" }, + "200": { value: "oklch(0.901 0.058 230.902)" }, + "300": { value: "oklch(0.828 0.111 230.318)" }, + "400": { value: "oklch(0.746 0.16 232.661)" }, + "500": { value: "oklch(0.685 0.169 237.323)" }, + "600": { value: "oklch(0.588 0.158 241.966)" }, + "700": { value: "oklch(0.5 0.134 242.749)" }, + "800": { value: "oklch(0.443 0.11 240.79)" }, + "900": { value: "oklch(0.391 0.09 240.876)" }, + "950": { value: "oklch(0.293 0.066 243.157)" }, + }, + blue: { + "50": { value: "oklch(0.97 0.014 254.604)" }, + "100": { value: "oklch(0.932 0.032 255.585)" }, + "200": { value: "oklch(0.882 0.059 254.128)" }, + "300": { value: "oklch(0.809 0.105 251.813)" }, + "400": { value: "oklch(0.707 0.165 254.624)" }, + "500": { value: "oklch(0.623 0.214 259.815)" }, + "600": { value: "oklch(0.546 0.245 262.881)" }, + "700": { value: "oklch(0.488 0.243 264.376)" }, + "800": { value: "oklch(0.424 0.199 265.638)" }, + "900": { value: "oklch(0.379 0.146 265.522)" }, + "950": { value: "oklch(0.282 0.091 267.935)" }, + }, + indigo: { + "50": { value: "oklch(0.962 0.018 272.314)" }, + "100": { value: "oklch(0.93 0.034 272.788)" }, + "200": { value: "oklch(0.87 0.065 274.039)" }, + "300": { value: "oklch(0.785 0.115 274.713)" }, + "400": { value: "oklch(0.673 0.182 276.935)" }, + "500": { value: "oklch(0.585 0.233 277.117)" }, + "600": { value: "oklch(0.511 0.262 276.966)" }, + "700": { value: "oklch(0.457 0.24 277.023)" }, + "800": { value: "oklch(0.398 0.195 277.366)" }, + "900": { value: "oklch(0.359 0.144 278.697)" }, + "950": { value: "oklch(0.257 0.09 281.288)" }, + }, + violet: { + "50": { value: "oklch(0.969 0.016 293.756)" }, + "100": { value: "oklch(0.943 0.029 294.588)" }, + "200": { value: "oklch(0.894 0.057 293.283)" }, + "300": { value: "oklch(0.811 0.111 293.571)" }, + "400": { value: "oklch(0.702 0.183 293.541)" }, + "500": { value: "oklch(0.606 0.25 292.717)" }, + "600": { value: "oklch(0.541 0.281 293.009)" }, + "700": { value: "oklch(0.491 0.27 292.581)" }, + "800": { value: "oklch(0.432 0.232 292.759)" }, + "900": { value: "oklch(0.38 0.189 293.745)" }, + "950": { value: "oklch(0.283 0.141 291.089)" }, + }, + purple: { + "50": { value: "oklch(0.977 0.014 308.299)" }, + "100": { value: "oklch(0.946 0.033 307.174)" }, + "200": { value: "oklch(0.902 0.063 306.703)" }, + "300": { value: "oklch(0.827 0.119 306.383)" }, + "400": { value: "oklch(0.714 0.203 305.504)" }, + "500": { value: "oklch(0.627 0.265 303.9)" }, + "600": { value: "oklch(0.558 0.288 302.321)" }, + "700": { value: "oklch(0.496 0.265 301.924)" }, + "800": { value: "oklch(0.438 0.218 303.724)" }, + "900": { value: "oklch(0.381 0.176 304.987)" }, + "950": { value: "oklch(0.291 0.149 302.717)" }, + }, + fuchsia: { + "50": { value: "oklch(0.977 0.017 320.058)" }, + "100": { value: "oklch(0.952 0.037 318.852)" }, + "200": { value: "oklch(0.903 0.076 319.62)" }, + "300": { value: "oklch(0.833 0.145 321.434)" }, + "400": { value: "oklch(0.74 0.238 322.16)" }, + "500": { value: "oklch(0.667 0.295 322.15)" }, + "600": { value: "oklch(0.591 0.293 322.896)" }, + "700": { value: "oklch(0.518 0.253 323.949)" }, + "800": { value: "oklch(0.452 0.211 324.591)" }, + "900": { value: "oklch(0.401 0.17 325.612)" }, + "950": { value: "oklch(0.293 0.136 325.661)" }, + }, + pink: { + "50": { value: "oklch(0.971 0.014 343.198)" }, + "100": { value: "oklch(0.948 0.028 342.258)" }, + "200": { value: "oklch(0.899 0.061 343.231)" }, + "300": { value: "oklch(0.823 0.12 346.018)" }, + "400": { value: "oklch(0.718 0.202 349.761)" }, + "500": { value: "oklch(0.656 0.241 354.308)" }, + "600": { value: "oklch(0.592 0.249 0.584)" }, + "700": { value: "oklch(0.525 0.223 3.958)" }, + "800": { value: "oklch(0.459 0.187 3.815)" }, + "900": { value: "oklch(0.408 0.153 2.432)" }, + "950": { value: "oklch(0.284 0.109 3.907)" }, + }, + rose: { + "50": { value: "oklch(0.969 0.015 12.422)" }, + "100": { value: "oklch(0.941 0.03 12.58)" }, + "200": { value: "oklch(0.892 0.058 10.001)" }, + "300": { value: "oklch(0.81 0.117 11.638)" }, + "400": { value: "oklch(0.712 0.194 13.428)" }, + "500": { value: "oklch(0.645 0.246 16.439)" }, + "600": { value: "oklch(0.586 0.253 17.585)" }, + "700": { value: "oklch(0.514 0.222 16.935)" }, + "800": { value: "oklch(0.455 0.188 13.697)" }, + "900": { value: "oklch(0.41 0.159 10.272)" }, + "950": { value: "oklch(0.271 0.105 12.094)" }, + }, + slate: { + "50": { value: "oklch(0.984 0.003 247.858)" }, + "100": { value: "oklch(0.968 0.007 247.896)" }, + "200": { value: "oklch(0.929 0.013 255.508)" }, + "300": { value: "oklch(0.869 0.022 252.894)" }, + "400": { value: "oklch(0.704 0.04 256.788)" }, + "500": { value: "oklch(0.554 0.046 257.417)" }, + "600": { value: "oklch(0.446 0.043 257.281)" }, + "700": { value: "oklch(0.372 0.044 257.287)" }, + "800": { value: "oklch(0.279 0.041 260.031)" }, + "900": { value: "oklch(0.208 0.042 265.755)" }, + "950": { value: "oklch(0.129 0.042 264.695)" }, + }, + zinc: { + "50": { value: "oklch(0.985 0 0)" }, + "100": { value: "oklch(0.967 0.001 286.375)" }, + "200": { value: "oklch(0.92 0.004 286.32)" }, + "300": { value: "oklch(0.871 0.006 286.286)" }, + "400": { value: "oklch(0.705 0.015 286.067)" }, + "500": { value: "oklch(0.552 0.016 285.938)" }, + "600": { value: "oklch(0.442 0.017 285.786)" }, + "700": { value: "oklch(0.37 0.013 285.805)" }, + "800": { value: "oklch(0.274 0.006 286.033)" }, + "900": { value: "oklch(0.21 0.006 285.885)" }, + "950": { value: "oklch(0.141 0.005 285.823)" }, + }, + neutral: { + "50": { value: "oklch(0.985 0 0)" }, + "100": { value: "oklch(0.97 0 0)" }, + "200": { value: "oklch(0.922 0 0)" }, + "300": { value: "oklch(0.87 0 0)" }, + "400": { value: "oklch(0.708 0 0)" }, + "500": { value: "oklch(0.556 0 0)" }, + "600": { value: "oklch(0.439 0 0)" }, + "700": { value: "oklch(0.371 0 0)" }, + "800": { value: "oklch(0.269 0 0)" }, + "900": { value: "oklch(0.205 0 0)" }, + "950": { value: "oklch(0.145 0 0)" }, + }, + stone: { + "50": { value: "oklch(0.985 0.001 106.423)" }, + "100": { value: "oklch(0.97 0.001 106.424)" }, + "200": { value: "oklch(0.923 0.003 48.717)" }, + "300": { value: "oklch(0.869 0.005 56.366)" }, + "400": { value: "oklch(0.709 0.01 56.259)" }, + "500": { value: "oklch(0.553 0.013 58.071)" }, + "600": { value: "oklch(0.444 0.011 73.639)" }, + "700": { value: "oklch(0.374 0.01 67.558)" }, + "800": { value: "oklch(0.268 0.007 34.298)" }, + "900": { value: "oklch(0.216 0.006 56.043)" }, + "950": { value: "oklch(0.147 0.004 49.25)" }, + }, + }, + }, + semanticTokens: { + colors: { + // Brand colors for consistent theming + brand: generateSemanticTokens("brand"), + // GENERIC STATE + danger: generateSemanticTokens("red"), + info: generateSemanticTokens("blue"), + warning: generateSemanticTokens("amber"), + error: generateSemanticTokens("red"), + // AIRFLOW TASK STATE + active: generateSemanticTokens("blue"), + success: generateSemanticTokens("green"), + failed: generateSemanticTokens("red"), + queued: generateSemanticTokens("stone"), + skipped: generateSemanticTokens("pink"), + up_for_reschedule: generateSemanticTokens("sky"), + up_for_retry: generateSemanticTokens("yellow"), + upstream_failed: generateSemanticTokens("orange"), + running: generateSemanticTokens("cyan"), + restarting: generateSemanticTokens("violet"), + deferred: generateSemanticTokens("purple"), + scheduled: generateSemanticTokens("zinc"), + none: generateSemanticTokens("gray"), + removed: generateSemanticTokens("slate"), + // TAILWIND 4.0 COLORS + red: generateSemanticTokens("red"), + orange: generateSemanticTokens("orange"), + amber: generateSemanticTokens("amber"), + yellow: generateSemanticTokens("yellow"), + lime: generateSemanticTokens("lime"), + green: generateSemanticTokens("green"), + emerald: generateSemanticTokens("emerald"), + teal: generateSemanticTokens("teal"), + cyan: generateSemanticTokens("cyan"), + sky: generateSemanticTokens("sky"), + blue: generateSemanticTokens("blue"), + indigo: generateSemanticTokens("indigo"), + violet: generateSemanticTokens("violet"), + purple: generateSemanticTokens("purple"), + fuchsia: generateSemanticTokens("fuchsia"), + pink: generateSemanticTokens("pink"), + rose: generateSemanticTokens("rose"), + slate: generateSemanticTokens("slate"), + gray: generateSemanticTokens("gray"), + zinc: generateSemanticTokens("zinc"), + neutral: generateSemanticTokens("neutral"), + stone: generateSemanticTokens("stone"), + }, + }, + }, +}); + +export const system = createSystem(defaultConfig, customConfig); + +// Utility function to resolve CSS variables to their computed values +// See: https://github.com/chakra-ui/panda/discussions/2200 +export const getComputedCSSVariableValue = (variable: string): string => + getComputedStyle(document.documentElement) + .getPropertyValue(variable.slice(4, variable.length - 1)) + .trim(); diff --git a/airflow-core/src/airflow/api_fastapi/auth/middlewares/__init__.py b/airflow-core/src/airflow/api_fastapi/auth/middlewares/__init__.py new file mode 100644 index 0000000000000..217e5db960782 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/auth/middlewares/__init__.py @@ -0,0 +1,17 @@ +# +# 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. diff --git a/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py b/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py new file mode 100644 index 0000000000000..a64da351d25b5 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py @@ -0,0 +1,81 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from fastapi import HTTPException, Request +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware + +from airflow.api_fastapi.app import get_auth_manager +from airflow.api_fastapi.auth.managers.base_auth_manager import COOKIE_NAME_JWT_TOKEN +from airflow.api_fastapi.auth.managers.exceptions import AuthManagerRefreshTokenExpiredException +from airflow.api_fastapi.auth.managers.models.base_user import BaseUser +from airflow.api_fastapi.core_api.security import resolve_user_from_token +from airflow.configuration import conf + + +class JWTRefreshMiddleware(BaseHTTPMiddleware): + """ + Middleware to handle JWT token refresh. + + This middleware: + 1. Extracts JWT token from cookies and build the user from the token + 2. Calls ``refresh_user`` method from auth manager with the user + 3. If ``refresh_user`` returns a user, generate a JWT token based upon this user and send it in the + response as cookie + """ + + async def dispatch(self, request: Request, call_next): + new_token = None + current_token = request.cookies.get(COOKIE_NAME_JWT_TOKEN) + try: + if current_token is not None: + try: + new_user, current_user = await self._refresh_user(current_token) + if user := (new_user or current_user): + request.state.user = user + if new_user: + # If we created a new user, serialize it and set it as a cookie + new_token = get_auth_manager().generate_jwt(new_user) + except (HTTPException, AuthManagerRefreshTokenExpiredException): + # Receive a HTTPException when the Airflow token is expired + # Receive a AuthManagerRefreshTokenExpiredException when the potential underlying refresh + # token used by the auth manager is expired + new_token = "" + + response = await call_next(request) + + if new_token is not None: + secure = bool(conf.get("api", "ssl_cert", fallback="")) + response.set_cookie( + COOKIE_NAME_JWT_TOKEN, + new_token, + httponly=True, + secure=secure, + samesite="lax", + max_age=0 if new_token == "" else None, + ) + except HTTPException as exc: + # If any HTTPException is raised during user resolution or refresh, return it as response + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) + return response + + @staticmethod + async def _refresh_user(current_token: str) -> tuple[BaseUser | None, BaseUser | None]: + user = await resolve_user_from_token(current_token) + return get_auth_manager().refresh_user(user=user), user diff --git a/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py b/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py index 1ca5f1d261b54..2a5fb76c8b850 100644 --- a/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py +++ b/airflow-core/src/airflow/api_fastapi/common/db/dag_runs.py @@ -18,9 +18,14 @@ from __future__ import annotations from sqlalchemy import func, select +from sqlalchemy.orm import joinedload, selectinload +from sqlalchemy.orm.interfaces import LoaderOption from airflow.models.dag import DagModel +from airflow.models.dag_version import DagVersion from airflow.models.dagrun import DagRun +from airflow.models.taskinstance import TaskInstance +from airflow.models.taskinstancehistory import TaskInstanceHistory dagruns_select_with_state_count = ( select( @@ -33,3 +38,17 @@ .group_by(DagRun.dag_id, DagRun.state, DagModel.dag_display_name) .order_by(DagRun.dag_id) ) + + +def eager_load_dag_run_for_validation() -> tuple[LoaderOption, ...]: + """Construct the eager loading options necessary for a DagRunResponse object.""" + return ( + joinedload(DagRun.dag_model), + selectinload(DagRun.task_instances) + .joinedload(TaskInstance.dag_version) + .joinedload(DagVersion.bundle), + selectinload(DagRun.task_instances_histories) + .joinedload(TaskInstanceHistory.dag_version) + .joinedload(DagVersion.bundle), + joinedload(DagRun.dag_run_note), + ) diff --git a/airflow-core/src/airflow/api_fastapi/common/db/dags.py b/airflow-core/src/airflow/api_fastapi/common/db/dags.py index cefce66279883..f1104f839e3d4 100644 --- a/airflow-core/src/airflow/api_fastapi/common/db/dags.py +++ b/airflow-core/src/airflow/api_fastapi/common/db/dags.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING from sqlalchemy import func, select +from sqlalchemy.orm import selectinload from airflow.api_fastapi.common.db.common import ( apply_filters_to_select, @@ -32,14 +33,30 @@ from sqlalchemy.sql import Select -def generate_dag_with_latest_run_query(max_run_filters: list[BaseParam], order_by: SortParam) -> Select: - query = select(DagModel) +def generate_dag_with_latest_run_query( + max_run_filters: list[BaseParam], order_by: SortParam, *, dag_ids: set[str] | None = None +) -> Select: + """ + Generate a query to fetch DAGs with their latest run. - max_run_id_query = ( # ordering by id will not always be "latest run", but it's a simplifying assumption - select(DagRun.dag_id, func.max(DagRun.id).label("max_dag_run_id")) - .group_by(DagRun.dag_id) - .subquery(name="mrq") - ) + :param max_run_filters: List of filters to apply to the latest run + :param order_by: Sort parameter for ordering results + :param dag_ids: Optional set of DAG IDs to limit the query to. When provided, both the main + DAG query and the subquery for finding the latest runs will be filtered to + only these DAG IDs, improving performance when users have limited DAG access. + :return: SQLAlchemy Select statement + """ + query = select(DagModel).options(selectinload(DagModel.tags)) + + # Filter main query by dag_ids if provided + if dag_ids is not None: + query = query.where(DagModel.dag_id.in_(dag_ids or set())) + + # Also filter the subquery for finding latest runs + max_run_id_query_stmt = select(DagRun.dag_id, func.max(DagRun.id).label("max_dag_run_id")) + if dag_ids is not None: + max_run_id_query_stmt = max_run_id_query_stmt.where(DagRun.dag_id.in_(dag_ids or set())) + max_run_id_query = max_run_id_query_stmt.group_by(DagRun.dag_id).subquery(name="mrq") has_max_run_filter = False diff --git a/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py b/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py new file mode 100644 index 0000000000000..423e57f2316ae --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/common/db/task_instances.py @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from sqlalchemy.orm import joinedload +from sqlalchemy.orm.interfaces import LoaderOption + +from airflow.models import Base +from airflow.models.dag_version import DagVersion +from airflow.models.dagrun import DagRun +from airflow.models.taskinstance import TaskInstance + + +def eager_load_TI_and_TIH_for_validation(orm_model: Base | None = None) -> tuple[LoaderOption, ...]: + """Construct the eager loading options necessary for both TaskInstanceResponse and TaskInstanceHistoryResponse objects.""" + if orm_model is None: + orm_model = TaskInstance + + options: tuple[LoaderOption, ...] = ( + joinedload(orm_model.dag_version).joinedload(DagVersion.bundle), + joinedload(orm_model.dag_run).options(joinedload(DagRun.dag_model)), + ) + if orm_model is TaskInstance: + options += (joinedload(orm_model.task_instance_note),) + return options diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index f1d876f29b631..1d48187f1cea9 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -34,7 +34,7 @@ from fastapi import Depends, HTTPException, Query, status from pendulum.parsing.exceptions import ParserError from pydantic import AfterValidator, BaseModel, NonNegativeInt -from sqlalchemy import Column, and_, case, func, not_, or_, select as sql_select +from sqlalchemy import Column, and_, func, not_, or_, select as sql_select from sqlalchemy.inspection import inspect from airflow._shared.timezones import timezone @@ -53,6 +53,7 @@ from airflow.models.dag_favorite import DagFavorite from airflow.models.dag_version import DagVersion from airflow.models.dagrun import DagRun +from airflow.models.errors import ParseImportError from airflow.models.hitl import HITLDetail from airflow.models.pool import Pool from airflow.models.taskinstance import TaskInstance @@ -65,6 +66,8 @@ if TYPE_CHECKING: from sqlalchemy.sql import ColumnElement, Select + from airflow.serialization.serialized_objects import SerializedDAG + T = TypeVar("T") @@ -180,6 +183,57 @@ def depends(cls, *args: Any, **kwargs: Any) -> Self: raise NotImplementedError("Use search_param_factory instead , depends is not implemented.") +class QueryTaskInstanceTaskGroupFilter(BaseParam[str]): + """Task group filter - returns all tasks in the specified group.""" + + def __init__(self, dag=None, skip_none: bool = True): + super().__init__(skip_none=skip_none) + self._dag: None | SerializedDAG = dag + + @property + def dag(self) -> None | SerializedDAG: + return self._dag + + @dag.setter + def dag(self, value: None | SerializedDAG) -> None: + self._dag = value + + def to_orm(self, select: Select) -> Select: + if self.value is None and self.skip_none: + return select + + if not self.dag: + raise ValueError("Dag must be set before calling to_orm") + + if not hasattr(self.dag, "task_group"): + return select + + # Exact matching on group_id + task_groups = self.dag.task_group.get_task_group_dict() + task_group = task_groups.get(self.value) + if not task_group: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + detail={ + "reason": "not_found", + "message": f"Task group {self.value} not found", + }, + ) + + return select.where(TaskInstance.task_id.in_(task.task_id for task in task_group.iter_tasks())) + + @classmethod + def depends( + cls, + value: str | None = Query( + alias="task_group_id", + default=None, + description="Filter by exact task group ID. Returns all tasks within the specified task group.", + ), + ) -> QueryTaskInstanceTaskGroupFilter: + return cls(dag=None).set_value(value) + + def search_param_factory( attribute: ColumnElement, pattern_name: str, @@ -247,11 +301,6 @@ def to_orm(self, select: Select) -> Select: if column is None: column = getattr(self.model, lstriped_orderby) - # MySQL does not support `nullslast`, and True/False ordering depends on the - # database implementation. - nullscheck = case((column.isnot(None), 0), else_=1) - - columns.append(nullscheck) if order_by_value.startswith("-"): columns.append(column.desc()) else: @@ -282,9 +331,15 @@ def depends(cls, *args: Any, **kwargs: Any) -> Self: raise NotImplementedError("Use dynamic_depends, depends not implemented.") def dynamic_depends(self, default: str | None = None) -> Callable: + to_replace_attrs = list(self.to_replace.keys()) if self.to_replace else [] + + all_attrs = self.allowed_attrs + to_replace_attrs + def inner( order_by: list[str] = Query( - default=[default] if default is not None else [self.get_primary_key_string()] + default=[default] if default is not None else [self.get_primary_key_string()], + description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. " + f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`", ), ) -> SortParam: return self.set_value(order_by) @@ -718,7 +773,10 @@ def to_orm(self, select: Select) -> Select: pending_actions_count_subquery = ( sql_select(func.count(HITLDetail.ti_id)) .join(TaskInstance, HITLDetail.ti_id == TaskInstance.id) - .where(HITLDetail.response_at.is_(None)) + .where( + HITLDetail.responded_at.is_(None), + TaskInstance.state == TaskInstanceState.DEFERRED, + ) .where(TaskInstance.dag_id == DagModel.dag_id) .scalar_subquery() ) @@ -857,6 +915,9 @@ def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | N QueryTITaskDisplayNamePatternSearch = Annotated[ _SearchParam, Depends(search_param_factory(TaskInstance.task_display_name, "task_display_name_pattern")) ] +QueryTITaskGroupFilter = Annotated[ + QueryTaskInstanceTaskGroupFilter, Depends(QueryTaskInstanceTaskGroupFilter.depends) +] QueryTIDagVersionFilter = Annotated[ FilterParam[list[int]], Depends( @@ -898,6 +959,15 @@ def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | N ), ] +QueryTIMapIndexFilter = Annotated[ + FilterParam[list[int]], + Depends( + filter_param_factory( + TaskInstance.map_index, list[int], FilterOptionEnum.ANY_EQUAL, default_factory=list + ) + ), +] + # XCom QueryXComKeyPatternSearch = Annotated[ _SearchParam, Depends(search_param_factory(XComModel.key, "xcom_key_pattern")) @@ -984,14 +1054,24 @@ def _optional_boolean(value: bool | None) -> bool | None: ) ), ] -QueryHITLDetailDagRunIdFilter = Annotated[ - FilterParam[str], +QueryHITLDetailTaskIdFilter = Annotated[ + FilterParam[str | None], Depends( filter_param_factory( - TaskInstance.run_id, - str, - filter_name="dag_run_id", - ), + TaskInstance.task_id, + str | None, + filter_name="task_id", + ) + ), +] +QueryHITLDetailMapIndexFilter = Annotated[ + FilterParam[int | None], + Depends( + filter_param_factory( + TaskInstance.map_index, + int | None, + filter_name="map_index", + ) ), ] QueryHITLDetailSubjectSearch = Annotated[ @@ -1026,11 +1106,11 @@ def _optional_boolean(value: bool | None) -> bool | None: FilterParam[list[str]], Depends( filter_param_factory( - HITLDetail.responded_user_id, + HITLDetail.responded_by_user_id, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list, - filter_name="responded_user_id", + filter_name="responded_by_user_id", ) ), ] @@ -1038,31 +1118,16 @@ def _optional_boolean(value: bool | None) -> bool | None: FilterParam[list[str]], Depends( filter_param_factory( - HITLDetail.responded_user_name, + HITLDetail.responded_by_user_name, list[str], FilterOptionEnum.ANY_EQUAL, default_factory=list, - filter_name="responded_user_name", - ) - ), -] -QueryHITLDetailDagIdFilter = Annotated[ - FilterParam[str | None], - Depends( - filter_param_factory( - TaskInstance.dag_id, - str | None, - filter_name="dag_id", + filter_name="responded_by_user_name", ) ), ] -QueryHITLDetailTaskIdFilter = Annotated[ - FilterParam[str | None], - Depends( - filter_param_factory( - TaskInstance.task_id, - str | None, - filter_name="task_id", - ) - ), + +# Parse Import Errors +QueryParseImportErrorFilenamePatternSearch = Annotated[ + _SearchParam, Depends(search_param_factory(ParseImportError.filename, "filename_pattern")) ] diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py b/airflow-core/src/airflow/api_fastapi/core_api/app.py index 7cf4e0c92f646..8db1fa66680bc 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py @@ -47,7 +47,7 @@ def init_views(app: FastAPI) -> None: app.include_router(ui_router) app.include_router(public_router) - dev_mode = os.environ.get("DEV_MODE", False) == "true" + dev_mode = os.environ.get("DEV_MODE", str(False)) == "true" directory = Path(AIRFLOW_PATH) / ("airflow/ui/dev" if dev_mode else "airflow/ui/dist") @@ -182,14 +182,9 @@ def init_error_handlers(app: FastAPI) -> None: def init_middlewares(app: FastAPI) -> None: - from airflow.configuration import conf - - if "SimpleAuthManager" in conf.get("core", "auth_manager") and conf.getboolean( - "core", "simple_auth_manager_all_admins" - ): - from airflow.api_fastapi.auth.managers.simple.middleware import SimpleAllAdminMiddleware + from airflow.api_fastapi.auth.middlewares.refresh_token import JWTRefreshMiddleware - app.add_middleware(SimpleAllAdminMiddleware) + app.add_middleware(JWTRefreshMiddleware) def init_ui_plugins(app: FastAPI) -> None: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py index 8261cfa11ea7a..00f58257b830b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/assets.py @@ -19,7 +19,7 @@ from datetime import datetime -from pydantic import AliasPath, Field, NonNegativeInt, field_validator +from pydantic import AliasPath, ConfigDict, Field, JsonValue, NonNegativeInt, field_validator from airflow._shared.secrets_masker import redact from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel @@ -65,7 +65,7 @@ class AssetResponse(BaseModel): name: str uri: str group: str - extra: dict | None = None + extra: dict[str, JsonValue] | None = None created_at: datetime updated_at: datetime scheduled_dags: list[DagScheduleAssetReference] @@ -123,7 +123,7 @@ class AssetEventResponse(BaseModel): uri: str | None = Field(alias="uri", default=None) name: str | None = Field(alias="name", default=None) group: str | None = Field(alias="group", default=None) - extra: dict | None = None + extra: dict[str, JsonValue] | None = None source_task_id: str | None = None source_dag_id: str | None = None source_run_id: str | None = None @@ -171,7 +171,4 @@ def set_from_rest_api(cls, v: dict) -> dict: v["from_rest_api"] = True return v - class Config: - """Pydantic config.""" - - extra = "forbid" + model_config = ConfigDict(extra="forbid") diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py index aa3a7fd0ef9b7..6b37b9e381694 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/backfills.py @@ -35,6 +35,7 @@ class BackfillPostBody(StrictBaseModel): dag_run_conf: dict = {} reprocess_behavior: ReprocessBehavior = ReprocessBehavior.NONE max_active_runs: int = 10 + run_on_latest_version: bool = True class BackfillResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py index ed7aac2ae000f..1160842dd2c64 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py @@ -26,7 +26,6 @@ from airflow._shared.timezones import timezone from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse -from airflow.models import DagRun from airflow.timetables.base import DataInterval from airflow.utils.state import DagRunState from airflow.utils.types import DagRunTriggeredByType, DagRunType @@ -106,12 +105,12 @@ class TriggerDAGRunPostBody(StrictBaseModel): note: str | None = None @model_validator(mode="after") - def check_data_intervals(cls, values): - if (values.data_interval_start is None) != (values.data_interval_end is None): + def check_data_intervals(self): + if (self.data_interval_start is None) != (self.data_interval_end is None): raise ValueError( "Either both data_interval_start and data_interval_end must be provided or both must be None" ) - return values + return self def validate_context(self, dag: SerializedDAG) -> dict: coerced_logical_date = timezone.coerce_datetime(self.logical_date) @@ -124,15 +123,12 @@ def validate_context(self, dag: SerializedDAG) -> dict: end=timezone.coerce_datetime(self.data_interval_end), ) else: - data_interval = dag.timetable.infer_manual_data_interval( - run_after=coerced_logical_date or timezone.coerce_datetime(run_after) - ) - run_after = data_interval.end + data_interval = dag.timetable.infer_manual_data_interval(run_after=coerced_logical_date) - run_id = self.dag_run_id or DagRun.generate_run_id( - run_type=DagRunType.SCHEDULED, - logical_date=coerced_logical_date, - run_after=run_after, + run_id = self.dag_run_id or dag.timetable.generate_run_id( + run_type=DagRunType.MANUAL, + run_after=timezone.coerce_datetime(run_after), + data_interval=data_interval, ) return { "run_id": run_id, @@ -143,14 +139,6 @@ def validate_context(self, dag: SerializedDAG) -> dict: "note": self.note, } - @model_validator(mode="after") - def validate_dag_run_id(self): - if not self.dag_run_id: - self.dag_run_id = DagRun.generate_run_id( - run_type=DagRunType.MANUAL, logical_date=self.logical_date, run_after=self.run_after - ) - return self - class DAGRunsBatchBody(StrictBaseModel): """List DAG Runs body for batch endpoint.""" diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py index 2475d49031fa1..ac0ebf56f4481 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_versions.py @@ -19,11 +19,9 @@ from datetime import datetime from uuid import UUID -from pydantic import AliasPath, Field, computed_field -from sqlalchemy import select +from pydantic import AliasPath, Field from airflow.api_fastapi.core_api.base import BaseModel -from airflow.dag_processing.bundles.manager import DagBundlesManager class DagVersionResponse(BaseModel): @@ -37,29 +35,7 @@ class DagVersionResponse(BaseModel): created_at: datetime dag_display_name: str = Field(validation_alias=AliasPath("dag_model", "dag_display_name")) - # Mypy issue https://github.com/python/mypy/issues/1362 - @computed_field # type: ignore[prop-decorator] - @property - def bundle_url(self) -> str | None: - if self.bundle_name: - # Get the bundle model from the database and render the URL - from airflow.models.dagbundle import DagBundleModel - from airflow.utils.session import create_session - - with create_session() as session: - bundle_model = session.scalar( - select(DagBundleModel).where(DagBundleModel.name == self.bundle_name) - ) - - if bundle_model and hasattr(bundle_model, "signed_url_template"): - return bundle_model.render_url(self.bundle_version) - # fallback to the deprecated option if the bundle model does not have a signed_url_template - # attribute - try: - return DagBundlesManager().view_url(self.bundle_name, self.bundle_version) - except ValueError: - return None - return None + bundle_url: str | None = Field(validation_alias="bundle_url") class DAGVersionCollectionResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py index b14b8b18cbdc3..4cd3ed93817ea 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py @@ -19,7 +19,6 @@ import inspect from collections import abc -from collections.abc import Iterable from datetime import datetime, timedelta from typing import Any @@ -29,6 +28,7 @@ AliasGenerator, ConfigDict, computed_field, + field_serializer, field_validator, ) @@ -82,6 +82,11 @@ class DAGResponse(BaseModel): next_dagrun_run_after: datetime | None owners: list[str] + @field_serializer("tags") + def serialize_tags(self, tags: list[DagTagResponse]) -> list[DagTagResponse]: + """Sort tags alphabetically by name.""" + return sorted(tags, key=lambda tag: tag.name) + @field_validator("owners", mode="before") @classmethod def get_owners(cls, v: Any) -> list[str] | None: @@ -151,13 +156,14 @@ class DAGDetailsResponse(DAGResponse): start_date: datetime | None end_date: datetime | None is_paused_upon_creation: bool | None - params: abc.MutableMapping | None + params: abc.Mapping | None render_template_as_native_obj: bool - template_search_path: Iterable[str] | None + template_search_path: list[str] | None timezone: str | None last_parsed: datetime | None default_args: abc.Mapping | None owner_links: dict[str, str] | None = None + is_favorite: bool = False @field_validator("timezone", mode="before") @classmethod @@ -177,7 +183,7 @@ def get_doc_md(cls, doc_md: str | None) -> str | None: @field_validator("params", mode="before") @classmethod - def get_params(cls, params: abc.MutableMapping | None) -> dict | None: + def get_params(cls, params: abc.Mapping | None) -> dict | None: """Convert params attribute to dict representation.""" if params is None: return None @@ -199,7 +205,9 @@ def concurrency(self) -> int: @property def latest_dag_version(self) -> DagVersionResponse | None: """Return the latest DagVersion.""" - latest_dag_version = DagVersion.get_latest_version(self.dag_id, load_dag_model=True) + latest_dag_version = DagVersion.get_latest_version( + self.dag_id, load_dag_model=True, load_bundle_model=True + ) if latest_dag_version is None: return latest_dag_version return DagVersionResponse.model_validate(latest_dag_version) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/event_logs.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/event_logs.py index d7355bc1294b1..8a1d429ad7027 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/event_logs.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/event_logs.py @@ -41,6 +41,9 @@ class EventLogResponse(BaseModel): dag_display_name: str | None = Field( validation_alias=AliasPath("dag_model", "dag_display_name"), default=None ) + task_display_name: str | None = Field( + validation_alias=AliasPath("task_instance", "task_display_name"), default=None + ) class EventLogCollectionResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py index ea0f045539ee6..4fa79eefe5045 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/hitl.py @@ -24,7 +24,6 @@ from airflow.api_fastapi.core_api.base import BaseModel from airflow.api_fastapi.core_api.datamodels.task_instances import TaskInstanceResponse -from airflow.sdk import Param class UpdateHITLDetailPayload(BaseModel): @@ -37,13 +36,19 @@ class UpdateHITLDetailPayload(BaseModel): class HITLDetailResponse(BaseModel): """Response of updating a Human-in-the-loop detail.""" - responded_user_id: str - responded_user_name: str - response_at: datetime + responded_by: HITLUser + responded_at: datetime chosen_options: list[str] = Field(min_length=1) params_input: Mapping = Field(default_factory=dict) +class HITLUser(BaseModel): + """Schema for a Human-in-the-loop users.""" + + id: str + name: str + + class HITLDetail(BaseModel): """Schema for Human-in-the-loop detail.""" @@ -55,13 +60,13 @@ class HITLDetail(BaseModel): body: str | None = None defaults: list[str] | None = None multiple: bool = False - params: dict[str, Any] = Field(default_factory=dict) - respondents: list[str] | None = None + params: Mapping = Field(default_factory=dict) + assigned_users: list[HITLUser] = Field(default_factory=list) + created_at: datetime # Response Content Detail - responded_user_id: str | None = None - responded_user_name: str | None = None - response_at: datetime | None = None + responded_by_user: HITLUser | None = None + responded_at: datetime | None = None chosen_options: list[str] | None = None params_input: dict[str, Any] = Field(default_factory=dict) @@ -71,7 +76,20 @@ class HITLDetail(BaseModel): @classmethod def get_params(cls, params: dict[str, Any]) -> dict[str, Any]: """Convert params attribute to dict representation.""" - return {k: v.dump() if isinstance(v, Param) else v for k, v in params.items()} + return { + key: value + if HITLDetail._is_param(value) + else { + "value": value, + "description": None, + "schema": {}, + } + for key, value in params.items() + } + + @staticmethod + def _is_param(value: Any) -> bool: + return isinstance(value, dict) and all(key in value for key in ("description", "schema", "value")) class HITLDetailCollection(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/import_error.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/import_error.py index 10f6e959e277b..084434cadbf52 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/import_error.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/import_error.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +from collections.abc import Iterable from datetime import datetime from pydantic import Field @@ -36,5 +37,5 @@ class ImportErrorResponse(BaseModel): class ImportErrorCollectionResponse(BaseModel): """Import Error Collection Response.""" - import_errors: list[ImportErrorResponse] + import_errors: Iterable[ImportErrorResponse] total_entries: int diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py index 4342d04c1e125..34cf4cb982550 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py @@ -39,7 +39,7 @@ class BasePool(BaseModel): pool: str = Field(serialization_alias="name") slots: int - description: str | None + description: str | None = Field(default=None) include_deferred: bool diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py index 8b515fafd2da7..ab4381c231b55 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/providers.py @@ -26,6 +26,7 @@ class ProviderResponse(BaseModel): package_name: str description: str version: str + documentation_url: str | None class ProviderCollectionResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instances.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instances.py index 0b6edcf98e25b..ca6823a123afe 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instances.py @@ -191,7 +191,11 @@ class ClearTaskInstancesBody(StrictBaseModel): only_failed: bool = True only_running: bool = False reset_dag_runs: bool = True - task_ids: list[str | tuple[str, int]] | None = None + task_ids: list[str | tuple[str, int]] | None = Field( + default=None, + description="A list of `task_id` or [`task_id`, `map_index`]. " + "If only the `task_id` is provided for a mapped task, all of its map indices will be targeted.", + ) dag_run_id: str | None = None include_upstream: bool = False include_downstream: bool = False diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py index 2dee03e6510cc..6c96367da2e9b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py @@ -81,10 +81,10 @@ class GridRunsResponse(BaseModel): run_type: DagRunType @computed_field - def duration(self) -> int: + def duration(self) -> float: if self.start_date: end_date = self.end_date or timezone.utcnow() - return (end_date - self.start_date).seconds + return (end_date - self.start_date).total_seconds() return 0 diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py index d99761f745753..6deaf958f4292 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py @@ -18,6 +18,8 @@ from datetime import datetime +from pydantic import computed_field + from airflow.api_fastapi.core_api.base import BaseModel from airflow.utils.state import DagRunState @@ -33,3 +35,9 @@ class DAGRunLightResponse(BaseModel): start_date: datetime | None end_date: datetime | None state: DagRunState + + @computed_field + def duration(self) -> float | None: + if self.end_date and self.start_date: + return (self.end_date - self.start_date).total_seconds() + return None diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py index e76c5414d6df1..fc2fe3ed2d2aa 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py @@ -18,17 +18,18 @@ from __future__ import annotations from airflow.api_fastapi.core_api.base import BaseModel -from airflow.api_fastapi.core_api.datamodels.dag_run import DAGRunResponse from airflow.api_fastapi.core_api.datamodels.dags import DAGResponse from airflow.api_fastapi.core_api.datamodels.hitl import HITLDetail +from airflow.api_fastapi.core_api.datamodels.ui.dag_runs import DAGRunLightResponse class DAGWithLatestDagRunsResponse(DAGResponse): """DAG with latest dag runs response serializer.""" asset_expression: dict | None - latest_dag_runs: list[DAGRunResponse] + latest_dag_runs: list[DAGRunLightResponse] pending_actions: list[HITLDetail] + is_favorite: bool class DAGWithLatestDagRunsCollectionResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py index 0f22cd7226224..174bfbadf3030 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py @@ -38,11 +38,31 @@ class XComResponse(BaseModel): task_display_name: str = Field(validation_alias=AliasPath("task", "task_display_name")) +def _stringify_if_needed(value): + """ + Check whether value is JSON-encodable (recursively if needed); stringify it if not. + + The list of JSON-ecodable types are taken from Python documentation: + https://docs.python.org/3/library/json.html#json.JSONEncoder + """ + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, dict): + return {str(k): _stringify_if_needed(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [_stringify_if_needed(v) for v in value] + return str(value) + + class XComResponseNative(XComResponse): """XCom response serializer with native return type.""" value: Any + @field_validator("value", mode="before") + def value_to_json_serializable(cls, v): + return _stringify_if_needed(v) + class XComResponseString(XComResponse): """XCom response serializer with string return type.""" diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index a4d85b16dd3c0..7ff7310516432 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -251,9 +251,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, + next_dagrun, state, start_date, last_run_state, last_run_start_date`' default: - dag_id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `dag_id, dag_display_name, + next_dagrun, state, start_date, last_run_state, last_run_start_date`' - name: is_favorite in: query required: false @@ -547,9 +553,13 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id`' - name: dag_id in: query required: false @@ -625,9 +635,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `run_after, + logical_date, start_date, end_date`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `run_after, logical_date, + start_date, end_date`' - name: run_after_gte in: query required: false @@ -752,9 +768,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `run_after, + logical_date, start_date, end_date`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `run_after, logical_date, + start_date, end_date`' - name: run_after_gte in: query required: false @@ -1325,137 +1347,24 @@ components: title: End Date state: $ref: '#/components/schemas/DagRunState' - type: object - required: - - id - - dag_id - - run_id - - logical_date - - run_after - - start_date - - end_date - - state - title: DAGRunLightResponse - description: DAG Run serializer for responses. - DAGRunResponse: - properties: - dag_run_id: - type: string - title: Dag Run Id - dag_id: - type: string - title: Dag Id - logical_date: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Logical Date - queued_at: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Queued At - start_date: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Start Date - end_date: - anyOf: - - type: string - format: date-time - - type: 'null' - title: End Date duration: anyOf: - type: number - type: 'null' title: Duration - data_interval_start: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Data Interval Start - data_interval_end: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Data Interval End - run_after: - type: string - format: date-time - title: Run After - last_scheduling_decision: - anyOf: - - type: string - format: date-time - - type: 'null' - title: Last Scheduling Decision - run_type: - $ref: '#/components/schemas/DagRunType' - state: - $ref: '#/components/schemas/DagRunState' - triggered_by: - anyOf: - - $ref: '#/components/schemas/DagRunTriggeredByType' - - type: 'null' - triggering_user_name: - anyOf: - - type: string - - type: 'null' - title: Triggering User Name - conf: - anyOf: - - additionalProperties: true - type: object - - type: 'null' - title: Conf - note: - anyOf: - - type: string - - type: 'null' - title: Note - dag_versions: - items: - $ref: '#/components/schemas/DagVersionResponse' - type: array - title: Dag Versions - bundle_version: - anyOf: - - type: string - - type: 'null' - title: Bundle Version - dag_display_name: - type: string - title: Dag Display Name + readOnly: true type: object required: - - dag_run_id + - id - dag_id + - run_id - logical_date - - queued_at + - run_after - start_date - end_date - - duration - - data_interval_start - - data_interval_end - - run_after - - last_scheduling_decision - - run_type - state - - triggered_by - - triggering_user_name - - conf - - note - - dag_versions - - bundle_version - - dag_display_name - title: DAGRunResponse + - duration + title: DAGRunLightResponse description: DAG Run serializer for responses. DAGRunStates: properties: @@ -1640,7 +1549,7 @@ components: title: Asset Expression latest_dag_runs: items: - $ref: '#/components/schemas/DAGRunResponse' + $ref: '#/components/schemas/DAGRunLightResponse' type: array title: Latest Dag Runs pending_actions: @@ -1648,6 +1557,9 @@ components: $ref: '#/components/schemas/HITLDetail' type: array title: Pending Actions + is_favorite: + type: boolean + title: Is Favorite file_token: type: string title: File Token @@ -1683,6 +1595,7 @@ components: - asset_expression - latest_dag_runs - pending_actions + - is_favorite - file_token title: DAGWithLatestDagRunsResponse description: DAG with latest dag runs response serializer. @@ -1702,19 +1615,6 @@ components: so please ensure that their values always match the ones with the same name in TaskInstanceState.' - DagRunTriggeredByType: - type: string - enum: - - cli - - operator - - rest_api - - ui - - test - - timetable - - asset - - backfill - title: DagRunTriggeredByType - description: Class with TriggeredBy types for DagRun. DagRunType: type: string enum: @@ -1776,7 +1676,6 @@ components: - type: string - type: 'null' title: Bundle Url - readOnly: true type: object required: - id @@ -1925,7 +1824,7 @@ components: run_type: $ref: '#/components/schemas/DagRunType' duration: - type: integer + type: number title: Duration readOnly: true type: object @@ -1994,29 +1893,25 @@ components: additionalProperties: true type: object title: Params - respondents: - anyOf: - - items: - type: string - type: array - - type: 'null' - title: Respondents - responded_user_id: - anyOf: - - type: string - - type: 'null' - title: Responded User Id - responded_user_name: + assigned_users: + items: + $ref: '#/components/schemas/HITLUser' + type: array + title: Assigned Users + created_at: + type: string + format: date-time + title: Created At + responded_by_user: anyOf: - - type: string + - $ref: '#/components/schemas/HITLUser' - type: 'null' - title: Responded User Name - response_at: + responded_at: anyOf: - type: string format: date-time - type: 'null' - title: Response At + title: Responded At chosen_options: anyOf: - items: @@ -2037,8 +1932,23 @@ components: - task_instance - options - subject + - created_at title: HITLDetail description: Schema for Human-in-the-loop detail. + HITLUser: + properties: + id: + type: string + title: Id + name: + type: string + title: Name + type: object + required: + - id + - name + title: HITLUser + description: Schema for a Human-in-the-loop users. HTTPExceptionResponse: properties: detail: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 7c23f712f1c7a..45f38707a6eea 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -80,9 +80,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, name, + uri, created_at, updated_at`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, name, uri, created_at, + updated_at`' responses: '200': description: Successful Response @@ -160,9 +166,13 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, name`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, name`' responses: '200': description: Successful Response @@ -275,9 +285,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `source_task_id, + source_dag_id, source_run_id, source_map_index, timestamp`' default: - timestamp title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `source_task_id, source_dag_id, + source_run_id, source_map_index, timestamp`' - name: asset_id in: query required: false @@ -916,9 +932,13 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id`' responses: '200': description: Successful Response @@ -1451,9 +1471,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `conn_id, + conn_type, description, host, port, id, connection_id`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `conn_id, conn_type, + description, host, port, id, connection_id`' - name: connection_id_pattern in: query required: false @@ -2206,9 +2232,17 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, state, + dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, + conf, duration, dag_run_id`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, state, dag_id, + run_id, logical_date, run_after, start_date, end_date, updated_at, conf, + duration, dag_run_id`' - name: run_id_pattern in: query required: false @@ -2336,7 +2370,8 @@ paths: summary: 'Experimental: Wait for a dag run to complete, and return task results if requested.' description: "\U0001F6A7 This is an experimental endpoint and may change or\ - \ be removed without notice." + \ be removed without notice.Successful response are streamed as newline-delimited\ + \ JSON (NDJSON). Each line is a JSON object representing the DAG run state." operationId: wait_dag_run_until_finished security: - OAuth2PasswordBearer: [] @@ -2607,53 +2642,6 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /api/v2/dagReports: - get: - tags: - - DagReport - summary: Get Dag Reports - description: Get DAG report. - operationId: get_dag_reports - security: - - OAuth2PasswordBearer: [] - - HTTPBearer: [] - parameters: - - name: subdir - in: query - required: true - schema: - type: string - title: Subdir - responses: - '200': - description: Successful Response - content: - application/json: - schema: {} - '401': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPExceptionResponse' - description: Unauthorized - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPExceptionResponse' - description: Forbidden - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPExceptionResponse' - description: Bad Request - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' /api/v2/config: get: tags: @@ -2867,9 +2855,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `dag_id, warning_type, + message, timestamp`' default: - dag_id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `dag_id, warning_type, + message, timestamp`' responses: '200': description: Successful Response @@ -3131,9 +3125,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, + next_dagrun, state, start_date, last_run_state, last_run_start_date`' default: - dag_id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `dag_id, dag_display_name, + next_dagrun, state, start_date, last_run_state, last_run_start_date`' - name: is_favorite in: query required: false @@ -3704,9 +3704,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, dttm, + dag_id, task_id, run_id, event, logical_date, owner, extra, when, event_log_id`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, dttm, dag_id, + task_id, run_id, event, logical_date, owner, extra, when, event_log_id`' - name: dag_id in: query required: false @@ -4036,9 +4042,27 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, timestamp, + filename, bundle_name, stacktrace, import_error_id`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, timestamp, filename, + bundle_name, stacktrace, import_error_id`' + - name: filename_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." + title: Filename Pattern + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." responses: '200': description: Successful Response @@ -4178,9 +4202,17 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, dag_id, + state, job_type, start_date, end_date, latest_heartbeat, executor_class, + hostname, unixname`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, dag_id, state, + job_type, start_date, end_date, latest_heartbeat, executor_class, hostname, + unixname`' - name: job_state in: query required: false @@ -4523,9 +4555,14 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, pool, + name`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, pool, name`' - name: pool_name_pattern in: query required: false @@ -5378,9 +5415,7 @@ paths: description: Successful Response content: application/json: - schema: - type: 'null' - title: Response Delete Task Instance + schema: {} '401': content: application/json: @@ -5702,6 +5737,14 @@ paths: items: type: string title: Operator + - name: map_index + in: query + required: false + schema: + type: array + items: + type: integer + title: Map Index - name: limit in: query required: false @@ -5725,9 +5768,19 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, state, + duration, start_date, end_date, map_index, try_number, logical_date, run_after, + data_interval_start, data_interval_end, rendered_map_index, operator, + run_after, logical_date, data_interval_start, data_interval_end`' default: - map_index title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, state, duration, + start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, + data_interval_end, rendered_map_index, operator, run_after, logical_date, + data_interval_start, data_interval_end`' responses: '200': description: Successful Response @@ -6446,6 +6499,18 @@ paths: title: Task Display Name Pattern description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ \ Regular expressions are **not** supported." + - name: task_group_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: Filter by exact task group ID. Returns all tasks within the + specified task group. + title: Task Group Id + description: Filter by exact task group ID. Returns all tasks within the specified + task group. - name: state in: query required: false @@ -6502,6 +6567,14 @@ paths: items: type: string title: Operator + - name: map_index + in: query + required: false + schema: + type: array + items: + type: integer + title: Map Index - name: limit in: query required: false @@ -6525,9 +6598,19 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, state, + duration, start_date, end_date, map_index, try_number, logical_date, run_after, + data_interval_start, data_interval_end, rendered_map_index, operator, + logical_date, run_after, data_interval_start, data_interval_end`' default: - map_index title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, state, duration, + start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, + data_interval_end, rendered_map_index, operator, logical_date, run_after, + data_interval_start, data_interval_end`' responses: '200': description: Successful Response @@ -7366,9 +7449,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `key, id, + _val, description, is_encrypted`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `key, id, _val, description, + is_encrypted`' - name: variable_key_pattern in: query required: false @@ -7702,9 +7791,7 @@ paths: description: Successful Response content: application/json: - schema: - type: 'null' - title: Response Reparse Dag File + schema: {} '401': content: application/json: @@ -7763,9 +7850,13 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `name`' default: - name title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `name`' - name: tag_name_pattern in: query required: false @@ -7921,9 +8012,15 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `id, version_number, + bundle_name, bundle_version`' default: - id title: Order By + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `id, version_number, + bundle_name, bundle_version`' responses: '200': description: Successful Response @@ -7955,10 +8052,10 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /api/v2/hitlDetails/{dag_id}/{dag_run_id}/{task_id}: + /api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails: patch: tags: - - HumanInTheLoop + - Task Instance summary: Update Hitl Detail description: Update a Human-in-the-loop detail. operationId: update_hitl_detail @@ -7985,11 +8082,10 @@ paths: type: string title: Task Id - name: map_index - in: query - required: false + in: path + required: true schema: type: integer - default: -1 title: Map Index requestBody: required: true @@ -8036,7 +8132,7 @@ paths: $ref: '#/components/schemas/HTTPValidationError' get: tags: - - HumanInTheLoop + - Task Instance summary: Get Hitl Detail description: Get a Human-in-the-loop detail of a specific task instance. operationId: get_hitl_detail @@ -8063,11 +8159,10 @@ paths: type: string title: Task Id - name: map_index - in: query - required: false + in: path + required: true schema: type: integer - default: -1 title: Map Index responses: '200': @@ -8100,10 +8195,10 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /api/v2/hitlDetails/: + /api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/hitlDetails: get: tags: - - HumanInTheLoop + - Task Instance summary: Get Hitl Details description: Get Human-in-the-loop details. operationId: get_hitl_details @@ -8111,6 +8206,18 @@ paths: - OAuth2PasswordBearer: [] - HTTPBearer: [] parameters: + - name: dag_id + in: path + required: true + schema: + type: string + title: Dag Id + - name: dag_run_id + in: path + required: true + schema: + type: string + title: Dag Run Id - name: limit in: query required: false @@ -8134,17 +8241,18 @@ paths: type: array items: type: string + description: 'Attributes to order by, multi criteria sort is supported. + Prefix with `-` for descending order. Supported attributes: `ti_id, subject, + responded_at, created_at, responded_by_user_id, responded_by_user_name, + dag_id, run_id, run_after, rendered_map_index, task_instance_operator, + task_instance_state`' default: - ti_id title: Order By - - name: dag_id - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Dag Id + description: 'Attributes to order by, multi criteria sort is supported. Prefix + with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, + created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, + run_after, rendered_map_index, task_instance_operator, task_instance_state`' - name: dag_id_pattern in: query required: false @@ -8157,12 +8265,6 @@ paths: title: Dag Id Pattern description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ \ Regular expressions are **not** supported." - - name: dag_run_id - in: query - required: false - schema: - type: string - title: Dag Run Id - name: task_id in: query required: false @@ -8183,6 +8285,14 @@ paths: title: Task Id Pattern description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ \ Regular expressions are **not** supported." + - name: map_index + in: query + required: false + schema: + anyOf: + - type: integer + - type: 'null' + title: Map Index - name: state in: query required: false @@ -8199,22 +8309,22 @@ paths: - type: boolean - type: 'null' title: Response Received - - name: responded_user_id + - name: responded_by_user_id in: query required: false schema: type: array items: type: string - title: Responded User Id - - name: responded_user_name + title: Responded By User Id + - name: responded_by_user_name in: query required: false schema: type: array items: type: string - title: Responded User Name + title: Responded By User Name - name: subject_search in: query required: false @@ -8239,6 +8349,42 @@ paths: title: Body Search description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ \ Regular expressions are **not** supported." + - name: created_at_gte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Gte + - name: created_at_gt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Gt + - name: created_at_lte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Lte + - name: created_at_lt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Lt responses: '200': description: Successful Response @@ -8332,15 +8478,6 @@ paths: summary: Logout description: Logout the user. operationId: logout - parameters: - - name: next - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Next responses: '200': description: Successful Response @@ -8348,51 +8485,11 @@ paths: application/json: schema: {} '307': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPExceptionResponse' description: Temporary Redirect - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /api/v2/auth/refresh: - get: - tags: - - Login - summary: Refresh - description: Refresh the authentication token. - operationId: refresh - parameters: - - name: next - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Next - responses: - '200': - description: Successful Response - content: - application/json: - schema: {} - '307': content: application/json: schema: $ref: '#/components/schemas/HTTPExceptionResponse' - description: Temporary Redirect - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' components: schemas: AppBuilderMenuItemResponse: @@ -8532,7 +8629,8 @@ components: title: Group extra: anyOf: - - additionalProperties: true + - additionalProperties: + $ref: '#/components/schemas/JsonValue' type: object - type: 'null' title: Extra @@ -8588,7 +8686,8 @@ components: title: Group extra: anyOf: - - additionalProperties: true + - additionalProperties: + $ref: '#/components/schemas/JsonValue' type: object - type: 'null' title: Extra @@ -8683,6 +8782,10 @@ components: type: integer title: Max Active Runs default: 10 + run_on_latest_version: + type: boolean + title: Run On Latest Version + default: true additionalProperties: false type: object required: @@ -9261,6 +9364,9 @@ components: type: array - type: 'null' title: Task Ids + description: A list of `task_id` or [`task_id`, `map_index`]. If only the + `task_id` is provided for a mapped task, all of its map indices will be + targeted. dag_run_id: anyOf: - type: string @@ -9705,6 +9811,10 @@ components: type: object - type: 'null' title: Owner Links + is_favorite: + type: boolean + title: Is Favorite + default: false file_token: type: string title: File Token @@ -10574,7 +10684,6 @@ components: - type: string - type: 'null' title: Bundle Url - readOnly: true type: object required: - id @@ -10700,6 +10809,11 @@ components: - type: string - type: 'null' title: Dag Display Name + task_display_name: + anyOf: + - type: string + - type: 'null' + title: Task Display Name type: object required: - event_log_id @@ -10855,29 +10969,25 @@ components: additionalProperties: true type: object title: Params - respondents: - anyOf: - - items: - type: string - type: array - - type: 'null' - title: Respondents - responded_user_id: - anyOf: - - type: string - - type: 'null' - title: Responded User Id - responded_user_name: + assigned_users: + items: + $ref: '#/components/schemas/HITLUser' + type: array + title: Assigned Users + created_at: + type: string + format: date-time + title: Created At + responded_by_user: anyOf: - - type: string + - $ref: '#/components/schemas/HITLUser' - type: 'null' - title: Responded User Name - response_at: + responded_at: anyOf: - type: string format: date-time - type: 'null' - title: Response At + title: Responded At chosen_options: anyOf: - items: @@ -10898,6 +11008,7 @@ components: - task_instance - options - subject + - created_at title: HITLDetail description: Schema for Human-in-the-loop detail. HITLDetailCollection: @@ -10918,16 +11029,12 @@ components: description: Schema for a collection of Human-in-the-loop details. HITLDetailResponse: properties: - responded_user_id: - type: string - title: Responded User Id - responded_user_name: - type: string - title: Responded User Name - response_at: + responded_by: + $ref: '#/components/schemas/HITLUser' + responded_at: type: string format: date-time - title: Response At + title: Responded At chosen_options: items: type: string @@ -10940,12 +11047,25 @@ components: title: Params Input type: object required: - - responded_user_id - - responded_user_name - - response_at + - responded_by + - responded_at - chosen_options title: HITLDetailResponse description: Response of updating a Human-in-the-loop detail. + HITLUser: + properties: + id: + type: string + title: Id + name: + type: string + title: Name + type: object + required: + - id + - name + title: HITLUser + description: Schema for a Human-in-the-loop users. HTTPExceptionResponse: properties: detail: @@ -11410,7 +11530,6 @@ components: required: - name - slots - - description - include_deferred - occupied_slots - running_slots @@ -11447,11 +11566,17 @@ components: version: type: string title: Version + documentation_url: + anyOf: + - type: string + - type: 'null' + title: Documentation Url type: object required: - package_name - description - version + - documentation_url title: ProviderResponse description: Provider serializer for responses. QueuedEventCollectionResponse: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py index 6db86ce2327a6..0b501b4f99f71 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py @@ -27,7 +27,6 @@ from airflow.api_fastapi.core_api.routes.public.config import config_router from airflow.api_fastapi.core_api.routes.public.connections import connections_router from airflow.api_fastapi.core_api.routes.public.dag_parsing import dag_parsing_router -from airflow.api_fastapi.core_api.routes.public.dag_report import dag_report_router from airflow.api_fastapi.core_api.routes.public.dag_run import dag_run_router from airflow.api_fastapi.core_api.routes.public.dag_sources import dag_sources_router from airflow.api_fastapi.core_api.routes.public.dag_stats import dag_stats_router @@ -37,7 +36,7 @@ from airflow.api_fastapi.core_api.routes.public.dags import dags_router from airflow.api_fastapi.core_api.routes.public.event_logs import event_logs_router from airflow.api_fastapi.core_api.routes.public.extra_links import extra_links_router -from airflow.api_fastapi.core_api.routes.public.hitl import hitl_router +from airflow.api_fastapi.core_api.routes.public.hitl import task_instances_hitl_router from airflow.api_fastapi.core_api.routes.public.import_error import import_error_router from airflow.api_fastapi.core_api.routes.public.job import job_router from airflow.api_fastapi.core_api.routes.public.log import task_instances_log_router @@ -65,7 +64,6 @@ authenticated_router.include_router(dag_run_router) authenticated_router.include_router(dag_sources_router) authenticated_router.include_router(dag_stats_router) -authenticated_router.include_router(dag_report_router) authenticated_router.include_router(config_router) authenticated_router.include_router(dag_warning_router) authenticated_router.include_router(dags_router) @@ -84,7 +82,7 @@ authenticated_router.include_router(dag_parsing_router) authenticated_router.include_router(dag_tags_router) authenticated_router.include_router(dag_versions_router) -authenticated_router.include_router(hitl_router) +authenticated_router.include_router(task_instances_hitl_router) # Include authenticated router in public router diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py index 839403352483e..a00739564f881 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py @@ -182,6 +182,7 @@ def get_assets( subqueryload(AssetModel.scheduled_dags), subqueryload(AssetModel.producing_tasks), subqueryload(AssetModel.consuming_tasks), + subqueryload(AssetModel.aliases), ) ) @@ -304,7 +305,9 @@ def get_asset_events( session=session, ) - assets_event_select = assets_event_select.options(subqueryload(AssetEvent.created_dagruns)) + assets_event_select = assets_event_select.options( + subqueryload(AssetEvent.created_dagruns), joinedload(AssetEvent.asset) + ) assets_events = session.scalars(assets_event_select) return AssetEventCollectionResponse( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py index 2e3a215f1753b..a97b7fd9972dc 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py @@ -19,6 +19,7 @@ from fastapi import HTTPException, Request, status from fastapi.responses import RedirectResponse +from airflow.api_fastapi.auth.managers.base_auth_manager import COOKIE_NAME_JWT_TOKEN from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc from airflow.api_fastapi.core_api.security import is_safe_url @@ -48,31 +49,19 @@ def login(request: Request, next: None | str = None) -> RedirectResponse: "/logout", responses=create_openapi_http_exception_doc([status.HTTP_307_TEMPORARY_REDIRECT]), ) -def logout(request: Request, next: None | str = None) -> RedirectResponse: +def logout(request: Request) -> RedirectResponse: """Logout the user.""" - logout_url = request.app.state.auth_manager.get_url_logout() - - if not logout_url: - logout_url = request.app.state.auth_manager.get_url_login() - - return RedirectResponse(logout_url) - - -@auth_router.get( - "/refresh", - responses=create_openapi_http_exception_doc([status.HTTP_307_TEMPORARY_REDIRECT]), -) -def refresh(request: Request, next: None | str = None) -> RedirectResponse: - """Refresh the authentication token.""" - refresh_url = request.app.state.auth_manager.get_url_refresh() - - if not refresh_url: - return RedirectResponse(f"{conf.get('api', 'base_url', fallback='/')}auth/logout") - - if next and not is_safe_url(next, request=request): - raise HTTPException(status_code=400, detail="Invalid or unsafe next URL") - - if next: - refresh_url += f"?next={next}" - - return RedirectResponse(refresh_url) + auth_manager = request.app.state.auth_manager + logout_url = auth_manager.get_url_logout() + if logout_url: + return RedirectResponse(logout_url) + + secure = request.base_url.scheme == "https" or bool(conf.get("api", "ssl_cert", fallback="")) + response = RedirectResponse(auth_manager.get_url_login()) + response.delete_cookie( + key=COOKIE_NAME_JWT_TOKEN, + secure=secure, + httponly=True, + ) + + return response diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py index 30efea4d79ab8..b9c1416b2ce72 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py @@ -243,6 +243,7 @@ def create_backfill( dag_run_conf=backfill_request.dag_run_conf, triggering_user_name=user.get_name(), reprocess_behavior=backfill_request.reprocess_behavior, + run_on_latest_version=backfill_request.run_on_latest_version, ) return BackfillResponse.model_validate(backfill_obj) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py index 295c6f156f038..b7e79f8661477 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/connections.py @@ -50,6 +50,7 @@ ) from airflow.api_fastapi.logging.decorators import action_logging from airflow.configuration import conf +from airflow.exceptions import AirflowNotFoundException from airflow.models import Connection from airflow.secrets.environment_variables import CONN_ENV_PREFIX from airflow.utils.db import create_default_connections as db_create_default_connections @@ -207,9 +208,7 @@ def patch_connection( @connections_router.post("/test", dependencies=[Depends(requires_access_connection(method="POST"))]) -def test_connection( - test_body: ConnectionBody, -) -> ConnectionTestResponse: +def test_connection(test_body: ConnectionBody) -> ConnectionTestResponse: """ Test an API connection. @@ -227,9 +226,17 @@ def test_connection( transient_conn_id = get_random_string() conn_env_var = f"{CONN_ENV_PREFIX}{transient_conn_id.upper()}" try: - data = test_body.model_dump(by_alias=True) - data["conn_id"] = transient_conn_id - conn = Connection(**data) + # Try to get existing connection and merge with provided values + try: + existing_conn = Connection.get_connection_from_secrets(test_body.connection_id) + existing_conn.conn_id = transient_conn_id + update_orm_from_pydantic(existing_conn, test_body) + conn = existing_conn + except AirflowNotFoundException: + data = test_body.model_dump(by_alias=True) + data["conn_id"] = transient_conn_id + conn = Connection(**data) + os.environ[conn_env_var] = conn.get_uri() test_status, test_message = conn.test_connection() return ConnectionTestResponse.model_validate({"status": test_status, "message": test_message}) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_report.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_report.py deleted file mode 100644 index 42539015b53d1..0000000000000 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_report.py +++ /dev/null @@ -1,75 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -from __future__ import annotations - -import ast -import os -from typing import cast - -from fastapi import Depends, HTTPException, status - -from airflow import settings -from airflow.api_fastapi.common.router import AirflowRouter -from airflow.api_fastapi.core_api.datamodels.dag_report import ( - DagReportCollectionResponse, - DagReportResponse, -) -from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc -from airflow.api_fastapi.core_api.security import ( - ReadableDagsFilterDep, - requires_access_dag, -) -from airflow.models.dagbag import DagBag - -dag_report_router = AirflowRouter(tags=["DagReport"], prefix="/dagReports") - - -@dag_report_router.get( - "", - responses=create_openapi_http_exception_doc( - [ - status.HTTP_400_BAD_REQUEST, - ] - ), - dependencies=[Depends(requires_access_dag(method="GET"))], -) -def get_dag_reports( - subdir: str, - readable_dags_filter: ReadableDagsFilterDep, -): - """Get DAG report.""" - fullpath = os.path.normpath(subdir) - if not fullpath.startswith(settings.DAGS_FOLDER): - raise HTTPException(status.HTTP_400_BAD_REQUEST, "subdir should be subpath of DAGS_FOLDER settings") - - dagbag = DagBag(fullpath) - - readable_dag_ids: set[str] | None = readable_dags_filter.value - if readable_dag_ids: - filtered_dagbag_stats = [ - file_load_stat - for file_load_stat in dagbag.dagbag_stats - if len(set(ast.literal_eval(file_load_stat.dags)) - readable_dag_ids) == 0 - ] - else: - filtered_dagbag_stats = [] - - return DagReportCollectionResponse( - dag_reports=cast("list[DagReportResponse]", filtered_dagbag_stats), - total_entries=len(filtered_dagbag_stats), - ) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py index 8056c6fbda41c..c269cae93d443 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py @@ -21,7 +21,7 @@ from typing import Annotated, Literal, cast import structlog -from fastapi import Depends, HTTPException, Query, status +from fastapi import Depends, HTTPException, Query, Request, status from fastapi.exceptions import RequestValidationError from fastapi.responses import StreamingResponse from pydantic import ValidationError @@ -36,6 +36,7 @@ from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity from airflow.api_fastapi.common.dagbag import DagBagDep, get_dag_for_run, get_latest_version_of_dag from airflow.api_fastapi.common.db.common import SessionDep, paginated_select +from airflow.api_fastapi.common.db.dag_runs import eager_load_dag_run_for_validation from airflow.api_fastapi.common.parameters import ( FilterOptionEnum, FilterParam, @@ -81,6 +82,7 @@ from airflow.exceptions import ParamValidationError from airflow.listeners.listener import get_listener_manager from airflow.models import DagModel, DagRun +from airflow.models.asset import AssetEvent from airflow.models.dag_version import DagVersion from airflow.utils.state import DagRunState from airflow.utils.types import DagRunTriggeredByType, DagRunType @@ -129,12 +131,22 @@ def get_dag_run(dag_id: str, dag_run_id: str, session: SessionDep) -> DAGRunResp def delete_dag_run(dag_id: str, dag_run_id: str, session: SessionDep): """Delete a DAG Run entry.""" dag_run = session.scalar(select(DagRun).filter_by(dag_id=dag_id, run_id=dag_run_id)) + deletable_states = {s.value for s in DAGRunPatchStates} if dag_run is None: raise HTTPException( status.HTTP_404_NOT_FOUND, f"The DagRun with dag_id: `{dag_id}` and run_id: `{dag_run_id}` was not found", ) + if dag_run.state not in deletable_states: + raise HTTPException( + status.HTTP_409_CONFLICT, + ( + f"The DagRun with dag_id: `{dag_id}` and run_id: `{dag_run_id}` " + f"cannot be deleted in {dag_run.state} state" + ), + ) + session.delete(dag_run) @@ -232,10 +244,12 @@ def get_upstream_asset_events( ) -> AssetEventCollectionResponse: """If dag run is asset-triggered, return the asset events that triggered it.""" dag_run: DagRun | None = session.scalar( - select(DagRun).where( + select(DagRun) + .where( DagRun.dag_id == dag_id, DagRun.run_id == dag_run_id, ) + .options(joinedload(DagRun.consumed_asset_events).joinedload(AssetEvent.asset)) ) if dag_run is None: raise HTTPException( @@ -357,11 +371,11 @@ def get_dag_runs( This endpoint allows specifying `~` as the dag_id to retrieve Dag Runs for all DAGs. """ - query = select(DagRun) + query = select(DagRun).options(*eager_load_dag_run_for_validation()) if dag_id != "~": get_latest_version_of_dag(dag_bag, dag_id, session) # Check if the DAG exists. - query = query.filter(DagRun.dag_id == dag_id).options(joinedload(DagRun.dag_model)) + query = query.filter(DagRun.dag_id == dag_id).options() # Add join with DagVersion if dag_version filter is active if dag_version.value: @@ -415,6 +429,7 @@ def trigger_dag_run( dag_bag: DagBagDep, user: GetUserDep, session: SessionDep, + request: Request, ) -> DAGRunResponse: """Trigger a DAG.""" dm = session.scalar(select(DagModel).where(~DagModel.is_stale, DagModel.dag_id == dag_id).limit(1)) @@ -427,6 +442,12 @@ def trigger_dag_run( f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered", ) + referer = request.headers.get("referer") + if referer: + triggered_by = DagRunTriggeredByType.UI + else: + triggered_by = DagRunTriggeredByType.REST_API + try: dag = get_latest_version_of_dag(dag_bag, dag_id, session) params = body.validate_context(dag) @@ -438,7 +459,7 @@ def trigger_dag_run( run_after=params["run_after"], conf=params["conf"], run_type=DagRunType.MANUAL, - triggered_by=DagRunTriggeredByType.REST_API, + triggered_by=triggered_by, triggering_user_name=user.get_name(), state=DagRunState.QUEUED, session=session, @@ -459,7 +480,7 @@ def trigger_dag_run( "/{dag_run_id}/wait", tags=["experimental"], summary="Experimental: Wait for a dag run to complete, and return task results if requested.", - description="🚧 This is an experimental endpoint and may change or be removed without notice.", + description="🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state.", responses={ **create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), status.HTTP_200_OK: { @@ -578,7 +599,8 @@ def get_list_dag_runs_batch( {"dag_run_id": "run_id"}, ).set_value([body.order_by] if body.order_by else None) - base_query = select(DagRun).options(joinedload(DagRun.dag_model)) + base_query = select(DagRun).options(*eager_load_dag_run_for_validation()) + dag_runs_select, total_entries = paginated_select( statement=base_query, filters=[dag_ids, logical_date, run_after, start_date, end_date, state, readable_dag_runs_filter], diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_versions.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_versions.py index e4f282f60880b..5f1c734148b2b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_versions.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_versions.py @@ -108,7 +108,7 @@ def get_dag_versions( This endpoint allows specifying `~` as the dag_id to retrieve DAG Versions for all DAGs. """ - query = select(DagVersion).options(joinedload(DagVersion.dag_model)) + query = select(DagVersion).options(joinedload(DagVersion.dag_model), joinedload(DagVersion.bundle)) if dag_id != "~": get_latest_version_of_dag(dag_bag, dag_id, session) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py index e62c12c5ecb16..5c4a7662b396b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py @@ -137,6 +137,7 @@ def get_dags( last_dag_run_state, ], order_by=order_by, + dag_ids=readable_dags_filter.value, ) dags_select, total_entries = paginated_select( @@ -209,7 +210,9 @@ def get_dag( ), dependencies=[Depends(requires_access_dag(method="GET"))], ) -def get_dag_details(dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> DAGDetailsResponse: +def get_dag_details( + dag_id: str, session: SessionDep, dag_bag: DagBagDep, user: GetUserDep +) -> DAGDetailsResponse: """Get details of DAG.""" dag = get_latest_version_of_dag(dag_bag, dag_id, session) @@ -221,7 +224,19 @@ def get_dag_details(dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> DAG if not key.startswith("_") and not hasattr(dag_model, key): setattr(dag_model, key, value) - return dag_model + # Check if this DAG is marked as favorite by the current user + user_id = str(user.get_id()) + is_favorite = ( + session.scalar( + select(DagFavorite.dag_id).where(DagFavorite.user_id == user_id, DagFavorite.dag_id == dag_id) + ) + is not None + ) + + # Add is_favorite field to the DAG model + setattr(dag_model, "is_favorite", is_favorite) + + return DAGDetailsResponse.model_validate(dag_model) @dags_router.patch( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py index 6e7603121e2ac..8909b5ff1bc64 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py @@ -21,6 +21,7 @@ from fastapi import Depends, HTTPException, status from sqlalchemy import select +from sqlalchemy.orm import joinedload from airflow.api_fastapi.common.db.common import ( SessionDep, @@ -57,7 +58,9 @@ def get_event_log( event_log_id: int, session: SessionDep, ) -> EventLogResponse: - event_log = session.scalar(select(Log).where(Log.id == event_log_id)) + event_log = session.scalar( + select(Log).where(Log.id == event_log_id).options(joinedload(Log.task_instance)) + ) if event_log is None: raise HTTPException(status.HTTP_404_NOT_FOUND, f"The Event Log with id: `{event_log_id}` not found") return event_log @@ -125,7 +128,7 @@ def get_event_logs( event_pattern: Annotated[_SearchParam, Depends(search_param_factory(Log.event, "event_pattern"))], ) -> EventLogCollectionResponse: """Get all Event Logs.""" - query = select(Log) + query = select(Log).options(joinedload(Log.task_instance), joinedload(Log.dag_model)) event_logs_select, total_entries = paginated_select( statement=query, order_by=order_by, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/extra_links.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/extra_links.py index 1dbbf0db8b70c..83d17b404ad82 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/extra_links.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/extra_links.py @@ -17,8 +17,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast - from fastapi import Depends, HTTPException, status from sqlalchemy.sql import select @@ -31,10 +29,6 @@ from airflow.exceptions import TaskNotFound from airflow.models import DagRun -if TYPE_CHECKING: - from airflow.models.mappedoperator import MappedOperator - from airflow.serialization.serialized_objects import SerializedBaseOperator - extra_links_router = AirflowRouter( tags=["Extra Links"], prefix="/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/links" ) @@ -62,8 +56,7 @@ def get_extra_links( dag = get_dag_for_run_or_latest_version(dag_bag, dag_run, dag_id, session) try: - # TODO (GH-52141): Make dag a db-backed object so it only returns db-backed tasks. - task = cast("MappedOperator | SerializedBaseOperator", dag.get_task(task_id)) + task = dag.get_task(task_id) except TaskNotFound: raise HTTPException(status.HTTP_404_NOT_FOUND, f"Task with ID = {task_id} not found") diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py index e04714e0a9137..ba7e29a123b81 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py @@ -28,9 +28,8 @@ from airflow.api_fastapi.common.db.common import SessionDep, paginated_select from airflow.api_fastapi.common.parameters import ( QueryHITLDetailBodySearch, - QueryHITLDetailDagIdFilter, QueryHITLDetailDagIdPatternSearch, - QueryHITLDetailDagRunIdFilter, + QueryHITLDetailMapIndexFilter, QueryHITLDetailRespondedUserIdFilter, QueryHITLDetailRespondedUserNameFilter, QueryHITLDetailResponseReceivedFilter, @@ -40,7 +39,9 @@ QueryLimit, QueryOffset, QueryTIStateFilter, + RangeFilter, SortParam, + datetime_range_filter_factory, ) from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.datamodels.hitl import ( @@ -50,13 +51,23 @@ UpdateHITLDetailPayload, ) from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc -from airflow.api_fastapi.core_api.security import GetUserDep, ReadableTIFilterDep, requires_access_dag +from airflow.api_fastapi.core_api.security import ( + GetUserDep, + ReadableTIFilterDep, + get_auth_manager, + requires_access_dag, +) from airflow.api_fastapi.logging.decorators import action_logging +from airflow.models.dag_version import DagVersion from airflow.models.dagrun import DagRun -from airflow.models.hitl import HITLDetail as HITLDetailModel +from airflow.models.hitl import HITLDetail as HITLDetailModel, HITLUser from airflow.models.taskinstance import TaskInstance as TI -hitl_router = AirflowRouter(tags=["HumanInTheLoop"], prefix="/hitlDetails") +task_instances_hitl_router = AirflowRouter( + tags=["Task Instance"], + prefix="/dags/{dag_id}/dagRuns/{dag_run_id}", +) +task_instance_hitl_path = "/taskInstances/{task_id}/{map_index}/hitlDetails" log = structlog.get_logger(__name__) @@ -100,8 +111,8 @@ def _get_task_instance_with_hitl_detail( return task_instance -@hitl_router.patch( - "/{dag_id}/{dag_run_id}/{task_id}", +@task_instances_hitl_router.patch( + task_instance_hitl_path, responses=create_openapi_http_exception_doc( [ status.HTTP_403_FORBIDDEN, @@ -144,20 +155,22 @@ def update_hitl_detail( user_id = user.get_id() user_name = user.get_name() - if hitl_detail_model.respondents: - if isinstance(user_id, int): - # FabAuthManager (ab_user) store user id as integer, but common interface is string type - user_id = str(user_id) - if user_id not in hitl_detail_model.respondents: + if isinstance(user_id, int): + # FabAuthManager (ab_user) store user id as integer, but common interface is string type + user_id = str(user_id) + hitl_user = HITLUser(id=user_id, name=user_name) + if hitl_detail_model.assigned_users: + # Convert assigned_users list to set of user IDs for authorization check + assigned_user_ids = {assigned_user["id"] for assigned_user in hitl_detail_model.assigned_users} + if not get_auth_manager().is_authorized_hitl_task(assigned_users=assigned_user_ids, user=user): log.error("User=%s (id=%s) is not a respondent for the task", user_name, user_id) raise HTTPException( status.HTTP_403_FORBIDDEN, f"User={user_name} (id={user_id}) is not a respondent for the task.", ) - hitl_detail_model.responded_user_id = user_id - hitl_detail_model.responded_user_name = user_name - hitl_detail_model.response_at = timezone.utcnow() + hitl_detail_model.responded_by = hitl_user + hitl_detail_model.responded_at = timezone.utcnow() hitl_detail_model.chosen_options = update_hitl_detail_payload.chosen_options hitl_detail_model.params_input = update_hitl_detail_payload.params_input session.add(hitl_detail_model) @@ -165,8 +178,8 @@ def update_hitl_detail( return HITLDetailResponse.model_validate(hitl_detail_model) -@hitl_router.get( - "/{dag_id}/{dag_run_id}/{task_id}", +@task_instances_hitl_router.get( + task_instance_hitl_path, status_code=status.HTTP_200_OK, responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), dependencies=[Depends(requires_access_dag(method="GET", access_entity=DagAccessEntity.HITL_DETAIL))], @@ -189,12 +202,14 @@ def get_hitl_detail( return task_instance.hitl_detail -@hitl_router.get( - "/", +@task_instances_hitl_router.get( + "/hitlDetails", status_code=status.HTTP_200_OK, dependencies=[Depends(requires_access_dag(method="GET", access_entity=DagAccessEntity.HITL_DETAIL))], ) def get_hitl_details( + dag_id: str, + dag_run_id: str, limit: QueryLimit, offset: QueryOffset, order_by: Annotated[ @@ -204,7 +219,10 @@ def get_hitl_details( allowed_attrs=[ "ti_id", "subject", - "response_at", + "responded_at", + "created_at", + "responded_by_user_id", + "responded_by_user_name", ], model=HITLDetailModel, to_replace={ @@ -213,25 +231,27 @@ def get_hitl_details( "run_after": DagRun.run_after, "rendered_map_index": TI.rendered_map_index, "task_instance_operator": TI.operator, + "task_instance_state": TI.state, }, ).dynamic_depends(), ), ], session: SessionDep, - # ti related filter + # permission filter readable_ti_filter: ReadableTIFilterDep, - dag_id: QueryHITLDetailDagIdFilter, + # ti related filter dag_id_pattern: QueryHITLDetailDagIdPatternSearch, - dag_run_id: QueryHITLDetailDagRunIdFilter, task_id: QueryHITLDetailTaskIdFilter, task_id_pattern: QueryHITLDetailTaskIdPatternSearch, + map_index: QueryHITLDetailMapIndexFilter, ti_state: QueryTIStateFilter, # hitl detail related filter response_received: QueryHITLDetailResponseReceivedFilter, - responded_user_id: QueryHITLDetailRespondedUserIdFilter, - responded_user_name: QueryHITLDetailRespondedUserNameFilter, + responded_by_user_id: QueryHITLDetailRespondedUserIdFilter, + responded_by_user_name: QueryHITLDetailRespondedUserNameFilter, subject_patten: QueryHITLDetailSubjectSearch, body_patten: QueryHITLDetailBodySearch, + created_at: Annotated[RangeFilter, Depends(datetime_range_filter_factory("created_at", HITLDetailModel))], ) -> HITLDetailCollection: """Get Human-in-the-loop details.""" query = ( @@ -240,27 +260,34 @@ def get_hitl_details( .join(TI.dag_run) .options( joinedload(HITLDetailModel.task_instance).options( - joinedload(TI.dag_run), - ) + joinedload(TI.dag_run).joinedload(DagRun.dag_model), + joinedload(TI.task_instance_note), + joinedload(TI.dag_version).joinedload(DagVersion.bundle), + ), ) ) + if dag_id != "~": + query = query.where(TI.dag_id == dag_id) + if dag_run_id != "~": + query = query.where(TI.run_id == dag_run_id) hitl_detail_select, total_entries = paginated_select( statement=query, filters=[ - # ti related filter + # permission filter readable_ti_filter, - dag_id, + # ti related filter dag_id_pattern, - dag_run_id, task_id, task_id_pattern, + map_index, ti_state, # hitl detail related filter response_received, - responded_user_id, - responded_user_name, + responded_by_user_id, + responded_by_user_name, subject_patten, body_patten, + created_at, ], offset=offset, limit=limit, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py index a989a7eef168c..8485bd66a467c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py @@ -36,6 +36,7 @@ from airflow.api_fastapi.common.parameters import ( QueryLimit, QueryOffset, + QueryParseImportErrorFilenamePatternSearch, SortParam, ) from airflow.api_fastapi.common.router import AirflowRouter @@ -78,11 +79,6 @@ def get_import_error( session.expunge(error) auth_manager = get_auth_manager() - can_read_all_dags = auth_manager.is_authorized_dag(method="GET", user=user) - if can_read_all_dags: - # Early return if the user has access to all DAGs - return error - readable_dag_ids = auth_manager.get_authorized_dag_ids(user=user) # We need file_dag_ids as a set for intersection, issubset operations file_dag_ids = set( @@ -126,29 +122,12 @@ def get_import_errors( ).dynamic_depends() ), ], + filename_pattern: QueryParseImportErrorFilenamePatternSearch, session: SessionDep, user: GetUserDep, ) -> ImportErrorCollectionResponse: """Get all import errors.""" - import_errors_select, total_entries = paginated_select( - statement=select(ParseImportError), - order_by=order_by, - offset=offset, - limit=limit, - session=session, - ) - auth_manager = get_auth_manager() - can_read_all_dags = auth_manager.is_authorized_dag(method="GET", user=user) - if can_read_all_dags: - # Early return if the user has access to all DAGs - import_errors = session.scalars(import_errors_select).all() - return ImportErrorCollectionResponse( - import_errors=import_errors, - total_entries=total_entries, - ) - - # if the user doesn't have access to all DAGs, only display errors from visible DAGs readable_dag_ids = auth_manager.get_authorized_dag_ids(method="GET", user=user) # Build a cte that fetches dag_ids for each file location visible_files_cte = ( @@ -174,12 +153,13 @@ def get_import_errors( # Paginate the import errors query import_errors_select, total_entries = paginated_select( statement=import_errors_stmt, + filters=[filename_pattern], order_by=order_by, offset=offset, limit=limit, session=session, ) - import_errors_result: Iterable[tuple[ParseImportError, Iterable[str]]] = groupby( + import_errors_result: Iterable[tuple[ParseImportError, Iterable]] = groupby( session.execute(import_errors_select), itemgetter(0) ) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/log.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/log.py index 5f1d04a244aa0..20379876ebe28 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/log.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/log.py @@ -170,7 +170,7 @@ def get_log( @task_instances_log_router.get( "/{task_id}/externalLogUrl/{try_number}", responses=create_openapi_http_exception_doc([status.HTTP_400_BAD_REQUEST, status.HTTP_404_NOT_FOUND]), - dependencies=[Depends(requires_access_dag("GET", DagAccessEntity.TASK_INSTANCE))], + dependencies=[Depends(requires_access_dag("GET", DagAccessEntity.TASK_LOGS))], ) def get_external_log_url( dag_id: str, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py index fe700178f6bf4..3b5368f50d232 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py @@ -17,9 +17,9 @@ from __future__ import annotations -from typing import cast - +import structlog from fastapi import Depends +from pydantic import ValidationError from airflow import plugins_manager from airflow.api_fastapi.auth.managers.models.resource_details import AccessView @@ -32,6 +32,8 @@ ) from airflow.api_fastapi.core_api.security import requires_access_view +logger = structlog.get_logger(__name__) + plugins_router = AirflowRouter(tags=["Plugin"], prefix="/plugins") @@ -44,9 +46,27 @@ def get_plugins( offset: QueryOffset, ) -> PluginCollectionResponse: plugins_info = sorted(plugins_manager.get_plugin_info(), key=lambda x: x["name"]) + valid_plugins: list[PluginResponse] = [] + for plugin_dict in plugins_info: + try: + # Validate each plugin individually + plugin = PluginResponse.model_validate(plugin_dict) + valid_plugins.append(plugin) + except ValidationError as e: + logger.warning( + "Skipping invalid plugin due to error", + plugin_name=plugin_dict.get("name", ""), + error=str(e), + ) + continue + + offset_value = offset.value or 0 + limit_value = limit.value if limit.value is not None else len(valid_plugins) + + paginated_plugins = valid_plugins[offset_value : offset_value + limit_value] return PluginCollectionResponse( - plugins=cast("list[PluginResponse]", plugins_info[offset.value :][: limit.value]), - total_entries=len(plugins_info), + plugins=paginated_plugins, + total_entries=len(valid_plugins), ) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py index 87d76fedc921f..79e59c009fae3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_instances.py @@ -33,6 +33,7 @@ get_latest_version_of_dag, ) from airflow.api_fastapi.common.db.common import SessionDep, paginated_select +from airflow.api_fastapi.common.db.task_instances import eager_load_TI_and_TIH_for_validation from airflow.api_fastapi.common.parameters import ( FilterOptionEnum, FilterParam, @@ -42,11 +43,13 @@ QueryOffset, QueryTIDagVersionFilter, QueryTIExecutorFilter, + QueryTIMapIndexFilter, QueryTIOperatorFilter, QueryTIPoolFilter, QueryTIQueueFilter, QueryTIStateFilter, QueryTITaskDisplayNamePatternSearch, + QueryTITaskGroupFilter, QueryTITryNumberFilter, Range, RangeFilter, @@ -149,6 +152,7 @@ def get_mapped_task_instances( version_number: QueryTIDagVersionFilter, try_number: QueryTITryNumberFilter, operator: QueryTIOperatorFilter, + map_index: QueryTIMapIndexFilter, limit: QueryLimit, offset: QueryOffset, order_by: Annotated[ @@ -187,8 +191,7 @@ def get_mapped_task_instances( select(TI) .where(TI.dag_id == dag_id, TI.run_id == dag_run_id, TI.task_id == task_id, TI.map_index >= 0) .join(TI.dag_run) - .options(joinedload(TI.dag_version)) - .options(joinedload(TI.dag_run).options(joinedload(DagRun.dag_model))) + .options(*eager_load_TI_and_TIH_for_validation()) ) # 0 can mean a mapped TI that expanded to an empty list, so it is not an automatic 404 unfiltered_total_count = get_query_count(query, session=session) @@ -220,6 +223,7 @@ def get_mapped_task_instances( version_number, try_number, operator, + map_index, ], order_by=order_by, offset=offset, @@ -314,8 +318,7 @@ def _query(orm_object: Base) -> Select: orm_object.task_id == task_id, orm_object.map_index == map_index, ) - .options(joinedload(orm_object.dag_version)) - .options(joinedload(orm_object.dag_run).options(joinedload(DagRun.dag_model))) + .options(*eager_load_TI_and_TIH_for_validation(orm_object)) ) return query @@ -405,6 +408,7 @@ def get_task_instances( update_at_range: Annotated[RangeFilter, Depends(datetime_range_filter_factory("updated_at", TI))], duration_range: Annotated[RangeFilter, Depends(float_range_filter_factory("duration", TI))], task_display_name_pattern: QueryTITaskDisplayNamePatternSearch, + task_group_id: QueryTITaskGroupFilter, state: QueryTIStateFilter, pool: QueryTIPoolFilter, queue: QueryTIQueueFilter, @@ -412,6 +416,7 @@ def get_task_instances( version_number: QueryTIDagVersionFilter, try_number: QueryTITryNumberFilter, operator: QueryTIOperatorFilter, + map_index: QueryTIMapIndexFilter, limit: QueryLimit, offset: QueryOffset, order_by: Annotated[ @@ -454,11 +459,7 @@ def get_task_instances( """ dag_run = None query = ( - select(TI) - .join(TI.dag_run) - .outerjoin(TI.dag_version) - .options(joinedload(TI.dag_version)) - .options(joinedload(TI.dag_run).options(joinedload(DagRun.dag_model))) + select(TI).join(TI.dag_run).outerjoin(TI.dag_version).options(*eager_load_TI_and_TIH_for_validation()) ) if dag_run_id != "~": dag_run = session.scalar(select(DagRun).filter_by(run_id=dag_run_id)) @@ -469,8 +470,10 @@ def get_task_instances( ) query = query.where(TI.run_id == dag_run_id) if dag_id != "~": - get_dag_for_run_or_latest_version(dag_bag, dag_run, dag_id, session) + dag = get_dag_for_run_or_latest_version(dag_bag, dag_run, dag_id, session) query = query.where(TI.dag_id == dag_id) + if dag: + task_group_id.dag = dag task_instance_select, total_entries = paginated_select( statement=query, @@ -487,10 +490,12 @@ def get_task_instances( executor, task_id, task_display_name_pattern, + task_group_id, version_number, readable_ti_filter, try_number, operator, + map_index, ], order_by=order_by, offset=offset, @@ -582,7 +587,9 @@ def get_task_instances_batch( TI, ).set_value([body.order_by] if body.order_by else None) - query = select(TI) + query = ( + select(TI).join(TI.dag_run).outerjoin(TI.dag_version).options(*eager_load_TI_and_TIH_for_validation()) + ) task_instance_select, total_entries = paginated_select( statement=query, filters=[ @@ -729,16 +736,25 @@ def post_clear_task_instances( task_ids = body.task_ids if task_ids is not None: - task_id = [task[0] if isinstance(task, tuple) else task for task in task_ids] - dag = dag.partial_subset( - task_ids=task_id, - include_downstream=downstream, - include_upstream=upstream, - ) + tasks = set(task_ids) + mapped_tasks_tuples = set(t for t in tasks if isinstance(t, tuple)) + # Unmapped tasks are expressed in their task_ids (without map_indexes) + unmapped_task_ids = set(t for t in tasks if not isinstance(t, tuple)) + + if upstream or downstream: + mapped_task_ids = set(tid for tid, _ in mapped_tasks_tuples) + relatives = dag.partial_subset( + task_ids=unmapped_task_ids | mapped_task_ids, + include_downstream=downstream, + include_upstream=upstream, + exclude_original=True, + ) + unmapped_task_ids = unmapped_task_ids | set(relatives.task_dict.keys()) - if len(dag.task_dict) > 1: - # If we had upstream/downstream etc then also include those! - task_ids.extend(tid for tid in dag.task_dict if tid != task_id) + mapped_tasks_list = [ + (tid, map_id) for tid, map_id in mapped_tasks_tuples if tid not in unmapped_task_ids + ] + task_ids = mapped_tasks_list + list(unmapped_task_ids) # Prepare common parameters common_params = { @@ -895,11 +911,23 @@ def patch_task_instance( for key, _ in data.items(): if key == "new_state": + # Create BulkTaskInstanceBody object with map_index field + bulk_ti_body = BulkTaskInstanceBody( + task_id=task_id, + map_index=map_index, + new_state=body.new_state, + note=body.note, + include_upstream=body.include_upstream, + include_downstream=body.include_downstream, + include_future=body.include_future, + include_past=body.include_past, + ) + _patch_task_instance_state( task_id=task_id, dag_run_id=dag_run_id, dag=dag, - task_instance_body=body, + task_instance_body=bulk_ti_body, data=data, session=session, ) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py index 57b83f0dbb3a2..b1a4bc84414fd 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/xcom.py @@ -60,7 +60,7 @@ @xcom_router.get( - "/{xcom_key}", + "/{xcom_key:path}", responses=create_openapi_http_exception_doc( [ status.HTTP_400_BAD_REQUEST, @@ -87,7 +87,7 @@ def get_xcom_entry( dag_ids=dag_id, map_indexes=map_index, limit=1, - ) + ).options(joinedload(XComModel.task), joinedload(XComModel.dag_run).joinedload(DR.dag_model)) # We use `BaseXCom.get_many` to fetch XComs directly from the database, bypassing the XCom Backend. # This avoids deserialization via the backend (e.g., from a remote storage like S3) and instead @@ -162,7 +162,7 @@ def get_xcom_entries( query = ( query.join(DR, and_(XComModel.dag_id == DR.dag_id, XComModel.run_id == DR.run_id)) .join(DagModel, DR.dag_id == DagModel.dag_id) - .options(joinedload(XComModel.dag_run).joinedload(DR.dag_model)) + .options(joinedload(XComModel.task), joinedload(XComModel.dag_run).joinedload(DR.dag_model)) ) if task_id != "~": @@ -285,14 +285,14 @@ def create_xcom_entry( XComModel.map_index == request_body.map_index, ) .limit(1) - .options(joinedload(XComModel.dag_run).joinedload(DR.dag_model)) + .options(joinedload(XComModel.task), joinedload(XComModel.dag_run).joinedload(DR.dag_model)) ) return XComResponseNative.model_validate(xcom) @xcom_router.patch( - "/{xcom_key}", + "/{xcom_key:path}", status_code=status.HTTP_200_OK, responses=create_openapi_http_exception_doc( [ @@ -326,7 +326,7 @@ def update_xcom_entry( XComModel.map_index == patch_body.map_index, ) .limit(1) - .options(joinedload(XComModel.dag_run).joinedload(DR.dag_model)) + .options(joinedload(XComModel.task), joinedload(XComModel.dag_run).joinedload(DR.dag_model)) ) if not xcom_entry: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py index ce476d94ffc07..a112bec9639d1 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py @@ -18,7 +18,7 @@ from __future__ import annotations from fastapi import Depends, HTTPException, status -from sqlalchemy import and_, func, select +from sqlalchemy import and_, case, func, select from airflow.api_fastapi.common.dagbag import DagBagDep from airflow.api_fastapi.common.db.common import SessionDep @@ -53,6 +53,12 @@ def next_run_assets( AssetModel.uri, AssetModel.name, func.max(AssetEvent.timestamp).label("lastUpdate"), + func.max( + case( + (AssetDagRunQueue.asset_id.is_not(None), 1), + else_=0, + ) + ).label("queued"), ) .join(DagScheduleAssetReference, DagScheduleAssetReference.asset_id == AssetModel.id) .join( @@ -81,5 +87,9 @@ def next_run_assets( ) ] + for event in events: + if not event.pop("queued", None): + event["lastUpdate"] = None + data = {"asset_expression": dag_model.asset_expression, "events": events} return data diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py index e0f310f7043be..24bf0f66fc3a5 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py @@ -20,6 +20,7 @@ from fastapi import Depends, status from sqlalchemy import select +from sqlalchemy.orm import joinedload from airflow.api_fastapi.common.db.common import SessionDep, paginated_select from airflow.api_fastapi.common.parameters import ( @@ -64,7 +65,7 @@ def list_backfills_ui( ], ) -> BackfillCollectionResponse: select_stmt, total_entries = paginated_select( - statement=select(Backfill), + statement=select(Backfill).options(joinedload(Backfill.dag_model)), filters=[dag_id, active], order_by=order_by, offset=offset, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py index f4fe5d5c6658c..c0b7dd2e3b638 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py @@ -21,6 +21,7 @@ from fastapi import Depends, status from airflow.api_fastapi.common.router import AirflowRouter +from airflow.api_fastapi.common.types import UIAlert from airflow.api_fastapi.core_api.datamodels.ui.config import ConfigResponse from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc from airflow.api_fastapi.core_api.security import requires_authenticated @@ -54,7 +55,8 @@ def get_configs() -> ConfigResponse: additional_config: dict[str, Any] = { "instance_name": conf.get("api", "instance_name", fallback="Airflow"), "test_connection": conf.get("core", "test_connection", fallback="Disabled"), - "dashboard_alert": DASHBOARD_UIALERTS, + # Expose "dashboard_alert" using a list comprehension so UIAlert instances can be expressed dynamically. + "dashboard_alert": [alert for alert in DASHBOARD_UIALERTS if isinstance(alert, UIAlert)], "show_external_log_redirect": task_log_reader.supports_external_link, "external_log_name": getattr(task_log_reader.log_handler, "log_name", None), } diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py index e953017a56b65..4ab981ab22331 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py @@ -52,7 +52,6 @@ filter_param_factory, ) from airflow.api_fastapi.common.router import AirflowRouter -from airflow.api_fastapi.core_api.datamodels.dag_run import DAGRunResponse from airflow.api_fastapi.core_api.datamodels.dags import DAGResponse from airflow.api_fastapi.core_api.datamodels.ui.dag_runs import DAGRunLightResponse from airflow.api_fastapi.core_api.datamodels.ui.dags import ( @@ -61,12 +60,15 @@ ) from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc from airflow.api_fastapi.core_api.security import ( + GetUserDep, ReadableDagsFilterDep, requires_access_dag, ) from airflow.models import DagModel, DagRun +from airflow.models.dag_favorite import DagFavorite from airflow.models.hitl import HITLDetail from airflow.models.taskinstance import TaskInstance +from airflow.utils.state import TaskInstanceState dags_router = AirflowRouter(prefix="/dags", tags=["DAG"]) @@ -113,6 +115,7 @@ def get_dags( has_pending_actions: QueryPendingActionsFilter, readable_dags_filter: ReadableDagsFilterDep, session: SessionDep, + user: GetUserDep, dag_runs_limit: int = 10, ) -> DAGWithLatestDagRunsCollectionResponse: """Get DAGs with recent DagRun.""" @@ -122,6 +125,7 @@ def get_dags( last_dag_run_state, ], order_by=order_by, + dag_ids=readable_dags_filter.value, ) dags_select, total_entries = paginated_select( @@ -152,6 +156,13 @@ def get_dags( dags = [dag for dag in session.scalars(dags_select)] + # Fetch favorite status for each DAG for the current user + user_id = str(user.get_id()) + favorites_select = select(DagFavorite.dag_id).where( + DagFavorite.user_id == user_id, DagFavorite.dag_id.in_([dag.dag_id for dag in dags]) + ) + favorite_dag_ids = set(session.scalars(favorites_select)) + # Populate the last 'dag_runs_limit' DagRuns for each DAG recent_runs_subquery = ( select( @@ -172,7 +183,15 @@ def get_dags( recent_dag_runs_select = ( select( recent_runs_subquery.c.run_after, - DagRun, + DagRun.id, + DagRun.dag_id, + DagRun.run_id, + DagRun.end_date, + DagRun.logical_date, + DagRun.run_after, + DagRun.start_date, + DagRun.state, + DagRun.duration, ) .join( DagRun, @@ -194,14 +213,17 @@ def get_dags( # Fetch pending HITL actions for each Dag if we are not certain whether some of the Dag might contain HITL actions pending_actions_by_dag_id: dict[str, list[HITLDetail]] = {dag.dag_id: [] for dag in dags} - if has_pending_actions.value is not False: + if has_pending_actions.value: pending_actions_select = ( select( TaskInstance.dag_id, HITLDetail, ) .join(TaskInstance, HITLDetail.ti_id == TaskInstance.id) - .where(HITLDetail.response_at.is_(None)) + .where( + HITLDetail.responded_at.is_(None), + TaskInstance.state == TaskInstanceState.DEFERRED, + ) .where(TaskInstance.dag_id.in_([dag.dag_id for dag in dags])) .order_by(TaskInstance.dag_id) ) @@ -220,15 +242,15 @@ def get_dags( "asset_expression": dag.asset_expression, "latest_dag_runs": [], "pending_actions": pending_actions_by_dag_id[dag.dag_id], + "is_favorite": dag.dag_id in favorite_dag_ids, } ) for dag in dags } for row in recent_dag_runs: - _, dag_run = row - dag_id = dag_run.dag_id - dag_run_response = DAGRunResponse.model_validate(dag_run) + dag_run_response = DAGRunLightResponse.model_validate(row) + dag_id = dag_run_response.dag_id dag_runs_by_dag_id[dag_id].latest_dag_runs.append(dag_run_response) return DAGWithLatestDagRunsCollectionResponse( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py index dbbada8cf39c7..26c1b6582d76e 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py @@ -127,7 +127,18 @@ def dag_stats( .subquery() ) - latest_runs = ( + # Active Dags need another query from DagModel, as a Dag may not have any runs but still be active + active_count_query = ( + select(func.count()) + .select_from(DagModel) + .where(DagModel.is_stale == false()) + .where(DagModel.is_paused == false()) + .where(DagModel.dag_id.in_(permitted_dag_ids)) + ) + active_count = session.execute(active_count_query).scalar_one() + + # Other metrics are based on latest DagRun states + latest_runs_cte = ( select( DagModel.dag_id, DagModel.is_paused, @@ -139,21 +150,22 @@ def dag_stats( (DagRun.dag_id == latest_dates_subq.c.dag_id) & (DagRun.logical_date == latest_dates_subq.c.max_logical_date), ) + .where(DagModel.is_stale == false()) .where(DagRun.dag_id.in_(permitted_dag_ids)) .cte() ) + combined_runs_query = select( + func.coalesce(func.sum(case((latest_runs_cte.c.state == DagRunState.FAILED, 1))), 0).label("failed"), + func.coalesce(func.sum(case((latest_runs_cte.c.state == DagRunState.RUNNING, 1))), 0).label( + "running" + ), + func.coalesce(func.sum(case((latest_runs_cte.c.state == DagRunState.QUEUED, 1))), 0).label("queued"), + ).select_from(latest_runs_cte) - combined_query = select( - func.coalesce(func.sum(case((latest_runs.c.is_paused == false(), 1))), 0).label("active"), - func.coalesce(func.sum(case((latest_runs.c.state == DagRunState.FAILED, 1))), 0).label("failed"), - func.coalesce(func.sum(case((latest_runs.c.state == DagRunState.RUNNING, 1))), 0).label("running"), - func.coalesce(func.sum(case((latest_runs.c.state == DagRunState.QUEUED, 1))), 0).label("queued"), - ).select_from(latest_runs) - - counts = session.execute(combined_query).first() + counts = session.execute(combined_runs_query).first() return DashboardDagStatsResponse( - active_dag_count=counts.active, + active_dag_count=active_count, failed_dag_count=counts.failed, running_dag_count=counts.running, queued_dag_count=counts.queued, diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py index 7ff49ac27c16d..0379bd957ca1a 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py @@ -18,11 +18,12 @@ from __future__ import annotations import collections -from typing import TYPE_CHECKING, Annotated +from typing import TYPE_CHECKING, Annotated, Any import structlog from fastapi import Depends, HTTPException, status from sqlalchemy import select +from sqlalchemy.orm import joinedload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity from airflow.api_fastapi.common.db.common import SessionDep, paginated_select @@ -47,6 +48,7 @@ from airflow.api_fastapi.core_api.security import requires_access_dag from airflow.api_fastapi.core_api.services.ui.grid import ( _find_aggregates, + _get_aggs_for_node, _merge_node_dicts, ) from airflow.api_fastapi.core_api.services.ui.task_group import ( @@ -81,16 +83,23 @@ def _get_latest_serdag(dag_id, session): def _get_serdag(dag_id, dag_version_id, session) -> SerializedDagModel | None: # this is a simplification - we account for structure based on the first task - version = session.scalar(select(DagVersion).where(DagVersion.id == dag_version_id)) + version = session.scalar( + select(DagVersion) + .where(DagVersion.id == dag_version_id) + .options(joinedload(DagVersion.serialized_dag)) + ) if not version: version = session.scalar( select(DagVersion) .where( DagVersion.dag_id == dag_id, ) + .options(joinedload(DagVersion.serialized_dag)) .order_by(DagVersion.id) # ascending cus this is mostly for pre-3.0 upgrade .limit(1) ) + if not version: + return None if not (serdag := version.serialized_dag): log.error( "No serialized dag found", @@ -148,21 +157,25 @@ def get_dag_structure( task_group_sort = get_task_group_children_getter() if not run_ids: nodes = [task_group_to_dict_grid(x) for x in task_group_sort(latest_dag.task_group)] - return nodes + return [GridNodeResponse(**n) for n in nodes] serdags = session.scalars( select(SerializedDagModel).where( + # Even though dag_id is filtered in base_query, + # adding this line here can improve the performance of this endpoint + SerializedDagModel.dag_id == dag_id, + SerializedDagModel.id != latest_serdag.id, SerializedDagModel.dag_version_id.in_( select(TaskInstance.dag_version_id) .join(TaskInstance.dag_run) .where( DagRun.id.in_(run_ids), - SerializedDagModel.id != latest_serdag.id, ) - ) + .distinct() + ), ) ) - merged_nodes: list[GridNodeResponse] = [] + merged_nodes: list[dict[str, Any]] = [] dags = [latest_dag] for serdag in serdags: if serdag: @@ -171,7 +184,37 @@ def get_dag_structure( nodes = [task_group_to_dict_grid(x) for x in task_group_sort(dag.task_group)] _merge_node_dicts(merged_nodes, nodes) - return merged_nodes + # Ensure historical tasks (e.g. removed) that exist in TIs for the selected runs are represented + def _collect_ids(nodes: list[dict[str, Any]]) -> set[str]: + ids: set[str] = set() + for n in nodes: + nid = n.get("id") + if nid: + ids.add(nid) + children = n.get("children") + if children: + ids |= _collect_ids(children) # recurse + return ids + + existing_ids = _collect_ids(merged_nodes) + historical_task_ids = session.scalars( + select(TaskInstance.task_id) + .join(TaskInstance.dag_run) + .where(TaskInstance.dag_id == dag_id, DagRun.id.in_(run_ids)) + .distinct() + ) + for task_id in historical_task_ids: + if task_id not in existing_ids: + merged_nodes.append( + { + "id": task_id, + "label": task_id, + "is_mapped": None, + "children": None, + } + ) + + return [GridNodeResponse(**n) for n in merged_nodes] @grid_router.get( @@ -337,19 +380,47 @@ def get_grid_ti_summaries( assert serdag def get_node_sumaries(): + yielded_task_ids: set[str] = set() + + # Yield all nodes discoverable from the serialized DAG structure for node in _find_aggregates( node=serdag.dag.task_group, parent_node=None, ti_details=ti_details, ): - if node["type"] == "task": - node["child_states"] = None - node["min_start_date"] = None - node["max_end_date"] = None + if node["type"] in {"task", "mapped_task"}: + yielded_task_ids.add(node["task_id"]) + if node["type"] == "task": + node["child_states"] = None + node["min_start_date"] = None + node["max_end_date"] = None yield node + # For good history: add synthetic leaf nodes for task_ids that have TIs in this run + # but are not present in the current DAG structure (e.g. removed tasks) + missing_task_ids = set(ti_details.keys()) - yielded_task_ids + for task_id in sorted(missing_task_ids): + detail = ti_details[task_id] + # Create a leaf task node with aggregated state from its TIs + agg = _get_aggs_for_node(detail) + yield { + "task_id": task_id, + "type": "task", + "parent_id": None, + **agg, + # Align with leaf behavior + "child_states": None, + "min_start_date": None, + "max_end_date": None, + } + + task_instances = list(get_node_sumaries()) + # If a group id and a task id collide, prefer the group record + group_ids = {n.get("task_id") for n in task_instances if n.get("type") == "group"} + filtered = [n for n in task_instances if not (n.get("type") == "task" and n.get("task_id") in group_ids)] + return { # type: ignore[return-value] "run_id": run_id, "dag_id": dag_id, - "task_instances": list(get_node_sumaries()), + "task_instances": filtered, } diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py index 1fba54ce54385..709cd7b8a65eb 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py @@ -18,6 +18,7 @@ from fastapi import Depends, HTTPException, status from sqlalchemy import select +from sqlalchemy.orm import joinedload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity from airflow.api_fastapi.common.db.common import SessionDep @@ -31,6 +32,7 @@ get_upstream_assets, ) from airflow.api_fastapi.core_api.services.ui.task_group import task_group_to_dict +from airflow.models.dag import DagModel from airflow.models.dag_version import DagVersion from airflow.models.serialized_dag import SerializedDagModel from airflow.utils.dag_edges import dag_edges @@ -70,6 +72,7 @@ def structure_data( select(SerializedDagModel) .join(DagVersion) .where(SerializedDagModel.dag_id == dag_id, DagVersion.version_number == version_number) + .options(joinedload(SerializedDagModel.dag_model).joinedload(DagModel.task_outlet_asset_references)), ) if serialized_dag is None: raise HTTPException( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/security.py b/airflow-core/src/airflow/api_fastapi/core_api/security.py index 84bd0ccdd2989..e17f92776da12 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/security.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py @@ -19,14 +19,15 @@ from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Annotated, cast -from urllib.parse import ParseResult, urljoin, urlparse +from urllib.parse import ParseResult, unquote, urljoin, urlparse from fastapi import Depends, HTTPException, Request, status -from fastapi.security import HTTPBearer, OAuth2PasswordBearer +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, OAuth2PasswordBearer from jwt import ExpiredSignatureError, InvalidTokenError from pydantic import NonNegativeInt from airflow.api_fastapi.app import get_auth_manager +from airflow.api_fastapi.auth.managers.base_auth_manager import COOKIE_NAME_JWT_TOKEN from airflow.api_fastapi.auth.managers.models.base_user import BaseUser from airflow.api_fastapi.auth.managers.models.batch_apis import ( IsAuthorizedConnectionRequest, @@ -46,7 +47,14 @@ VariableDetails, ) from airflow.api_fastapi.core_api.base import OrmClause -from airflow.api_fastapi.core_api.datamodels.common import BulkAction, BulkBody +from airflow.api_fastapi.core_api.datamodels.common import ( + BulkAction, + BulkActionOnExistence, + BulkBody, + BulkCreateAction, + BulkDeleteAction, + BulkUpdateAction, +) from airflow.api_fastapi.core_api.datamodels.connections import ConnectionBody from airflow.api_fastapi.core_api.datamodels.pools import PoolBody from airflow.api_fastapi.core_api.datamodels.variables import VariableBody @@ -58,7 +66,6 @@ from airflow.models.xcom import XComModel if TYPE_CHECKING: - from fastapi.security import HTTPAuthorizationCredentials from sqlalchemy.sql import Select from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager, ResourceMethod @@ -96,14 +103,22 @@ async def resolve_user_from_token(token_str: str | None) -> BaseUser: async def get_user( + request: Request, oauth_token: str | None = Depends(oauth2_scheme), bearer_credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), ) -> BaseUser: - token_str = None + # A user might have been already built by a middleware, if so, it is stored in `request.state.user` + user: BaseUser | None = getattr(request.state, "user", None) + if user: + return user + + token_str: str | None if bearer_credentials and bearer_credentials.scheme.lower() == "bearer": token_str = bearer_credentials.credentials elif oauth_token: token_str = oauth_token + else: + token_str = request.cookies.get(COOKIE_NAME_JWT_TOKEN) return await resolve_user_from_token(token_str) @@ -257,19 +272,22 @@ def inner( ) -> None: requests: list[IsAuthorizedPoolRequest] = [] for action in request.actions: - requests.extend( - [ - { - "method": MAP_BULK_ACTION_TO_AUTH_METHOD[action.action], + methods = _get_resource_methods_from_bulk_request(action) + for pool in action.entities: + pool_name = ( + cast("str", pool) if action.action == BulkAction.DELETE else cast("PoolBody", pool).pool + ) + # For each pool, build a `IsAuthorizedPoolRequest` + # The list of `IsAuthorizedPoolRequest` will then be sent using `batch_is_authorized_pool` + # Each `IsAuthorizedPoolRequest` is similar to calling `is_authorized_pool` + for method in methods: + req: IsAuthorizedPoolRequest = { + "method": method, "details": PoolDetails( - name=cast("str", pool) - if action.action == BulkAction.DELETE - else cast("PoolBody", pool).pool + name=pool_name, ), } - for pool in action.entities - ] - ) + requests.append(req) _requires_access( is_authorized_callback=lambda: get_auth_manager().batch_is_authorized_pool( @@ -307,19 +325,24 @@ def inner( ) -> None: requests: list[IsAuthorizedConnectionRequest] = [] for action in request.actions: - requests.extend( - [ - { - "method": MAP_BULK_ACTION_TO_AUTH_METHOD[action.action], + methods = _get_resource_methods_from_bulk_request(action) + for connection in action.entities: + connection_id = ( + cast("str", connection) + if action.action == BulkAction.DELETE + else cast("ConnectionBody", connection).connection_id + ) + # For each pool, build a `IsAuthorizedConnectionRequest` + # The list of `IsAuthorizedConnectionRequest` will then be sent using `batch_is_authorized_connection` + # Each `IsAuthorizedConnectionRequest` is similar to calling `is_authorized_connection` + for method in methods: + req: IsAuthorizedConnectionRequest = { + "method": method, "details": ConnectionDetails( - conn_id=cast("str", connection) - if action.action == BulkAction.DELETE - else cast("ConnectionBody", connection).connection_id + conn_id=connection_id, ), } - for connection in action.entities - ] - ) + requests.append(req) _requires_access( is_authorized_callback=lambda: get_auth_manager().batch_is_authorized_connection( @@ -373,19 +396,24 @@ def inner( ) -> None: requests: list[IsAuthorizedVariableRequest] = [] for action in request.actions: - requests.extend( - [ - { - "method": MAP_BULK_ACTION_TO_AUTH_METHOD[action.action], + methods = _get_resource_methods_from_bulk_request(action) + for variable in action.entities: + variable_key = ( + cast("str", variable) + if action.action == BulkAction.DELETE + else cast("VariableBody", variable).key + ) + # For each variable, build a `IsAuthorizedVariableRequest` + # The list of `IsAuthorizedVariableRequest` will then be sent using `batch_is_authorized_variable` + # Each `IsAuthorizedVariableRequest` is similar to calling `is_authorized_variable` + for method in methods: + req: IsAuthorizedVariableRequest = { + "method": method, "details": VariableDetails( - key=cast("str", entity) - if action.action == BulkAction.DELETE - else cast("VariableBody", entity).key + key=variable_key, ), } - for entity in action.entities - ] - ) + requests.append(req) _requires_access( is_authorized_callback=lambda: get_auth_manager().batch_is_authorized_variable( @@ -484,7 +512,7 @@ def is_safe_url(target_url: str, request: Request | None = None) -> bool: return True for base_url, parsed_base in parsed_bases: - parsed_target = urlparse(urljoin(base_url, target_url)) # Resolves relative URLs + parsed_target = urlparse(urljoin(base_url, unquote(target_url))) # Resolves relative URLs target_path = Path(parsed_target.path).resolve() @@ -494,3 +522,15 @@ def is_safe_url(target_url: str, request: Request | None = None) -> bool: if parsed_target.scheme in {"http", "https"} and parsed_target.netloc == parsed_base.netloc: return True return False + + +def _get_resource_methods_from_bulk_request( + action: BulkCreateAction | BulkUpdateAction | BulkDeleteAction, +) -> list[ResourceMethod]: + resource_methods: list[ResourceMethod] = [MAP_BULK_ACTION_TO_AUTH_METHOD[action.action]] + # If ``action_on_existence`` == ``overwrite``, we need to check the user has ``PUT`` access as well. + # With ``action_on_existence`` == ``overwrite``, a create request is actually an update request if the + # resource already exists, hence adding this check. + if action.action == BulkAction.CREATE and action.action_on_existence == BulkActionOnExistence.OVERWRITE: + resource_methods.append("PUT") + return resource_methods diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py b/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py index 2f9fa3a1b86d4..8d659792b9771 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/public/providers.py @@ -31,4 +31,5 @@ def _provider_mapper(provider: ProviderInfo) -> ProviderResponse: package_name=provider.data["package-name"], description=_remove_rst_syntax(provider.data["description"]), version=provider.version, + documentation_url=provider.data["documentation-url"], ) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py index 68881ac0f108e..ef045813e874f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py @@ -17,8 +17,6 @@ from __future__ import annotations -import contextlib -import importlib import logging from collections.abc import MutableMapping from functools import cache @@ -132,69 +130,47 @@ def mock_any_of(allowed_values: list) -> HookMetaService.MockEnum: """Mock for wtforms.validators.any_of.""" return HookMetaService.MockEnum(allowed_values) - with contextlib.ExitStack() as stack: + # Before importing ProvidersManager, we need to mock all FAB and WTForms + # dependencies to avoid ImportErrors when FAB is not installed. + import sys + from importlib.util import find_spec + from unittest.mock import MagicMock + + for mod_name in [ + "wtforms", + "wtforms.csrf", + "wtforms.fields", + "wtforms.fields.simple", + "wtforms.validators", + "flask_babel", + "flask_appbuilder", + "flask_appbuilder.fieldwidgets", + ]: try: - importlib.import_module("wtforms") - stack.enter_context(mock.patch("wtforms.StringField", HookMetaService.MockStringField)) - stack.enter_context(mock.patch("wtforms.fields.StringField", HookMetaService.MockStringField)) - stack.enter_context( - mock.patch("wtforms.fields.simple.StringField", HookMetaService.MockStringField) - ) - - stack.enter_context(mock.patch("wtforms.IntegerField", HookMetaService.MockIntegerField)) - stack.enter_context( - mock.patch("wtforms.fields.IntegerField", HookMetaService.MockIntegerField) - ) - stack.enter_context(mock.patch("wtforms.PasswordField", HookMetaService.MockPasswordField)) - stack.enter_context(mock.patch("wtforms.BooleanField", HookMetaService.MockBooleanField)) - stack.enter_context( - mock.patch("wtforms.fields.BooleanField", HookMetaService.MockBooleanField) - ) - stack.enter_context( - mock.patch("wtforms.fields.simple.BooleanField", HookMetaService.MockBooleanField) - ) - stack.enter_context(mock.patch("wtforms.validators.Optional", HookMetaService.MockOptional)) - stack.enter_context(mock.patch("wtforms.validators.any_of", mock_any_of)) - except ImportError: - pass - - try: - importlib.import_module("flask_babel") - stack.enter_context(mock.patch("flask_babel.lazy_gettext", mock_lazy_gettext)) - except ImportError: - pass - - try: - importlib.import_module("flask_appbuilder") - stack.enter_context( - mock.patch( - "flask_appbuilder.fieldwidgets.BS3TextFieldWidget", HookMetaService.MockAnyWidget - ) - ) - stack.enter_context( - mock.patch( - "flask_appbuilder.fieldwidgets.BS3TextAreaFieldWidget", HookMetaService.MockAnyWidget - ) - ) - stack.enter_context( - mock.patch( - "flask_appbuilder.fieldwidgets.BS3PasswordFieldWidget", HookMetaService.MockAnyWidget - ) - ) - except ImportError: - pass - + if not find_spec(mod_name): + raise ModuleNotFoundError + except ModuleNotFoundError: + sys.modules[mod_name] = MagicMock() + with ( + mock.patch("wtforms.StringField", HookMetaService.MockStringField), + mock.patch("wtforms.fields.StringField", HookMetaService.MockStringField), + mock.patch("wtforms.fields.simple.StringField", HookMetaService.MockStringField), + mock.patch("wtforms.IntegerField", HookMetaService.MockIntegerField), + mock.patch("wtforms.fields.IntegerField", HookMetaService.MockIntegerField), + mock.patch("wtforms.PasswordField", HookMetaService.MockPasswordField), + mock.patch("wtforms.BooleanField", HookMetaService.MockBooleanField), + mock.patch("wtforms.fields.BooleanField", HookMetaService.MockBooleanField), + mock.patch("wtforms.fields.simple.BooleanField", HookMetaService.MockBooleanField), + mock.patch("flask_babel.lazy_gettext", mock_lazy_gettext), + mock.patch("flask_appbuilder.fieldwidgets.BS3TextFieldWidget", HookMetaService.MockAnyWidget), + mock.patch("flask_appbuilder.fieldwidgets.BS3TextAreaFieldWidget", HookMetaService.MockAnyWidget), + mock.patch("flask_appbuilder.fieldwidgets.BS3PasswordFieldWidget", HookMetaService.MockAnyWidget), + mock.patch("wtforms.validators.Optional", HookMetaService.MockOptional), + mock.patch("wtforms.validators.any_of", mock_any_of), + ): pm = ProvidersManager() - pm._cleanup() # Remove any cached hooks with non mocked FAB - pm._init_airflow_core_hooks() # Initialize core hooks return pm.hooks, pm.connection_form_widgets, pm.field_behaviours # Will init providers hooks - return ( - {}, - {}, - {}, - ) # Make mypy happy, should never been reached https://github.com/python/mypy/issues/7726 - @staticmethod def _make_standard_fields(field_behaviour: dict | None) -> StandardHookFields | None: if not field_behaviour: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py index 1f64ffcefa866..5b20511d31f47 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py @@ -85,38 +85,39 @@ def _find_aggregates( """Recursively fill the Task Group Map.""" node_id = node.node_id parent_id = parent_node.node_id if parent_node else None - details = ti_details[node_id] + # Do not mutate ti_details by accidental key creation + details = ti_details.get(node_id, []) if node is None: return if isinstance(node, MappedOperator): + # For unmapped tasks, reflect a single None state so UI shows one square + mapped_details = details or [{"state": None, "start_date": None, "end_date": None}] yield { "task_id": node_id, "type": "mapped_task", "parent_id": parent_id, - **_get_aggs_for_node(details), + **_get_aggs_for_node(mapped_details), + "details": mapped_details, } return if isinstance(node, SerializedTaskGroup): - children = [] + children_details = [] for child in get_task_group_children_getter()(node): for child_node in _find_aggregates(node=child, parent_node=node, ti_details=ti_details): if child_node["parent_id"] == node_id: - children.append( - { - "state": child_node["state"], - "start_date": child_node["min_start_date"], - "end_date": child_node["max_end_date"], - } - ) + # Collect detailed task instance data from all children + if child_node.get("details"): + children_details.extend(child_node["details"]) yield child_node if node_id: yield { "task_id": node_id, "type": "group", "parent_id": parent_id, - **_get_aggs_for_node(children), + **_get_aggs_for_node(children_details), + "details": children_details, } return if isinstance(node, SerializedBaseOperator): @@ -125,5 +126,6 @@ def _find_aggregates( "type": "task", "parent_id": parent_id, **_get_aggs_for_node(details), + "details": details, } return diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/task_group.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/task_group.py index ed9a96718e9c6..1458641a3623f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/task_group.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/task_group.py @@ -25,6 +25,7 @@ from airflow.configuration import conf from airflow.models.mappedoperator import MappedOperator, is_mapped +from airflow.sdk import TaskGroup from airflow.serialization.serialized_objects import SerializedBaseOperator @@ -40,9 +41,11 @@ def get_task_group_children_getter() -> Callable: def task_group_to_dict(task_item_or_group, parent_group_is_mapped=False): """Create a nested dict representation of this TaskGroup and its children used to construct the Graph.""" if isinstance(task := task_item_or_group, (SerializedBaseOperator, MappedOperator)): + # we explicitly want the short task ID here, not the full doted notation if in a group + task_display_name = task.task_display_name if task.task_display_name != task.task_id else task.label node_operator = { "id": task.task_id, - "label": task.label, + "label": task_display_name, "operator": task.operator_name, "type": "task", } @@ -54,7 +57,7 @@ def task_group_to_dict(task_item_or_group, parent_group_is_mapped=False): node_operator["is_mapped"] = True return node_operator - task_group = task_item_or_group + task_group: TaskGroup = task_item_or_group mapped = is_mapped(task_group) children = [ task_group_to_dict(child, parent_group_is_mapped=parent_group_is_mapped or mapped) @@ -71,7 +74,7 @@ def task_group_to_dict(task_item_or_group, parent_group_is_mapped=False): return { "id": task_group.group_id, - "label": task_group.label, + "label": task_group.group_display_name or task_group.label, "tooltip": task_group.tooltip, "is_mapped": mapped, "children": children, @@ -90,15 +93,17 @@ def task_group_to_dict_grid(task_item_or_group, parent_group_is_mapped=False): setup_teardown_type = "setup" elif task.is_teardown is True: setup_teardown_type = "teardown" + # we explicitly want the short task ID here, not the full doted notation if in a group + task_display_name = task.task_display_name if task.task_display_name != task.task_id else task.label return { "id": task.task_id, - "label": task.label, + "label": task_display_name, "is_mapped": mapped, "children": None, "setup_teardown_type": setup_teardown_type, } - task_group = task_item_or_group + task_group: TaskGroup = task_item_or_group task_group_sort = get_task_group_children_getter() mapped = is_mapped(task_group) children = [ @@ -108,7 +113,7 @@ def task_group_to_dict_grid(task_item_or_group, parent_group_is_mapped=False): return { "id": task_group.group_id, - "label": task_group.label, + "label": task_group.group_display_name or task_group.label, "is_mapped": mapped or None, "children": children or None, } diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/app.py b/airflow-core/src/airflow/api_fastapi/execution_api/app.py index df2ed70340fd7..a956496c56a34 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/app.py @@ -18,6 +18,7 @@ from __future__ import annotations import json +import time from contextlib import AsyncExitStack from functools import cached_property from typing import TYPE_CHECKING, Any @@ -29,6 +30,7 @@ ) from fastapi import FastAPI, Request from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware from airflow.api_fastapi.auth.tokens import ( JWTGenerator, @@ -39,6 +41,7 @@ if TYPE_CHECKING: import httpx + from fastapi import Response from fastapi.routing import APIRoute import structlog @@ -96,6 +99,39 @@ async def lifespan(app: FastAPI, registry: svcs.Registry): yield +class JWTReissueMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + from airflow.configuration import conf + + response: Response = await call_next(request) + + refreshed_token: str | None = None + auth_header = request.headers.get("authorization") + if auth_header and auth_header.lower().startswith("bearer "): + token = auth_header.split(" ", 1)[1] + try: + async with svcs.Container(request.app.state.svcs_registry) as services: + validator: JWTValidator = await services.aget(JWTValidator) + claims = await validator.avalidated_claims(token, {}) + + now = int(time.time()) + validity = conf.getint("execution_api", "jwt_expiration_time") + refresh_when_less_than = max(int(validity * 0.20), 30) + valid_left = int(claims.get("exp", 0)) - now + if valid_left <= refresh_when_less_than: + generator: JWTGenerator = await services.aget(JWTGenerator) + refreshed_token = generator.generate(claims) + except Exception as err: + # Do not block the response if refreshing fails; log a warning for visibility + logger.warning( + "JWT reissue middleware failed to refresh token", error=str(err), exc_info=True + ) + + if refreshed_token: + response.headers["Refreshed-API-Token"] = refreshed_token + return response + + class CadwynWithOpenAPICustomization(Cadwyn): # Workaround lack of customzation https://github.com/zmievsa/cadwyn/issues/255 async def openapi_jsons(self, req: Request) -> JSONResponse: @@ -179,6 +215,7 @@ def custom_generate_unique_id(route: APIRoute): versions=bundle, ) + app.add_middleware(JWTReissueMiddleware) app.generate_and_include_versioned_routers(execution_api_router) # As we are mounted as a sub app, we don't get any logs for unhandled exceptions without this! @@ -233,7 +270,6 @@ def app(self): from airflow.api_fastapi.execution_api.deps import ( JWTBearerDep, JWTBearerTIPathDep, - JWTRefresherDep, ) from airflow.api_fastapi.execution_api.routes.connections import has_connection_access from airflow.api_fastapi.execution_api.routes.variables import has_variable_access @@ -248,7 +284,6 @@ async def always_allow(): ... self._app.dependency_overrides[JWTBearerDep.dependency] = always_allow self._app.dependency_overrides[JWTBearerTIPathDep.dependency] = always_allow - self._app.dependency_overrides[JWTRefresherDep.dependency] = always_allow self._app.dependency_overrides[has_connection_access] = always_allow self._app.dependency_overrides[has_variable_access] = always_allow self._app.dependency_overrides[has_xcom_access] = always_allow diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset.py index 4cd23ddbb822b..c11bccdd59a60 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset.py @@ -17,6 +17,8 @@ from __future__ import annotations +from pydantic.types import JsonValue + from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel @@ -26,7 +28,7 @@ class AssetResponse(BaseModel): name: str uri: str group: str - extra: dict | None = None + extra: dict[str, JsonValue] | None = None class AssetAliasResponse(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py index a9a66f242fc80..b050133212ebb 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/asset_event.py @@ -19,6 +19,8 @@ from datetime import datetime +from pydantic.types import JsonValue + from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel from airflow.api_fastapi.execution_api.datamodels.asset import AssetResponse @@ -41,7 +43,7 @@ class AssetEventResponse(BaseModel): id: int timestamp: datetime - extra: dict | None = None + extra: dict[str, JsonValue] | None = None asset: AssetResponse created_dagruns: list[DagRunAssetReference] diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py index 4162d2cf7717c..4cccbb69e712e 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/hitl.py @@ -26,6 +26,13 @@ from airflow.models.hitl import HITLDetail +class HITLUser(BaseModel): + """Schema for a Human-in-the-loop users.""" + + id: str + name: str + + class HITLDetailRequest(BaseModel): """Schema for the request part of a Human-in-the-loop detail for a specific task instance.""" @@ -36,7 +43,7 @@ class HITLDetailRequest(BaseModel): defaults: list[str] | None = None multiple: bool = False params: dict[str, Any] = Field(default_factory=dict) - respondents: list[str] | None = None + assigned_users: list[HITLUser] = Field(default_factory=list) class UpdateHITLDetailPayload(BaseModel): @@ -51,20 +58,27 @@ class HITLDetailResponse(BaseModel): """Schema for the response part of a Human-in-the-loop detail for a specific task instance.""" response_received: bool - responded_user_name: str | None - responded_user_id: str | None - response_at: datetime | None + responded_by_user: HITLUser | None = None + responded_at: datetime | None # It's empty if the user has not yet responded. chosen_options: list[str] | None params_input: dict[str, Any] = Field(default_factory=dict) @classmethod def from_hitl_detail_orm(cls, hitl_detail: HITLDetail) -> HITLDetailResponse: + hitl_user = ( + HITLUser( + id=hitl_detail.responded_by_user_id, + name=hitl_detail.responded_by_user_name, + ) + if hitl_detail.responded_by_user + else None + ) + return HITLDetailResponse( response_received=hitl_detail.response_received, - response_at=hitl_detail.response_at, - responded_user_id=hitl_detail.responded_user_id, - responded_user_name=hitl_detail.responded_user_name, + responded_at=hitl_detail.responded_at, + responded_by_user=hitl_user, chosen_options=hitl_detail.chosen_options, params_input=hitl_detail.params_input or {}, ) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py index 7fa329e0f32b5..daa073b5fe533 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py @@ -23,8 +23,8 @@ from pydantic import ( AwareDatetime, - Discriminator, Field, + JsonValue, Tag, TypeAdapter, WithJsonSchema, @@ -225,7 +225,7 @@ def ti_state_discriminator(v: dict[str, str] | StrictBaseModel) -> str: | Annotated[TIDeferredStatePayload, Tag("deferred")] | Annotated[TIRescheduleStatePayload, Tag("up_for_reschedule")] | Annotated[TIRetryStatePayload, Tag("up_for_retry")], - Discriminator(ti_state_discriminator), + Field(discriminator=ti_state_discriminator), ] @@ -258,7 +258,7 @@ class AssetReferenceAssetEventDagRun(StrictBaseModel): name: str uri: str - extra: dict + extra: dict[str, JsonValue] class AssetAliasReferenceAssetEventDagRun(StrictBaseModel): @@ -271,7 +271,7 @@ class AssetEventDagRunReference(StrictBaseModel): """Schema for AssetEvent model used in DagRun.""" asset: AssetReferenceAssetEventDagRun - extra: dict + extra: dict[str, JsonValue] source_task_id: str | None source_dag_id: str | None source_run_id: str | None @@ -298,7 +298,8 @@ class DagRun(StrictBaseModel): clear_number: int = 0 run_type: DagRunType state: DagRunState - conf: Annotated[dict[str, Any], Field(default_factory=dict)] + conf: dict[str, Any] | None = None + triggering_user_name: str | None = None consumed_asset_events: list[AssetEventDagRunReference] diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/deps.py b/airflow-core/src/airflow/api_fastapi/execution_api/deps.py index 2648a64ffad7a..d247a31f5f4fa 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/deps.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/deps.py @@ -18,17 +18,14 @@ # Disable future annotations in this file to work around https://github.com/fastapi/fastapi/issues/13056 # ruff: noqa: I002 -import sys -import time from typing import Any import structlog import svcs -from fastapi import Depends, HTTPException, Request, Response, status +from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPBearer -from starlette.exceptions import HTTPException as StarletteHTTPException -from airflow.api_fastapi.auth.tokens import JWTGenerator, JWTValidator +from airflow.api_fastapi.auth.tokens import JWTValidator from airflow.api_fastapi.execution_api.datamodels.token import TIToken log = structlog.get_logger(logger_name=__name__) @@ -98,58 +95,3 @@ async def __call__( # type: ignore[override] # This checks that the UUID in the url matches the one in the token for us. JWTBearerTIPathDep = Depends(JWTBearer(path_param_name="task_instance_id")) - - -class JWTReissuer: - """Re-issue JWTs to requests when they are about to run out.""" - - def __init__(self): - from airflow.configuration import conf - - self.refresh_when_less_than = max( - # Issue a new token to a task when the current one is valid for only either 20% of the total validity, - # or 30s - int(conf.getint("execution_api", "jwt_expiration_time") * 0.20), - 30, - ) - - async def __call__( - self, - response: Response, - token=JWTBearerDep, - services=DepContainer, - ): - try: - yield - finally: - # We want to run this even in the case of 404 errors etc - now = int(time.time()) - - try: - valid_left = token.claims["exp"] - now - if valid_left <= self.refresh_when_less_than: - generator: JWTGenerator = await services.aget(JWTGenerator) - new = generator.generate(token.claims) - response.headers["Refreshed-API-Token"] = new - log.debug( - "Refreshed token issued to Task", - valid_left=valid_left, - refresh_when_less_than=self.refresh_when_less_than, - ) - - exc, val, _ = sys.exc_info() - if val and isinstance(val, StarletteHTTPException): - # If there is an exception thrown, we need to set the headers there instead - if val.headers is None: - val.headers = {} - - # Defined as a "mapping type", but 99.9% of the time it's a mutable dict. We catch - # errors if not - val.headers["Refreshed-API-Token"] = new # type: ignore[index] - - except Exception as e: - # Don't 500 if there's a problem - log.warning("Error refreshing Task JWT", err=f"{type(e).__name__}: {e}") - - -JWTRefresherDep = Depends(JWTReissuer()) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py index 89d96083876db..562b8588fbf2c 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py @@ -19,7 +19,7 @@ from cadwyn import VersionedAPIRouter from fastapi import APIRouter -from airflow.api_fastapi.execution_api.deps import JWTBearerDep, JWTRefresherDep +from airflow.api_fastapi.execution_api.deps import JWTBearerDep from airflow.api_fastapi.execution_api.routes import ( asset_events, assets, @@ -37,7 +37,7 @@ execution_api_router.include_router(health.router, prefix="/health", tags=["Health"]) # _Every_ single endpoint under here must be authenticated. Some do further checks on top of these -authenticated_router = VersionedAPIRouter(dependencies=[JWTBearerDep, JWTRefresherDep]) # type: ignore[list-item] +authenticated_router = VersionedAPIRouter(dependencies=[JWTBearerDep]) # type: ignore[list-item] authenticated_router.include_router(assets.router, prefix="/assets", tags=["Assets"]) authenticated_router.include_router(asset_events.router, prefix="/asset-events", tags=["Asset Events"]) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py index c9efbda78f04a..50b9377d2f814 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py @@ -71,14 +71,14 @@ def upsert_hitl_detail( defaults=payload.defaults, multiple=payload.multiple, params=payload.params, - respondents=payload.respondents, + assignees=[user.model_dump() for user in payload.assigned_users], ) session.add(hitl_detail_model) elif hitl_detail_model.response_received: # Cleanup the response part of HITLDetail as we only store one response for one task instance. # It normally happens after retry, we keep only the latest response. hitl_detail_model.responded_by = None - hitl_detail_model.response_at = None + hitl_detail_model.responded_at = None hitl_detail_model.chosen_options = None hitl_detail_model.params_input = {} session.add(hitl_detail_model) @@ -116,9 +116,8 @@ def update_hitl_detail( f"Human-in-the-loop detail for Task Instance with id {ti_id_str} already exists.", ) - hitl_detail_model.responded_user_id = HITLDetail.DEFAULT_USER_NAME - hitl_detail_model.responded_user_name = HITLDetail.DEFAULT_USER_NAME - hitl_detail_model.response_at = datetime.now(timezone.utc) + hitl_detail_model.responded_by = None + hitl_detail_model.responded_at = datetime.now(timezone.utc) hitl_detail_model.chosen_options = payload.chosen_options hitl_detail_model.params_input = payload.params_input session.add(hitl_detail_model) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py index c30cf1e9c4c4b..54c777fc59a44 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py @@ -66,14 +66,14 @@ from airflow.models.xcom import XComModel from airflow.sdk.definitions._internal.expandinput import NotFullyPopulated from airflow.sdk.definitions.asset import Asset, AssetUniqueKey +from airflow.serialization.serialized_objects import SerializedDAG +from airflow.task.trigger_rule import TriggerRule from airflow.utils.state import DagRunState, TaskInstanceState, TerminalTIState if TYPE_CHECKING: from sqlalchemy.sql.dml import Update from airflow.models.expandinput import SchedulerExpandInput - from airflow.models.mappedoperator import MappedOperator - from airflow.serialization.serialized_objects import SerializedBaseOperator router = VersionedAPIRouter() @@ -255,11 +255,8 @@ def ti_run( if dag := dag_bag.get_dag_for_run(dag_run=dr, session=session): upstream_map_indexes = dict( _get_upstream_map_indexes( - # TODO (GH-52141): This get_task should return scheduler - # types instead, but currently it inherits SDK's DAG. - cast("MappedOperator | SerializedBaseOperator", dag.get_task(ti.task_id)), - ti.map_index, - ti.run_id, + serialized_dag=dag, + ti=ti, session=session, ) ) @@ -292,30 +289,41 @@ def ti_run( def _get_upstream_map_indexes( - task: MappedOperator | SerializedBaseOperator, ti_map_index: int, run_id: str, session: SessionDep + *, + serialized_dag: SerializedDAG, + ti: TI, + session: SessionDep, ) -> Iterator[tuple[str, int | list[int] | None]]: - task_mapped_group = task.get_closest_mapped_task_group() + task = serialized_dag.get_task(ti.task_id) for upstream_task in task.upstream_list: - upstream_mapped_group = upstream_task.get_closest_mapped_task_group() map_indexes: int | list[int] | None - if upstream_mapped_group is None: + if (upstream_mapped_group := upstream_task.get_closest_mapped_task_group()) is None: # regular tasks or non-mapped task groups map_indexes = None - elif task_mapped_group == upstream_mapped_group: + elif task.get_closest_mapped_task_group() == upstream_mapped_group: # tasks in the same mapped task group hierarchy - map_indexes = ti_map_index + map_indexes = ti.map_index else: # tasks not in the same mapped task group # the upstream mapped task group should combine the return xcom as a list and return it - mapped_ti_count: int + mapped_ti_count: int | None = None + try: - # for cases that does not need to resolve xcom + # First try: without resolving XCom mapped_ti_count = upstream_mapped_group.get_parse_time_mapped_ti_count() except NotFullyPopulated: - # for cases that needs to resolve xcom to get the correct count - mapped_ti_count = cast( - "SchedulerExpandInput", upstream_mapped_group._expand_input - ).get_total_map_length(run_id, session=session) + # Second try: resolve XCom for correct count + try: + expand_input = cast("SchedulerExpandInput", upstream_mapped_group._expand_input) + mapped_ti_count = expand_input.get_total_map_length(ti.run_id, session=session) + except NotFullyPopulated: + # For these trigger rules, unresolved map indexes are acceptable. + # The success of the upstream task is not the main reason for triggering the current task. + # Therefore, whether the upstream task is fully populated can be ignored. + if task.trigger_rule != TriggerRule.ALL_SUCCESS: + mapped_ti_count = None + + # Compute map indexes if we have a valid count map_indexes = list(range(mapped_ti_count)) if mapped_ti_count is not None else None yield upstream_task.task_id, map_indexes @@ -444,7 +452,7 @@ def _create_ti_state_update_query_and_update_state( ti = session.get(TI, ti_id_str) updated_state = ti_patch_payload.state query = TI.duration_expression_update(ti_patch_payload.end_date, query, session.bind) - query = query.values(state=updated_state) + query = query.values(state=updated_state, next_method=None, next_kwargs=None) if updated_state == TerminalTIState.FAILED: # This is the only case needs extra handling for TITerminalStatePayload diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py index e168598786617..d2f5d21349c44 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/variables.py @@ -55,7 +55,7 @@ async def has_variable_access( @router.get( - "/{variable_key}", + "/{variable_key:path}", responses={ status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"}, status.HTTP_403_FORBIDDEN: {"description": "Task does not have access to the variable"}, @@ -63,6 +63,9 @@ async def has_variable_access( ) def get_variable(variable_key: str) -> VariableResponse: """Get an Airflow Variable.""" + if not variable_key: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Not Found") + try: variable_value = Variable.get(variable_key) except KeyError: @@ -78,7 +81,7 @@ def get_variable(variable_key: str) -> VariableResponse: @router.put( - "/{variable_key}", + "/{variable_key:path}", status_code=status.HTTP_201_CREATED, responses={ status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"}, @@ -87,12 +90,15 @@ def get_variable(variable_key: str) -> VariableResponse: ) def put_variable(variable_key: str, body: VariablePostBody): """Set an Airflow Variable.""" + if not variable_key: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Not Found") + Variable.set(key=variable_key, value=body.value, description=body.description) return {"message": "Variable successfully set"} @router.delete( - "/{variable_key}", + "/{variable_key:path}", status_code=status.HTTP_204_NO_CONTENT, responses={ status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"}, @@ -101,4 +107,7 @@ def put_variable(variable_key: str, body: VariablePostBody): ) def delete_variable(variable_key: str): """Delete an Airflow Variable.""" + if not variable_key: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Not Found") + Variable.delete(key=variable_key) diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py index b2399635499cc..1408adcefee50 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py @@ -21,7 +21,7 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request, Response, status -from pydantic import BaseModel, JsonValue, StringConstraints +from pydantic import BaseModel, JsonValue from sqlalchemy import delete from sqlalchemy.sql.selectable import Select @@ -41,7 +41,7 @@ async def has_xcom_access( dag_id: str, run_id: str, task_id: str, - xcom_key: Annotated[str, Path(alias="key")], + xcom_key: Annotated[str, Path(alias="key", min_length=1)], request: Request, token=JWTBearerDep, ) -> bool: @@ -88,111 +88,15 @@ async def xcom_query( return query -@router.head( - "/{dag_id}/{run_id}/{task_id}/{key}", - responses={ - status.HTTP_200_OK: { - "description": "Metadata about the number of matching XCom values", - "headers": { - "Content-Range": { - "schema": {"pattern": r"^map_indexes \d+$"}, - "description": "The number of (mapped) XCom values found for this task.", - }, - }, - }, - }, - description="Returns the count of mapped XCom values found in the `Content-Range` response header", -) -def head_xcom( - response: Response, - session: SessionDep, - xcom_query: Annotated[Select, Depends(xcom_query)], - map_index: Annotated[int | None, Query()] = None, -) -> None: - """Get the count of XComs from database - not other XCom Backends.""" - if map_index is not None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail={"reason": "invalid_request", "message": "Cannot specify map_index in a HEAD request"}, - ) - - count = get_query_count(xcom_query, session=session) - # Tell the caller how many items in this query. We define a custom range unit (HTTP spec only defines - # "bytes" but we can add our own) - response.headers["Content-Range"] = f"map_indexes {count}" - - -class GetXcomFilterParams(BaseModel): - """Class to house the params that can optionally be set for Get XCom.""" - - map_index: int = -1 - include_prior_dates: bool = False - offset: int | None = None - - @router.get( - "/{dag_id}/{run_id}/{task_id}/{key}", - description="Get a single XCom Value", -) -def get_xcom( - dag_id: str, - run_id: str, - task_id: str, - key: Annotated[str, StringConstraints(min_length=1)], - session: SessionDep, - params: Annotated[GetXcomFilterParams, Query()], -) -> XComResponse: - """Get an Airflow XCom from database - not other XCom Backends.""" - xcom_query = XComModel.get_many( - run_id=run_id, - key=key, - task_ids=task_id, - dag_ids=dag_id, - include_prior_dates=params.include_prior_dates, - ) - if params.offset is not None: - xcom_query = xcom_query.where(XComModel.value.is_not(None)).order_by(None) - if params.offset >= 0: - xcom_query = xcom_query.order_by(XComModel.map_index.asc()).offset(params.offset) - else: - xcom_query = xcom_query.order_by(XComModel.map_index.desc()).offset(-1 - params.offset) - else: - xcom_query = xcom_query.where(XComModel.map_index == params.map_index) - - # We use `BaseXCom.get_many` to fetch XComs directly from the database, bypassing the XCom Backend. - # This avoids deserialization via the backend (e.g., from a remote storage like S3) and instead - # retrieves the raw serialized value from the database. By not relying on `XCom.get_many` or `XCom.get_one` - # (which automatically deserializes using the backend), we avoid potential - # performance hits from retrieving large data files into the API server. - result = session.scalars(xcom_query).first() - if result is None: - if params.offset is None: - message = ( - f"XCom with {key=} map_index={params.map_index} not found for " - f"task {task_id!r} in DAG run {run_id!r} of {dag_id!r}" - ) - else: - message = ( - f"XCom with {key=} offset={params.offset} not found for " - f"task {task_id!r} in DAG run {run_id!r} of {dag_id!r}" - ) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail={"reason": "not_found", "message": message}, - ) - - return XComResponse(key=key, value=result.value) - - -@router.get( - "/{dag_id}/{run_id}/{task_id}/{key}/item/{offset}", + "/{dag_id}/{run_id}/{task_id}/{key:path}/item/{offset}", description="Get a single XCom value from a mapped task by sequence index", ) def get_mapped_xcom_by_index( dag_id: str, run_id: str, task_id: str, - key: str, + key: Annotated[str, Path(min_length=1)], offset: int, session: SessionDep, ) -> XComSequenceIndexResponse: @@ -229,14 +133,14 @@ class GetXComSliceFilterParams(BaseModel): @router.get( - "/{dag_id}/{run_id}/{task_id}/{key}/slice", + "/{dag_id}/{run_id}/{task_id}/{key:path}/slice", description="Get XCom values from a mapped task by sequence slice", ) def get_mapped_xcom_by_slice( dag_id: str, run_id: str, task_id: str, - key: str, + key: Annotated[str, Path(min_length=1)], params: Annotated[GetXComSliceFilterParams, Query()], session: SessionDep, ) -> XComSequenceSliceResponse: @@ -310,17 +214,113 @@ def get_mapped_xcom_by_slice( return XComSequenceSliceResponse(values) +@router.head( + "/{dag_id}/{run_id}/{task_id}/{key:path}", + responses={ + status.HTTP_200_OK: { + "description": "Metadata about the number of matching XCom values", + "headers": { + "Content-Range": { + "schema": {"pattern": r"^map_indexes \d+$"}, + "description": "The number of (mapped) XCom values found for this task.", + }, + }, + }, + }, + description="Returns the count of mapped XCom values found in the `Content-Range` response header", +) +def head_xcom( + response: Response, + session: SessionDep, + xcom_query: Annotated[Select, Depends(xcom_query)], + map_index: Annotated[int | None, Query()] = None, +) -> None: + """Get the count of XComs from database - not other XCom Backends.""" + if map_index is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"reason": "invalid_request", "message": "Cannot specify map_index in a HEAD request"}, + ) + + count = get_query_count(xcom_query, session=session) + # Tell the caller how many items in this query. We define a custom range unit (HTTP spec only defines + # "bytes" but we can add our own) + response.headers["Content-Range"] = f"map_indexes {count}" + + +class GetXcomFilterParams(BaseModel): + """Class to house the params that can optionally be set for Get XCom.""" + + map_index: int = -1 + include_prior_dates: bool = False + offset: int | None = None + + +@router.get( + "/{dag_id}/{run_id}/{task_id}/{key:path}", + description="Get a single XCom Value", +) +def get_xcom( + dag_id: str, + run_id: str, + task_id: str, + key: Annotated[str, Path(min_length=1)], + session: SessionDep, + params: Annotated[GetXcomFilterParams, Query()], +) -> XComResponse: + """Get an Airflow XCom from database - not other XCom Backends.""" + xcom_query = XComModel.get_many( + run_id=run_id, + key=key, + task_ids=task_id, + dag_ids=dag_id, + include_prior_dates=params.include_prior_dates, + ) + if params.offset is not None: + xcom_query = xcom_query.where(XComModel.value.is_not(None)).order_by(None) + if params.offset >= 0: + xcom_query = xcom_query.order_by(XComModel.map_index.asc()).offset(params.offset) + else: + xcom_query = xcom_query.order_by(XComModel.map_index.desc()).offset(-1 - params.offset) + else: + xcom_query = xcom_query.where(XComModel.map_index == params.map_index) + + # We use `BaseXCom.get_many` to fetch XComs directly from the database, bypassing the XCom Backend. + # This avoids deserialization via the backend (e.g., from a remote storage like S3) and instead + # retrieves the raw serialized value from the database. By not relying on `XCom.get_many` or `XCom.get_one` + # (which automatically deserializes using the backend), we avoid potential + # performance hits from retrieving large data files into the API server. + result = session.scalars(xcom_query).first() + if result is None: + if params.offset is None: + message = ( + f"XCom with {key=} map_index={params.map_index} not found for " + f"task {task_id!r} in DAG run {run_id!r} of {dag_id!r}" + ) + else: + message = ( + f"XCom with {key=} offset={params.offset} not found for " + f"task {task_id!r} in DAG run {run_id!r} of {dag_id!r}" + ) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"reason": "not_found", "message": message}, + ) + + return XComResponse(key=key, value=result.value) + + # TODO: once we have JWT tokens, then remove dag_id/run_id/task_id from the URL and just use the info in # the token @router.post( - "/{dag_id}/{run_id}/{task_id}/{key}", + "/{dag_id}/{run_id}/{task_id}/{key:path}", status_code=status.HTTP_201_CREATED, ) def set_xcom( dag_id: str, run_id: str, task_id: str, - key: Annotated[str, StringConstraints(min_length=1)], + key: Annotated[str, Path(min_length=1)], session: SessionDep, value: Annotated[ JsonValue, @@ -431,7 +431,7 @@ def set_xcom( @router.delete( - "/{dag_id}/{run_id}/{task_id}/{key}", + "/{dag_id}/{run_id}/{task_id}/{key:path}", responses={status.HTTP_404_NOT_FOUND: {"description": "XCom not found"}}, description="Delete a single XCom Value", ) @@ -440,7 +440,7 @@ def delete_xcom( dag_id: str, run_id: str, task_id: str, - key: str, + key: Annotated[str, Path(min_length=1)], map_index: Annotated[int, Query()] = -1, ): """Delete a single XCom Value.""" diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py index 84d676347ac87..9364cb12c80b3 100644 --- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py @@ -26,9 +26,15 @@ AddIncludePriorDatesToGetXComSlice, ) from airflow.api_fastapi.execution_api.versions.v2025_09_23 import AddDagVersionIdField +from airflow.api_fastapi.execution_api.versions.v2025_10_27 import ( + MakeDagRunConfNullable, +) +from airflow.api_fastapi.execution_api.versions.v2025_11_05 import AddTriggeringUserNameField bundle = VersionBundle( HeadVersion(), + Version("2025-11-05", AddTriggeringUserNameField), + Version("2025-10-27", MakeDagRunConfNullable), Version("2025-09-23", AddDagVersionIdField), Version( "2025-08-10", diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2025_10_27.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2025_10_27.py new file mode 100644 index 0000000000000..b16f00519bb56 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2025_10_27.py @@ -0,0 +1,37 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from cadwyn import ResponseInfo, VersionChange, convert_response_to_previous_version_for + +from airflow.api_fastapi.execution_api.datamodels.taskinstance import TIRunContext + + +class MakeDagRunConfNullable(VersionChange): + """Make DagRun.conf field nullable to match database schema.""" + + description = __doc__ + + instructions_to_migrate_to_previous_version = () + + @convert_response_to_previous_version_for(TIRunContext) # type: ignore[arg-type] + def ensure_conf_is_dict_in_dag_run(response: ResponseInfo) -> None: # type: ignore[misc] + """Ensure conf is always a dict (never None) in previous versions.""" + if "dag_run" in response.body and isinstance(response.body["dag_run"], dict): + if response.body["dag_run"].get("conf") is None: + response.body["dag_run"]["conf"] = {} diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2025_11_05.py b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2025_11_05.py new file mode 100644 index 0000000000000..ced2917afda05 --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2025_11_05.py @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from cadwyn import ResponseInfo, VersionChange, convert_response_to_previous_version_for, schema + +from airflow.api_fastapi.execution_api.datamodels.taskinstance import DagRun, TIRunContext + + +class AddTriggeringUserNameField(VersionChange): + """Add the `triggering_user_name` field to DagRun model.""" + + description = __doc__ + + instructions_to_migrate_to_previous_version = (schema(DagRun).field("triggering_user_name").didnt_exist,) + + @convert_response_to_previous_version_for(TIRunContext) # type: ignore[arg-type] + def remove_triggering_user_name_from_dag_run(response: ResponseInfo) -> None: # type: ignore[misc] + """Remove the `triggering_user_name` field from the dag_run object when converting to the previous version.""" + if "dag_run" in response.body and isinstance(response.body["dag_run"], dict): + response.body["dag_run"].pop("triggering_user_name", None) diff --git a/airflow-core/src/airflow/api_fastapi/main.py b/airflow-core/src/airflow/api_fastapi/main.py index 16419b9485ead..195f98a5bf328 100644 --- a/airflow-core/src/airflow/api_fastapi/main.py +++ b/airflow-core/src/airflow/api_fastapi/main.py @@ -19,6 +19,10 @@ import os +# Mark this as a server context before any airflow imports +# This ensures plugins loaded at import time get the correct secrets backend chain +os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "server" + from airflow.api_fastapi.app import cached_app # There is no way to pass the apps to this file from Airflow CLI diff --git a/airflow-core/src/airflow/assets/manager.py b/airflow-core/src/airflow/assets/manager.py index a00c7cae27d1d..c0dce3861eca2 100644 --- a/airflow-core/src/airflow/assets/manager.py +++ b/airflow-core/src/airflow/assets/manager.py @@ -134,7 +134,11 @@ def register_asset_change( ) ) if not asset_model: - cls.logger().warning("AssetModel %s not found", asset) + msg = f"AssetModel {asset} not found; cannot create asset event." + cls.logger().warning(msg) + # if there is a task_instance, write to task log + if task_instance is not None and hasattr(task_instance, "log"): + task_instance.log.warning(msg) return None if not asset_model.active: diff --git a/airflow-core/src/airflow/callbacks/callback_requests.py b/airflow-core/src/airflow/callbacks/callback_requests.py index e0666b397c235..d2bdf0968bc20 100644 --- a/airflow-core/src/airflow/callbacks/callback_requests.py +++ b/airflow-core/src/airflow/callbacks/callback_requests.py @@ -77,7 +77,7 @@ def is_failure_callback(self) -> bool: } -class EmailNotificationRequest(BaseCallbackRequest): +class EmailRequest(BaseCallbackRequest): """Email notification request for task failures/retries.""" ti: ti_datamodel.TaskInstance @@ -86,7 +86,7 @@ class EmailNotificationRequest(BaseCallbackRequest): """Whether this is for a failure or retry email""" context_from_server: ti_datamodel.TIRunContext """Task execution context from the Server""" - type: Literal["EmailNotificationRequest"] = "EmailNotificationRequest" + type: Literal["EmailRequest"] = "EmailRequest" class DagRunContext(BaseModel): @@ -108,6 +108,9 @@ class DagCallbackRequest(BaseCallbackRequest): CallbackRequest = Annotated[ - DagCallbackRequest | TaskCallbackRequest | EmailNotificationRequest, + DagCallbackRequest | TaskCallbackRequest | EmailRequest, Field(discriminator="type"), ] + +# Backwards compatibility alias +EmailNotificationRequest = EmailRequest diff --git a/airflow-core/src/airflow/callbacks/database_callback_sink.py b/airflow-core/src/airflow/callbacks/database_callback_sink.py index 390f762002263..e7e27cf86b50e 100644 --- a/airflow-core/src/airflow/callbacks/database_callback_sink.py +++ b/airflow-core/src/airflow/callbacks/database_callback_sink.py @@ -35,5 +35,5 @@ class DatabaseCallbackSink(BaseCallbackSink): @provide_session def send(self, callback: CallbackRequest, session: Session = NEW_SESSION) -> None: """Send callback for execution.""" - db_callback = DbCallbackRequest(callback=callback, priority_weight=10) + db_callback = DbCallbackRequest(callback=callback, priority_weight=1) session.add(db_callback) diff --git a/airflow-core/src/airflow/cli/cli_config.py b/airflow-core/src/airflow/cli/cli_config.py index 5c8497ead6e4f..15ca79178573b 100644 --- a/airflow-core/src/airflow/cli/cli_config.py +++ b/airflow-core/src/airflow/cli/cli_config.py @@ -167,8 +167,24 @@ def string_lower_type(val): default=None, action="append", ) -ARG_START_DATE = Arg(("-s", "--start-date"), help="Override start_date YYYY-MM-DD", type=parsedate) -ARG_END_DATE = Arg(("-e", "--end-date"), help="Override end_date YYYY-MM-DD", type=parsedate) +ARG_START_DATE = Arg( + ("-s", "--start-date"), + help=( + "Override start_date. Accepts multiple datetime formats including: " + "YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DDTHH:MM:SS±HH:MM (ISO 8601), " + "and other formats supported by pendulum.parse()" + ), + type=parsedate, +) +ARG_END_DATE = Arg( + ("-e", "--end-date"), + help=( + "Override end_date. Accepts multiple datetime formats including: " + "YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DDTHH:MM:SS±HH:MM (ISO 8601), " + "and other formats supported by pendulum.parse()" + ), + type=parsedate, +) ARG_OUTPUT_PATH = Arg( ( "-o", @@ -344,10 +360,11 @@ def string_lower_type(val): ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg( ("--run-on-latest-version",), help=( - "(Experimental) If set, the backfill will run tasks using the latest bundle version instead of " - "the version that was active when the original Dag run was created." + "(Experimental) The backfill will run tasks using the latest bundle version instead of " + "the version that was active when the original Dag run was created. Defaults to True." ), action="store_true", + default=True, ) diff --git a/airflow-core/src/airflow/cli/commands/api_server_command.py b/airflow-core/src/airflow/cli/commands/api_server_command.py index ccc79a1093804..65a77509e57d5 100644 --- a/airflow-core/src/airflow/cli/commands/api_server_command.py +++ b/airflow-core/src/airflow/cli/commands/api_server_command.py @@ -81,6 +81,7 @@ def _run_api_server(args, apps: str, num_workers: int, worker_timeout: int, prox "workers": num_workers, "timeout_keep_alive": worker_timeout, "timeout_graceful_shutdown": worker_timeout, + "timeout_worker_healthcheck": worker_timeout, "ssl_keyfile": ssl_key, "ssl_certfile": ssl_cert, "access_log": True, diff --git a/airflow-core/src/airflow/cli/commands/backfill_command.py b/airflow-core/src/airflow/cli/commands/backfill_command.py index 444bc35aea34a..a6b35d6d2391b 100644 --- a/airflow-core/src/airflow/cli/commands/backfill_command.py +++ b/airflow-core/src/airflow/cli/commands/backfill_command.py @@ -17,6 +17,7 @@ from __future__ import annotations +import json import logging import signal @@ -80,13 +81,22 @@ def create_backfill(args) -> None: except AirflowConfigException as e: log.warning("Failed to get user name from os: %s, not setting the triggering user", e) user = None + + # Parse dag_run_conf if provided + dag_run_conf = None + if args.dag_run_conf: + try: + dag_run_conf = json.loads(args.dag_run_conf) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in --dag-run-conf: {e}") + _create_backfill( dag_id=args.dag_id, from_date=args.from_date, to_date=args.to_date, max_active_runs=args.max_active_runs, reverse=args.run_backwards, - dag_run_conf=args.dag_run_conf, + dag_run_conf=dag_run_conf, triggering_user_name=user, reprocess_behavior=reprocess_behavior, run_on_latest_version=args.run_on_latest_version, diff --git a/airflow-core/src/airflow/cli/commands/config_command.py b/airflow-core/src/airflow/cli/commands/config_command.py index 0c30ce111af65..b766351229cd6 100644 --- a/airflow-core/src/airflow/cli/commands/config_command.py +++ b/airflow-core/src/airflow/cli/commands/config_command.py @@ -640,6 +640,9 @@ def message(self) -> str | None: ConfigChange( config=ConfigParameter("scheduler", "allow_trigger_in_future"), ), + ConfigChange( + config=ConfigParameter("scheduler", "dag_stale_not_seen_duration"), + ), ConfigChange( config=ConfigParameter("scheduler", "catchup_by_default"), default_change=True, diff --git a/airflow-core/src/airflow/cli/commands/connection_command.py b/airflow-core/src/airflow/cli/commands/connection_command.py index 7fe430c05bfe3..6cf4378c42aa4 100644 --- a/airflow-core/src/airflow/cli/commands/connection_command.py +++ b/airflow-core/src/airflow/cli/commands/connection_command.py @@ -65,6 +65,8 @@ def _connection_mapper(conn: Connection) -> dict[str, Any]: @providers_configuration_loaded def connections_get(args): """Get a connection.""" + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "server" + try: conn = Connection.get_connection_from_secrets(args.conn_id) except AirflowNotFoundException: @@ -358,6 +360,8 @@ def _import_helper(file_path: str, overwrite: bool) -> None: @providers_configuration_loaded def connections_test(args) -> None: """Test an Airflow connection.""" + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "server" + console = AirflowConsole() if conf.get("core", "test_connection", fallback="Disabled").lower().strip() != "enabled": console.print( diff --git a/airflow-core/src/airflow/cli/commands/dag_processor_command.py b/airflow-core/src/airflow/cli/commands/dag_processor_command.py index c866763338ff1..9f054eb846755 100644 --- a/airflow-core/src/airflow/cli/commands/dag_processor_command.py +++ b/airflow-core/src/airflow/cli/commands/dag_processor_command.py @@ -22,7 +22,7 @@ from typing import Any from airflow.cli.commands.daemon_utils import run_command_with_daemon_option -from airflow.dag_processing.manager import DagFileProcessorManager, reload_configuration_for_dag_processing +from airflow.dag_processing.manager import DagFileProcessorManager from airflow.jobs.dag_processor_job_runner import DagProcessorJobRunner from airflow.jobs.job import Job, run_job from airflow.utils import cli as cli_utils @@ -50,7 +50,6 @@ def dag_processor(args): """Start Airflow Dag Processor Job.""" job_runner = _create_dag_processor_job_runner(args) - reload_configuration_for_dag_processing() run_command_with_daemon_option( args=args, process_name="dag-processor", diff --git a/airflow-core/src/airflow/cli/commands/db_command.py b/airflow-core/src/airflow/cli/commands/db_command.py index a0a02ade87b24..ea3241320fdf8 100644 --- a/airflow-core/src/airflow/cli/commands/db_command.py +++ b/airflow-core/src/airflow/cli/commands/db_command.py @@ -81,8 +81,7 @@ def _get_version_revision(version: str, revision_heads_map: dict[str, str] | Non if current < wanted: return head - else: - return None + return None def run_db_migrate_command(args, command, revision_heads_map: dict[str, str]): diff --git a/airflow-core/src/airflow/cli/commands/scheduler_command.py b/airflow-core/src/airflow/cli/commands/scheduler_command.py index 15093fd24d0c7..f0437710f95b7 100644 --- a/airflow-core/src/airflow/cli/commands/scheduler_command.py +++ b/airflow-core/src/airflow/cli/commands/scheduler_command.py @@ -62,9 +62,9 @@ def _serve_logs(skip_serve_logs: bool = False): from airflow.utils.serve_logs import serve_logs sub_proc = None - executor_class, _ = ExecutorLoader.import_default_executor_cls() - if executor_class.serve_logs: - if skip_serve_logs is False: + if skip_serve_logs is False: + executor_class, _ = ExecutorLoader.import_default_executor_cls() + if executor_class.serve_logs: sub_proc = Process(target=serve_logs) sub_proc.start() try: diff --git a/airflow-core/src/airflow/cli/commands/task_command.py b/airflow-core/src/airflow/cli/commands/task_command.py index 5b54d76100a50..9b4cd4114f4dd 100644 --- a/airflow-core/src/airflow/cli/commands/task_command.py +++ b/airflow-core/src/airflow/cli/commands/task_command.py @@ -46,6 +46,7 @@ get_bagged_dag, get_dag_by_file_location, get_dags, + get_db_dag, suppress_logs_and_warning, ) from airflow.utils.helpers import ask_yesno @@ -82,7 +83,7 @@ def _generate_temporary_run_id() -> str: def _get_dag_run( *, - dag: DAG, + dag: SerializedDAG, create_if_necessary: CreateIfNecessary, logical_date_or_run_id: str | None = None, session: Session | None = None, @@ -144,9 +145,8 @@ def _get_dag_run( ) return dag_run, True if create_if_necessary == "db": - scheduler_dag = SerializedDAG.deserialize_dag(SerializedDAG.serialize_dag(dag)) # type: ignore[arg-type] dag_run = get_or_create_dagrun( - dag=scheduler_dag, + dag=dag, run_id=_generate_temporary_run_id(), logical_date=dag_run_logical_date, data_interval=data_interval, @@ -246,10 +246,7 @@ def task_failed_deps(args) -> None: Trigger Rule: Task's trigger rule 'all_success' requires all upstream tasks to have succeeded, but found 1 non-success(es). """ - dag = get_bagged_dag(args.bundle_name, args.dag_id) - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - task = cast("Operator", dag.get_task(task_id=args.task_id)) + task = get_db_dag(args.bundle_name, args.dag_id).get_task(task_id=args.task_id) ti, _ = _get_ti(task, args.map_index, logical_date_or_run_id=args.logical_date_or_run_id) dep_context = DepContext(deps=SCHEDULER_QUEUED_DEPS) failed_deps = list(ti.get_failed_dep_statuses(dep_context=dep_context)) @@ -274,9 +271,7 @@ def task_state(args) -> None: """ if not (dag := SerializedDagModel.get_dag(args.dag_id)): raise SystemExit(f"Can not find dag {args.dag_id!r}") - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - task = cast("Operator", dag.get_task(task_id=args.task_id)) + task = dag.get_task(task_id=args.task_id) ti, _ = _get_ti(task, args.map_index, logical_date_or_run_id=args.logical_date_or_run_id) print(ti.state) @@ -389,29 +384,35 @@ def task_test(args, dag: DAG | None = None) -> None: env_vars.update(args.env_vars) os.environ.update(env_vars) - dag = dag or get_bagged_dag(args.bundle_name, args.dag_id) + if dag: + sdk_dag = dag + scheduler_dag = SerializedDAG.from_dict(SerializedDAG.to_dict(dag)) + else: + sdk_dag = get_bagged_dag(args.bundle_name, args.dag_id) + scheduler_dag = get_db_dag(args.bundle_name, args.dag_id) - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - task = cast("Operator", dag.get_task(task_id=args.task_id)) + sdk_task = sdk_dag.get_task(args.task_id) # Add CLI provided task_params to task.params if args.task_params: passed_in_params = json.loads(args.task_params) - task.params.update(passed_in_params) + sdk_task.params.update(passed_in_params) - if task.params and isinstance(task.params, ParamsDict): - task.params.validate() + if sdk_task.params and isinstance(sdk_task.params, ParamsDict): + sdk_task.params.validate() ti, dr_created = _get_ti( - task, args.map_index, logical_date_or_run_id=args.logical_date_or_run_id, create_if_necessary="db" + scheduler_dag.get_task(args.task_id), + args.map_index, + logical_date_or_run_id=args.logical_date_or_run_id, + create_if_necessary="db", ) try: # TODO: move bulk of this logic into the SDK: http://github.com/apache/airflow/issues/54658 from airflow.sdk._shared.secrets_masker import RedactedIO with redirect_stdout(RedactedIO()): - _run_task(ti=ti, task=task, run_triggerer=True) + _run_task(ti=ti, task=sdk_task, run_triggerer=True) if ti.state == State.FAILED and args.post_mortem: debugger = _guess_debugger() debugger.set_trace() @@ -434,9 +435,7 @@ def task_render(args, dag: DAG | None = None) -> None: dag = get_bagged_dag(args.bundle_name, args.dag_id) serialized_dag = SerializedDAG.deserialize_dag(SerializedDAG.serialize_dag(dag)) ti, _ = _get_ti( - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - cast("Operator", serialized_dag.get_task(task_id=args.task_id)), + serialized_dag.get_task(task_id=args.task_id), args.map_index, logical_date_or_run_id=args.logical_date_or_run_id, create_if_necessary="memory", diff --git a/airflow-core/src/airflow/config_templates/airflow_local_settings.py b/airflow-core/src/airflow/config_templates/airflow_local_settings.py index 4c2cddb043c96..6270fba7ba3ba 100644 --- a/airflow-core/src/airflow/config_templates/airflow_local_settings.py +++ b/airflow-core/src/airflow/config_templates/airflow_local_settings.py @@ -43,16 +43,11 @@ "logging", "LOG_FORMATTER_CLASS", fallback="airflow.utils.log.timezone_aware.TimezoneAware" ) -COLORED_LOG_FORMAT: str = conf.get_mandatory_value("logging", "COLORED_LOG_FORMAT") - -COLORED_LOG: bool = conf.getboolean("logging", "COLORED_CONSOLE_LOG") - -COLORED_FORMATTER_CLASS: str = conf.get_mandatory_value("logging", "COLORED_FORMATTER_CLASS") - DAG_PROCESSOR_LOG_TARGET: str = conf.get_mandatory_value("logging", "DAG_PROCESSOR_LOG_TARGET") BASE_LOG_FOLDER: str = os.path.expanduser(conf.get_mandatory_value("logging", "BASE_LOG_FOLDER")) +# This isn't used anymore, but kept for compat of people who might have imported it DEFAULT_LOGGING_CONFIG: dict[str, Any] = { "version": 1, "disable_existing_loggers": False, @@ -61,10 +56,6 @@ "format": LOG_FORMAT, "class": LOG_FORMATTER_CLASS, }, - "airflow_coloured": { - "format": COLORED_LOG_FORMAT if COLORED_LOG else LOG_FORMAT, - "class": COLORED_FORMATTER_CLASS if COLORED_LOG else LOG_FORMATTER_CLASS, - }, "source_processor": { "format": DAG_PROCESSOR_LOG_FORMAT, "class": LOG_FORMATTER_CLASS, @@ -77,8 +68,9 @@ }, "handlers": { "console": { - "class": "airflow.utils.log.logging_mixin.RedirectStdHandler", - "formatter": "airflow_coloured", + "class": "logging.StreamHandler", + # "class": "airflow.utils.log.logging_mixin.RedirectStdHandler", + "formatter": "airflow", "stream": "sys.stdout", "filters": ["mask_secrets_core"], }, diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 5da744041a2e3..169e2589a624e 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -121,12 +121,11 @@ core: default: "32" max_active_tasks_per_dag: description: | - The maximum number of task instances allowed to run concurrently in each DAG. To calculate - the number of tasks that is running concurrently for a DAG, add up the number of running - tasks for all DAG runs of the DAG. This is configurable at the DAG level with ``max_active_tasks``, + The maximum number of task instances allowed to run concurrently in each dag run. + This is also configurable per-dag with ``max_active_tasks``, which is defaulted as ``[core] max_active_tasks_per_dag``. - An example scenario when this would be useful is when you want to stop a new dag with an early + An example scenario when this would be useful is when you want to stop a new dag run with an early start date from stealing all the executor slots in a cluster. version_added: 2.2.0 type: integer @@ -828,29 +827,32 @@ logging: type: string example: ~ default: "True" - colored_log_format: - description: | - Log format for when Colored logs is enabled - version_added: 2.0.0 - type: string - example: ~ - default: >- - [%%(blue)s%%(asctime)s%%(reset)s] {{%%(blue)s%%(filename)s:%%(reset)s%%(lineno)d}} - %%(log_color)s%%(levelname)s%%(reset)s - %%(log_color)s%%(message)s%%(reset)s - colored_formatter_class: - description: | - Specifies the class utilized by Airflow to implement colored logging - version_added: 2.0.0 - type: string - example: ~ - default: "airflow.utils.log.colored_log.CustomTTYColoredFormatter" log_format: description: | Format of Log line + + *Changed in 3.1.0*: This can now contain color escape sequences ``%(blue)s`` etc which will only + result in colours if :ref:`config:logging__colored_console_log` is true. + version_added: 2.0.0 type: string + example: "[%%(asctime)s] {{%%(filename)s:%%(lineno)d}} %%(levelname)s - %%(message)s" + default: "" + callsite_parameters: + description: | + A comma separated list of information about the callsite (such as line number of filename etc) of + logger calls to include in each message. + + See :class:`structlog.processors.CallsiteParameter` for the possible values.The values should be the + constant names (``FUNC_NAME``) or the values (``func_name``) + + Including these in a log message adds a lot to the usability to the logs, but collecting these has a + (tiny) cost -- if you are super concerned with eking out every last ounce of performance you could + turn these off (by setting this value to an empty string) + version_added: 3.1.0 + type: string + default: "filename,lineno" example: ~ - default: "[%%(asctime)s] {{%%(filename)s:%%(lineno)d}} %%(levelname)s - %%(message)s" simple_log_format: description: | Defines the format of log messages for simple logging configuration @@ -1386,11 +1388,13 @@ api: default: "8080" workers: description: | - Number of workers to run on the API server + Number of workers to run on the API server. Should be roughly equal to the number of cpu cores + available. If you need to scale the API server, strongly consider deploying multiple API servers + instead of increasing the number of workers; See https://github.com/apache/airflow/issues/52270. version_added: ~ type: integer example: ~ - default: "4" + default: "1" worker_timeout: description: | Number of seconds the API server waits before timing out on a worker @@ -1581,6 +1585,15 @@ workers: type: float example: ~ default: "90.0" + execution_api_timeout: + description: | + The timeout (in seconds) for HTTP requests from workers to the Execution API server. + This controls how long a worker will wait for a response from the API server before + timing out. Increase this value if you experience timeout errors under high load. + version_added: 3.1.1 + type: float + example: ~ + default: "5.0" socket_cleanup_timeout: description: | Number of seconds to wait after a task process exits before forcibly closing any @@ -1590,6 +1603,33 @@ workers: type: float example: ~ default: "60.0" + missing_dag_retires: + description: | + Maximum number of times a task will be rescheduled if the worker fails to + load the Dag or task definition during startup. + + This situation can occur due to transient infrastructure issues such as + missing Dag files, temporary filesystem or network problems, or bundle + synchronization delays. Rescheduling in this case does not count as a + task retry. + + Set this value to 0 to disable rescheduling and fail the task immediately + on startup failures. + version_added: 3.1.7 + type: integer + example: ~ + default: "3" + missing_dag_retry_delay: + description: | + Delay in seconds before a task is rescheduled after a worker startup + failure caused by an inability to load the Dag or task definition. + + This delay is applied when the task runner requests the scheduler to + reschedule the task instance in UP_FOR_RESCHEDULE state. + version_added: 3.1.7 + type: integer + example: ~ + default: "60" api_auth: description: Settings relating to authentication on the Airflow APIs options: @@ -2158,13 +2198,6 @@ scheduler: type: integer default: "20" see_also: ":ref:`scheduler:ha:tunables`" - dag_stale_not_seen_duration: - description: | - Time in seconds after which dags, which were not updated by Dag Processor are deactivated. - version_added: 2.4.0 - type: integer - example: ~ - default: "600" use_job_schedule: description: | Turn off scheduler use of cron intervals by setting this to ``False``. @@ -2467,6 +2500,11 @@ dag_processor: description: | Always run tasks with the latest code. If set to True, the bundle version will not be stored on the dag run and therefore, the latest code will always be used. + + .. note:: + + This setting only applies to bundles that support versioning and does not affect + DAG versions displayed in the UI. version_added: ~ type: boolean example: ~ diff --git a/airflow-core/src/airflow/configuration.py b/airflow-core/src/airflow/configuration.py index 24a439e848e87..fd48364cd6f09 100644 --- a/airflow-core/src/airflow/configuration.py +++ b/airflow-core/src/airflow/configuration.py @@ -346,7 +346,9 @@ def sensitive_config_values(self) -> set[tuple[str, str]]: # When reading new option, the old option will be checked to see if it exists. If it does a # DeprecationWarning will be issued and the old option will be used instead deprecated_options: dict[tuple[str, str], tuple[str, str, str]] = { + ("dag_processor", "dag_file_processor_timeout"): ("core", "dag_file_processor_timeout", "3.0"), ("dag_processor", "refresh_interval"): ("scheduler", "dag_dir_list_interval", "3.0"), + ("api", "base_url"): ("webserver", "base_url", "3.0"), ("api", "host"): ("webserver", "web_server_host", "3.0"), ("api", "port"): ("webserver", "web_server_port", "3.0"), ("api", "workers"): ("webserver", "workers", "3.0"), @@ -589,6 +591,14 @@ def _write_value( value = "\n# ".join(value_lines) file.write(f"# {option} = {value}\n") else: + if "\n" in value: + try: + value = json.dumps(json.loads(value), indent=4) + value = value.replace( + "\n", "\n " + ) # indent multi-line JSON to satisfy configparser format + except JSONDecodeError: + pass file.write(f"{option} = {value}\n") if needs_separation: file.write("\n") @@ -1235,7 +1245,7 @@ def getlist(self, section: str, key: str, delimiter=",", **kwargs): val = self.get(section, key, **kwargs) if val is None: if "fallback" in kwargs: - return val + return kwargs["fallback"] raise AirflowConfigException( f"Failed to convert value None to list. " f'Please check "{key}" key in "{section}" section is set.' @@ -1717,8 +1727,7 @@ def _deprecated_value_is_set_in_config( deprecated_section_array = config.items(section=deprecated_section, raw=True) if any(key == deprecated_key for key, _ in deprecated_section_array): return True - else: - return False + return False @staticmethod def _deprecated_variable_is_set(deprecated_section: str, deprecated_key: str) -> bool: @@ -1845,7 +1854,7 @@ def load_test_config(self): """ # We need those globals before we run "get_all_expansion_variables" because this is where # the variables are expanded from in the configuration - global FERNET_KEY, AIRFLOW_HOME, JWT_SECRET_KEY + global FERNET_KEY, JWT_SECRET_KEY from cryptography.fernet import Fernet unit_test_config_file = pathlib.Path(__file__).parent / "config_templates" / "unit_tests.cfg" diff --git a/airflow-core/src/airflow/dag_processing/bundles/base.py b/airflow-core/src/airflow/dag_processing/bundles/base.py index 1ab14abd047fb..2f4654382cb07 100644 --- a/airflow-core/src/airflow/dag_processing/bundles/base.py +++ b/airflow-core/src/airflow/dag_processing/bundles/base.py @@ -32,8 +32,8 @@ from pathlib import Path from typing import TYPE_CHECKING +import pendulum from pendulum.parsing import ParserError -from sqlalchemy_utils.types.enriched_datetime.pendulum_datetime import pendulum from airflow.configuration import conf @@ -98,7 +98,8 @@ class BundleUsageTrackingManager: def _parse_dt(self, val) -> DateTime | None: try: - return pendulum.parse(val) + dt = pendulum.parse(val) + return dt if isinstance(dt, pendulum.DateTime) else None except ParserError: return None @@ -413,7 +414,7 @@ def __init__(self, bundle_name, bundle_version, **kwargs): def _log_exc(self, msg): log.exception( - "% name=%s version=%s lock_file=%s", + "%s name=%s version=%s lock_file=%s", msg, self.bundle_name, self.version, diff --git a/airflow-core/src/airflow/dag_processing/bundles/manager.py b/airflow-core/src/airflow/dag_processing/bundles/manager.py index cf3b7c5104824..9b3cf4189c5e7 100644 --- a/airflow-core/src/airflow/dag_processing/bundles/manager.py +++ b/airflow-core/src/airflow/dag_processing/bundles/manager.py @@ -205,7 +205,11 @@ def _extract_and_sign_template(bundle_name: str) -> tuple[str | None, dict]: for name in self._bundle_config.keys(): if bundle := stored.pop(name, None): bundle.active = True - new_template, new_params = _extract_and_sign_template(name) + try: + new_template, new_params = _extract_and_sign_template(name) + except Exception as e: + self.log.exception("Error creating bundle '%s': %s", name, e) + continue if new_template != bundle.signed_url_template: bundle.signed_url_template = new_template self.log.debug("Updated URL template for bundle %s", name) @@ -213,7 +217,11 @@ def _extract_and_sign_template(bundle_name: str) -> tuple[str | None, dict]: bundle.template_params = new_params self.log.debug("Updated template parameters for bundle %s", name) else: - new_template, new_params = _extract_and_sign_template(name) + try: + new_template, new_params = _extract_and_sign_template(name) + except Exception as e: + self.log.exception("Error creating bundle '%s': %s", name, e) + continue new_bundle = DagBundleModel(name=name) new_bundle.signed_url_template = new_template new_bundle.template_params = new_params @@ -280,7 +288,12 @@ def get_all_dag_bundles(self) -> Iterable[BaseDagBundle]: :return: list of DAG bundles. """ for name, (class_, kwargs) in self._bundle_config.items(): - yield class_(name=name, version=None, **kwargs) + try: + yield class_(name=name, version=None, **kwargs) + except Exception as e: + self.log.exception("Error creating bundle '%s': %s", name, e) + # Skip this bundle and continue with others + continue def view_url(self, name: str, version: str | None = None) -> str | None: warnings.warn( diff --git a/airflow-core/src/airflow/dag_processing/collection.py b/airflow-core/src/airflow/dag_processing/collection.py index ae247b1f00352..324232c2986fa 100644 --- a/airflow-core/src/airflow/dag_processing/collection.py +++ b/airflow-core/src/airflow/dag_processing/collection.py @@ -27,10 +27,10 @@ from __future__ import annotations -import logging import traceback from typing import TYPE_CHECKING, NamedTuple, TypeVar +import structlog from sqlalchemy import delete, func, insert, select, tuple_, update from sqlalchemy.exc import OperationalError from sqlalchemy.orm import joinedload, load_only @@ -73,7 +73,7 @@ AssetT = TypeVar("AssetT", bound=BaseAsset) -log = logging.getLogger(__name__) +log = structlog.get_logger(__name__) def _create_orm_dags( @@ -158,10 +158,32 @@ def calculate(cls, dags: dict[str, LazyDeserializedDAG], *, session: Session) -> def _update_dag_tags(tag_names: set[str], dm: DagModel, *, session: Session) -> None: orm_tags = {t.name: t for t in dm.tags} + tags_to_delete = [] for name, orm_tag in orm_tags.items(): if name not in tag_names: session.delete(orm_tag) - dm.tags.extend(DagTag(name=name, dag_id=dm.dag_id) for name in tag_names.difference(orm_tags)) + tags_to_delete.append(orm_tag) + + tags_to_add = tag_names.difference(orm_tags) + if tags_to_delete: + # Remove deleted tags from the collection to keep it in sync + for tag in tags_to_delete: + dm.tags.remove(tag) + + # Check if there's a potential case-only rename on MySQL (e.g., 'tag' -> 'TAG'). + # MySQL uses case-insensitive collation for the (name, dag_id) primary key by default, + # which can cause duplicate key errors when renaming tags with only case changes. + if session.bind.dialect.name == "mysql": + orm_tags_lower = {name.lower(): name for name in orm_tags} + has_case_only_change = any(tag.lower() in orm_tags_lower for tag in tags_to_add) + + if has_case_only_change: + # Force DELETE operations to execute before INSERT operations. + session.flush() + # Refresh the tags relationship from the database to reflect the deletions. + session.expire(dm, ["tags"]) + + dm.tags.extend(DagTag(name=name, dag_id=dm.dag_id) for name in tags_to_add) def _update_dag_owner_links(dag_owner_links: dict[str, str], dm: DagModel, *, session: Session) -> None: @@ -263,21 +285,22 @@ def _update_import_errors( ): from airflow.listeners.listener import get_listener_manager - # We can remove anything from files parsed in this batch that doesn't have an error. We need to remove old - # errors (i.e. from files that are removed) separately - - session.execute( - delete(ParseImportError).where( - tuple_(ParseImportError.bundle_name, ParseImportError.filename).in_(files_parsed) - ) - ) - - # the below query has to match (bundle_name, filename) tuple in that order since the - # import_errors list is a dict with keys as (bundle_name, relative_fileloc) + # Check existing import errors BEFORE deleting, so we can determine if we should update or create existing_import_error_files = set( session.execute(select(ParseImportError.bundle_name, ParseImportError.filename)) ) - # Add the errors of the processed files + + # Delete errors for files that were parsed but don't have errors in import_errors + # (i.e., files that were successfully parsed without errors) + files_to_clear = files_parsed.difference(import_errors) + if files_to_clear: + session.execute( + delete(ParseImportError).where( + tuple_(ParseImportError.bundle_name, ParseImportError.filename).in_(files_to_clear) + ) + ) + + # Add or update the errors of the processed files for key, stacktrace in import_errors.items(): bundle_name_, relative_fileloc = key @@ -348,6 +371,7 @@ def update_dag_parsing_results_in_db( session: Session, *, warning_types: tuple[DagWarningType] = (DagWarningType.NONEXISTENT_POOL,), + files_parsed: set[tuple[str, str]] | None = None, ): """ Update everything to do with DAG parsing in the DB. @@ -365,6 +389,10 @@ def update_dag_parsing_results_in_db( then all warnings and errors related to this file will be removed. ``import_errors`` will be updated in place with an new errors + + :param files_parsed: Set of (bundle_name, relative_fileloc) tuples for all files that were parsed. + If None, will be inferred from dags and import_errors. Passing this explicitly ensures that + import errors are cleared for files that were parsed but no longer contain DAGs. """ # Retry 'DAG.bulk_write_to_db' & 'SerializedDagModel.bulk_sync_to_db' in case # of any Operational Errors @@ -400,16 +428,8 @@ def update_dag_parsing_results_in_db( import_errors.update(serialize_errors) # Record import errors into the ORM - we don't retry on this one as it's not as critical that it works try: - # TODO: This won't clear errors for files that exist that no longer contain DAGs. Do we need to pass - # in the list of file parsed? - - good_dag_filelocs = { - (bundle_name, dag.relative_fileloc) - for dag in dags - if dag.relative_fileloc is not None and (bundle_name, dag.relative_fileloc) not in import_errors - } _update_import_errors( - files_parsed=good_dag_filelocs, + files_parsed=files_parsed if files_parsed is not None else set(), bundle_name=bundle_name, import_errors=import_errors, session=session, diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index 2b81e10019a86..fed39d77675f9 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -21,7 +21,7 @@ import contextlib import functools -import importlib +import gc import inspect import logging import os @@ -35,7 +35,6 @@ from collections.abc import Callable, Iterable, Iterator from dataclasses import dataclass, field from datetime import datetime, timedelta -from importlib import import_module from operator import attrgetter, itemgetter from pathlib import Path from typing import TYPE_CHECKING, Any, NamedTuple, cast @@ -47,7 +46,6 @@ from tabulate import tabulate from uuid6 import uuid7 -import airflow.models from airflow._shared.timezones import timezone from airflow.api_fastapi.execution_api.app import InProcessExecutionAPI from airflow.configuration import conf @@ -253,6 +251,16 @@ def run(self): By processing them in separate processes, we can get parallelism and isolation from potentially harmful user code. """ + # TODO: Temporary until AIP-92 removes DB access from DagProcessorManager. + # The manager needs MetastoreBackend to retrieve connections from the database + # during bundle initialization (e.g., GitDagBundle.__init__ → GitHook needs git credentials). + # This marks the manager as "server" context so ensure_secrets_backend_loaded() provides + # MetastoreBackend instead of falling back to EnvironmentVariablesBackend only. + # Child parser processes explicitly override this by setting _AIRFLOW_PROCESS_CONTEXT=client + # in _parse_file_entrypoint() to prevent inheriting server privileges. + # Related: https://github.com/apache/airflow/pull/57459 + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "server" + self.register_exit_signals() self.log.info("Processing files using up to %s processes at a time ", self._parallelism) @@ -272,6 +280,9 @@ def run(self): self._symlink_latest_log_directory() + # To prevent COW in forked process parsing dag file + gc.freeze() + return self._run_parsing_loop() def _scan_stale_dags(self): @@ -313,7 +324,12 @@ def deactivate_stale_dags( file_info = DagFileInfo(rel_path=Path(dag.relative_fileloc), bundle_name=dag.bundle_name) if last_finish_time := last_parsed.get(file_info, None): if dag.last_parsed_time + timedelta(seconds=self.stale_dag_threshold) < last_finish_time: - self.log.info("DAG %s is missing and will be deactivated.", dag.dag_id) + self.log.info( + "Deactivating stale DAG %s. Not parsed for %s seconds (last parsed: %s).", + dag.dag_id, + int((last_finish_time - dag.last_parsed_time).total_seconds()), + dag.last_parsed_time, + ) to_deactivate.add(dag.dag_id) if to_deactivate: @@ -453,13 +469,19 @@ def _fetch_callbacks( callback_queue: list[CallbackRequest] = [] with prohibit_commit(session) as guard: + bundle_names = [bundle.name for bundle in self._dag_bundles] query = select(DbCallbackRequest) - query = query.order_by(DbCallbackRequest.priority_weight.asc()).limit(self.max_callbacks_per_loop) + query = query.order_by(DbCallbackRequest.priority_weight.desc()).limit( + self.max_callbacks_per_loop + ) query = with_row_locks(query, of=DbCallbackRequest, session=session, skip_locked=True) callbacks = session.scalars(query) for callback in callbacks: + req = callback.get_callback_request() + if req.bundle_name not in bundle_names: + continue try: - callback_queue.append(callback.get_callback_request()) + callback_queue.append(req) session.delete(callback) except Exception as e: self.log.warning("Error adding callback for execution: %s, %s", callback, e) @@ -611,7 +633,7 @@ def find_zipped_dags(abs_path: os.PathLike) -> Iterator[str]: if might_contain_dag(info.filename, True, z): yield os.path.join(abs_path, info.filename) except zipfile.BadZipFile: - self.log.exception("There was an error accessing ZIP file %s %s", abs_path) + self.log.exception("There was an error accessing ZIP file %s", abs_path) rel_filelocs: list[str] = [] for info in present: @@ -802,8 +824,9 @@ def terminate_orphan_processes(self, present: set[DagFileInfo]): processor = self._processors.pop(file, None) if not processor: continue - self.log.warning("Stopping processor for %s", file) - Stats.decr("dag_processing.processes", tags={"file_path": file, "action": "stop"}) + file_name = str(file.rel_path) + self.log.warning("Stopping processor for %s", file_name) + Stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "stop"}) processor.kill(signal.SIGKILL) processor.logger_filehandle.close() self._file_stats.pop(file, None) @@ -818,6 +841,12 @@ def _collect_results(self, session: Session = NEW_SESSION): continue finished.append(file) + # Detect if this was callback-only processing + # For such-cases, we don't serialize the dags and hence send parsing_result as None. + is_callback_only = proc.had_callbacks and proc.parsing_result is None + if is_callback_only: + self.log.debug("Detected callback-only processing for %s", file) + # Collect the DAGS and import errors into the DB, emit metrics etc. self._file_stats[file] = process_parse_results( run_duration=time.monotonic() - proc.start_time, @@ -827,6 +856,8 @@ def _collect_results(self, session: Session = NEW_SESSION): bundle_version=self._bundle_versions[file.bundle_name], parsing_result=proc.parsing_result, session=session, + is_callback_only=is_callback_only, + relative_fileloc=str(file.rel_path), ) for file in finished: @@ -876,7 +907,7 @@ def _get_logger_for_dag_file(self, dag_file: DagFileInfo): log_file = init_log_file(log_filename) logger_filehandle = log_file.open("ab") underlying_logger = structlog.BytesLogger(logger_filehandle) - processors = logging_processors(enable_pretty_log=False)[0] + processors = logging_processors(json_output=True) return structlog.wrap_logger( underlying_logger, processors=processors, logger_name="processor" ).bind(), logger_filehandle @@ -916,7 +947,7 @@ def _start_new_processes(self): continue processor = self._create_process(file) - Stats.incr("dag_processing.processes", tags={"file_path": file, "action": "start"}) + Stats.incr("dag_processing.processes", tags={"file_path": str(file.rel_path), "action": "start"}) self._processors[file] = processor Stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue)) @@ -1027,8 +1058,9 @@ def _kill_timed_out_processors(self): processor.pid, duration, ) - Stats.decr("dag_processing.processes", tags={"file_path": file, "action": "timeout"}) - Stats.incr("dag_processing.processor_timeouts", tags={"file_path": file}) + file_name = str(file.rel_path) + Stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "timeout"}) + Stats.incr("dag_processing.processor_timeouts", tags={"file_path": file_name}) processor.kill(signal.SIGKILL) processors_to_remove.append(file) @@ -1069,7 +1101,9 @@ def terminate(self): """Stop all running processors.""" for file, processor in self._processors.items(): # todo: AIP-66 what to do about file_path tag? replace with bundle name and rel path? - Stats.decr("dag_processing.processes", tags={"file_path": file, "action": "terminate"}) + Stats.decr( + "dag_processing.processes", tags={"file_path": str(file.rel_path), "action": "terminate"} + ) # SIGTERM, wait 5s, SIGKILL if still alive processor.kill(signal.SIGTERM, escalation_delay=5.0) @@ -1102,29 +1136,6 @@ def emit_metrics(self): ) -def reload_configuration_for_dag_processing(): - # Reload configurations and settings to avoid collision with parent process. - # Because this process may need custom configurations that cannot be shared, - # e.g. RotatingFileHandler. And it can cause connection corruption if we - # do not recreate the SQLA connection pool. - os.environ["CONFIG_PROCESSOR_MANAGER_LOGGER"] = "True" - os.environ["AIRFLOW__LOGGING__COLORED_CONSOLE_LOG"] = "False" - # Replicating the behavior of how logging module was loaded - # in logging_config.py - # TODO: This reloading should be removed when we fix our logging behaviour - # In case of "spawn" method of starting processes for multiprocessing, reinitializing of the - # SQLAlchemy engine causes extremely unexpected behaviour of messing with objects already loaded - # in a parent process (likely via resources shared in memory by the ORM libraries). - # This caused flaky tests in our CI for many months and has been discovered while - # iterating on https://github.com/apache/airflow/pull/19860 - # The issue that describes the problem and possible remediation is - # at https://github.com/apache/airflow/issues/19934 - importlib.reload(import_module(airflow.settings.LOGGING_CLASS_PATH.rsplit(".", 1)[0])) - importlib.reload(airflow.settings) - airflow.settings.initialize() - del os.environ["CONFIG_PROCESSOR_MANAGER_LOGGER"] - - def process_parse_results( run_duration: float, finish_time: datetime, @@ -1133,13 +1144,25 @@ def process_parse_results( bundle_version: str | None, parsing_result: DagFileParsingResult | None, session: Session, + *, + is_callback_only: bool = False, + relative_fileloc: str | None = None, ) -> DagFileStat: """Take the parsing result and stats about the parser process and convert it into a DagFileStat.""" - stat = DagFileStat( - last_finish_time=finish_time, - last_duration=run_duration, - run_count=run_count + 1, - ) + if is_callback_only: + # Callback-only processing - don't update timestamps to avoid stale DAG detection issues + stat = DagFileStat( + last_duration=run_duration, + run_count=run_count, # Don't increment for callback-only processing + ) + Stats.incr("dag_processing.callback_only_count") + else: + # Actual DAG parsing or import error + stat = DagFileStat( + last_finish_time=finish_time, + last_duration=run_duration, + run_count=run_count + 1, + ) # TODO: AIP-66 emit metrics # file_name = Path(dag_file.path).stem @@ -1147,7 +1170,10 @@ def process_parse_results( # Stats.timing("dag_processing.last_duration", stat.last_duration, tags={"file_name": file_name}) if parsing_result is None: - stat.import_errors = 1 + # No DAGs were parsed - this happens for callback-only processing + # Don't treat this as an import error when it's callback-only + if not is_callback_only: + stat.import_errors = 1 else: # record DAGs and import errors to database import_errors = {} @@ -1155,6 +1181,14 @@ def process_parse_results( import_errors = { (bundle_name, rel_path): error for rel_path, error in parsing_result.import_errors.items() } + + # Build the set of files that were parsed. This includes the file that was parsed, + # even if it no longer contains DAGs, so we can clear old import errors. + files_parsed: set[tuple[str, str]] | None = None + if relative_fileloc is not None: + files_parsed = {(bundle_name, relative_fileloc)} + files_parsed.update(import_errors.keys()) + update_dag_parsing_results_in_db( bundle_name=bundle_name, bundle_version=bundle_version, @@ -1163,6 +1197,7 @@ def process_parse_results( parse_duration=run_duration, warnings=set(parsing_result.warnings or []), session=session, + files_parsed=files_parsed, ) stat.num_dags = len(parsing_result.serialized_dags) if parsing_result.import_errors: diff --git a/airflow-core/src/airflow/dag_processing/processor.py b/airflow-core/src/airflow/dag_processing/processor.py index d4c73e61fea56..ac30dc03358b7 100644 --- a/airflow-core/src/airflow/dag_processing/processor.py +++ b/airflow-core/src/airflow/dag_processing/processor.py @@ -31,10 +31,11 @@ from airflow.callbacks.callback_requests import ( CallbackRequest, DagCallbackRequest, - EmailNotificationRequest, + EmailRequest, TaskCallbackRequest, ) from airflow.configuration import conf +from airflow.exceptions import TaskNotFound from airflow.models.dagbag import DagBag from airflow.sdk.execution_time.comms import ( ConnectionResult, @@ -43,13 +44,24 @@ GetConnection, GetPreviousDagRun, GetPrevSuccessfulDagRun, + GetTaskStates, + GetTICount, GetVariable, + GetXCom, + GetXComCount, + GetXComSequenceItem, + GetXComSequenceSlice, MaskSecret, OKResponse, PreviousDagRunResult, PrevSuccessfulDagRunResult, PutVariable, + TaskStatesResult, VariableResult, + XComCountResponse, + XComResult, + XComSequenceIndexResult, + XComSequenceSliceResult, ) from airflow.sdk.execution_time.supervisor import WatchedSubprocess from airflow.sdk.execution_time.task_runner import RuntimeTaskInstance, _send_task_error_email @@ -63,7 +75,10 @@ from airflow.api_fastapi.execution_api.app import InProcessExecutionAPI from airflow.sdk.api.client import Client + from airflow.sdk.bases.operator import BaseOperator from airflow.sdk.definitions.context import Context + from airflow.sdk.definitions.dag import DAG + from airflow.sdk.definitions.mappedoperator import MappedOperator from airflow.typing_compat import Self @@ -104,9 +119,15 @@ class DagFileParsingResult(BaseModel): | GetConnection | GetVariable | PutVariable + | GetTaskStates + | GetTICount | DeleteVariable | GetPrevSuccessfulDagRun | GetPreviousDagRun + | GetXCom + | GetXComCount + | GetXComSequenceItem + | GetXComSequenceSlice | MaskSecret, Field(discriminator="type"), ] @@ -115,10 +136,15 @@ class DagFileParsingResult(BaseModel): DagFileParseRequest | ConnectionResult | VariableResult + | TaskStatesResult | PreviousDagRunResult | PrevSuccessfulDagRunResult | ErrorResponse - | OKResponse, + | OKResponse + | XComCountResponse + | XComResult + | XComSequenceIndexResult + | XComSequenceSliceResult, Field(discriminator="type"), ] @@ -140,11 +166,15 @@ def _pre_import_airflow_modules(file_path: str, log: FilteringBoundLogger) -> No for module in iter_airflow_imports(file_path): try: importlib.import_module(module) - except ModuleNotFoundError as e: + except Exception as e: log.warning("Error when trying to pre-import module '%s' found in %s: %s", module, file_path, e) def _parse_file_entrypoint(): + # Mark as client-side (runs user DAG code) + # Prevents inheriting server context from parent DagProcessorManager + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "client" + import structlog from airflow.sdk.execution_time import comms, task_runner @@ -218,6 +248,39 @@ def _serialize_dags( return serialized_dags, serialization_import_errors +def _get_dag_with_task( + dagbag: DagBag, dag_id: str, task_id: str | None = None +) -> tuple[DAG, BaseOperator | MappedOperator | None]: + """ + Retrieve a DAG and optionally a task from the DagBag. + + :param dagbag: DagBag to retrieve from + :param dag_id: DAG ID to retrieve + :param task_id: Optional task ID to retrieve from the DAG + :return: tuple of (dag, task) where task is None if not requested + :raises ValueError: If DAG or task is not found + """ + if dag_id not in dagbag.dags: + raise ValueError( + f"DAG '{dag_id}' not found in DagBag. " + f"This typically indicates a race condition where the DAG was removed or failed to parse." + ) + + dag = dagbag.dags[dag_id] + + if task_id is not None: + try: + task = dag.get_task(task_id) + return dag, task + except TaskNotFound: + raise ValueError( + f"Task '{task_id}' not found in DAG '{dag_id}'. " + f"This typically indicates a race condition where the task was removed or the DAG structure changed." + ) from None + + return dag, None + + def _execute_callbacks( dagbag: DagBag, callback_requests: list[CallbackRequest], log: FilteringBoundLogger ) -> None: @@ -227,15 +290,14 @@ def _execute_callbacks( _execute_task_callbacks(dagbag, request, log) elif isinstance(request, DagCallbackRequest): _execute_dag_callbacks(dagbag, request, log) - elif isinstance(request, EmailNotificationRequest): + elif isinstance(request, EmailRequest): _execute_email_callbacks(dagbag, request, log) def _execute_dag_callbacks(dagbag: DagBag, request: DagCallbackRequest, log: FilteringBoundLogger) -> None: from airflow.sdk.api.datamodels._generated import TIRunContext - dag = dagbag.dags[request.dag_id] - + dag, _ = _get_dag_with_task(dagbag, request.dag_id) callbacks = dag.on_failure_callback if request.is_failure_callback else dag.on_success_callback if not callbacks: log.warning("Callback requested, but dag didn't have any", dag_id=request.dag_id) @@ -287,8 +349,10 @@ def _execute_task_callbacks(dagbag: DagBag, request: TaskCallbackRequest, log: F ) return - dag = dagbag.dags[request.ti.dag_id] - task = dag.get_task(request.ti.task_id) + dag, task = _get_dag_with_task(dagbag, request.ti.dag_id, request.ti.task_id) + + if TYPE_CHECKING: + assert task is not None if request.task_callback_type is TaskInstanceState.UP_FOR_RETRY: callbacks = task.on_retry_callback @@ -338,12 +402,12 @@ def get_callback_representation(callback): log.exception("Error in callback at index %d: %s", idx, callback_repr) -def _execute_email_callbacks( - dagbag: DagBag, request: EmailNotificationRequest, log: FilteringBoundLogger -) -> None: +def _execute_email_callbacks(dagbag: DagBag, request: EmailRequest, log: FilteringBoundLogger) -> None: """Execute email notification for task failure/retry.""" - dag = dagbag.dags[request.ti.dag_id] - task = dag.get_task(request.ti.task_id) + dag, task = _get_dag_with_task(dagbag, request.ti.dag_id, request.ti.task_id) + + if TYPE_CHECKING: + assert task is not None if not task.email: log.warning( @@ -422,6 +486,7 @@ class DagFileProcessorProcess(WatchedSubprocess): logger_filehandle: BinaryIO parsing_result: DagFileParsingResult | None = None decoder: ClassVar[TypeAdapter[ToManager]] = TypeAdapter[ToManager](ToManager) + had_callbacks: bool = False # Track if this process was started with callbacks to prevent stale DAG detection false positives client: Client """The HTTP client to use for communication with the API server.""" @@ -442,6 +507,7 @@ def start( # type: ignore[override] _pre_import_airflow_modules(os.fspath(path), logger) proc: Self = super().start(target=target, client=client, **kwargs) + proc.had_callbacks = bool(callbacks) # Track if this process had callbacks proc._on_child_started(callbacks, path, bundle_path) return proc @@ -459,7 +525,12 @@ def _on_child_started( self.send_msg(msg, request_id=0) def _handle_request(self, msg: ToManager, log: FilteringBoundLogger, req_id: int) -> None: - from airflow.sdk.api.datamodels._generated import ConnectionResponse, VariableResponse + from airflow.sdk.api.datamodels._generated import ( + ConnectionResponse, + TaskStatesResponse, + VariableResponse, + XComSequenceIndexResponse, + ) resp: BaseModel | None = None dump_opts = {} @@ -496,11 +567,62 @@ def _handle_request(self, msg: ToManager, log: FilteringBoundLogger, req_id: int dagrun_result = PrevSuccessfulDagRunResult.from_dagrun_response(dagrun_resp) resp = dagrun_result dump_opts = {"exclude_unset": True} + elif isinstance(msg, GetXCom): + xcom = self.client.xcoms.get( + msg.dag_id, msg.run_id, msg.task_id, msg.key, msg.map_index, msg.include_prior_dates + ) + xcom_result = XComResult.from_xcom_response(xcom) + resp = xcom_result + elif isinstance(msg, GetXComCount): + resp = self.client.xcoms.head(msg.dag_id, msg.run_id, msg.task_id, msg.key) + elif isinstance(msg, GetXComSequenceItem): + xcom = self.client.xcoms.get_sequence_item( + msg.dag_id, msg.run_id, msg.task_id, msg.key, msg.offset + ) + if isinstance(xcom, XComSequenceIndexResponse): + resp = XComSequenceIndexResult.from_response(xcom) + else: + resp = xcom + elif isinstance(msg, GetXComSequenceSlice): + xcoms = self.client.xcoms.get_sequence_slice( + msg.dag_id, + msg.run_id, + msg.task_id, + msg.key, + msg.start, + msg.stop, + msg.step, + msg.include_prior_dates, + ) + resp = XComSequenceSliceResult.from_response(xcoms) elif isinstance(msg, MaskSecret): # Use sdk masker in dag processor and triggerer because those use the task sdk machinery from airflow.sdk.log import mask_secret mask_secret(msg.value, msg.name) + elif isinstance(msg, GetTICount): + resp = self.client.task_instances.get_count( + dag_id=msg.dag_id, + map_index=msg.map_index, + task_ids=msg.task_ids, + task_group_id=msg.task_group_id, + logical_dates=msg.logical_dates, + run_ids=msg.run_ids, + states=msg.states, + ) + elif isinstance(msg, GetTaskStates): + task_states_map = self.client.task_instances.get_task_states( + dag_id=msg.dag_id, + map_index=msg.map_index, + task_ids=msg.task_ids, + task_group_id=msg.task_group_id, + logical_dates=msg.logical_dates, + run_ids=msg.run_ids, + ) + if isinstance(task_states_map, TaskStatesResponse): + resp = TaskStatesResult.from_api_response(task_states_map) + else: + resp = task_states_map else: log.error("Unhandled request", msg=msg) self.send_msg( diff --git a/airflow-core/src/airflow/example_dags/example_dag_decorator.py b/airflow-core/src/airflow/example_dags/example_dag_decorator.py index 9f6e637c9178e..c4c367f467324 100644 --- a/airflow-core/src/airflow/example_dags/example_dag_decorator.py +++ b/airflow-core/src/airflow/example_dags/example_dag_decorator.py @@ -51,11 +51,11 @@ def execute(self, context: Context): catchup=False, tags=["example"], ) -def example_dag_decorator(url: str = "http://httpbin.org/get"): +def example_dag_decorator(url: str = "https://httpbingo.org/get"): """ DAG to get IP address and echo it via BashOperator. - :param url: URL to get IP address from. Defaults to "http://httpbin.org/get". + :param url: URL to get IP address from. Defaults to "https://httpbingo.org/get". """ get_ip = GetRequestOperator(task_id="get_ip", url=url) diff --git a/airflow-core/src/airflow/executors/local_executor.py b/airflow-core/src/airflow/executors/local_executor.py index d85bb210e4d96..b624e6f1bdad0 100644 --- a/airflow-core/src/airflow/executors/local_executor.py +++ b/airflow-core/src/airflow/executors/local_executor.py @@ -35,8 +35,7 @@ from typing import TYPE_CHECKING from airflow.executors import workloads -from airflow.executors.base_executor import PARALLELISM, BaseExecutor -from airflow.utils.session import NEW_SESSION, provide_session +from airflow.executors.base_executor import BaseExecutor from airflow.utils.state import TaskInstanceState # add logger to parameter of setproctitle to support logging @@ -48,8 +47,6 @@ setproctitle = lambda title, logger: real_setproctitle(title) if TYPE_CHECKING: - from sqlalchemy.orm import Session - TaskInstanceStateType = tuple[workloads.TaskInstance, TaskInstanceState, Exception | None] @@ -141,10 +138,11 @@ class LocalExecutor(BaseExecutor): It uses the multiprocessing Python library and queues to parallelize the execution of tasks. - :param parallelism: how many parallel processes are run in the executor + :param parallelism: how many parallel processes are run in the executor, must be > 0 """ is_local: bool = True + is_mp_using_fork: bool = multiprocessing.get_start_method() == "fork" serve_logs: bool = True @@ -153,11 +151,6 @@ class LocalExecutor(BaseExecutor): workers: dict[int, multiprocessing.Process] _unread_messages: multiprocessing.sharedctypes.Synchronized[int] - def __init__(self, parallelism: int = PARALLELISM): - super().__init__(parallelism=parallelism) - if self.parallelism < 0: - raise ValueError("parallelism must be greater than or equal to 0") - def start(self) -> None: """Start the executor.""" # We delay opening these queues until the start method mostly for unit tests. ExecutorLoader caches @@ -171,6 +164,11 @@ def start(self) -> None: # (it looks like an int to python) self._unread_messages = multiprocessing.Value(ctypes.c_uint) + if self.is_mp_using_fork: + # This creates the maximum number of worker processes (parallelism) at once + # to minimize gc freeze/unfreeze cycles when using fork in multiprocessing + self._spawn_workers_with_gc_freeze(self.parallelism) + def _check_workers(self): # Reap any dead workers to_remove = set() @@ -193,10 +191,15 @@ def _check_workers(self): # If we're using spawn in multiprocessing (default on macOS now) to start tasks, this can get called a # via `sync()` a few times before the spawned process actually starts picking up messages. Try not to # create too much - if num_outstanding and (self.parallelism == 0 or len(self.workers) < self.parallelism): - # This only creates one worker, which is fine as we call this directly after putting a message on - # activity_queue in execute_async - self._spawn_worker() + if num_outstanding and len(self.workers) < self.parallelism: + if self.is_mp_using_fork: + # This creates the maximum number of worker processes at once + # to minimize gc freeze/unfreeze cycles when using fork in multiprocessing + self._spawn_workers_with_gc_freeze(self.parallelism - len(self.workers)) + else: + # This only creates one worker, which is fine as we call this directly after putting a message on + # activity_queue in execute_async when using spawn in multiprocessing + self._spawn_worker() def _spawn_worker(self): p = multiprocessing.Process( @@ -213,6 +216,26 @@ def _spawn_worker(self): assert p.pid # Since we've called start self.workers[p.pid] = p + def _spawn_workers_with_gc_freeze(self, spawn_number): + """ + Freeze the GC before forking worker process and unfreeze it after forking. + + This is done to prevent memory increase due to COW (Copy-on-Write) by moving all + existing objects to the permanent generation before forking the process. After forking, + unfreeze is called to ensure there is no impact on gc operations + in the original running process. + + Ref: https://docs.python.org/3/library/gc.html#gc.freeze + """ + import gc + + gc.freeze() + try: + for _ in range(spawn_number): + self._spawn_worker() + finally: + gc.unfreeze() + def sync(self) -> None: """Sync will get called periodically by the heartbeat method.""" self._read_results() @@ -253,9 +276,10 @@ def end(self) -> None: def terminate(self): """Terminate the executor is not doing anything.""" - @provide_session - def queue_workload(self, workload: workloads.All, session: Session = NEW_SESSION): - self.activity_queue.put(workload) + def _process_workloads(self, workloads): + for workload in workloads: + self.activity_queue.put(workload) + del self.queued_tasks[workload.ti.key] with self._unread_messages: - self._unread_messages.value += 1 + self._unread_messages.value += len(workloads) self._check_workers() diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py b/airflow-core/src/airflow/jobs/scheduler_job_runner.py index e8778d17d1814..237aacde975fb 100644 --- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py +++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py @@ -30,7 +30,7 @@ from datetime import date, datetime, timedelta from functools import lru_cache, partial from itertools import groupby -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from sqlalchemy import and_, delete, desc, exists, func, inspect, or_, select, text, tuple_, update from sqlalchemy.exc import OperationalError @@ -43,11 +43,12 @@ from airflow.callbacks.callback_requests import ( DagCallbackRequest, DagRunContext, - EmailNotificationRequest, + EmailRequest, TaskCallbackRequest, ) from airflow.configuration import conf from airflow.dag_processing.bundles.base import BundleUsageTrackingManager +from airflow.exceptions import DagNotFound from airflow.executors import workloads from airflow.jobs.base_job_runner import BaseJobRunner from airflow.jobs.job import Job, JobState, perform_heartbeat @@ -60,6 +61,7 @@ AssetModel, DagScheduleAssetAliasReference, DagScheduleAssetReference, + TaskInletAssetReference, TaskOutletAssetReference, asset_trigger_association_table, ) @@ -89,17 +91,16 @@ from airflow.utils.types import DagRunTriggeredByType, DagRunType if TYPE_CHECKING: - import logging from types import FrameType from pendulum.datetime import DateTime - from sqlalchemy.orm import Query, Session + from sqlalchemy.orm import Load, Query, Session + from airflow._shared.logging.types import Logger from airflow.executors.base_executor import BaseExecutor from airflow.executors.executor_utils import ExecutorName - from airflow.models.mappedoperator import MappedOperator from airflow.models.taskinstance import TaskInstanceKey - from airflow.serialization.serialized_objects import SerializedBaseOperator, SerializedDAG + from airflow.serialization.serialized_objects import SerializedDAG from airflow.utils.sqlalchemy import CommitProhibitorGuard TI = TaskInstance @@ -110,6 +111,31 @@ """:meta private:""" +def _eager_load_dag_run_for_validation() -> tuple[Load, Load]: + """ + Eager-load DagRun relations required for execution API datamodel validation. + + When building TaskCallbackRequest with DRDataModel.model_validate(ti.dag_run), + the consumed_asset_events collection and nested asset/source_aliases must be + preloaded to avoid DetachedInstanceError after the session closes. + + Returns a tuple of two load options: + - Asset loader: TI.dag_run → consumed_asset_events → asset + - Alias loader: TI.dag_run → consumed_asset_events → source_aliases + + Example usage:: + + asset_loader, alias_loader = _eager_load_dag_run_for_validation() + query = select(TI).options(asset_loader).options(alias_loader) + """ + # Traverse TI → dag_run → consumed_asset_events once, then branch to asset/aliases + base = joinedload(TI.dag_run).selectinload(DagRun.consumed_asset_events) + return ( + base.selectinload(AssetEvent.asset), + base.selectinload(AssetEvent.source_aliases), + ) + + def _get_current_dag(dag_id: str, session: Session) -> SerializedDAG | None: serdag = SerializedDagModel.get(dag_id=dag_id, session=session) # grabs the latest version if not serdag: @@ -189,7 +215,7 @@ def __init__( job: Job, num_runs: int = conf.getint("scheduler", "num_runs"), scheduler_idle_sleep_time: float = conf.getfloat("scheduler", "scheduler_idle_sleep_time"), - log: logging.Logger | None = None, + log: Logger | None = None, ): super().__init__(job) self.num_runs = num_runs @@ -198,7 +224,6 @@ def __init__( self._task_instance_heartbeat_timeout_secs = conf.getint( "scheduler", "task_instance_heartbeat_timeout" ) - self._dag_stale_not_seen_duration = conf.getint("scheduler", "dag_stale_not_seen_duration") self._task_queued_timeout = conf.getfloat("scheduler", "task_queued_timeout") self._enable_tracemalloc = conf.getboolean("scheduler", "enable_tracemalloc") @@ -807,11 +832,12 @@ def process_executor_events( # Check state of finished tasks filter_for_tis = TI.filter_for_tis(tis_with_right_state) + asset_loader, _ = _eager_load_dag_run_for_validation() query = ( select(TI) .where(filter_for_tis) .options(selectinload(TI.dag_model)) - .options(joinedload(TI.dag_run).selectinload(DagRun.consumed_asset_events)) + .options(asset_loader) .options(joinedload(TI.dag_run).selectinload(DagRun.created_dag_version)) .options(joinedload(TI.dag_version)) ) @@ -910,16 +936,15 @@ def process_executor_events( # Get task from the Serialized DAG try: dag = scheduler_dag_bag.get_dag_for_run(dag_run=ti.dag_run, session=session) - cls.logger().error( - "DAG '%s' for task instance %s not found in serialized_dag table", - ti.dag_id, - ti, - ) - if TYPE_CHECKING: - assert dag - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - task = cast("MappedOperator | SerializedBaseOperator", dag.get_task(ti.task_id)) + if not dag: + cls.logger().error( + "DAG '%s' for task instance %s not found in serialized_dag table", + ti.dag_id, + ti, + ) + raise DagNotFound(f"DAG '{ti.dag_id}' not found in serialized_dag table") + + task = dag.get_task(ti.task_id) except Exception: cls.logger().exception("Marking task instance %s as %s", ti, state) ti.set_state(state) @@ -935,6 +960,11 @@ def process_executor_events( bundle_version=ti.dag_version.bundle_version, ti=ti, msg=msg, + task_callback_type=( + TaskInstanceState.UP_FOR_RETRY + if ti.is_eligible_to_retry() + else TaskInstanceState.FAILED + ), context_from_server=TIRunContext( dag_run=DRDataModel.model_validate(ti.dag_run, from_attributes=True), max_tries=ti.max_tries, @@ -961,7 +991,7 @@ def process_executor_events( "Sending email request for task %s to DAG Processor", ti, ) - email_request = EmailNotificationRequest( + email_request = EmailRequest( filepath=ti.dag_model.relative_fileloc, bundle_name=ti.dag_version.bundle_name, bundle_version=ti.dag_version.bundle_version, @@ -1017,6 +1047,11 @@ def set_ti_span_attrs(cls, span, state, ti): span.add_event(name="airflow.task.ended", timestamp=datetime_to_nano(ti.end_date)) def _execute(self) -> int | None: + import os + + # Mark this as a server context for secrets backend detection + os.environ["_AIRFLOW_PROCESS_CONTEXT"] = "server" + self.log.info("Starting the scheduler") reset_signals = self.register_signals() @@ -1729,12 +1764,41 @@ def _should_update_dag_next_dagruns( return False return True + def _lock_backfills(self, dag_runs: Collection[DagRun], session: Session) -> dict[int, Backfill]: + """ + Lock Backfill rows to prevent race conditions when multiple schedulers run concurrently. + + :param dag_runs: Collection of Dag runs to process + :param session: DB session + :return: Dict mapping backfill_id to locked Backfill objects + """ + if not (backfill_ids := {dr.backfill_id for dr in dag_runs if dr.backfill_id is not None}): + return {} + + locked_backfills = { + b.id: b + for b in session.scalars( + select(Backfill).where(Backfill.id.in_(backfill_ids)).with_for_update(skip_locked=True) + ) + } + + if skipped_backfills := backfill_ids - locked_backfills.keys(): + self.log.debug( + "Skipping backfill runs for backfill_ids=%s - locked by another scheduler", + skipped_backfills, + ) + + return locked_backfills + @add_debug_span def _start_queued_dagruns(self, session: Session) -> None: """Find DagRuns in queued state and decide moving them to running state.""" # added all() to save runtime, otherwise query is executed more than once dag_runs: Collection[DagRun] = DagRun.get_queued_dag_runs_to_set_running(session).all() + # Lock backfills to prevent race conditions with concurrent schedulers + locked_backfills = self._lock_backfills(dag_runs, session) + query = ( select( DagRun.dag_id, @@ -1798,13 +1862,16 @@ def _update_state(dag: SerializedDAG, dag_run: DagRun): dag_id = dag_run.dag_id run_id = dag_run.run_id backfill_id = dag_run.backfill_id - backfill = dag_run.backfill dag = dag_run.dag = cached_get_dag(dag_run) if not dag: self.log.error("DAG '%s' not found in serialized_dag table", dag_run.dag_id) continue active_runs = active_runs_of_dags[(dag_id, backfill_id)] if backfill_id is not None: + if backfill_id not in locked_backfills: + # Another scheduler has this backfill locked, skip this run + continue + backfill = dag_run.backfill if active_runs >= backfill.max_active_runs: # todo: delete all "candidate dag runs" from list for this dag right now self.log.info( @@ -1816,8 +1883,10 @@ def _update_state(dag: SerializedDAG, dag_run: DagRun): run_id, ) continue - elif dag.max_active_runs: - if active_runs >= dag.max_active_runs: + elif dag_run.max_active_runs: + # Using dag_run.max_active_runs which links to DagModel to ensure we are checking + # against the most recent changes on the dag and not using stale serialized dag + if active_runs >= dag_run.max_active_runs: # todo: delete all candidate dag runs for this dag from list right now self.log.info( "dag cannot be started due to dag max_active_runs constraint; " @@ -1910,6 +1979,14 @@ def _schedule_dag_run( ): dag_model.calculate_dagrun_date_fields(dag, get_run_data_interval(dag.timetable, dag_run)) + dag_run = session.scalar( + select(DagRun) + .where(DagRun.id == dag_run.id) + .options( + selectinload(DagRun.consumed_asset_events).selectinload(AssetEvent.asset), + selectinload(DagRun.consumed_asset_events).selectinload(AssetEvent.source_aliases), + ) + ) callback_to_execute = DagCallbackRequest( filepath=dag_model.relative_fileloc, dag_id=dag.dag_id, @@ -1925,9 +2002,11 @@ def _schedule_dag_run( ) dag_run.notify_dagrun_state_changed() - duration = dag_run.end_date - dag_run.start_date - Stats.timing(f"dagrun.duration.failed.{dag_run.dag_id}", duration) - Stats.timing("dagrun.duration.failed", duration, tags={"dag_id": dag_run.dag_id}) + duration: timedelta | None = None + if dag_run.end_date and dag_run.start_date: + duration = dag_run.end_date - dag_run.start_date + Stats.timing(f"dagrun.duration.failed.{dag_run.dag_id}", duration) + Stats.timing("dagrun.duration.failed", duration, tags={"dag_id": dag_run.dag_id}) span.set_attribute("error", True) if span.is_recording(): span.add_event( @@ -2288,11 +2367,25 @@ def adopt_or_reset_orphaned_tasks(self, session: Session = NEW_SESSION) -> int: reset_tis_message = [] for ti in to_reset: reset_tis_message.append(repr(ti)) + # If we reset a TI, it will be eligible to be scheduled again. + # This can cause the scheduler to increase the try_number on the TI. + # Record the current try to TaskInstanceHistory first so users have an audit trail for + # the attempt that was abandoned. + ti.prepare_db_for_next_try(session=session) + ti.state = None ti.queued_by_job_id = None + ti.external_executor_id = None + ti.clear_next_method_args() for ti in set(tis_to_adopt_or_reset) - set(to_reset): ti.queued_by_job_id = self.job.id + # If old ti from Airflow 2 and last_heartbeat_at is None, set last_heartbeat_at to now + if ti.last_heartbeat_at is None: + ti.last_heartbeat_at = timezone.utcnow() + # If old ti from Airflow 2 and dag_run.conf is None, set dag_run.conf to {} + if ti.dag_run.conf is None: + ti.dag_run.conf = {} Stats.incr("scheduler.orphaned_tasks.cleared", len(to_reset)) Stats.incr("scheduler.orphaned_tasks.adopted", len(tis_to_adopt_or_reset) - len(to_reset)) @@ -2361,10 +2454,12 @@ def _find_and_purge_task_instances_without_heartbeats(self) -> None: def _find_task_instances_without_heartbeats(self, *, session: Session) -> list[TI]: self.log.debug("Finding 'running' jobs without a recent heartbeat") limit_dttm = timezone.utcnow() - timedelta(seconds=self._task_instance_heartbeat_timeout_secs) + asset_loader, alias_loader = _eager_load_dag_run_for_validation() task_instances_without_heartbeats = session.scalars( select(TI) .options(selectinload(TI.dag_model)) - .options(selectinload(TI.dag_run).selectinload(DagRun.consumed_asset_events)) + .options(asset_loader) + .options(alias_loader) .options(selectinload(TI.dag_version)) .with_hint(TI, "USE INDEX (ti_state)", dialect_name="mysql") .join(DM, TI.dag_id == DM.dag_id) @@ -2389,6 +2484,13 @@ def _purge_task_instances_without_heartbeats( task_instance_heartbeat_timeout_message_details = ( self._generate_task_instance_heartbeat_timeout_message_details(ti) ) + if not ti.dag_version: + # If old ti from Airflow 2 and dag_version is None, skip heartbeat timeout handling. + self.log.warning( + "DAG Version not found for TaskInstance %s. Skipping heartbeat timeout handling.", + ti, + ) + continue request = TaskCallbackRequest( filepath=ti.dag_model.relative_fileloc, bundle_name=ti.dag_version.bundle_name, @@ -2471,20 +2573,26 @@ def _update_asset_orphanage(self, session: Session = NEW_SESSION) -> None: """ Check assets orphanization and update their active entry. - An orphaned asset is no longer referenced in any DAG schedule parameters - or task outlets. Active assets (non-orphaned) have entries in AssetActive - and must have unique names and URIs. + An orphaned asset is no longer referenced in any DAG schedule parameters, + task outlets, or task inlets. Active assets (non-orphaned) have entries in + AssetActive and must have unique names and URIs. :seealso: :meth:`AssetModelOperation.activate_assets_if_possible`. """ # Group assets into orphaned=True and orphaned=False groups. orphaned = ( - (func.count(DagScheduleAssetReference.dag_id) + func.count(TaskOutletAssetReference.dag_id)) == 0 + ( + func.count(DagScheduleAssetReference.dag_id) + + func.count(TaskOutletAssetReference.dag_id) + + func.count(TaskInletAssetReference.dag_id) + ) + == 0 ).label("orphaned") asset_reference_query = session.execute( select(orphaned, AssetModel) .outerjoin(DagScheduleAssetReference) .outerjoin(TaskOutletAssetReference) + .outerjoin(TaskInletAssetReference) .group_by(AssetModel.id) .order_by(orphaned) ) diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py b/airflow-core/src/airflow/jobs/triggerer_job_runner.py index 63deea50f450f..cc78e21245aef 100644 --- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py +++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py @@ -70,12 +70,13 @@ UpdateHITLDetail, VariableResult, XComResult, + _new_encoder, _RequestFrame, ) from airflow.sdk.execution_time.supervisor import WatchedSubprocess, make_buffered_socket_reader +from airflow.triggers.base import BaseEventTrigger, BaseTrigger, DiscrimatedTriggerEvent, TriggerEvent from airflow.stats import Stats from airflow.traces.tracer import DebugTrace, Trace, add_debug_span -from airflow.triggers import base as events from airflow.utils.helpers import log_filename_template_renderer from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.module_loading import import_string @@ -155,12 +156,13 @@ def _exit_gracefully(self, signum, frame) -> None: self.trigger_runner.stop = True else: self.log.warning("Forcing exit due to second exit signal %s", signum) - - self.trigger_runner.kill(signal.SIGKILL) + if self.trigger_runner: + self.trigger_runner.kill(signal.SIGKILL) sys.exit(os.EX_SOFTWARE) def _execute(self) -> int | None: self.log.info("Starting the triggerer") + self.register_signals() try: # Kick off runner sub-process without DB access self.trigger_runner = TriggerRunnerSupervisor.start( @@ -202,7 +204,7 @@ class TriggerStateChanges(BaseModel): type: Literal["TriggerStateChanges"] = "TriggerStateChanges" events: Annotated[ - list[tuple[int, events.DiscrimatedTriggerEvent]] | None, + list[tuple[int, DiscrimatedTriggerEvent]] | None, # We have to specify a default here, as otherwise Pydantic struggles to deal with the discriminated # union :shrug: Field(default=None), @@ -354,7 +356,7 @@ class TriggerRunnerSupervisor(WatchedSubprocess): creating_triggers: deque[workloads.RunTrigger] = attrs.field(factory=deque, init=False) # Outbound queue of events - events: deque[tuple[int, events.TriggerEvent]] = attrs.field(factory=deque, init=False) + events: deque[tuple[int, TriggerEvent]] = attrs.field(factory=deque, init=False) # Outbound queue of failed triggers failed_triggers: deque[tuple[int, list[str] | None]] = attrs.field(factory=deque, init=False) @@ -650,7 +652,13 @@ def update_triggers(self, requested_trigger_ids: set[int]): ) if new_trigger_orm.task_instance: log_path = render_log_fname(ti=new_trigger_orm.task_instance) - + if not new_trigger_orm.task_instance.dag_version_id: + # This is to handle 2 to 3 upgrade where TI.dag_version_id can be none + log.warning( + "TaskInstance associated with Trigger has no associated Dag Version, skipping the trigger", + ti_id=new_trigger_orm.task_instance.id, + ) + continue ser_ti = workloads.TaskInstance.model_validate( new_trigger_orm.task_instance, from_attributes=True ) @@ -697,7 +705,7 @@ def _process_log_messages_from_subprocess(self) -> Generator[None, bytes | bytea from airflow.sdk.log import logging_processors - processors, _ = logging_processors(enable_pretty_log=False) + processors = logging_processors(json_output=True) def get_logger(trigger_id: int) -> WrappedLogger: # TODO: Is a separate dict worth it, or should we make `self.running_triggers` a dict? @@ -723,16 +731,6 @@ def get_logger(trigger_id: int) -> WrappedLogger: # Log message about the TriggerRunner itself -- just output it log = fallback_log - if ts := event.get("timestamp"): - # We use msgspec to decode the timestamp as it does it orders of magnitude quicker than - # datetime.strptime - # - # We remove the timezone info here, as the json encoding has `+00:00`, and since the log came - # from a subprocess we know that the timezone of the log message is the same, so having some - # messages include tz (from subprocess) but others not (ones from supervisor process) is - # confusing. - event["timestamp"] = msgspec.json.decode(f'"{ts}"', type=datetime).replace(tzinfo=None) - if exc := event.pop("exception", None): # TODO: convert the dict back to a pretty stack trace event["error_detail"] = exc @@ -761,8 +759,6 @@ class TriggerCommsDecoder(CommsDecoder[ToTriggerRunner, ToTriggerSupervisor]): factory=lambda: TypeAdapter(ToTriggerRunner), repr=False ) - _lock: asyncio.Lock = attrs.field(factory=asyncio.Lock, repr=False) - def _read_frame(self): from asgiref.sync import async_to_sync @@ -774,7 +770,10 @@ def send(self, msg: ToTriggerSupervisor) -> ToTriggerRunner | None: return async_to_sync(self.asend)(msg) async def _aread_frame(self): - len_bytes = await self._async_reader.readexactly(4) + try: + len_bytes = await self._async_reader.readexactly(4) + except ConnectionResetError: + asyncio.current_task().cancel("Supervisor closed") length = int.from_bytes(len_bytes, byteorder="big") if length >= 2**32: raise OverflowError(f"Refusing to receive messages larger than 4GiB {length=}") @@ -794,7 +793,7 @@ async def asend(self, msg: ToTriggerSupervisor) -> ToTriggerRunner | None: frame = _RequestFrame(id=next(self.id_counter), body=msg.model_dump()) bytes = frame.as_bytes() - async with self._lock: + async with self._async_lock: self._async_writer.write(bytes) return await self._aget_response(frame.id) @@ -821,13 +820,12 @@ class TriggerRunner: to_cancel: deque[int] # Outbound queue of events - events: deque[tuple[int, events.TriggerEvent]] + events: deque[tuple[int, TriggerEvent]] # Outbound queue of failed triggers failed_triggers: deque[tuple[int, BaseException | None]] # Should-we-stop flag - # TODO: set this in a sig-int handler stop: bool = False # TODO: connect this to the parent process @@ -845,8 +843,14 @@ def __init__(self): self.failed_triggers = deque() self.job_id = None + def _handle_signal(self, signum, frame) -> None: + """Handle termination signals gracefully.""" + self.stop = True + def run(self): - """Sync entrypoint - just run a run in an async loop.""" + """Sync entrypoint - just run arun in an async loop.""" + signal.signal(signal.SIGINT, self._handle_signal) + signal.signal(signal.SIGTERM, self._handle_signal) asyncio.run(self.arun()) async def arun(self): @@ -878,8 +882,10 @@ async def arun(self): await asyncio.sleep(1) # Every minute, log status if (now := time.monotonic()) - last_status >= 60: - count = len(self.triggers) - self.log.info("%i triggers currently running", count) + watchers = len([trigger for trigger in self.triggers.values() if trigger["is_watcher"]]) + triggers = len(self.triggers) - watchers + self.log.info("%i triggers currently running", triggers) + self.log.info("%i watchers currently running", watchers) last_status = now except Exception: @@ -962,8 +968,9 @@ async def create_triggers(self): ) self.triggers[trigger_id] = { "task": asyncio.create_task( - self.run_trigger(trigger_id, trigger_instance), name=trigger_name + self.run_trigger(trigger_id, trigger_instance, workload.timeout_after), name=trigger_name ), + "is_watcher": isinstance(trigger_instance, BaseEventTrigger), "name": trigger_name, "events": 0, } @@ -1009,7 +1016,7 @@ async def cleanup_finished_triggers(self) -> list[int]: saved_exc = e else: # See if they foolishly returned a TriggerEvent - if isinstance(result, events.TriggerEvent): + if isinstance(result, TriggerEvent): self.log.error( "Trigger returned a TriggerEvent rather than yielding it", trigger=details["name"], @@ -1029,46 +1036,78 @@ async def cleanup_finished_triggers(self) -> list[int]: await asyncio.sleep(0) return finished_ids - async def sync_state_to_supervisor(self, finished_ids: list[int]): + def process_trigger_events(self, finished_ids: list[int]) -> messages.TriggerStateChanges: # Copy out of our deques in threadsafe manner to sync state with parent - events_to_send = [] + events_to_send: list[tuple[int, DiscrimatedTriggerEvent]] = [] + failures_to_send: list[tuple[int, list[str] | None]] = [] + while self.events: - data = self.events.popleft() - events_to_send.append(data) + trigger_id, trigger_event = self.events.popleft() + events_to_send.append((trigger_id, trigger_event)) - failures_to_send = [] while self.failed_triggers: - id, exc = self.failed_triggers.popleft() + trigger_id, exc = self.failed_triggers.popleft() tb = format_exception(type(exc), exc, exc.__traceback__) if exc else None - failures_to_send.append((id, tb)) + failures_to_send.append((trigger_id, tb)) - msg = messages.TriggerStateChanges( - events=events_to_send, finished=finished_ids, failures=failures_to_send + return messages.TriggerStateChanges( + events=events_to_send if events_to_send else None, + finished=finished_ids if finished_ids else None, + failures=failures_to_send if failures_to_send else None, ) - if not events_to_send: - msg.events = None + def sanitize_trigger_events(self, msg: messages.TriggerStateChanges) -> messages.TriggerStateChanges: + req_encoder = _new_encoder() + events_to_send: list[tuple[int, DiscrimatedTriggerEvent]] = [] + + if msg.events: + for trigger_id, trigger_event in msg.events: + try: + req_encoder.encode(trigger_event) + except Exception as e: + logger.error( + "Trigger %s returned non-serializable result %r. Cancelling trigger.", + trigger_id, + trigger_event, + ) + self.failed_triggers.append((trigger_id, e)) + else: + events_to_send.append((trigger_id, trigger_event)) - if not failures_to_send: - msg.failures = None + return messages.TriggerStateChanges( + events=events_to_send if events_to_send else None, + finished=msg.finished, + failures=msg.failures, + ) - if not finished_ids: - msg.finished = None + async def sync_state_to_supervisor(self, finished_ids: list[int]) -> None: + msg = self.process_trigger_events(finished_ids=finished_ids) # Tell the monitor that we've finished triggers so it can update things try: - resp = await self.comms_decoder.asend(msg) + resp = await self.asend(msg) + except NotImplementedError: + # A non-serializable trigger event was detected, remove it and fail associated trigger + resp = await self.asend(self.sanitize_trigger_events(msg)) + + if resp: + self.to_create.extend(resp.to_create) + self.to_cancel.extend(resp.to_cancel) + + async def asend(self, msg: messages.TriggerStateChanges) -> messages.TriggerStateSync | None: + try: + response = await self.comms_decoder.asend(msg) + + if not isinstance(response, messages.TriggerStateSync): + raise RuntimeError(f"Expected to get a TriggerStateSync message, instead we got {type(msg)}") + + return response except asyncio.IncompleteReadError: if task := asyncio.current_task(): task.cancel("EOF - shutting down") return raise - if not isinstance(resp, messages.TriggerStateSync): - raise RuntimeError(f"Expected to get a TriggerStateSync message, instead we got {type(msg)}") - self.to_create.extend(resp.to_create) - self.to_cancel.extend(resp.to_cancel) - async def block_watchdog(self): """ Watchdog loop that detects blocking (badly-written) triggers. @@ -1096,8 +1135,13 @@ async def block_watchdog(self): ) Stats.incr("triggers.blocked_main_thread") - async def run_trigger(self, trigger_id, trigger): + async def run_trigger(self, trigger_id: int, trigger: BaseTrigger, timeout_after: datetime | None = None): """Run a trigger (they are async generators) and push their events into our outbound event deque.""" + if not os.environ.get("AIRFLOW_DISABLE_GREENBACK_PORTAL", "").lower() == "true": + import greenback + + await greenback.ensure_portal() + bind_log_contextvars(trigger_id=trigger_id) name = self.triggers[trigger_id]["name"] @@ -1112,7 +1156,7 @@ async def run_trigger(self, trigger_id, trigger): except asyncio.CancelledError: # We get cancelled by the scheduler changing the task state. But if we do lets give a nice error # message about it - if timeout := trigger.timeout_after: + if timeout := timeout_after: timeout = timeout.replace(tzinfo=timezone.utc) if not timeout.tzinfo else timeout if timeout < timezone.utcnow(): await self.log.aerror("Trigger cancelled due to timeout") diff --git a/airflow-core/src/airflow/lineage/hook.py b/airflow-core/src/airflow/lineage/hook.py index b69f12484dc1b..ede28a7855f03 100644 --- a/airflow-core/src/airflow/lineage/hook.py +++ b/airflow-core/src/airflow/lineage/hook.py @@ -29,6 +29,8 @@ from airflow.utils.log.logging_mixin import LoggingMixin if TYPE_CHECKING: + from pydantic.types import JsonValue + from airflow.sdk import BaseHook, ObjectStoragePath # Store context what sent lineage. @@ -107,7 +109,7 @@ def create_asset( name: str | None = None, group: str | None = None, asset_kwargs: dict | None = None, - asset_extra: dict | None = None, + asset_extra: dict[str, JsonValue] | None = None, ) -> Asset | None: """ Create an asset instance using the provided parameters. @@ -161,7 +163,7 @@ def add_input_asset( name: str | None = None, group: str | None = None, asset_kwargs: dict | None = None, - asset_extra: dict | None = None, + asset_extra: dict[str, JsonValue] | None = None, ): """Add the input asset and its corresponding hook execution context to the collector.""" if len(self._inputs) >= MAX_COLLECTED_ASSETS: @@ -186,7 +188,7 @@ def add_output_asset( name: str | None = None, group: str | None = None, asset_kwargs: dict | None = None, - asset_extra: dict | None = None, + asset_extra: dict[str, JsonValue] | None = None, ): """Add the output asset and its corresponding hook execution context to the collector.""" if len(self._outputs) >= MAX_COLLECTED_ASSETS: diff --git a/airflow-core/src/airflow/logging_config.py b/airflow-core/src/airflow/logging_config.py index a391c1c6361a8..fb29b155c9417 100644 --- a/airflow-core/src/airflow/logging_config.py +++ b/airflow-core/src/airflow/logging_config.py @@ -20,7 +20,6 @@ import logging import warnings from importlib import import_module -from logging.config import dictConfig from typing import TYPE_CHECKING, Any from airflow.configuration import conf @@ -39,7 +38,6 @@ def __getattr__(name: str): if name == "REMOTE_TASK_LOG": - global REMOTE_TASK_LOG load_logging_config() return REMOTE_TASK_LOG @@ -86,33 +84,56 @@ def load_logging_config() -> tuple[dict[str, Any], str]: def configure_logging(): + from airflow._shared.logging import configure_logging, init_log_folder, translate_config_values + logging_config, logging_class_path = load_logging_config() try: - # Ensure that the password masking filter is applied to the 'task' handler - # no matter what the user did. - if "filters" in logging_config and "mask_secrets" in logging_config["filters"]: - # But if they replace the logging config _entirely_, don't try to set this, it won't work - task_handler_config = logging_config["handlers"]["task"] - - task_handler_config.setdefault("filters", []) - - if "mask_secrets" not in task_handler_config["filters"]: - task_handler_config["filters"].append("mask_secrets") - + level: str = getattr( + logging_config, "LOG_LEVEL", conf.get("logging", "logging_level", fallback="INFO") + ).upper() + + colors = getattr( + logging_config, + "COLORED_LOG", + conf.getboolean("logging", "colored_console_log", fallback=True), + ) # Try to init logging - dictConfig(logging_config) + + log_fmt, callsite_params = translate_config_values( + log_format=getattr(logging_config, "LOG_FORMAT", conf.get("logging", "log_format", fallback="")), + callsite_params=conf.getlist("logging", "callsite_parameters", fallback=[]), + ) + configure_logging( + log_level=level, + stdlib_config=logging_config, + log_format=log_fmt, + callsite_parameters=callsite_params, + colors=colors, + ) except (ValueError, KeyError) as e: log.error("Unable to load the config, contains a configuration error.") # When there is an error in the config, escalate the exception # otherwise Airflow would silently fall back on the default config raise e - validate_logging_config(logging_config) + validate_logging_config() + + new_folder_permissions = int( + conf.get("logging", "file_task_handler_new_folder_permissions", fallback="0o775"), + 8, + ) + + base_log_folder = conf.get("logging", "base_log_folder") + + return init_log_folder( + base_log_folder, + new_folder_permissions=new_folder_permissions, + ) return logging_class_path -def validate_logging_config(logging_config): +def validate_logging_config(): """Validate the provided Logging Config.""" # Now lets validate the other logging-related settings task_log_reader = conf.get("logging", "task_log_reader") diff --git a/airflow-core/src/airflow/migrations/versions/0041_3_0_0_rename_dataset_as_asset.py b/airflow-core/src/airflow/migrations/versions/0041_3_0_0_rename_dataset_as_asset.py index 4d00f66c490e8..abe776f082bc0 100644 --- a/airflow-core/src/airflow/migrations/versions/0041_3_0_0_rename_dataset_as_asset.py +++ b/airflow-core/src/airflow/migrations/versions/0041_3_0_0_rename_dataset_as_asset.py @@ -31,6 +31,7 @@ import sqlalchemy as sa import sqlalchemy_jsonfield from alembic import op +from sqlalchemy import text from airflow.migrations.utils import mysql_drop_foreignkey_if_exists from airflow.settings import json @@ -101,14 +102,19 @@ def _rename_pk_constraint( def _drop_fkey_if_exists(table, constraint_name): - dialect = op.get_bind().dialect.name - if dialect == "sqlite": + conn = op.get_bind() + dialect_name = conn.dialect.name + + if dialect_name == "sqlite": + # SQLite requires foreign key constraints to be disabled during batch operations + conn.execute(text("PRAGMA foreign_keys=OFF")) try: with op.batch_alter_table(table, schema=None) as batch_op: batch_op.drop_constraint(op.f(constraint_name), type_="foreignkey") except ValueError: pass - elif dialect == "mysql": + conn.execute(text("PRAGMA foreign_keys=ON")) + elif dialect_name == "mysql": mysql_drop_foreignkey_if_exists(constraint_name, table, op) else: op.execute(f"ALTER TABLE {table} DROP CONSTRAINT IF EXISTS {constraint_name}") diff --git a/airflow-core/src/airflow/migrations/versions/0049_3_0_0_remove_pickled_data_from_xcom_table.py b/airflow-core/src/airflow/migrations/versions/0049_3_0_0_remove_pickled_data_from_xcom_table.py index f7d7093649e39..b7b7dad83bd3c 100644 --- a/airflow-core/src/airflow/migrations/versions/0049_3_0_0_remove_pickled_data_from_xcom_table.py +++ b/airflow-core/src/airflow/migrations/versions/0049_3_0_0_remove_pickled_data_from_xcom_table.py @@ -114,10 +114,20 @@ def upgrade(): # Update the values from nan to nan string if dialect == "postgresql": + # Replace NaN in JSON value positions (after :, , or [) + # This explicitly matches JSON structure, not relying on word boundaries conn.execute( - text(""" + text(r""" UPDATE xcom - SET value = convert_to(replace(convert_from(value, 'UTF8'), 'NaN', '"nan"'), 'UTF8') + SET value = convert_to( + regexp_replace( + convert_from(value, 'UTF8'), + '([:,\[]\s*)NaN(\s*[,}\]])', + '\1"nan"\2', + 'g' + ), + 'UTF8' + ) WHERE value IS NOT NULL AND get_byte(value, 0) != 128 """) ) @@ -133,10 +143,21 @@ def upgrade(): """ ) elif dialect == "mysql": + # Replace NaN in JSON value positions (after :, , or [) + # Use alternation with proper grouping for MySQL compatibility conn.execute( text(""" UPDATE xcom - SET value = CONVERT(REPLACE(CONVERT(value USING utf8mb4), 'NaN', '"nan"') USING BINARY) + SET value = CONVERT( + REGEXP_REPLACE( + CONVERT(value USING utf8mb4), + '(:|,|\\\\[)[ ]*NaN[ ]*([,}\\]])', + '\\\\1"nan"\\\\2', + 1, + 0, + 'c' + ) USING BINARY + ) WHERE value IS NOT NULL AND HEX(SUBSTRING(value, 1, 1)) != '80' """) ) diff --git a/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py b/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py index 3e0ede0183ee1..3105d453e457e 100644 --- a/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py +++ b/airflow-core/src/airflow/migrations/versions/0076_3_1_0_add_human_in_the_loop_response.py @@ -32,6 +32,7 @@ from sqlalchemy import Boolean, Column, ForeignKeyConstraint, String, Text from sqlalchemy.dialects import postgresql +from airflow._shared.timezones import timezone from airflow.settings import json from airflow.utils.sqlalchemy import UtcDateTime @@ -59,10 +60,10 @@ def upgrade(): Column("defaults", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("multiple", Boolean, unique=False, default=False), Column("params", sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}), - Column("respondents", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), - Column("response_at", UtcDateTime, nullable=True), - Column("responded_user_id", String(128), nullable=True), - Column("responded_user_name", String(128), nullable=True), + Column("assignees", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), + Column("created_at", UtcDateTime(timezone=True), nullable=False, default=timezone.utcnow), + Column("responded_at", UtcDateTime, nullable=True), + Column("responded_by", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("chosen_options", sqlalchemy_jsonfield.JSONField(json=json), nullable=True), Column("params_input", sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}), ForeignKeyConstraint( diff --git a/airflow-core/src/airflow/migrations/versions/0082_3_1_0_make_bundle_name_not_nullable.py b/airflow-core/src/airflow/migrations/versions/0082_3_1_0_make_bundle_name_not_nullable.py index 37afc71b0b790..e4507454bb43f 100644 --- a/airflow-core/src/airflow/migrations/versions/0082_3_1_0_make_bundle_name_not_nullable.py +++ b/airflow-core/src/airflow/migrations/versions/0082_3_1_0_make_bundle_name_not_nullable.py @@ -31,6 +31,7 @@ from sqlalchemy.sql import text from airflow.migrations.db_types import StringID +from airflow.migrations.utils import ignore_sqlite_value_error # revision identifiers, used by Alembic. revision = "7582ea3f3dd5" @@ -56,21 +57,22 @@ def upgrade(): op.execute( text(""" INSERT IGNORE INTO dag_bundle (name) VALUES - ('example_dags'), - ('dags-folder'); + ('example_dags'), + ('dags-folder'); """) ) if dialect_name == "sqlite": + op.execute(text("PRAGMA foreign_keys=OFF")) op.execute( text(""" INSERT OR IGNORE INTO dag_bundle (name) VALUES - ('example_dags'), - ('dags-folder'); + ('example_dags'), + ('dags-folder'); """) ) conn = op.get_bind() - with op.batch_alter_table("dag", schema=None) as batch_op: + with ignore_sqlite_value_error(), op.batch_alter_table("dag", schema=None) as batch_op: conn.execute( text( """ @@ -96,6 +98,9 @@ def upgrade(): batch_op.f("dag_bundle_name_fkey"), "dag_bundle", ["bundle_name"], ["name"] ) + if dialect_name == "sqlite": + op.execute(text("PRAGMA foreign_keys=ON")) + def downgrade(): """Make bundle_name nullable.""" diff --git a/airflow-core/src/airflow/migrations/versions/0084_3_1_0_add_last_parse_duration_to_dag_model.py b/airflow-core/src/airflow/migrations/versions/0084_3_1_0_add_last_parse_duration_to_dag_model.py index 2fd90d631e841..1a922d97deed5 100644 --- a/airflow-core/src/airflow/migrations/versions/0084_3_1_0_add_last_parse_duration_to_dag_model.py +++ b/airflow-core/src/airflow/migrations/versions/0084_3_1_0_add_last_parse_duration_to_dag_model.py @@ -29,6 +29,7 @@ import sqlalchemy as sa from alembic import op +from sqlalchemy import text # revision identifiers, used by Alembic. revision = "eaf332f43c7c" @@ -46,5 +47,15 @@ def upgrade(): def downgrade(): """Unapply add last_parse_duration to dag model.""" - with op.batch_alter_table("dag", schema=None) as batch_op: - batch_op.drop_column("last_parse_duration") + conn = op.get_bind() + dialect_name = conn.dialect.name + + if dialect_name == "sqlite": + # SQLite requires foreign key constraints to be disabled during batch operations + conn.execute(text("PRAGMA foreign_keys=OFF")) + with op.batch_alter_table("dag", schema=None) as batch_op: + batch_op.drop_column("last_parse_duration") + conn.execute(text("PRAGMA foreign_keys=ON")) + else: + with op.batch_alter_table("dag", schema=None) as batch_op: + batch_op.drop_column("last_parse_duration") diff --git a/airflow-core/src/airflow/migrations/versions/0085_3_1_0_downgrade_serialized_dag_version_to_v2.py b/airflow-core/src/airflow/migrations/versions/0085_3_1_0_downgrade_serialized_dag_version_to_v2.py new file mode 100644 index 0000000000000..3a5045dda5096 --- /dev/null +++ b/airflow-core/src/airflow/migrations/versions/0085_3_1_0_downgrade_serialized_dag_version_to_v2.py @@ -0,0 +1,199 @@ +# +# 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. + +""" +Add backward compatibility for serialized DAG format v3 to v2. + +Revision ID: cc92b33c6709 +Revises: eaf332f43c7c +Create Date: 2025-09-22 22:50:48.035121 + +""" + +from __future__ import annotations + +from textwrap import dedent + +import sqlalchemy as sa +from alembic import context, op + +# revision identifiers, used by Alembic. +revision = "cc92b33c6709" +down_revision = "eaf332f43c7c" +branch_labels = None +depends_on = None +airflow_version = "3.1.0" + + +def upgrade(): + """Apply Downgrade Serialized Dag version to v2.""" + # No-op: Server handles v1/v2/v3 DAGs at runtime via conversion functions + pass + + +def downgrade(): + """Convert v3 serialized DAGs back to v2 format for compatibility with older Airflow versions.""" + if context.is_offline_mode(): + print( + dedent(""" + ------------ + -- Manual v3 to v2 DAG conversion required (offline mode) + -- + -- PostgreSQL: + -- UPDATE serialized_dag SET data = jsonb_set((data::jsonb - 'client_defaults'), '{__version}', '2')::json + -- WHERE id IN (SELECT id FROM serialized_dag WHERE data->>'__version' = '3' AND data_compressed IS NULL); + -- + -- MySQL/SQLite: + -- UPDATE serialized_dag SET data = JSON_SET(JSON_REMOVE(data, '$.client_defaults'), '$.__version', 2) + -- WHERE JSON_EXTRACT(data, '$.__version') = '3' AND data_compressed IS NULL; + -- + -- For compressed DAGs: run online migration. + ------------ + """) + ) + return + + import gzip + import json + + connection = op.get_bind() + dialect = connection.dialect.name + + if dialect == "postgresql": + # PostgreSQL - pre-filter v3 DAGs to avoid parsing all rows + connection.execute( + sa.text(""" + UPDATE serialized_dag + SET data = jsonb_set( + (data::jsonb - 'client_defaults'), + '{__version}', + '2' + )::json + WHERE id IN ( + SELECT id FROM serialized_dag + WHERE data->>'__version' = '3' + AND data_compressed IS NULL + ) + """) + ) + elif dialect == "mysql": + connection.execute( + sa.text(""" + UPDATE serialized_dag + SET data = JSON_SET( + JSON_REMOVE(data, '$.client_defaults'), + '$.__version', + 2 + ) + WHERE JSON_EXTRACT(data, '$.__version') = '3' + AND data_compressed IS NULL + """) + ) + else: + json_functions_available = False + try: + connection.execute(sa.text("SELECT JSON_SET('{}', '$.test', 'value')")).fetchone() + json_functions_available = True + print("SQLite JSON functions detected, using optimized SQL approach") + except Exception: + print("SQLite JSON functions not available, using Python fallback for JSON processing") + + if json_functions_available: + connection.execute( + sa.text(""" + UPDATE serialized_dag + SET data = JSON_SET( + JSON_REMOVE(data, '$.client_defaults'), + '$.__version', + 2 + ) + WHERE JSON_EXTRACT(data, '$.__version') = '3' + AND data_compressed IS NULL + """) + ) + else: + result = connection.execute( + sa.text(""" + SELECT id, data + FROM serialized_dag + WHERE data_compressed IS NULL + """) + ) + + for row in result: + dag_id, data_json = row + try: + if data_json is None: + continue + + dag_data = json.loads(data_json) + + if dag_data.get("__version") != 3: + continue + + if "client_defaults" in dag_data: + del dag_data["client_defaults"] + dag_data["__version"] = 2 + + new_json = json.dumps(dag_data) + connection.execute( + sa.text("UPDATE serialized_dag SET data = :data WHERE id = :id"), + {"data": new_json, "id": dag_id}, + ) + + except Exception as e: + print(f"Failed to downgrade uncompressed DAG {dag_id}: {e}") + continue + try: + result = connection.execute( + sa.text(""" + SELECT id, data_compressed + FROM serialized_dag + WHERE data_compressed IS NOT NULL + """) + ) + + for row in result: + dag_id, compressed_data = row + try: + if compressed_data is None: + continue + + decompressed = gzip.decompress(compressed_data) + dag_data = json.loads(decompressed) + + if dag_data.get("__version") != 3: + continue + + if "client_defaults" in dag_data: + del dag_data["client_defaults"] + dag_data["__version"] = 2 + + new_compressed = gzip.compress(json.dumps(dag_data).encode("utf-8")) + connection.execute( + sa.text("UPDATE serialized_dag SET data_compressed = :data WHERE id = :id"), + {"data": new_compressed, "id": dag_id}, + ) + + except Exception as e: + print(f"Failed to downgrade compressed DAG {dag_id}: {e}") + continue + + except Exception as e: + print(f"Failed to process compressed DAGs during downgrade: {e}") + raise diff --git a/airflow-core/src/airflow/models/asset.py b/airflow-core/src/airflow/models/asset.py index 8b28f2bea6184..650cb8589b424 100644 --- a/airflow-core/src/airflow/models/asset.py +++ b/airflow-core/src/airflow/models/asset.py @@ -108,12 +108,17 @@ def remove_references_to_deleted_dags(session: Session): DagScheduleAssetAliasReference, TaskOutletAssetReference, ] - for model in models_to_check: - session.execute( - delete(model) - .where(model.dag_id.in_(select(DagModel.dag_id).where(DagModel.is_stale))) - .execution_options(synchronize_session="fetch") - ) + + # The queries need to be done in separate steps, because in the case of multiple + # dag processors on MySQL, there could be a deadlock caused by acquiring both an + # exclusive lock for deletion and shared lock for query in reverse sequence + if stale_dag_ids := session.scalars(select(DagModel.dag_id).where(DagModel.is_stale)).all(): + for model in models_to_check: + session.execute( + delete(model) + .where(model.dag_id.in_(stale_dag_ids)) + .execution_options(synchronize_session="fetch") + ) alias_association_table = Table( diff --git a/airflow-core/src/airflow/models/connection.py b/airflow-core/src/airflow/models/connection.py index 1567cab1dd2bc..167df13c80c9e 100644 --- a/airflow-core/src/airflow/models/connection.py +++ b/airflow-core/src/airflow/models/connection.py @@ -241,6 +241,11 @@ def _parse_from_uri(self, uri: str): if self.EXTRA_KEY in query: self.extra = query[self.EXTRA_KEY] else: + for key, value in query.items(): + try: + query[key] = json.loads(value) + except (JSONDecodeError, TypeError): + self.log.info("Failed parsing the json for key %s", key) self.extra = json.dumps(query) @staticmethod @@ -316,15 +321,22 @@ def get_uri(self) -> str: uri += host_block if self.extra: + extra_dict = self.extra_dejson + can_flatten = True + for value in extra_dict.values(): + if not isinstance(value, str): + can_flatten = False + break + try: - query: str | None = urlencode(self.extra_dejson) + query: str | None = urlencode(extra_dict) except TypeError: query = None - if query and self.extra_dejson == dict(parse_qsl(query, keep_blank_values=True)): + + if can_flatten and query and extra_dict == dict(parse_qsl(query, keep_blank_values=True)): uri += ("?" if self.schema else "/?") + query else: uri += ("?" if self.schema else "/?") + urlencode({self.EXTRA_KEY: self.extra}) - return uri def get_password(self) -> str | None: diff --git a/airflow-core/src/airflow/models/dag.py b/airflow-core/src/airflow/models/dag.py index 316eaf3617313..1dfe5c3b46e56 100644 --- a/airflow-core/src/airflow/models/dag.py +++ b/airflow-core/src/airflow/models/dag.py @@ -135,9 +135,14 @@ def get_run_data_interval(timetable: Timetable, run: DagRun) -> DataInterval: :meta private: """ - data_interval = _get_model_data_interval(run, "data_interval_start", "data_interval_end") - if data_interval is not None: + if ( + data_interval := _get_model_data_interval(run, "data_interval_start", "data_interval_end") + ) is not None: + return data_interval + + if (data_interval := timetable.infer_manual_data_interval(run_after=run.run_after)) is not None: return data_interval + # Compatibility: runs created before AIP-39 implementation don't have an # explicit data interval. Try to infer from the logical date. return infer_automated_data_interval(timetable, run.logical_date) @@ -452,12 +457,25 @@ def next_dagrun_data_interval(self, value: tuple[datetime, datetime] | None) -> @property def deadline(self): """Get the deserialized deadline alert.""" - return DeadlineAlert.deserialize_deadline_alert(self._deadline) if self._deadline else None + if self._deadline is None: + return None + if isinstance(self._deadline, list): + return [DeadlineAlert.deserialize_deadline_alert(item) for item in self._deadline] + return DeadlineAlert.deserialize_deadline_alert(self._deadline) @deadline.setter def deadline(self, value): """Set and serialize the deadline alert.""" - self._deadline = value if isinstance(value, dict) else value.serialize_deadline_alert() + if value is None: + self._deadline = None + elif isinstance(value, list): + self._deadline = [ + item if isinstance(item, dict) else item.serialize_deadline_alert() for item in value + ] + elif isinstance(value, dict): + self._deadline = value + else: + self._deadline = value.serialize_deadline_alert() @property def timezone(self): diff --git a/airflow-core/src/airflow/models/dag_version.py b/airflow-core/src/airflow/models/dag_version.py index a51f3cb301867..1245f3c9b99b9 100644 --- a/airflow-core/src/airflow/models/dag_version.py +++ b/airflow-core/src/airflow/models/dag_version.py @@ -26,6 +26,7 @@ from sqlalchemy_utils import UUIDType from airflow._shared.timezones import timezone +from airflow.dag_processing.bundles.manager import DagBundlesManager from airflow.models.base import Base, StringID from airflow.utils.session import NEW_SESSION, provide_session from airflow.utils.sqlalchemy import UtcDateTime, with_row_locks @@ -47,6 +48,12 @@ class DagVersion(Base): dag_model = relationship("DagModel", back_populates="dag_versions") bundle_name = Column(StringID(), nullable=True) bundle_version = Column(StringID()) + bundle = relationship( + "DagBundleModel", + primaryjoin="foreign(DagVersion.bundle_name) == DagBundleModel.name", + uselist=False, + viewonly=True, + ) dag_code = relationship( "DagCode", back_populates="dag_version", @@ -73,6 +80,20 @@ def __repr__(self): """Represent the object as a string.""" return f"" + @property + def bundle_url(self) -> str | None: + """Render the bundle URL using the joined bundle metadata if available.""" + # Prefer using the joined bundle relationship when present to avoid extra queries + if getattr(self, "bundle", None) is not None and hasattr(self.bundle, "signed_url_template"): + return self.bundle.render_url(self.bundle_version) + + # fallback to the deprecated option if the bundle model does not have a signed_url_template + # attribute + try: + return DagBundlesManager().view_url(self.bundle_name, self.bundle_version) + except ValueError: + return None + @classmethod @provide_session def write_dag( @@ -108,13 +129,16 @@ def write_dag( ) log.debug("Writing DagVersion %s to the DB", dag_version) session.add(dag_version) - session.commit() log.debug("DagVersion %s written to the DB", dag_version) return dag_version @classmethod def _latest_version_select( - cls, dag_id: str, bundle_version: str | None = None, load_dag_model: bool = False + cls, + dag_id: str, + bundle_version: str | None = None, + load_dag_model: bool = False, + load_bundle_model: bool = False, ) -> Select: """ Get the select object to get the latest version of the DAG. @@ -129,6 +153,9 @@ def _latest_version_select( if load_dag_model: query = query.options(joinedload(cls.dag_model)) + if load_bundle_model: + query = query.options(joinedload(cls.bundle)) + query = query.order_by(cls.created_at.desc()).limit(1) return query @@ -140,6 +167,7 @@ def get_latest_version( *, bundle_version: str | None = None, load_dag_model: bool = False, + load_bundle_model: bool = False, session: Session = NEW_SESSION, ) -> DagVersion | None: """ @@ -148,10 +176,16 @@ def get_latest_version( :param dag_id: The DAG ID. :param session: The database session. :param load_dag_model: Whether to load the DAG model. + :param load_bundle_model: Whether to load the DagBundle model. :return: The latest version of the DAG or None if not found. """ return session.scalar( - cls._latest_version_select(dag_id, bundle_version=bundle_version, load_dag_model=load_dag_model) + cls._latest_version_select( + dag_id, + bundle_version=bundle_version, + load_dag_model=load_dag_model, + load_bundle_model=load_bundle_model, + ) ) @classmethod diff --git a/airflow-core/src/airflow/models/dagbag.py b/airflow-core/src/airflow/models/dagbag.py index fc29cf9915503..5ee277d949d06 100644 --- a/airflow-core/src/airflow/models/dagbag.py +++ b/airflow-core/src/airflow/models/dagbag.py @@ -662,6 +662,16 @@ def sync_bag_to_db( from airflow.dag_processing.collection import update_dag_parsing_results_in_db import_errors = {(bundle_name, rel_path): error for rel_path, error in dagbag.import_errors.items()} + + # Build the set of all files that were parsed and include files with import errors + # in case they are not in file_last_changed + files_parsed = set(import_errors) + if dagbag.bundle_path: + files_parsed.update( + (bundle_name, dagbag._get_relative_fileloc(abs_filepath)) + for abs_filepath in dagbag.file_last_changed + ) + update_dag_parsing_results_in_db( bundle_name, bundle_version, @@ -670,6 +680,7 @@ def sync_bag_to_db( None, # file parsing duration is not well defined when parsing multiple files / multiple DAGs. dagbag.dag_warnings, session=session, + files_parsed=files_parsed, ) diff --git a/airflow-core/src/airflow/models/dagrun.py b/airflow-core/src/airflow/models/dagrun.py index 26d1d42e524df..39fad84be6a70 100644 --- a/airflow-core/src/airflow/models/dagrun.py +++ b/airflow-core/src/airflow/models/dagrun.py @@ -1173,7 +1173,7 @@ def recalculate(self) -> _UnfinishedStates: self.notify_dagrun_state_changed(msg="task_failure") if execute_callbacks and dag.has_on_failure_callback: - self.handle_dag_callback(dag=dag, success=False, reason="task_failure") + self.handle_dag_callback(dag=cast("SDKDAG", dag), success=False, reason="task_failure") elif dag.has_on_failure_callback: callback = DagCallbackRequest( filepath=self.dag_model.relative_fileloc, @@ -1206,7 +1206,7 @@ def recalculate(self) -> _UnfinishedStates: self.notify_dagrun_state_changed(msg="success") if execute_callbacks and dag.has_on_success_callback: - self.handle_dag_callback(dag=dag, success=True, reason="success") + self.handle_dag_callback(dag=cast("SDKDAG", dag), success=True, reason="success") elif dag.has_on_success_callback: callback = DagCallbackRequest( filepath=self.dag_model.relative_fileloc, @@ -1222,9 +1222,13 @@ def recalculate(self) -> _UnfinishedStates: msg="success", ) - if (deadline := dag.deadline) and isinstance(deadline.reference, DeadlineReference.TYPES.DAGRUN): - # The dagrun has succeeded. If there wre any Deadlines for it which were not breached, they are no longer needed. - Deadline.prune_deadlines(session=session, conditions={DagRun.run_id: self.run_id}) + if dag.deadline: + # The dagrun has succeeded. If there were any Deadlines for it which were not breached, they are no longer needed. + if any( + isinstance(d.reference, DeadlineReference.TYPES.DAGRUN) + for d in cast("list", dag.deadline) + ): + Deadline.prune_deadlines(session=session, conditions={DagRun.id: self.id}) # if *all tasks* are deadlocked, the run failed elif unfinished.should_schedule and not are_runnable_tasks: @@ -1233,7 +1237,11 @@ def recalculate(self) -> _UnfinishedStates: self.notify_dagrun_state_changed(msg="all_tasks_deadlocked") if execute_callbacks and dag.has_on_failure_callback: - self.handle_dag_callback(dag=dag, success=False, reason="all_tasks_deadlocked") + self.handle_dag_callback( + dag=cast("SDKDAG", dag), + success=False, + reason="all_tasks_deadlocked", + ) elif dag.has_on_failure_callback: callback = DagCallbackRequest( filepath=self.dag_model.relative_fileloc, @@ -1302,9 +1310,7 @@ def _filter_tis_and_exclude_removed(dag: SerializedDAG, tis: list[TI]) -> Iterab """Populate ``ti.task`` while excluding those missing one, marking them as REMOVED.""" for ti in tis: try: - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - ti.task = cast("Operator", dag.get_task(ti.task_id)) + ti.task = dag.get_task(ti.task_id) except TaskNotFound: if ti.state != TaskInstanceState.REMOVED: self.log.error("Failed to get task for ti %s. Marking it as removed.", ti) @@ -1535,7 +1541,12 @@ def _expand_mapped_task_if_needed(ti: TI) -> Iterable[TI] | None: ) ) revised_map_index_task_ids.add(schedulable.task.task_id) - ready_tis.append(schedulable) + + # _revise_map_indexes_if_mapped might mark the current task as REMOVED + # after calculating mapped task length, so we need to re-check + # the task state to ensure it's still schedulable + if schedulable.state in SCHEDULEABLE_STATES: + ready_tis.append(schedulable) # Check if any ti changed state tis_filter = TI.filter_for_tis(old_states) diff --git a/airflow-core/src/airflow/models/hitl.py b/airflow-core/src/airflow/models/hitl.py index 448939e2c6f61..b6bbb2bc402b0 100644 --- a/airflow-core/src/airflow/models/hitl.py +++ b/airflow-core/src/airflow/models/hitl.py @@ -16,16 +16,69 @@ # under the License. from __future__ import annotations +from typing import TYPE_CHECKING, Any, TypedDict + import sqlalchemy_jsonfield -from sqlalchemy import Boolean, Column, ForeignKeyConstraint, String, Text +from sqlalchemy import Boolean, Column, ForeignKeyConstraint, String, Text, func, literal from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.compiler import compiles from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship +from sqlalchemy.sql.functions import FunctionElement +from airflow._shared.timezones import timezone from airflow.models.base import Base from airflow.settings import json from airflow.utils.sqlalchemy import UtcDateTime +if TYPE_CHECKING: + from sqlalchemy.sql import ColumnElement + from sqlalchemy.sql.compiler import SQLCompiler + + +class JSONExtract(FunctionElement): + """ + Cross-dialect JSON key extractor. + + :meta: private + """ + + type = String() + inherit_cache = True + + def __init__(self, column: ColumnElement[Any], key: str, **kwargs: dict[str, Any]) -> None: + super().__init__(column, literal(key), **kwargs) + + +@compiles(JSONExtract, "postgresql") +def compile_postgres(element: JSONExtract, compiler: SQLCompiler, **kwargs: dict[str, Any]) -> str: + """ + Compile JSONExtract for PostgreSQL. + + :meta: private + """ + column, key = element.clauses + return compiler.process(func.json_extract_path_text(column, key), **kwargs) + + +@compiles(JSONExtract, "sqlite") +@compiles(JSONExtract, "mysql") +def compile_sqlite_mysql(element: JSONExtract, compiler: SQLCompiler, **kwargs: dict[str, Any]) -> str: + """ + Compile JSONExtract for SQLite/MySQL. + + :meta: private + """ + column, key = element.clauses + return compiler.process(func.json_extract(column, f"$.{key.value}"), **kwargs) + + +class HITLUser(TypedDict): + """Typed dict for saving a Human-in-the-loop user information.""" + + id: str + name: str + class HITLDetail(Base): """Human-in-the-loop request and corresponding response.""" @@ -44,12 +97,12 @@ class HITLDetail(Base): defaults = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) multiple = Column(Boolean, unique=False, default=False) params = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=False, default={}) - respondents = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) + assignees = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) + created_at = Column(UtcDateTime, default=timezone.utcnow, nullable=False) # Response Content Detail - response_at = Column(UtcDateTime, nullable=True) - responded_user_id = Column(String(128), nullable=True) - responded_user_name = Column(String(128), nullable=True) + responded_at = Column(UtcDateTime, nullable=True) + responded_by = Column(sqlalchemy_jsonfield.JSONField(json=json), nullable=True) chosen_options = Column( sqlalchemy_jsonfield.JSONField(json=json), nullable=True, @@ -74,10 +127,45 @@ class HITLDetail(Base): @hybrid_property def response_received(self) -> bool: - return self.response_at is not None + return self.responded_at is not None @response_received.expression # type: ignore[no-redef] def response_received(cls): - return cls.response_at.is_not(None) + return cls.responded_at.is_not(None) + + @hybrid_property + def responded_by_user_id(self) -> str | None: + return self.responded_by["id"] if self.responded_by else None + + @responded_by_user_id.expression # type: ignore[no-redef] + def responded_by_user_id(cls): + return JSONExtract(cls.responded_by, "id") - DEFAULT_USER_NAME = "Fallback to defaults" + @hybrid_property + def responded_by_user_name(self) -> str | None: + return self.responded_by["name"] if self.responded_by else None + + @responded_by_user_name.expression # type: ignore[no-redef] + def responded_by_user_name(cls): + return JSONExtract(cls.responded_by, "name") + + @hybrid_property + def assigned_users(self) -> list[HITLUser]: + if not self.assignees: + return [] + return [ + HITLUser( + id=assignee["id"], + name=assignee["name"], + ) + for assignee in self.assignees + ] + + @hybrid_property + def responded_by_user(self) -> HITLUser | None: + if self.responded_by is None: + return None + return HITLUser( + id=self.responded_by["id"], + name=self.responded_by["name"], + ) diff --git a/airflow-core/src/airflow/models/log.py b/airflow-core/src/airflow/models/log.py index 9347e4a17536c..aa95b17d495f1 100644 --- a/airflow-core/src/airflow/models/log.py +++ b/airflow-core/src/airflow/models/log.py @@ -56,6 +56,14 @@ class Log(Base): primaryjoin="Log.dag_id == DagModel.dag_id", ) + task_instance = relationship( + "TaskInstance", + viewonly=True, + foreign_keys=[dag_id, task_id, run_id, map_index], + primaryjoin="and_(Log.dag_id == TaskInstance.dag_id, Log.task_id == TaskInstance.task_id, Log.run_id == TaskInstance.run_id, Log.map_index == TaskInstance.map_index)", + lazy="noload", + ) + __table_args__ = ( Index("idx_log_dttm", dttm), Index("idx_log_event", event), diff --git a/airflow-core/src/airflow/models/mappedoperator.py b/airflow-core/src/airflow/models/mappedoperator.py index 7c7988d3c9bd3..9e7a294e5b52c 100644 --- a/airflow-core/src/airflow/models/mappedoperator.py +++ b/airflow-core/src/airflow/models/mappedoperator.py @@ -114,6 +114,10 @@ class MappedOperator(DAGNode): dag: SerializedDAG = attrs.field(init=False) # type: ignore[assignment] task_group: SerializedTaskGroup = attrs.field(init=False) # type: ignore[assignment] + doc: str | None = attrs.field(init=False) + doc_json: str | None = attrs.field(init=False) + doc_rst: str | None = attrs.field(init=False) + doc_yaml: str | None = attrs.field(init=False) start_date: pendulum.DateTime | None = attrs.field(init=False, default=None) end_date: pendulum.DateTime | None = attrs.field(init=False, default=None) upstream_task_ids: set[str] = attrs.field(factory=set, init=False) @@ -136,6 +140,9 @@ class MappedOperator(DAGNode): is_mapped: ClassVar[bool] = True + def __repr__(self) -> str: + return f"" + @property def node_id(self) -> str: return self.task_id @@ -267,12 +274,16 @@ def priority_weight(self) -> int: @property def retry_delay(self) -> datetime.timedelta: - return self.partial_kwargs["retry_delay"] + return self.partial_kwargs.get("retry_delay", SerializedBaseOperator.retry_delay) @property def retry_exponential_backoff(self) -> bool: return bool(self.partial_kwargs.get("retry_exponential_backoff")) + @property + def max_retry_delay(self) -> datetime.timedelta | None: + return self.partial_kwargs.get("max_retry_delay") + @property def weight_rule(self) -> PriorityWeightStrategy: return validate_and_load_priority_weight_strategy( diff --git a/airflow-core/src/airflow/models/renderedtifields.py b/airflow-core/src/airflow/models/renderedtifields.py index b19b7f1f6edf9..100da273154af 100644 --- a/airflow-core/src/airflow/models/renderedtifields.py +++ b/airflow-core/src/airflow/models/renderedtifields.py @@ -20,7 +20,7 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import sqlalchemy_jsonfield from sqlalchemy import ( @@ -51,6 +51,28 @@ from airflow.serialization.serialized_objects import SerializedBaseOperator +def _get_nested_value(obj: Any, path: str) -> Any: + """ + Get a nested value from an object using a dot-separated path. + + :param obj: The object to extract the value from + :param path: A dot-separated path (e.g., "configuration.query.sql") + :return: The value at the nested path, or None if the path doesn't exist + """ + keys = path.split(".") + current = obj + for key in keys: + if isinstance(current, dict): + current = current.get(key) + elif hasattr(current, key): + current = getattr(current, key) + else: + return None + if current is None: + return None + return current + + def get_serialized_template_fields(task: SerializedBaseOperator): """ Get and serialize the template fields for a task. @@ -61,7 +83,24 @@ def get_serialized_template_fields(task: SerializedBaseOperator): :meta private: """ - return {field: serialize_template_field(getattr(task, field), field) for field in task.template_fields} + rendered_fields = {} + + for field in task.template_fields: + rendered_fields[field] = serialize_template_field(getattr(task, field), field) + + renderers = getattr(task, "template_fields_renderers", {}) + for renderer_path in renderers: + if "." in renderer_path: + base_field = renderer_path.split(".", 1)[0] + + if base_field in task.template_fields: + base_value = getattr(task, base_field) + nested_value = _get_nested_value(base_value, renderer_path[len(base_field) + 1 :]) + + if nested_value is not None: + rendered_fields[renderer_path] = serialize_template_field(nested_value, renderer_path) + + return rendered_fields class RenderedTaskInstanceFields(TaskInstanceDependencies): diff --git a/airflow-core/src/airflow/models/serialized_dag.py b/airflow-core/src/airflow/models/serialized_dag.py index b6bbf67fae3c2..9a425585cff15 100644 --- a/airflow-core/src/airflow/models/serialized_dag.py +++ b/airflow-core/src/airflow/models/serialized_dag.py @@ -27,7 +27,7 @@ import sqlalchemy_jsonfield import uuid6 -from sqlalchemy import Column, ForeignKey, LargeBinary, String, exc, select, tuple_ +from sqlalchemy import Column, ForeignKey, LargeBinary, String, exc, exists, select, tuple_, update from sqlalchemy.orm import backref, foreign, relationship from sqlalchemy.sql.expression import func, literal from sqlalchemy_utils import UUIDType @@ -43,6 +43,7 @@ from airflow.models.dag_version import DagVersion from airflow.models.dagcode import DagCode from airflow.models.dagrun import DagRun +from airflow.models.taskinstance import TaskInstance from airflow.sdk.definitions.asset import AssetUniqueKey from airflow.serialization.dag_dependency import DagDependency from airflow.serialization.serialized_objects import LazyDeserializedDAG, SerializedDAG @@ -423,18 +424,33 @@ def write_dag( log.debug("Serialized DAG (%s) is unchanged. Skipping writing to DB", dag.dag_id) return False - if dag_version and not dag_version.task_instances: + has_task_instances: bool = False + if dag_version: + has_task_instances = bool( + session.scalar(select(exists().where(TaskInstance.dag_version_id == dag_version.id))) + ) + + if dag_version and not has_task_instances: # This is for dynamic DAGs that the hashes changes often. We should update # the serialized dag, the dag_version and the dag_code instead of a new version # if the dag_version is not associated with any task instances - latest_ser_dag = cls.get(dag.dag_id, session=session) - if TYPE_CHECKING: - assert latest_ser_dag is not None - # Update the serialized DAG with the new_serialized_dag - latest_ser_dag._data = new_serialized_dag._data - latest_ser_dag._data_compressed = new_serialized_dag._data_compressed - latest_ser_dag.dag_hash = new_serialized_dag.dag_hash - session.merge(latest_ser_dag) + + # Use direct UPDATE to avoid loading the full serialized DAG + result = session.execute( + update(cls) + .where(cls.dag_version_id == dag_version.id) + .values( + { + cls._data: new_serialized_dag._data, + cls._data_compressed: new_serialized_dag._data_compressed, + cls.dag_hash: new_serialized_dag.dag_hash, + } + ) + ) + + if result.rowcount == 0: + # No rows updated - serialized DAG doesn't exist + return False # The dag_version and dag_code may not have changed, still we should # do the below actions: # Update the latest dag version @@ -460,6 +476,15 @@ def write_dag( @classmethod def latest_item_select_object(cls, dag_id): + from airflow.settings import engine + + if engine.dialect.name == "mysql": + # Prevent "Out of sort memory" caused by large values in cls.data column for MySQL. + # Details in https://github.com/apache/airflow/pull/55589 + latest_item_id = ( + select(cls.id).where(cls.dag_id == dag_id).order_by(cls.created_at.desc()).limit(1) + ) + return select(cls).where(cls.id == latest_item_id) return select(cls).where(cls.dag_id == dag_id).order_by(cls.created_at.desc()).limit(1) @classmethod diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 4d440bd38a042..94b9179c6e272 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -20,6 +20,7 @@ import contextlib import hashlib import itertools +import json import logging import math import uuid @@ -27,7 +28,7 @@ from collections.abc import Collection, Iterable from datetime import timedelta from functools import cache -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from urllib.parse import quote import attrs @@ -70,9 +71,6 @@ from airflow._shared.timezones import timezone from airflow.assets.manager import asset_manager from airflow.configuration import conf -from airflow.exceptions import ( - AirflowInactiveAssetInInletOrOutletException, -) from airflow.listeners.listener import get_listener_manager from airflow.models.asset import AssetEvent, AssetModel from airflow.models.base import Base, StringID, TaskInstanceDependencies @@ -89,6 +87,7 @@ from airflow.stats import Stats from airflow.ti_deps.dep_context import DepContext from airflow.ti_deps.dependencies_deps import REQUEUEABLE_DEPS, RUNNING_DEPS +from airflow.ti_deps.deps.ready_to_reschedule import ReadyToRescheduleDep from airflow.utils.helpers import prune_dict from airflow.utils.log.logging_mixin import LoggingMixin from airflow.utils.net import get_hostname @@ -120,7 +119,7 @@ from airflow.models.mappedoperator import MappedOperator from airflow.sdk import DAG from airflow.sdk.api.datamodels._generated import AssetProfile - from airflow.sdk.definitions.asset import AssetNameRef, AssetUniqueKey, AssetUriRef + from airflow.sdk.definitions.asset import AssetUniqueKey from airflow.sdk.types import RuntimeTaskInstanceProtocol from airflow.serialization.definitions.taskgroup import SerializedTaskGroup from airflow.serialization.serialized_objects import SerializedBaseOperator @@ -237,8 +236,7 @@ def clear_task_instances( log.warning("No serialized dag found for dag '%s'", dr.dag_id) task_id = ti.task_id if ti_dag and ti_dag.has_task(task_id): - # TODO (GH-52141): Make dag a db-backed object so it only returns db-backed tasks. - task = cast("Operator", ti_dag.get_task(task_id)) + task = ti_dag.get_task(task_id) ti.refresh_from_task(task) if TYPE_CHECKING: assert ti.task @@ -297,6 +295,7 @@ def clear_task_instances( dr.last_scheduling_decision = None dr.start_date = None dr.clear_number += 1 + dr.queued_at = timezone.utcnow() session.flush() @@ -623,7 +622,7 @@ def log_url(self) -> str: base_url = conf.get("api", "base_url", fallback="http://localhost:8080/") map_index = f"/mapped/{self.map_index}" if self.map_index >= 0 else "" try_number = f"?try_number={self.try_number}" if self.try_number > 0 else "" - _log_uri = f"{base_url}dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" + _log_uri = f"{base_url.rstrip('/')}/dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" return _log_uri @@ -900,6 +899,17 @@ def are_dependencies_met( :param verbose: whether log details on failed dependencies on info or debug log level """ dep_context = dep_context or DepContext() + if self.state == TaskInstanceState.UP_FOR_RESCHEDULE: + # This DepContext is used when a task instance is in UP_FOR_RESCHEDULE state. + # + # Tasks can be put into UP_FOR_RESCHEDULE by the task runner itself (e.g. when + # the worker cannot load the Dag or task). In this case, the scheduler must respect + # the task instance's reschedule_date before scheduling it again. + # + # ReadyToRescheduleDep is the only dependency that enforces this time-based gating. + # We therefore extend the normal scheduling dependency set with it, instead of + # modifying the global scheduler dependencies. + dep_context.deps.add(ReadyToRescheduleDep()) failed = False verbose_aware_logger = self.log.info if verbose else self.log.debug for dep_status in self.get_failed_dep_statuses(dep_context=dep_context, session=session): @@ -1076,8 +1086,6 @@ def _check_and_change_state_before_execution( ti: TaskInstance = task_instance task = task_instance.task - if TYPE_CHECKING: - assert isinstance(task, Operator) # TODO (GH-52141): This shouldn't be needed. ti.refresh_from_task(task, pool_override=pool) ti.test_mode = test_mode ti.refresh_from_db(session=session, lock_for_update=True) @@ -1277,9 +1285,16 @@ def _run_raw_task( log.info("[DAG TEST] Marking success for %s ", self.task_id) return None - taskrun_result = _run_task(ti=self, task=self.task) - if taskrun_result is not None and taskrun_result.error: + # TODO (TaskSDK): This is the old ti execution path. The only usage is + # in TI.run(...), someone needs to analyse if it's still actually used + # somewhere and fix it, likely by rewriting TI.run(...) to use the same + # mechanism as Operator.test(). + taskrun_result = _run_task(ti=self, task=self.task) # type: ignore[arg-type] + if taskrun_result is None: + return None + if taskrun_result.error: raise taskrun_result.error + self.task = taskrun_result.ti.task # type: ignore[assignment] return None @staticmethod @@ -1324,13 +1339,15 @@ def register_asset_changes_in_db( if "source_alias_name" not in event } - bad_asset_keys: set[AssetUniqueKey | AssetNameRef | AssetUriRef] = set() - for key in asset_keys: try: am = asset_models[key] except KeyError: - bad_asset_keys.add(key) + ti.log.warning( + 'Task has inactive assets "Asset(name=%s, uri=%s)" in inlets or outlets', + key.name, + key.uri, + ) continue ti.log.debug("register event for asset %s", am) asset_manager.register_asset_change( @@ -1347,7 +1364,9 @@ def register_asset_changes_in_db( try: am = asset_models_by_name[nref.name] except KeyError: - bad_asset_keys.add(nref) + ti.log.warning( + 'Task has inactive assets "Asset.ref(name=%s)" in inlets or outlets', nref.name + ) continue ti.log.debug("register event for asset name ref %s", am) asset_manager.register_asset_change( @@ -1363,7 +1382,9 @@ def register_asset_changes_in_db( try: am = asset_models_by_uri[uref.uri] except KeyError: - bad_asset_keys.add(uref) + ti.log.warning( + 'Task has inactive assets "Asset.ref(uri=%s)" in inlets or outlets', uref.uri + ) continue ti.log.debug("register event for asset uri ref %s", am) asset_manager.register_asset_change( @@ -1373,7 +1394,7 @@ def register_asset_changes_in_db( session=session, ) - def _asset_event_extras_from_aliases() -> dict[tuple[AssetUniqueKey, frozenset], set[str]]: + def _asset_event_extras_from_aliases() -> dict[tuple[AssetUniqueKey, str, str], set[str]]: d = defaultdict(set) for event in outlet_events: try: @@ -1383,36 +1404,41 @@ def _asset_event_extras_from_aliases() -> dict[tuple[AssetUniqueKey, frozenset], if alias_name not in outlet_alias_names: continue asset_key = AssetUniqueKey(**event["dest_asset_key"]) - extra_key = frozenset(event["extra"].items()) - d[asset_key, extra_key].add(alias_name) + # fallback for backward compatibility + asset_extra_json = json.dumps(event.get("dest_asset_extra", {}), sort_keys=True) + asset_event_extra_json = json.dumps(event["extra"], sort_keys=True) + d[asset_key, asset_extra_json, asset_event_extra_json].add(alias_name) return d outlet_alias_names = {o.name for o in task_outlets if o.type == AssetAlias.__name__ and o.name} if outlet_alias_names and (event_extras_from_aliases := _asset_event_extras_from_aliases()): - for (asset_key, extra_key), event_aliase_names in event_extras_from_aliases.items(): + for ( + asset_key, + asset_extra_json, + asset_event_extras_json, + ), event_aliase_names in event_extras_from_aliases.items(): + asset_event_extra = json.loads(asset_event_extras_json) + asset = Asset(name=asset_key.name, uri=asset_key.uri, extra=json.loads(asset_extra_json)) ti.log.debug("register event for asset %s with aliases %s", asset_key, event_aliase_names) event = asset_manager.register_asset_change( task_instance=ti, - asset=asset_key, + asset=asset, source_alias_names=event_aliase_names, - extra=dict(extra_key), + extra=asset_event_extra, session=session, ) if event is None: ti.log.info("Dynamically creating AssetModel %s", asset_key) - session.add(AssetModel(name=asset_key.name, uri=asset_key.uri)) + session.add(AssetModel.from_public(asset)) session.flush() # So event can set up its asset fk. asset_manager.register_asset_change( task_instance=ti, - asset=asset_key, + asset=asset, source_alias_names=event_aliase_names, - extra=dict(extra_key), + extra=asset_event_extra, session=session, ) - if bad_asset_keys: - raise AirflowInactiveAssetInInletOrOutletException(bad_asset_keys) - @provide_session def update_rtif(self, rendered_fields, session: Session = NEW_SESSION): from airflow.models.renderedtifields import RenderedTaskInstanceFields @@ -1455,9 +1481,11 @@ def run( assert original_task is not None assert original_task.dag is not None - self.task = SerializedDAG.deserialize_dag(SerializedDAG.serialize_dag(original_task.dag)).task_dict[ - original_task.task_id - ] + # We don't set up all tests well... + if not isinstance(original_task.dag, SerializedDAG): + serialized_dag = SerializedDAG.deserialize_dag(SerializedDAG.serialize_dag(original_task.dag)) + self.task = serialized_dag.get_task(original_task.task_id) + res = self.check_and_change_state_before_execution( verbose=verbose, ignore_all_deps=ignore_all_deps, diff --git a/airflow-core/src/airflow/models/xcom.py b/airflow-core/src/airflow/models/xcom.py index 709d6bf69030c..0d3236eb408af 100644 --- a/airflow-core/src/airflow/models/xcom.py +++ b/airflow-core/src/airflow/models/xcom.py @@ -107,7 +107,7 @@ class XComModel(TaskInstanceDependencies): task = relationship( "TaskInstance", viewonly=True, - lazy="selectin", + lazy="noload", ) @classmethod diff --git a/airflow-core/src/airflow/models/xcom_arg.py b/airflow-core/src/airflow/models/xcom_arg.py index 75cccba50334d..bc9326b2b2f52 100644 --- a/airflow-core/src/airflow/models/xcom_arg.py +++ b/airflow-core/src/airflow/models/xcom_arg.py @@ -19,7 +19,7 @@ from collections.abc import Iterator, Sequence from functools import singledispatch -from typing import TYPE_CHECKING, Any, TypeAlias, cast +from typing import TYPE_CHECKING, Any, TypeAlias import attrs from sqlalchemy import func, or_, select @@ -92,8 +92,7 @@ class SchedulerPlainXComArg(SchedulerXComArg): @classmethod def _deserialize(cls, data: dict[str, Any], dag: SerializedDAG) -> Self: - # TODO (GH-52141): SerializedDAG should return scheduler operator instead. - return cls(cast("Operator", dag.get_task(data["task_id"])), data["key"]) + return cls(dag.get_task(data["task_id"]), data["key"]) def iter_references(self) -> Iterator[tuple[Operator, str]]: yield self.operator, self.key diff --git a/airflow-core/src/airflow/operators/__init__.py b/airflow-core/src/airflow/operators/__init__.py index 70d2da8f06b57..84a526c134f3b 100644 --- a/airflow-core/src/airflow/operators/__init__.py +++ b/airflow-core/src/airflow/operators/__init__.py @@ -27,7 +27,7 @@ from airflow.utils.deprecation_tools import add_deprecated_classes __deprecated_classes = { - "python":{ + "python": { "PythonOperator": "airflow.providers.standard.operators.python.PythonOperator", "BranchPythonOperator": "airflow.providers.standard.operators.python.BranchPythonOperator", "ShortCircuitOperator": "airflow.providers.standard.operators.python.ShortCircuitOperator", @@ -37,10 +37,10 @@ "BranchPythonVirtualenvOperator": "airflow.providers.standard.operators.python.BranchPythonVirtualenvOperator", "get_current_context": "airflow.sdk.get_current_context", }, - "bash":{ + "bash": { "BashOperator": "airflow.providers.standard.operators.bash.BashOperator", }, - "datetime":{ + "datetime": { "BranchDateTimeOperator": "airflow.providers.standard.operators.datetime.BranchDateTimeOperator", }, "generic_transfer": { @@ -64,7 +64,7 @@ "smooth": { "SmoothOperator": "airflow.providers.standard.operators.smooth.SmoothOperator", }, - "branch":{ + "branch": { "BranchMixIn": "airflow.providers.standard.operators.branch.BranchMixIn", "BaseBranchOperator": "airflow.providers.standard.operators.branch.BaseBranchOperator", } diff --git a/airflow-core/src/airflow/plugins_manager.py b/airflow-core/src/airflow/plugins_manager.py index aa228342c1d80..6f74a8e958fe7 100644 --- a/airflow-core/src/airflow/plugins_manager.py +++ b/airflow-core/src/airflow/plugins_manager.py @@ -19,7 +19,6 @@ from __future__ import annotations -import importlib import importlib.machinery import importlib.util import inspect @@ -214,8 +213,6 @@ def is_valid_plugin(plugin_obj): :return: Whether or not the obj is a valid subclass of AirflowPlugin """ - global plugins - if ( inspect.isclass(plugin_obj) and issubclass(plugin_obj, AirflowPlugin) @@ -234,8 +231,6 @@ def register_plugin(plugin_instance): :param plugin_instance: subclass of AirflowPlugin """ - global plugins - if plugin_instance.name in loaded_plugins: return @@ -250,8 +245,6 @@ def load_entrypoint_plugins(): The entry_point group should be 'airflow.plugins'. """ - global import_errors - log.debug("Loading plugins from entrypoints") for entry_point, dist in entry_points_with_dist("airflow.plugins"): @@ -271,7 +264,6 @@ def load_entrypoint_plugins(): def load_plugins_from_plugin_directory(): """Load and register Airflow Plugins from plugins directory.""" - global import_errors log.debug("Loading plugins from directory: %s", settings.PLUGINS_FOLDER) files = find_path_from_directory(settings.PLUGINS_FOLDER, ".airflowignore") plugin_search_locations: list[tuple[str, Generator[str, None, None]]] = [("", files)] @@ -373,7 +365,6 @@ def ensure_plugins_loaded(): def initialize_ui_plugins(): """Collect extension points for the UI.""" - global plugins global external_views global react_apps @@ -456,7 +447,6 @@ def _remove_list_item(lst, item): def initialize_flask_plugins(): """Collect flask extension points for WEB UI (legacy).""" - global plugins global flask_blueprints global flask_appbuilder_views global flask_appbuilder_menu_links @@ -496,7 +486,6 @@ def initialize_flask_plugins(): def initialize_fastapi_plugins(): """Collect extension points for the API.""" - global plugins global fastapi_apps global fastapi_root_middlewares @@ -593,7 +582,6 @@ def initialize_hook_lineage_readers_plugins(): def integrate_macros_plugins() -> None: """Integrates macro plugins.""" - global plugins global macros_modules from airflow.sdk.execution_time import macros @@ -626,8 +614,6 @@ def integrate_macros_plugins() -> None: def integrate_listener_plugins(listener_manager: ListenerManager) -> None: """Add listeners from plugins.""" - global plugins - ensure_plugins_loaded() if plugins: diff --git a/airflow-core/src/airflow/providers_manager.py b/airflow-core/src/airflow/providers_manager.py index 20fc79bd7fac5..f82a1293449b9 100644 --- a/airflow-core/src/airflow/providers_manager.py +++ b/airflow-core/src/airflow/providers_manager.py @@ -601,6 +601,21 @@ def _discover_all_providers_from_packages(self) -> None: f"The package '{package_name}' from packaging information " f"{provider_info_package_name} do not match. Please make sure they are aligned" ) + + # issue-59576: Retrieve the project.urls.documentation from dist.metadata + project_urls = dist.metadata.get_all("Project-URL") + documentation_url: str | None = None + + if project_urls: + for entry in project_urls: + if "," in entry: + name, url = entry.split(",") + if name.strip().lower() == "documentation": + documentation_url = url + break + + provider_info["documentation-url"] = documentation_url + if package_name not in self._provider_dict: self._provider_dict[package_name] = ProviderInfo(version, provider_info) else: diff --git a/airflow-core/src/airflow/secrets/__init__.py b/airflow-core/src/airflow/secrets/__init__.py index 5ff034b247ec1..63de92c4f3f63 100644 --- a/airflow-core/src/airflow/secrets/__init__.py +++ b/airflow-core/src/airflow/secrets/__init__.py @@ -29,7 +29,7 @@ from airflow.utils.deprecation_tools import add_deprecated_classes -__all__ = ["BaseSecretsBackend", "DEFAULT_SECRETS_SEARCH_PATH", "DEFAULT_SECRETS_SEARCH_PATH_WORKERS"] +__all__ = ["BaseSecretsBackend", "DEFAULT_SECRETS_SEARCH_PATH"] from airflow.secrets.base_secrets import BaseSecretsBackend @@ -38,10 +38,6 @@ "airflow.secrets.metastore.MetastoreBackend", ] -DEFAULT_SECRETS_SEARCH_PATH_WORKERS = [ - "airflow.secrets.environment_variables.EnvironmentVariablesBackend", -] - __deprecated_classes = { "cache": { @@ -49,3 +45,26 @@ }, } add_deprecated_classes(__deprecated_classes, __name__) + + +def __getattr__(name): + if name == "DEFAULT_SECRETS_SEARCH_PATH_WORKERS": + import warnings + + warnings.warn( + "airflow.secrets.DEFAULT_SECRETS_SEARCH_PATH_WORKERS is moved to the Task SDK. " + "Use airflow.sdk.execution_time.secrets.DEFAULT_SECRETS_SEARCH_PATH_WORKERS instead.", + DeprecationWarning, + stacklevel=2, + ) + try: + from airflow.sdk.execution_time.secrets import DEFAULT_SECRETS_SEARCH_PATH_WORKERS + + return DEFAULT_SECRETS_SEARCH_PATH_WORKERS + except (ImportError, AttributeError): + # Back-compat for older Task SDK clients + return [ + "airflow.secrets.environment_variables.EnvironmentVariablesBackend", + ] + + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/airflow-core/src/airflow/sensors/__init__.py b/airflow-core/src/airflow/sensors/__init__.py index db378f4550324..366837c20fd12 100644 --- a/airflow-core/src/airflow/sensors/__init__.py +++ b/airflow-core/src/airflow/sensors/__init__.py @@ -32,13 +32,13 @@ "PokeReturnValue": "airflow.sdk.bases.sensor.PokeReturnValue", "poke_mode_only": "airflow.sdk.bases.sensor.poke_mode_only", }, - "python":{ + "python": { "PythonSensor": "airflow.providers.standard.sensors.python.PythonSensor", }, - "bash":{ + "bash": { "BashSensor": "airflow.providers.standard.sensors.bash.BashSensor", }, - "date_time":{ + "date_time": { "DateTimeSensor": "airflow.providers.standard.sensors.date_time.DateTimeSensor", "DateTimeSensorAsync": "airflow.providers.standard.sensors.date_time.DateTimeSensorAsync", }, diff --git a/airflow-core/src/airflow/serialization/definitions/taskgroup.py b/airflow-core/src/airflow/serialization/definitions/taskgroup.py index e26c6cfb4aeea..6c0add8cdfb92 100644 --- a/airflow-core/src/airflow/serialization/definitions/taskgroup.py +++ b/airflow-core/src/airflow/serialization/definitions/taskgroup.py @@ -45,7 +45,8 @@ class SerializedTaskGroup(DAGNode): group_display_name: str | None = attrs.field() prefix_group_id: bool = attrs.field() parent_group: SerializedTaskGroup | None = attrs.field() - dag: SerializedDAG = attrs.field() + # TODO (GH-52141): Replace DAGNode dependency. + dag: SerializedDAG = attrs.field() # type: ignore[assignment] tooltip: str = attrs.field() default_args: dict[str, Any] = attrs.field(factory=dict) @@ -61,6 +62,9 @@ class SerializedTaskGroup(DAGNode): is_mapped: ClassVar[bool] = False + def __repr__(self) -> str: + return f"" + @staticmethod def _iter_child(child): """Iterate over the children of this TaskGroup.""" @@ -234,6 +238,10 @@ def topological_sort(self) -> list[DAGNode]: if tg.node_id in graph_unsorted: break tg = tg.parent_group + + if tg: + # We are already going to visit that TG + break else: del graph_unsorted[node.node_id] graph_sorted.append(node) @@ -257,6 +265,9 @@ class SerializedMappedTaskGroup(SerializedTaskGroup): is_mapped: ClassVar[bool] = True + def __repr__(self) -> str: + return f"" + @methodtools.lru_cache(maxsize=None) def get_parse_time_mapped_ti_count(self) -> int: """ diff --git a/airflow-core/src/airflow/serialization/helpers.py b/airflow-core/src/airflow/serialization/helpers.py index 949b3cb9c9f09..f315ff12f6a0b 100644 --- a/airflow-core/src/airflow/serialization/helpers.py +++ b/airflow-core/src/airflow/serialization/helpers.py @@ -23,12 +23,16 @@ from airflow._shared.secrets_masker import redact from airflow.configuration import conf from airflow.settings import json +from airflow.utils.module_loading import qualname def serialize_template_field(template_field: Any, name: str) -> str | dict | list | int | float: """ Return a serializable representation of the templated field. + If ``templated_field`` is provided via a callable, compute MD5 hash of source + and return following serialized value: `` Any: + """Recursively sort dictionaries to ensure consistent ordering.""" + if isinstance(obj, dict): + return {k: sort_dict_recursively(v) for k, v in sorted(obj.items())} + if isinstance(obj, list): + return [sort_dict_recursively(item) for item in obj] + if isinstance(obj, tuple): + return tuple(sort_dict_recursively(item) for item in obj) + return obj + max_length = conf.getint("core", "max_templated_field_length") if not is_jsonable(template_field): try: serialized = template_field.serialize() except AttributeError: - serialized = str(template_field) + if callable(template_field): + full_qualified_name = qualname(template_field, True) + serialized = f"" + else: + serialized = str(template_field) if len(serialized) > max_length: rendered = redact(serialized, name) return ( @@ -70,6 +88,10 @@ def translate_tuples_to_lists(obj: Any): # and need to be converted to lists return template_field template_field = translate_tuples_to_lists(template_field) + # Sort dictionaries recursively to ensure consistent string representation + # This prevents hash inconsistencies when dict ordering varies + if isinstance(template_field, dict): + template_field = sort_dict_recursively(template_field) serialized = str(template_field) if len(serialized) > max_length: rendered = redact(serialized, name) diff --git a/airflow-core/src/airflow/serialization/schema.json b/airflow-core/src/airflow/serialization/schema.json index 0ce253e2262c2..d79c7477297e6 100644 --- a/airflow-core/src/airflow/serialization/schema.json +++ b/airflow-core/src/airflow/serialization/schema.json @@ -175,8 +175,8 @@ "value": { "$ref": "#/definitions/dict" } } }, - "catchup": { "type": "boolean" }, - "fail_fast": { "type": "boolean" }, + "catchup": { "type": "boolean", "default": false }, + "fail_fast": { "type": "boolean", "default": false }, "fileloc": { "type" : "string"}, "relative_fileloc": { "type" : "string"}, "_processor_dags_folder": { @@ -190,13 +190,17 @@ "deadline": { "anyOf": [ { "$ref": "#/definitions/dict" }, + { + "type": "array", + "items": { "$ref": "#/definitions/dict" } + }, { "type": "null" } ] }, "_concurrency": { "type" : "number"}, - "max_active_tasks": { "type" : "number"}, - "max_active_runs": { "type" : "number"}, - "max_consecutive_failed_dag_runs": { "type" : "number"}, + "max_active_tasks": { "type" : "number", "default": 16}, + "max_active_runs": { "type" : "number", "default": 16}, + "max_consecutive_failed_dag_runs": { "type" : "number", "default": 0}, "default_args": { "$ref": "#/definitions/dict" }, "start_date": { "$ref": "#/definitions/datetime" }, "end_date": { "$ref": "#/definitions/datetime" }, @@ -204,9 +208,9 @@ "doc_md": { "type" : "string"}, "access_control": {"$ref": "#/definitions/dict" }, "is_paused_upon_creation": { "type": "boolean" }, - "has_on_success_callback": { "type": "boolean" }, - "has_on_failure_callback": { "type": "boolean" }, - "render_template_as_native_obj": { "type": "boolean" }, + "has_on_success_callback": { "type": "boolean", "default": false }, + "has_on_failure_callback": { "type": "boolean", "default": false }, + "render_template_as_native_obj": { "type": "boolean", "default": false }, "tags": { "type": "array" }, "task_group": {"anyOf": [ { "type": "null" }, @@ -214,7 +218,7 @@ ]}, "edge_info": { "$ref": "#/definitions/edge_info" }, "dag_dependencies": { "$ref": "#/definitions/dag_dependencies" }, - "disable_bundle_versioning": {"type": "boolean"} + "disable_bundle_versioning": {"type": "boolean", "default": false } }, "required": [ "dag_id", @@ -279,7 +283,7 @@ "pool": { "type": "string", "default": "default_pool" }, "pool_slots": { "type": "number", "default": 1 }, "execution_timeout": { "$ref": "#/definitions/timedelta" }, - "retry_delay": { "$ref": "#/definitions/timedelta" }, + "retry_delay": { "$ref": "#/definitions/timedelta", "default": 300.0 }, "retry_exponential_backoff": { "type": "boolean", "default": false }, "max_retry_delay": { "$ref": "#/definitions/timedelta" }, "params": { "$ref": "#/definitions/params" }, diff --git a/airflow-core/src/airflow/serialization/serialized_objects.py b/airflow-core/src/airflow/serialization/serialized_objects.py index 3e75993b2e2eb..62259d131f0f5 100644 --- a/airflow-core/src/airflow/serialization/serialized_objects.py +++ b/airflow-core/src/airflow/serialization/serialized_objects.py @@ -28,6 +28,7 @@ import logging import math import re +import sys import weakref from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence from functools import cached_property, lru_cache @@ -58,7 +59,13 @@ from airflow._shared.timezones.timezone import coerce_datetime, from_timestamp, parse_timezone, utcnow from airflow.callbacks.callback_requests import DagCallbackRequest, TaskCallbackRequest from airflow.configuration import conf as airflow_conf -from airflow.exceptions import AirflowException, DeserializationError, SerializationError, TaskDeferred +from airflow.exceptions import ( + AirflowException, + DeserializationError, + SerializationError, + TaskDeferred, + TaskNotFound, +) from airflow.models.connection import Connection from airflow.models.dag import DagModel from airflow.models.dag_version import DagVersion @@ -125,6 +132,7 @@ from airflow.models.mappedoperator import MappedOperator as SerializedMappedOperator from airflow.models.taskinstance import TaskInstance from airflow.sdk import BaseOperatorLink + from airflow.sdk.definitions.edges import EdgeInfoType from airflow.serialization.json_schema import Validator from airflow.task.trigger_rule import TriggerRule from airflow.ti_deps.deps.base_ti_dep import BaseTIDep @@ -390,6 +398,8 @@ def decode_outlet_event_accessor(var: dict[str, Any]) -> OutletEventAccessor: dest_asset_key=AssetUniqueKey( name=e["dest_asset_key"]["name"], uri=e["dest_asset_key"]["uri"] ), + # fallback for backward compatibility + dest_asset_extra=e.get("dest_asset_extra", {}), extra=e["extra"], ) for e in asset_alias_events @@ -609,7 +619,7 @@ class BaseSerialization: _CONSTRUCTOR_PARAMS: dict[str, Parameter] = {} - SERIALIZER_VERSION = 2 + SERIALIZER_VERSION = 3 @classmethod def to_json(cls, var: Any) -> str: @@ -915,8 +925,13 @@ def deserialize(cls, encoded_var: Any) -> Any: elif type_ == DAT.DATETIME: return from_timestamp(var) elif type_ == DAT.POD: - if not _has_kubernetes(): - raise RuntimeError("Cannot deserialize POD objects without kubernetes libraries installed!") + # Attempt to import kubernetes for deserialization. Using attempt_import=True allows + # lazy loading of kubernetes libraries only when actually needed for POD deserialization. + if not _has_kubernetes(attempt_import=True): + raise RuntimeError( + "Cannot deserialize POD objects without kubernetes libraries. " + "Please install the cncf.kubernetes provider." + ) pod = PodGenerator.deserialize_model_dict(var) return pod elif type_ == DAT.TIMEDELTA: @@ -973,7 +988,12 @@ def deserialize(cls, encoded_var: Any) -> Any: else: raise TypeError(f"Invalid type {type_!s} in deserialization.") - _deserialize_datetime = from_timestamp + @classmethod + def _deserialize_datetime(cls, arg): + if isinstance(arg, str): + return arg + return from_timestamp(arg) + _deserialize_timezone = parse_timezone @classmethod @@ -1087,21 +1107,7 @@ def _deserialize_params_dict(cls, encoded_params: list[tuple[str, dict]]) -> Par return ParamsDict(op_params) @classmethod - def get_operator_optional_fields_from_schema(cls) -> set[str]: - schema_loader = cls._json_schema - - if schema_loader is None: - return set() - - schema_data = schema_loader.schema - operator_def = schema_data.get("definitions", {}).get("operator", {}) - operator_fields = set(operator_def.get("properties", {}).keys()) - required_fields = set(operator_def.get("required", [])) - - optional_fields = operator_fields - required_fields - return optional_fields - - @classmethod + @lru_cache(maxsize=4) # Cache for "operator", "dag", and a few others def get_schema_defaults(cls, object_type: str) -> dict[str, Any]: """ Extract default values from JSON schema for any object type. @@ -1141,10 +1147,6 @@ def detect_task_dependencies(task: SdkOperator) -> list[DagDependency]: from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator from airflow.providers.standard.sensors.external_task import ExternalTaskSensor - # TODO (GH-52141): Separate MappedOperator implementation to get rid of this. - if TYPE_CHECKING: - assert isinstance(task.operator_class, type) - deps = [] if isinstance(task, TriggerDagRunOperator): deps.append( @@ -1241,6 +1243,8 @@ class SerializedBaseOperator(DAGNode, BaseSerialization): _json_schema: ClassVar[Validator] = lazy_object_proxy.Proxy(load_dag_schema) + _const_fields: ClassVar[set[str] | None] = None + _can_skip_downstream: bool _is_empty: bool _needs_expansion: bool @@ -1301,7 +1305,7 @@ class SerializedBaseOperator(DAGNode, BaseSerialization): resources: dict[str, Any] | None = None retries: int = 0 - retry_delay: datetime.timedelta + retry_delay: datetime.timedelta = datetime.timedelta(seconds=300) retry_exponential_backoff: bool = False run_as_user: str | None = None @@ -1350,11 +1354,18 @@ def __eq__(self, other: Any) -> bool: getattr(self, c, None) == getattr(other, c, None) for c in BaseOperator._comps ) + def __hash__(self): + return hash((self.task_type, *[getattr(self, c, None) for c in BaseOperator._comps])) + + def __repr__(self) -> str: + return f"" + @property def node_id(self) -> str: return self.task_id - def get_dag(self) -> DAG | None: + # TODO (GH-52141): Replace DAGNode with a scheduler type. + def get_dag(self) -> SerializedDAG | None: # type: ignore[override] return self.dag @property @@ -1401,7 +1412,8 @@ def get_extra_links(self, ti: TaskInstance, name: str) -> str | None: link = self.operator_extra_link_dict.get(name) or self.global_operator_extra_link_dict.get(name) if not link: return None - return link.get_link(self, ti_key=ti.key) # type: ignore[arg-type] # TODO: GH-52141 - BaseOperatorLink.get_link expects BaseOperator but receives SerializedBaseOperator + # TODO: GH-52141 - BaseOperatorLink.get_link expects BaseOperator but receives SerializedBaseOperator. + return link.get_link(self, ti_key=ti.key) # type: ignore[arg-type] @property def operator_name(self) -> str: @@ -1422,6 +1434,8 @@ def expand_start_trigger_args(self, *, context: Context) -> StartTriggerArgs | N @property def weight_rule(self) -> PriorityWeightStrategy: + if isinstance(self._weight_rule, PriorityWeightStrategy): + return self._weight_rule return validate_and_load_priority_weight_strategy(self._weight_rule) def __getattr__(self, name): @@ -1708,6 +1722,38 @@ def set_task_dag_references(task: SerializedOperator | MappedOperator, dag: Seri # Bypass set_upstream etc here - it does more than we want dag.task_dict[task_id].upstream_task_ids.add(task.task_id) + @classmethod + def get_operator_const_fields(cls) -> set[str]: + """Get the set of operator fields that are marked as const in the JSON schema.""" + if (schema_loader := cls._json_schema) is None: + return set() + + schema_data = schema_loader.schema + operator_def = schema_data.get("definitions", {}).get("operator", {}) + properties = operator_def.get("properties", {}) + + return { + field_name + for field_name, field_def in properties.items() + if isinstance(field_def, dict) and field_def.get("const") + } + + @classmethod + @lru_cache(maxsize=1) # Only one type: "operator" + def get_operator_optional_fields_from_schema(cls) -> set[str]: + schema_loader = cls._json_schema + + if schema_loader is None: + return set() + + schema_data = schema_loader.schema + operator_def = schema_data.get("definitions", {}).get("operator", {}) + operator_fields = set(operator_def.get("properties", {}).keys()) + required_fields = set(operator_def.get("required", [])) + + optional_fields = operator_fields - required_fields + return optional_fields + @classmethod def deserialize_operator( cls, @@ -1809,7 +1855,7 @@ def detect_dependencies(cls, op: SdkOperator) -> set[DagDependency]: return deps @classmethod - def _matches_client_defaults(cls, var: Any, attrname: str, op: DAGNode) -> bool: + def _matches_client_defaults(cls, var: Any, attrname: str) -> bool: """ Check if a field value matches client_defaults and should be excluded. @@ -1818,7 +1864,6 @@ def _matches_client_defaults(cls, var: Any, attrname: str, op: DAGNode) -> bool: :param var: The value to check :param attrname: The attribute name - :param op: The operator instance :return: True if value matches client_defaults and should be excluded """ try: @@ -1846,12 +1891,41 @@ def _is_excluded(cls, var: Any, attrname: str, op: DAGNode): :return: True if a variable is excluded, False otherwise. """ # Check if value matches client_defaults (hierarchical defaults optimization) - if cls._matches_client_defaults(var, attrname, op): + if cls._matches_client_defaults(var, attrname): + return True + + # for const fields, we should always be excluded when False, regardless of client_defaults + # Use class-level cache for optimisation + if cls._const_fields is None: + cls._const_fields = cls.get_operator_const_fields() + if attrname in cls._const_fields and var is False: return True - schema_defaults = cls.get_schema_defaults("operator") + schema_defaults = cls.get_schema_defaults("operator") if attrname in schema_defaults: if schema_defaults[attrname] == var: + # If it also matches client_defaults, exclude (optimization) + client_defaults = cls.generate_client_defaults() + if attrname in client_defaults: + if client_defaults[attrname] == var: + return True + # If client_defaults differs, preserve explicit override from user + # Example: default_args={"retries": 0}, schema default=0, client_defaults={"retries": 3} + if client_defaults[attrname] != var: + if op.has_dag(): + dag = op.dag + if dag and attrname in dag.default_args and dag.default_args[attrname] == var: + return False + if ( + hasattr(op, "_BaseOperator__init_kwargs") + and attrname in op._BaseOperator__init_kwargs + and op._BaseOperator__init_kwargs[attrname] == var + ): + return False + + # If client_defaults doesn't have this field (matches schema default), + # exclude for optimization even if in default_args + # Example: default_args={"depends_on_past": False}, schema default=False return True optional_fields = cls.get_operator_optional_fields_from_schema() if var is None: @@ -2045,19 +2119,26 @@ def generate_client_defaults(cls) -> dict[str, Any]: for k, v in OPERATOR_DEFAULTS.items(): if k not in cls.get_serialized_fields(): continue - # Exclude values that are the same as the schema defaults - if k in schema_defaults and schema_defaults[k] == v: - continue # Exclude values that are None or empty collections if v is None or v in [[], (), set(), {}]: continue + # Check schema defaults first with raw value comparison (fast path) + if k in schema_defaults and schema_defaults[k] == v: + continue + # Use the existing serialize method to ensure consistent format serialized_value = cls.serialize(v) # Extract just the value part, consistent with serialize_to_json behavior if isinstance(serialized_value, dict) and Encoding.TYPE in serialized_value: serialized_value = serialized_value[Encoding.VAR] + + # For cases where raw comparison failed but serialized values might match + # (e.g., timedelta vs float), check again with serialized value + if k in schema_defaults and schema_defaults[k] == serialized_value: + continue + client_defaults[k] = serialized_value return client_defaults @@ -2331,7 +2412,7 @@ def _create_orm_dagrun( return run -class SerializedDAG(DAG, BaseSerialization): +class SerializedDAG(BaseSerialization): """ A JSON serializable representation of DAG. @@ -2342,16 +2423,71 @@ class SerializedDAG(DAG, BaseSerialization): _decorated_fields: ClassVar[set[str]] = {"default_args", "access_control"} - # TODO (GH-52141): These should contain serialized containers, but currently - # this class inherits from an SDK one. - task_group: SerializedTaskGroup # type: ignore[assignment] - task_dict: dict[str, SerializedBaseOperator | SerializedMappedOperator] # type: ignore[assignment] + access_control: dict[str, dict[str, Collection[str]]] | None = None + catchup: bool + dag_id: str + dag_display_name: str + dagrun_timeout: datetime.timedelta | None + deadline: list[DeadlineAlert] | DeadlineAlert | None + default_args: dict[str, Any] + description: str | None + disable_bundle_versioning: bool + doc_md: str | None + edge_info: dict[str, dict[str, EdgeInfoType]] + end_date: datetime.datetime | None + fail_fast: bool + has_on_failure_callback: bool + has_on_success_callback: bool + is_paused_upon_creation: bool | None + max_active_runs: int + max_active_tasks: int + max_consecutive_failed_dag_runs: int + owner_links: dict[str, str] + params: ParamsDict # TODO (GH-52141): Should use a scheduler-specific type. + partial: bool + render_template_as_native_obj: bool + start_date: datetime.datetime | None + tags: set[str] + task_dict: dict[str, SerializedOperator] + task_group: SerializedTaskGroup + template_searchpath: tuple[str, ...] | None + timetable: Timetable + timezone: FixedTimezone | Timezone last_loaded: datetime.datetime # this will only be set at serialization time # it's only use is for determining the relative fileloc based only on the serialize dag _processor_dags_folder: str + def __init__(self, *, dag_id: str) -> None: + self.catchup = False # Schema default + self.dag_id = self.dag_display_name = dag_id + self.dagrun_timeout = None + self.deadline = None + self.default_args = {} + self.description = None + self.disable_bundle_versioning = False + self.doc_md = None + self.edge_info = {} + self.end_date = None + self.fail_fast = False + self.has_on_failure_callback = False + self.has_on_success_callback = False + self.is_paused_upon_creation = None + self.max_active_runs = 16 # Schema default + self.max_active_tasks = 16 # Schema default + self.max_consecutive_failed_dag_runs = 0 # Schema default + self.owner_links = {} + self.params = ParamsDict() + self.partial = False + self.render_template_as_native_obj = False + self.start_date = None + self.tags = set() + self.template_searchpath = None + + def __repr__(self) -> str: + return f"" + @staticmethod def __get_constructor_defaults(): param_to_attr = { @@ -2368,6 +2504,39 @@ def __get_constructor_defaults(): _json_schema: ClassVar[Validator] = lazy_object_proxy.Proxy(load_dag_schema) + @classmethod + def get_serialized_fields(cls) -> frozenset[str]: + return frozenset( + { + "access_control", + "catchup", + "dag_display_name", + "dag_id", + "dagrun_timeout", + "deadline", + "default_args", + "description", + "disable_bundle_versioning", + "doc_md", + "edge_info", + "end_date", + "fail_fast", + "fileloc", + "is_paused_upon_creation", + "max_active_runs", + "max_active_tasks", + "max_consecutive_failed_dag_runs", + "owner_links", + "relative_fileloc", + "render_template_as_native_obj", + "start_date", + "tags", + "task_group", + "timetable", + "timezone", + } + ) + @classmethod def serialize_dag(cls, dag: DAG) -> dict: """Serialize a DAG into a JSON object.""" @@ -2385,7 +2554,11 @@ def serialize_dag(cls, dag: DAG) -> dict: serialized_dag["dag_dependencies"] = [x.__dict__ for x in sorted(dag_deps)] serialized_dag["task_group"] = TaskGroupSerialization.serialize_task_group(dag.task_group) - serialized_dag["deadline"] = dag.deadline.serialize_deadline_alert() if dag.deadline else None + serialized_dag["deadline"] = ( + [deadline.serialize_deadline_alert() for deadline in dag.deadline] + if isinstance(dag.deadline, list) + else None + ) # Edge info in the JSON exactly matches our internal structure serialized_dag["edge_info"] = dag.edge_info @@ -2396,6 +2569,23 @@ def serialize_dag(cls, dag: DAG) -> dict: serialized_dag["has_on_success_callback"] = True if dag.has_on_failure_callback: serialized_dag["has_on_failure_callback"] = True + + # TODO: Move this logic to a better place -- ideally before serializing contents of default_args. + # There is some duplication with this and SerializedBaseOperator.partial_kwargs serialization. + # Ideally default_args goes through same logic as fields of SerializedBaseOperator. + if serialized_dag.get("default_args", {}): + default_args_dict = serialized_dag["default_args"][Encoding.VAR] + callbacks_to_remove = [] + for k, v in list(default_args_dict.items()): + if k in [ + f"on_{x}_callback" for x in ("execute", "failure", "success", "retry", "skipped") + ]: + if bool(v): + default_args_dict[f"has_{k}"] = True + callbacks_to_remove.append(k) + for k in callbacks_to_remove: + del default_args_dict[k] + return serialized_dag except SerializationError: raise @@ -2409,7 +2599,8 @@ def deserialize_dag( """Deserializes a DAG from a JSON object.""" if "dag_id" not in encoded_dag: raise DeserializationError( - message="Encoded dag object has no dag_id key. You may need to run `airflow dags reserialize`." + message="Encoded dag object has no dag_id key. " + "You may need to run `airflow dags reserialize`." ) dag_id = encoded_dag["dag_id"] @@ -2428,7 +2619,7 @@ def _deserialize_dag_internal( cls, encoded_dag: dict[str, Any], client_defaults: dict[str, Any] | None = None ) -> SerializedDAG: """Handle the main Dag deserialization logic.""" - dag = SerializedDAG(dag_id=encoded_dag["dag_id"], schedule=None) + dag = SerializedDAG(dag_id=encoded_dag["dag_id"]) dag.last_loaded = utcnow() # Note: Context is passed explicitly through method parameters, no class attributes needed @@ -2501,7 +2692,14 @@ def _deserialize_dag_internal( dag.has_on_failure_callback = True if "deadline" in encoded_dag and encoded_dag["deadline"] is not None: - dag.deadline = DeadlineAlert.deserialize_deadline_alert(encoded_dag["deadline"]) + dag.deadline = ( + [ + DeadlineAlert.deserialize_deadline_alert(deadline_data) + for deadline_data in encoded_dag["deadline"] + ] + if encoded_dag["deadline"] + else None + ) keys_to_set_none = dag.get_serialized_fields() - encoded_dag.keys() - cls._CONSTRUCTOR_PARAMS.keys() for k in keys_to_set_none: @@ -2520,8 +2718,38 @@ def _is_excluded(cls, var: Any, attrname: str, op: DAGNode): return False if attrname == "dag_display_name" and var == op.dag_id: return True + + # DAG schema defaults exclusion (same pattern as SerializedBaseOperator) + dag_schema_defaults = cls.get_schema_defaults("dag") + if attrname in dag_schema_defaults: + if dag_schema_defaults[attrname] == var: + return True + + optional_fields = cls.get_dag_optional_fields_from_schema() + if var is None: + return True + if attrname in optional_fields: + if var in [[], (), set(), {}]: + return True + return super()._is_excluded(var, attrname, op) + @classmethod + @lru_cache(maxsize=1) # Only one type: "dag" + def get_dag_optional_fields_from_schema(cls) -> set[str]: + schema_loader = cls._json_schema + + if schema_loader is None: + return set() + + schema_data = schema_loader.schema + operator_def = schema_data.get("definitions", {}).get("dag", {}) + operator_fields = set(operator_def.get("properties", {}).keys()) + required_fields = set(operator_def.get("required", [])) + + optional_fields = operator_fields - required_fields + return optional_fields + @classmethod def to_dict(cls, var: Any) -> dict: """Stringifies DAGs and operators contained by var and returns a dict of var.""" @@ -2672,14 +2900,22 @@ def _create_compat_timetable(value): # Set on the root TG dag_dict["task_group"]["group_display_name"] = "" + @staticmethod + def conversion_v2_to_v3(ser_obj: dict): + # V2 to V3 changes are minimal - mainly client_defaults optimization and + # field presence differences. Only version bump needed. + ser_obj["__version"] = 3 + @classmethod def from_dict(cls, serialized_obj: dict) -> SerializedDAG: """Deserializes a python dict in to the DAG and operators it contains.""" ver = serialized_obj.get("__version", "") - if ver not in (1, 2): + if ver not in (1, 2, 3): raise ValueError(f"Unsure how to deserialize version {ver!r}") if ver == 1: cls.conversion_v1_to_v2(serialized_obj) + if ver == 2: + cls.conversion_v2_to_v3(serialized_obj) # Extract client_defaults for hierarchical defaults resolution client_defaults = serialized_obj.get("client_defaults", {}) @@ -2736,18 +2972,45 @@ def bulk_write_to_db( dag_op.update_dag_asset_expression(orm_dags=orm_dags, orm_assets=orm_assets) session.flush() - # TODO (GH-52141): This needs to take scheduler types, but currently it inherits SDK's DAG. - # TODO (GH-52141): This shouldn't need to be writable, but SDK's DAG defines it as such. - @property # type: ignore[misc] - def tasks(self) -> Sequence[SerializedOperator]: # type: ignore[override] + @property + def tasks(self) -> Sequence[SerializedOperator]: return list(self.task_dict.values()) + @property + def task_ids(self) -> list[str]: + return list(self.task_dict) + + @property + def roots(self) -> list[SerializedOperator]: + return [task for task in self.tasks if not task.upstream_list] + + @property + def owner(self) -> str: + return ", ".join({t.owner for t in self.tasks}) + + @property + def timetable_summary(self) -> str: + return self.timetable.summary + + def has_task(self, task_id: str) -> bool: + return task_id in self.task_dict + + def get_task(self, task_id: str) -> SerializedOperator: + if task_id in self.task_dict: + return self.task_dict[task_id] + raise TaskNotFound(f"Task {task_id} not found") + + @property + def task_group_dict(self): + return {k: v for k, v in self.task_group.get_task_group_dict().items() if k is not None} + def partial_subset( self, task_ids: str | Iterable[str], include_downstream: bool = False, include_upstream: bool = True, include_direct_upstream: bool = False, + exclude_original: bool = False, ): from airflow.models.mappedoperator import MappedOperator as SerializedMappedOperator @@ -2798,6 +3061,8 @@ def _deepcopy_task(t) -> SerializedOperator: return copy.deepcopy(t, memo) # Compiling the unique list of tasks that made the cut + if exclude_original: + matched_tasks = [] dag.task_dict = { t.task_id: _deepcopy_task(t) for t in itertools.chain(matched_tasks, also_include, direct_upstreams) @@ -3096,19 +3361,21 @@ def create_dagrun( session=session, ) - if self.deadline and isinstance(self.deadline.reference, DeadlineReference.TYPES.DAGRUN): - session.add( - Deadline( - deadline_time=self.deadline.reference.evaluate_with( - session=session, - interval=self.deadline.interval, - dag_id=self.dag_id, - run_id=run_id, - ), - callback=self.deadline.callback, - dagrun_id=orm_dagrun.id, - ) - ) + if self.deadline: + for deadline in cast("list", self.deadline): + if isinstance(deadline.reference, DeadlineReference.TYPES.DAGRUN): + session.add( + Deadline( + deadline_time=deadline.reference.evaluate_with( + session=session, + interval=deadline.interval, + dag_id=self.dag_id, + run_id=run_id, + ), + callback=deadline.callback, + dagrun_id=orm_dagrun.id, + ) + ) return orm_dagrun @@ -3143,9 +3410,7 @@ def set_task_instance_state( """ from airflow.api.common.mark_tasks import set_state - # TODO (GH-52141): get_task in scheduler needs to return scheduler types - # instead, but currently it inherits SDK's DAG. - task = cast("SerializedOperator", self.get_task(task_id)) + task = self.get_task(task_id) task.dag = self tasks_to_set_state: list[SerializedOperator | tuple[SerializedOperator, int]] @@ -3523,6 +3788,14 @@ def _coerce_dag(dag): for dag in dags ) + def get_edge_info(self, upstream_task_id: str, downstream_task_id: str) -> EdgeInfoType: + """Return edge information for the given pair of tasks or an empty edge if there is no information.""" + # Note - older serialized dags may not have edge_info being a dict at all + empty = cast("EdgeInfoType", {}) + if self.edge_info: + return self.edge_info.get(upstream_task_id, {}).get(downstream_task_id, empty) + return empty + class TaskGroupSerialization(BaseSerialization): """JSON serializable representation of a task group.""" @@ -3615,11 +3888,23 @@ class SerializedAssetWatcher(AssetWatcher): trigger: dict -def _has_kubernetes() -> bool: +def _has_kubernetes(attempt_import: bool = False) -> bool: + """ + Check if kubernetes libraries are available. + + :param attempt_import: If true, attempt to import kubernetes libraries if not already loaded. If + False, only check if already in sys.modules (avoids expensive import). + :return: True if kubernetes libraries are available, False otherwise. + """ global HAS_KUBERNETES if "HAS_KUBERNETES" in globals(): return HAS_KUBERNETES + # Check if kubernetes is already imported before triggering expensive import + if "kubernetes.client" not in sys.modules and not attempt_import: + HAS_KUBERNETES = False + return False + # Loading kube modules is expensive, so delay it until the last moment try: @@ -3650,16 +3935,33 @@ class LazyDeserializedDAG(pydantic.BaseModel): last_loaded: datetime.datetime | None = None NULLABLE_PROPERTIES: ClassVar[set[str]] = { - "is_paused_upon_creation", + # Non attr fields that should be nullable, or attrs with a different default "owner", + "owner_links", "dag_display_name", + "has_on_success_callback", + "has_on_failure_callback", + "tags", + # Attr properties that are nullable, or have a default that loads from config "description", - "relative_fileloc", + "start_date", + "end_date", + "template_searchpath", + "user_defined_macros", + "user_defined_filters", "max_active_tasks", "max_active_runs", "max_consecutive_failed_dag_runs", - "owner_links", + "dagrun_timeout", + "deadline", + "catchup", + "doc_md", "access_control", + "is_paused_upon_creation", + "jinja_environment_kwargs", + "relative_fileloc", + "disable_bundle_versioning", + "last_loaded", } @classmethod diff --git a/airflow-core/src/airflow/serialization/serializers/pydantic.py b/airflow-core/src/airflow/serialization/serializers/pydantic.py index 91db381264315..b07cc634176af 100644 --- a/airflow-core/src/airflow/serialization/serializers/pydantic.py +++ b/airflow-core/src/airflow/serialization/serializers/pydantic.py @@ -46,7 +46,7 @@ def serialize(o: object) -> tuple[U, str, int, bool]: if not is_pydantic_model(o): return "", "", 0, False - data = o.model_dump() # type: ignore + data = o.model_dump(mode="json") # type: ignore return data, qualname(o), __version__, True diff --git a/airflow-core/src/airflow/serialization/typing.py b/airflow-core/src/airflow/serialization/typing.py index a6169b23a78d5..35166710b7810 100644 --- a/airflow-core/src/airflow/serialization/typing.py +++ b/airflow-core/src/airflow/serialization/typing.py @@ -17,6 +17,7 @@ # under the License. from __future__ import annotations +from dataclasses import is_dataclass from typing import Any @@ -29,4 +30,9 @@ def is_pydantic_model(cls: Any) -> bool: """ # __pydantic_fields__ is always present on Pydantic V2 models and is a dict[str, FieldInfo] # __pydantic_validator__ is an internal validator object, always set after model build - return hasattr(cls, "__pydantic_fields__") and hasattr(cls, "__pydantic_validator__") + # Check if it is not a dataclass to prevent detecting pydantic dataclasses as pydantic models + return ( + hasattr(cls, "__pydantic_fields__") + and hasattr(cls, "__pydantic_validator__") + and not is_dataclass(cls) + ) diff --git a/airflow-core/src/airflow/settings.py b/airflow-core/src/airflow/settings.py index 733a539b34423..e2d96d96f52bf 100644 --- a/airflow-core/src/airflow/settings.py +++ b/airflow-core/src/airflow/settings.py @@ -19,7 +19,7 @@ import atexit import functools -import json +import json as json_lib import logging import os import sys @@ -122,7 +122,7 @@ AsyncSession: Callable[..., SAAsyncSession] # The JSON library to use for DAG Serialization and De-Serialization -json = json +json = json_lib # Display alerts on the dashboard # Useful for warning about setup issues or announcing changes to end users @@ -633,6 +633,21 @@ def prepare_syspath_for_config_and_plugins(): sys.path.append(PLUGINS_FOLDER) +def __getattr__(name: str): + """Handle deprecated module attributes.""" + if name == "MASK_SECRETS_IN_LOGS": + import warnings + + warnings.warn( + "settings.MASK_SECRETS_IN_LOGS has been removed. This shim returns default value of False. " + "Use SecretsMasker.enable_log_masking(), disable_log_masking(), or is_log_masking_enabled() instead.", + DeprecationWarning, + stacklevel=2, + ) + return False + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + def import_local_settings(): """Import airflow_local_settings.py files to allow overriding any configs in settings.py file.""" try: @@ -685,17 +700,16 @@ def initialize(): configure_adapters() # The webservers import this file from models.py with the default settings. - if not os.environ.get("PYTHON_OPERATORS_VIRTUAL_ENV_MODE", None): - is_worker = os.environ.get("_AIRFLOW__REEXECUTED_PROCESS") == "1" - if not is_worker: - configure_orm() - configure_action_logging() - # Configure secrets masker before masking secrets _configure_secrets_masker() - # mask the sensitive_config_values - conf.mask_secrets() + is_worker = os.environ.get("_AIRFLOW__REEXECUTED_PROCESS") == "1" + if not os.environ.get("PYTHON_OPERATORS_VIRTUAL_ENV_MODE", None) and not is_worker: + configure_orm() + + # mask the sensitive_config_values + conf.mask_secrets() + configure_action_logging() # Run any custom runtime checks that needs to be executed for providers run_providers_custom_runtime_checks() diff --git a/airflow-core/src/airflow/task/priority_strategy.py b/airflow-core/src/airflow/task/priority_strategy.py index 05209fec8a3e3..90abf3aa0a32c 100644 --- a/airflow-core/src/airflow/task/priority_strategy.py +++ b/airflow-core/src/airflow/task/priority_strategy.py @@ -73,6 +73,9 @@ def __eq__(self, other: object) -> bool: return False return self.serialize() == other.serialize() + def __hash__(self): + return hash(self.serialize()) + class _AbsolutePriorityWeightStrategy(PriorityWeightStrategy): """Priority weight strategy that uses the task's priority weight directly.""" diff --git a/airflow-core/src/airflow/ti_deps/dep_context.py b/airflow-core/src/airflow/ti_deps/dep_context.py index 056b633f36122..1feafdd041ae1 100644 --- a/airflow-core/src/airflow/ti_deps/dep_context.py +++ b/airflow-core/src/airflow/ti_deps/dep_context.py @@ -18,7 +18,7 @@ from __future__ import annotations import contextlib -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import attr @@ -29,9 +29,7 @@ from sqlalchemy.orm.session import Session from airflow.models.dagrun import DagRun - from airflow.models.mappedoperator import MappedOperator from airflow.models.taskinstance import TaskInstance - from airflow.serialization.serialized_objects import SerializedBaseOperator @attr.define @@ -100,9 +98,7 @@ def ensure_finished_tis(self, dag_run: DagRun, session: Session) -> list[TaskIns if getattr(ti, "task", None) is not None or (dag := dag_run.dag) is None: continue with contextlib.suppress(TaskNotFound): - # TODO (GH-52141): get_task in scheduler should contain scheduler - # types instead, but currently it inherits SDK's DAG. - ti.task = cast("MappedOperator | SerializedBaseOperator", dag.get_task(ti.task_id)) + ti.task = dag.get_task(ti.task_id) self.finished_tis = finished_tis else: finished_tis = self.finished_tis diff --git a/airflow-core/src/airflow/ti_deps/deps/base_ti_dep.py b/airflow-core/src/airflow/ti_deps/deps/base_ti_dep.py index 4d54783b020af..bc4fb303117d4 100644 --- a/airflow-core/src/airflow/ti_deps/deps/base_ti_dep.py +++ b/airflow-core/src/airflow/ti_deps/deps/base_ti_dep.py @@ -17,13 +17,14 @@ # under the License. from __future__ import annotations -from collections.abc import Iterator from typing import TYPE_CHECKING, NamedTuple from airflow.ti_deps.dep_context import DepContext from airflow.utils.session import provide_session if TYPE_CHECKING: + from collections.abc import Iterator + from sqlalchemy.orm import Session from airflow.models.taskinstance import TaskInstance diff --git a/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py b/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py index 205cc5338b96a..5238ca97d1c11 100644 --- a/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py +++ b/airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py @@ -20,7 +20,7 @@ from airflow.models.taskinstance import PAST_DEPENDS_MET from airflow.ti_deps.deps.base_ti_dep import BaseTIDep -## The following constants are taken from the SkipMixin class in the standard provider +# The following constants are taken from the SkipMixin class in the standard provider # The key used by SkipMixin to store XCom data. XCOM_SKIPMIXIN_KEY = "skipmixin_key" diff --git a/airflow-core/src/airflow/ti_deps/deps/ready_to_reschedule.py b/airflow-core/src/airflow/ti_deps/deps/ready_to_reschedule.py index 501b1574205e1..2855fa25499d2 100644 --- a/airflow-core/src/airflow/ti_deps/deps/ready_to_reschedule.py +++ b/airflow-core/src/airflow/ti_deps/deps/ready_to_reschedule.py @@ -17,13 +17,23 @@ # under the License. from __future__ import annotations +from typing import TYPE_CHECKING + from airflow._shared.timezones import timezone -from airflow.executors.executor_loader import ExecutorLoader from airflow.models.taskreschedule import TaskReschedule from airflow.ti_deps.deps.base_ti_dep import BaseTIDep from airflow.utils.session import provide_session from airflow.utils.state import TaskInstanceState +if TYPE_CHECKING: + from collections.abc import Iterator + + from sqlalchemy.orm import Session + + from airflow.models.taskinstance import TaskInstance + from airflow.ti_deps.dep_context import DepContext + from airflow.ti_deps.deps.base_ti_dep import TIDepStatus + class ReadyToRescheduleDep(BaseTIDep): """Determines whether a task is ready to be rescheduled.""" @@ -34,27 +44,22 @@ class ReadyToRescheduleDep(BaseTIDep): RESCHEDULEABLE_STATES = {TaskInstanceState.UP_FOR_RESCHEDULE, None} @provide_session - def _get_dep_statuses(self, ti, session, dep_context): + def _get_dep_statuses( + self, + ti: TaskInstance, + session: Session, + dep_context: DepContext, + ) -> Iterator[TIDepStatus]: """ Determine whether a task is ready to be rescheduled. - Only tasks in NONE state with at least one row in task_reschedule table are + Only tasks in NONE or UP_FOR_RESCHEDULE state with at least one row in task_reschedule table are handled by this dependency class, otherwise this dependency is considered as passed. This dependency fails if the latest reschedule request's reschedule date is still in the future. """ from airflow.models.mappedoperator import MappedOperator - is_mapped = isinstance(ti.task, MappedOperator) - executor, _ = ExecutorLoader.import_default_executor_cls() - if ( - # Mapped sensors don't have the reschedule property (it can only be calculated after unmapping), - # so we don't check them here. They are handled below by checking TaskReschedule instead. - not is_mapped and not getattr(ti.task, "reschedule", False) - ): - yield self._passing_status(reason="Task is not in reschedule mode.") - return - if dep_context.ignore_in_reschedule_period: yield self._passing_status( reason="The context specified that being in a reschedule period was permitted." @@ -75,14 +80,13 @@ def _get_dep_statuses(self, ti, session, dep_context): if not next_reschedule_date: # Because mapped sensors don't have the reschedule property, here's the last resort # and we need a slightly different passing reason - if is_mapped: + if isinstance(ti.task, MappedOperator): yield self._passing_status(reason="The task is mapped and not in reschedule mode") return yield self._passing_status(reason="There is no reschedule request for this task instance.") return - now = timezone.utcnow() - if now >= next_reschedule_date: + if (now := timezone.utcnow()) >= next_reschedule_date: yield self._passing_status(reason="Task instance id ready for reschedule.") return diff --git a/airflow-core/src/airflow/timetables/_cron.py b/airflow-core/src/airflow/timetables/_cron.py index e62f96de77029..b8bc22921ba86 100644 --- a/airflow-core/src/airflow/timetables/_cron.py +++ b/airflow-core/src/airflow/timetables/_cron.py @@ -71,17 +71,52 @@ def __init__(self, cron: str, timezone: str | Timezone | FixedTimezone) -> None: self._timezone = timezone try: - descriptor = ExpressionDescriptor( - expression=self._expression, casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True - ) # checking for more than 5 parameters in Cron and avoiding evaluation for now, # as Croniter has inconsistent evaluation with other libraries if len(croniter(self._expression).expanded) > 5: raise FormatException() - interval_description: str = descriptor.get_description() + + self.description = self._describe_with_dom_dow_fix(self._expression) + except (CroniterBadCronError, FormatException, MissingFieldException): - interval_description = "" - self.description: str = interval_description + self.description = "" + + def _describe_with_dom_dow_fix(self, expression: str) -> str: + """ + Return cron description with fix for DOM+DOW conflicts. + + If both DOM and DOW are restricted, explain them as OR. + """ + cron_fields = expression.split() + + if len(cron_fields) < 5: + return ExpressionDescriptor( + expression, casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True + ).get_description() + + dom = cron_fields[2] + dow = cron_fields[4] + + if dom != "*" and dow != "*": + # Case: conflict → DOM OR DOW + cron_fields_dom = cron_fields.copy() + cron_fields_dom[4] = "*" + day_of_month_desc = ExpressionDescriptor( + " ".join(cron_fields_dom), casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True + ).get_description() + + cron_fields_dow = cron_fields.copy() + cron_fields_dow[2] = "*" + day_of_week_desc = ExpressionDescriptor( + " ".join(cron_fields_dow), casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True + ).get_description() + + return f"{day_of_month_desc} (or) {day_of_week_desc}" + + # no conflict → return normal description + return ExpressionDescriptor( + expression, casing_type=CasingTypeEnum.Sentence, use_24hour_time_format=True + ).get_description() def __eq__(self, other: object) -> bool: """ @@ -93,6 +128,9 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self._expression == other._expression and self._timezone == other._timezone + def __hash__(self): + return hash((self._expression, self._timezone)) + @property def summary(self) -> str: return self._expression diff --git a/airflow-core/src/airflow/timetables/interval.py b/airflow-core/src/airflow/timetables/interval.py index e248dd67da206..c116b2343fbf6 100644 --- a/airflow-core/src/airflow/timetables/interval.py +++ b/airflow-core/src/airflow/timetables/interval.py @@ -201,6 +201,9 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self._delta == other._delta + def __hash__(self): + return hash(self._delta) + def serialize(self) -> dict[str, Any]: from airflow.serialization.serialized_objects import encode_relativedelta diff --git a/airflow-core/src/airflow/timetables/simple.py b/airflow-core/src/airflow/timetables/simple.py index b5b6f2468f369..3723d7321ee22 100644 --- a/airflow-core/src/airflow/timetables/simple.py +++ b/airflow-core/src/airflow/timetables/simple.py @@ -50,6 +50,9 @@ def __eq__(self, other: object) -> bool: return NotImplemented return True + def __hash__(self): + return hash(self.__class__.__name__) + def serialize(self) -> dict[str, Any]: return {} diff --git a/airflow-core/src/airflow/ui/dev/index.html b/airflow-core/src/airflow/ui/dev/index.html index f77a08175d1cd..10afd373f8fae 100644 --- a/airflow-core/src/airflow/ui/dev/index.html +++ b/airflow-core/src/airflow/ui/dev/index.html @@ -1,6 +1,6 @@ - + diff --git a/airflow-core/src/airflow/ui/eslint.config.js b/airflow-core/src/airflow/ui/eslint.config.js index 8a9c5f67c0b36..d85f4fde83c93 100644 --- a/airflow-core/src/airflow/ui/eslint.config.js +++ b/airflow-core/src/airflow/ui/eslint.config.js @@ -27,6 +27,7 @@ import { jsoncRules } from "./rules/jsonc.js"; import { perfectionistRules } from "./rules/perfectionist.js"; import { prettierRules } from "./rules/prettier.js"; import { reactRules } from "./rules/react.js"; +import { remRules } from "./rules/rem.js"; import { stylisticRules } from "./rules/stylistic.js"; import { typescriptRules } from "./rules/typescript.js"; import { unicornRules } from "./rules/unicorn.js"; @@ -46,6 +47,7 @@ export default /** @type {const} @satisfies {ReadonlyArray} * prettierRules, reactRules, stylisticRules, + remRules, unicornRules, i18nextRules, i18nRules, diff --git a/airflow-core/src/airflow/ui/index.html b/airflow-core/src/airflow/ui/index.html index e3f4f943f2530..952a8d3becb42 100644 --- a/airflow-core/src/airflow/ui/index.html +++ b/airflow-core/src/airflow/ui/index.html @@ -1,5 +1,5 @@ - + diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index 457aac893eef9..de50c91bd1c09 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -1,7 +1,7 @@ // generated with @7nohe/openapi-react-query-codegen@1.6.2 import { UseQueryResult } from "@tanstack/react-query"; -import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagParsingService, DagReportService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, HumanInTheLoopService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; +import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagParsingService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; import { DagRunState, DagWarningType } from "../requests/types.gen"; export type AssetServiceGetAssetsDefaultResponse = Awaited>; export type AssetServiceGetAssetsQueryResult = UseQueryResult; @@ -205,12 +205,6 @@ export const useDagStatsServiceGetDagStatsKey = "DagStatsServiceGetDagStats"; export const UseDagStatsServiceGetDagStatsKeyFn = ({ dagIds }: { dagIds?: string[]; } = {}, queryKey?: Array) => [useDagStatsServiceGetDagStatsKey, ...(queryKey ?? [{ dagIds }])]; -export type DagReportServiceGetDagReportsDefaultResponse = Awaited>; -export type DagReportServiceGetDagReportsQueryResult = UseQueryResult; -export const useDagReportServiceGetDagReportsKey = "DagReportServiceGetDagReports"; -export const UseDagReportServiceGetDagReportsKeyFn = ({ subdir }: { - subdir: string; -}, queryKey?: Array) => [useDagReportServiceGetDagReportsKey, ...(queryKey ?? [{ subdir }])]; export type ConfigServiceGetConfigDefaultResponse = Awaited>; export type ConfigServiceGetConfigQueryResult = UseQueryResult; export const useConfigServiceGetConfigKey = "ConfigServiceGetConfig"; @@ -382,7 +376,7 @@ export const UseTaskInstanceServiceGetTaskInstanceKeyFn = ({ dagId, dagRunId, ta export type TaskInstanceServiceGetMappedTaskInstancesDefaultResponse = Awaited>; export type TaskInstanceServiceGetMappedTaskInstancesQueryResult = UseQueryResult; export const useTaskInstanceServiceGetMappedTaskInstancesKey = "TaskInstanceServiceGetMappedTaskInstances"; -export const UseTaskInstanceServiceGetMappedTaskInstancesKeyFn = ({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const UseTaskInstanceServiceGetMappedTaskInstancesKeyFn = ({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -399,6 +393,7 @@ export const UseTaskInstanceServiceGetMappedTaskInstancesKeyFn = ({ dagId, dagRu logicalDateGte?: string; logicalDateLt?: string; logicalDateLte?: string; + mapIndex?: number[]; offset?: number; operator?: string[]; orderBy?: string[]; @@ -420,7 +415,7 @@ export const UseTaskInstanceServiceGetMappedTaskInstancesKeyFn = ({ dagId, dagRu updatedAtLt?: string; updatedAtLte?: string; versionNumber?: number[]; -}, queryKey?: Array) => [useTaskInstanceServiceGetMappedTaskInstancesKey, ...(queryKey ?? [{ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }])]; +}, queryKey?: Array) => [useTaskInstanceServiceGetMappedTaskInstancesKey, ...(queryKey ?? [{ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }])]; export type TaskInstanceServiceGetTaskInstanceDependenciesByMapIndexDefaultResponse = Awaited>; export type TaskInstanceServiceGetTaskInstanceDependenciesByMapIndexQueryResult = UseQueryResult; export const useTaskInstanceServiceGetTaskInstanceDependenciesByMapIndexKey = "TaskInstanceServiceGetTaskInstanceDependenciesByMapIndex"; @@ -469,7 +464,7 @@ export const UseTaskInstanceServiceGetMappedTaskInstanceKeyFn = ({ dagId, dagRun export type TaskInstanceServiceGetTaskInstancesDefaultResponse = Awaited>; export type TaskInstanceServiceGetTaskInstancesQueryResult = UseQueryResult; export const useTaskInstanceServiceGetTaskInstancesKey = "TaskInstanceServiceGetTaskInstances"; -export const UseTaskInstanceServiceGetTaskInstancesKeyFn = ({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const UseTaskInstanceServiceGetTaskInstancesKeyFn = ({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -486,6 +481,7 @@ export const UseTaskInstanceServiceGetTaskInstancesKeyFn = ({ dagId, dagRunId, d logicalDateGte?: string; logicalDateLt?: string; logicalDateLte?: string; + mapIndex?: number[]; offset?: number; operator?: string[]; orderBy?: string[]; @@ -501,6 +497,7 @@ export const UseTaskInstanceServiceGetTaskInstancesKeyFn = ({ dagId, dagRunId, d startDateLte?: string; state?: string[]; taskDisplayNamePattern?: string; + taskGroupId?: string; taskId?: string; tryNumber?: number[]; updatedAtGt?: string; @@ -508,7 +505,7 @@ export const UseTaskInstanceServiceGetTaskInstancesKeyFn = ({ dagId, dagRunId, d updatedAtLt?: string; updatedAtLte?: string; versionNumber?: number[]; -}, queryKey?: Array) => [useTaskInstanceServiceGetTaskInstancesKey, ...(queryKey ?? [{ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }])]; +}, queryKey?: Array) => [useTaskInstanceServiceGetTaskInstancesKey, ...(queryKey ?? [{ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }])]; export type TaskInstanceServiceGetTaskInstanceTryDetailsDefaultResponse = Awaited>; export type TaskInstanceServiceGetTaskInstanceTryDetailsQueryResult = UseQueryResult; export const useTaskInstanceServiceGetTaskInstanceTryDetailsKey = "TaskInstanceServiceGetTaskInstanceTryDetails"; @@ -552,6 +549,39 @@ export const UseTaskInstanceServiceGetExternalLogUrlKeyFn = ({ dagId, dagRunId, taskId: string; tryNumber: number; }, queryKey?: Array) => [useTaskInstanceServiceGetExternalLogUrlKey, ...(queryKey ?? [{ dagId, dagRunId, mapIndex, taskId, tryNumber }])]; +export type TaskInstanceServiceGetHitlDetailDefaultResponse = Awaited>; +export type TaskInstanceServiceGetHitlDetailQueryResult = UseQueryResult; +export const useTaskInstanceServiceGetHitlDetailKey = "TaskInstanceServiceGetHitlDetail"; +export const UseTaskInstanceServiceGetHitlDetailKeyFn = ({ dagId, dagRunId, mapIndex, taskId }: { + dagId: string; + dagRunId: string; + mapIndex: number; + taskId: string; +}, queryKey?: Array) => [useTaskInstanceServiceGetHitlDetailKey, ...(queryKey ?? [{ dagId, dagRunId, mapIndex, taskId }])]; +export type TaskInstanceServiceGetHitlDetailsDefaultResponse = Awaited>; +export type TaskInstanceServiceGetHitlDetailsQueryResult = UseQueryResult; +export const useTaskInstanceServiceGetHitlDetailsKey = "TaskInstanceServiceGetHitlDetails"; +export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { + bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; + dagId: string; + dagIdPattern?: string; + dagRunId: string; + limit?: number; + mapIndex?: number; + offset?: number; + orderBy?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; + responseReceived?: boolean; + state?: string[]; + subjectSearch?: string; + taskId?: string; + taskIdPattern?: string; +}, queryKey?: Array) => [useTaskInstanceServiceGetHitlDetailsKey, ...(queryKey ?? [{ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }])]; export type ImportErrorServiceGetImportErrorDefaultResponse = Awaited>; export type ImportErrorServiceGetImportErrorQueryResult = UseQueryResult; export const useImportErrorServiceGetImportErrorKey = "ImportErrorServiceGetImportError"; @@ -561,11 +591,12 @@ export const UseImportErrorServiceGetImportErrorKeyFn = ({ importErrorId }: { export type ImportErrorServiceGetImportErrorsDefaultResponse = Awaited>; export type ImportErrorServiceGetImportErrorsQueryResult = UseQueryResult; export const useImportErrorServiceGetImportErrorsKey = "ImportErrorServiceGetImportErrors"; -export const UseImportErrorServiceGetImportErrorsKeyFn = ({ limit, offset, orderBy }: { +export const UseImportErrorServiceGetImportErrorsKeyFn = ({ filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: Array) => [useImportErrorServiceGetImportErrorsKey, ...(queryKey ?? [{ limit, offset, orderBy }])]; +} = {}, queryKey?: Array) => [useImportErrorServiceGetImportErrorsKey, ...(queryKey ?? [{ filenamePattern, limit, offset, orderBy }])]; export type JobServiceGetJobsDefaultResponse = Awaited>; export type JobServiceGetJobsQueryResult = UseQueryResult; export const useJobServiceGetJobsKey = "JobServiceGetJobs"; @@ -705,34 +736,6 @@ export const UseDagVersionServiceGetDagVersionsKeyFn = ({ bundleName, bundleVers orderBy?: string[]; versionNumber?: number; }, queryKey?: Array) => [useDagVersionServiceGetDagVersionsKey, ...(queryKey ?? [{ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }])]; -export type HumanInTheLoopServiceGetHitlDetailDefaultResponse = Awaited>; -export type HumanInTheLoopServiceGetHitlDetailQueryResult = UseQueryResult; -export const useHumanInTheLoopServiceGetHitlDetailKey = "HumanInTheLoopServiceGetHitlDetail"; -export const UseHumanInTheLoopServiceGetHitlDetailKeyFn = ({ dagId, dagRunId, mapIndex, taskId }: { - dagId: string; - dagRunId: string; - mapIndex?: number; - taskId: string; -}, queryKey?: Array) => [useHumanInTheLoopServiceGetHitlDetailKey, ...(queryKey ?? [{ dagId, dagRunId, mapIndex, taskId }])]; -export type HumanInTheLoopServiceGetHitlDetailsDefaultResponse = Awaited>; -export type HumanInTheLoopServiceGetHitlDetailsQueryResult = UseQueryResult; -export const useHumanInTheLoopServiceGetHitlDetailsKey = "HumanInTheLoopServiceGetHitlDetails"; -export const UseHumanInTheLoopServiceGetHitlDetailsKeyFn = ({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { - bodySearch?: string; - dagId?: string; - dagIdPattern?: string; - dagRunId?: string; - limit?: number; - offset?: number; - orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; - responseReceived?: boolean; - state?: string[]; - subjectSearch?: string; - taskId?: string; - taskIdPattern?: string; -} = {}, queryKey?: Array) => [useHumanInTheLoopServiceGetHitlDetailsKey, ...(queryKey ?? [{ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }])]; export type MonitorServiceGetHealthDefaultResponse = Awaited>; export type MonitorServiceGetHealthQueryResult = UseQueryResult; export const useMonitorServiceGetHealthKey = "MonitorServiceGetHealth"; @@ -750,15 +753,7 @@ export const UseLoginServiceLoginKeyFn = ({ next }: { export type LoginServiceLogoutDefaultResponse = Awaited>; export type LoginServiceLogoutQueryResult = UseQueryResult; export const useLoginServiceLogoutKey = "LoginServiceLogout"; -export const UseLoginServiceLogoutKeyFn = ({ next }: { - next?: string; -} = {}, queryKey?: Array) => [useLoginServiceLogoutKey, ...(queryKey ?? [{ next }])]; -export type LoginServiceRefreshDefaultResponse = Awaited>; -export type LoginServiceRefreshQueryResult = UseQueryResult; -export const useLoginServiceRefreshKey = "LoginServiceRefresh"; -export const UseLoginServiceRefreshKeyFn = ({ next }: { - next?: string; -} = {}, queryKey?: Array) => [useLoginServiceRefreshKey, ...(queryKey ?? [{ next }])]; +export const UseLoginServiceLogoutKeyFn = (queryKey?: Array) => [useLoginServiceLogoutKey, ...(queryKey ?? [])]; export type AuthLinksServiceGetAuthMenusDefaultResponse = Awaited>; export type AuthLinksServiceGetAuthMenusQueryResult = UseQueryResult; export const useAuthLinksServiceGetAuthMenusKey = "AuthLinksServiceGetAuthMenus"; @@ -870,12 +865,12 @@ export type TaskInstanceServicePatchTaskInstanceByMapIndexMutationResult = Await export type TaskInstanceServiceBulkTaskInstancesMutationResult = Awaited>; export type TaskInstanceServicePatchTaskInstanceDryRunByMapIndexMutationResult = Awaited>; export type TaskInstanceServicePatchTaskInstanceDryRunMutationResult = Awaited>; +export type TaskInstanceServiceUpdateHitlDetailMutationResult = Awaited>; export type PoolServicePatchPoolMutationResult = Awaited>; export type PoolServiceBulkPoolsMutationResult = Awaited>; export type XcomServiceUpdateXcomEntryMutationResult = Awaited>; export type VariableServicePatchVariableMutationResult = Awaited>; export type VariableServiceBulkVariablesMutationResult = Awaited>; -export type HumanInTheLoopServiceUpdateHitlDetailMutationResult = Awaited>; export type AssetServiceDeleteAssetQueuedEventsMutationResult = Awaited>; export type AssetServiceDeleteDagAssetQueuedEventsMutationResult = Awaited>; export type AssetServiceDeleteDagAssetQueuedEventMutationResult = Awaited>; diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts index 75803561cfa32..b376420458833 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1,7 +1,7 @@ // generated with @7nohe/openapi-react-query-codegen@1.6.2 import { type QueryClient } from "@tanstack/react-query"; -import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagReportService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, HumanInTheLoopService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; +import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; import { DagRunState, DagWarningType } from "../requests/types.gen"; import * as Common from "./common"; /** @@ -14,7 +14,7 @@ import * as Common from "./common"; * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.dagIds * @param data.onlyActive -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name, uri, created_at, updated_at` * @returns AssetCollectionResponse Successful Response * @throws ApiError */ @@ -34,7 +34,7 @@ export const ensureUseAssetServiceGetAssetsData = (queryClient: QueryClient, { d * @param data.limit * @param data.offset * @param data.namePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name` * @returns AssetAliasCollectionResponse Successful Response * @throws ApiError */ @@ -61,7 +61,7 @@ export const ensureUseAssetServiceGetAssetAliasData = (queryClient: QueryClient, * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `source_task_id, source_dag_id, source_run_id, source_map_index, timestamp` * @param data.assetId * @param data.sourceDagId * @param data.sourceTaskId @@ -156,7 +156,7 @@ export const ensureUseAssetServiceNextRunAssetsData = (queryClient: QueryClient, * @param data.dagId * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @returns BackfillCollectionResponse Successful Response * @throws ApiError */ @@ -181,7 +181,7 @@ export const ensureUseBackfillServiceGetBackfillData = (queryClient: QueryClient * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @param data.dagId * @param data.active * @returns BackfillCollectionResponse Successful Response @@ -211,7 +211,7 @@ export const ensureUseConnectionServiceGetConnectionData = (queryClient: QueryCl * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `conn_id, conn_type, description, host, port, id, connection_id` * @param data.connectionIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ConnectionCollectionResponse Successful Response * @throws ApiError @@ -286,7 +286,7 @@ export const ensureUseDagRunServiceGetUpstreamAssetEventsData = (queryClient: Qu * @param data.runType * @param data.state * @param data.dagVersion -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, conf, duration, dag_run_id` * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGRunCollectionResponse Successful Response @@ -325,7 +325,7 @@ export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { }) => queryClient.ensureQueryData({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -342,7 +342,7 @@ export const ensureUseDagRunServiceWaitDagRunUntilFinishedData = (queryClient: Q }) => queryClient.ensureQueryData({ queryKey: Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, interval, result }), queryFn: () => DagRunService.waitDagRunUntilFinished({ dagId, dagRunId, interval, result }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -384,17 +384,6 @@ export const ensureUseDagStatsServiceGetDagStatsData = (queryClient: QueryClient dagIds?: string[]; } = {}) => queryClient.ensureQueryData({ queryKey: Common.UseDagStatsServiceGetDagStatsKeyFn({ dagIds }), queryFn: () => DagStatsService.getDagStats({ dagIds }) }); /** -* Get Dag Reports -* Get DAG report. -* @param data The data for the request. -* @param data.subdir -* @returns unknown Successful Response -* @throws ApiError -*/ -export const ensureUseDagReportServiceGetDagReportsData = (queryClient: QueryClient, { subdir }: { - subdir: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseDagReportServiceGetDagReportsKeyFn({ subdir }), queryFn: () => DagReportService.getDagReports({ subdir }) }); -/** * Get Config * @param data The data for the request. * @param data.section @@ -435,7 +424,7 @@ export const ensureUseConfigServiceGetConfigsData = (queryClient: QueryClient) = * @param data.warningType * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, warning_type, message, timestamp` * @returns DAGWarningCollectionResponse Successful Response * @throws ApiError */ @@ -474,7 +463,7 @@ export const ensureUseDagWarningServiceListDagWarningsData = (queryClient: Query * @param data.dagRunEndDateLte * @param data.dagRunEndDateLt * @param data.dagRunState -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` * @param data.isFavorite * @returns DAGCollectionResponse Successful Response * @throws ApiError @@ -535,7 +524,7 @@ export const ensureUseDagServiceGetDagDetailsData = (queryClient: QueryClient, { * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `name` * @param data.tagNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGTagCollectionResponse Successful Response * @throws ApiError @@ -565,7 +554,7 @@ export const ensureUseDagServiceGetDagTagsData = (queryClient: QueryClient, { li * @param data.lastDagRunState * @param data.bundleName * @param data.bundleVersion -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` * @param data.isFavorite * @param data.hasAssetSchedule Filter Dags with asset-based scheduling * @param data.assetDependency Filter Dags by asset dependency (name or URI) @@ -622,7 +611,7 @@ export const ensureUseEventLogServiceGetEventLogData = (queryClient: QueryClient * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dttm, dag_id, task_id, run_id, event, logical_date, owner, extra, when, event_log_id` * @param data.dagId * @param data.taskId * @param data.runId @@ -750,13 +739,14 @@ export const ensureUseTaskInstanceServiceGetTaskInstanceData = (queryClient: Que * @param data.versionNumber * @param data.tryNumber * @param data.operator +* @param data.mapIndex * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end` * @returns TaskInstanceCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseTaskInstanceServiceGetMappedTaskInstancesData = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const ensureUseTaskInstanceServiceGetMappedTaskInstancesData = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -773,6 +763,7 @@ export const ensureUseTaskInstanceServiceGetMappedTaskInstancesData = (queryClie logicalDateGte?: string; logicalDateLt?: string; logicalDateLte?: string; + mapIndex?: number[]; offset?: number; operator?: string[]; orderBy?: string[]; @@ -794,7 +785,7 @@ export const ensureUseTaskInstanceServiceGetMappedTaskInstancesData = (queryClie updatedAtLt?: string; updatedAtLte?: string; versionNumber?: number[]; -}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); /** * Get Task Instance Dependencies * Get dependencies blocking task from getting scheduled. @@ -914,6 +905,7 @@ export const ensureUseTaskInstanceServiceGetMappedTaskInstanceData = (queryClien * @param data.durationLte * @param data.durationLt * @param data.taskDisplayNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.taskGroupId Filter by exact task group ID. Returns all tasks within the specified task group. * @param data.state * @param data.pool * @param data.queue @@ -921,13 +913,14 @@ export const ensureUseTaskInstanceServiceGetMappedTaskInstanceData = (queryClien * @param data.versionNumber * @param data.tryNumber * @param data.operator +* @param data.mapIndex * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end` * @returns TaskInstanceCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseTaskInstanceServiceGetTaskInstancesData = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const ensureUseTaskInstanceServiceGetTaskInstancesData = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -944,6 +937,7 @@ export const ensureUseTaskInstanceServiceGetTaskInstancesData = (queryClient: Qu logicalDateGte?: string; logicalDateLt?: string; logicalDateLte?: string; + mapIndex?: number[]; offset?: number; operator?: string[]; orderBy?: string[]; @@ -959,6 +953,7 @@ export const ensureUseTaskInstanceServiceGetTaskInstancesData = (queryClient: Qu startDateLte?: string; state?: string[]; taskDisplayNamePattern?: string; + taskGroupId?: string; taskId?: string; tryNumber?: number[]; updatedAtGt?: string; @@ -966,7 +961,7 @@ export const ensureUseTaskInstanceServiceGetTaskInstancesData = (queryClient: Qu updatedAtLt?: string; updatedAtLte?: string; versionNumber?: number[]; -}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); /** * Get Task Instance Try Details * Get task instance details by try number. @@ -1049,6 +1044,70 @@ export const ensureUseTaskInstanceServiceGetExternalLogUrlData = (queryClient: Q tryNumber: number; }) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetExternalLogUrlKeyFn({ dagId, dagRunId, mapIndex, taskId, tryNumber }), queryFn: () => TaskInstanceService.getExternalLogUrl({ dagId, dagRunId, mapIndex, taskId, tryNumber }) }); /** +* Get Hitl Detail +* Get a Human-in-the-loop detail of a specific task instance. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.taskId +* @param data.mapIndex +* @returns HITLDetail Successful Response +* @throws ApiError +*/ +export const ensureUseTaskInstanceServiceGetHitlDetailData = (queryClient: QueryClient, { dagId, dagRunId, mapIndex, taskId }: { + dagId: string; + dagRunId: string; + mapIndex: number; + taskId: string; +}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }), queryFn: () => TaskInstanceService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) }); +/** +* Get Hitl Details +* Get Human-in-the-loop details. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.limit +* @param data.offset +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, run_after, rendered_map_index, task_instance_operator, task_instance_state` +* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.taskId +* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.mapIndex +* @param data.state +* @param data.responseReceived +* @param data.respondedByUserId +* @param data.respondedByUserName +* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt +* @returns HITLDetailCollection Successful Response +* @throws ApiError +*/ +export const ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { + bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; + dagId: string; + dagIdPattern?: string; + dagRunId: string; + limit?: number; + mapIndex?: number; + offset?: number; + orderBy?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; + responseReceived?: boolean; + state?: string[]; + subjectSearch?: string; + taskId?: string; + taskIdPattern?: string; +}) => queryClient.ensureQueryData({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); +/** * Get Import Error * Get an import error. * @param data The data for the request. @@ -1065,15 +1124,17 @@ export const ensureUseImportErrorServiceGetImportErrorData = (queryClient: Query * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, timestamp, filename, bundle_name, stacktrace, import_error_id` +* @param data.filenamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ImportErrorCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: QueryClient, { limit, offset, orderBy }: { +export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: QueryClient, { filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) }); +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) }); /** * Get Jobs * Get all jobs. @@ -1089,7 +1150,7 @@ export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: Quer * @param data.endDateLt * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dag_id, state, job_type, start_date, end_date, latest_heartbeat, executor_class, hostname, unixname` * @param data.jobState * @param data.jobType * @param data.hostname @@ -1150,7 +1211,7 @@ export const ensureUsePoolServiceGetPoolData = (queryClient: QueryClient, { pool * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, pool, name` * @param data.poolNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns PoolCollectionResponse Successful Response * @throws ApiError @@ -1291,7 +1352,7 @@ export const ensureUseVariableServiceGetVariableData = (queryClient: QueryClient * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `key, id, _val, description, is_encrypted` * @param data.variableKeyPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns VariableCollectionResponse Successful Response * @throws ApiError @@ -1327,7 +1388,7 @@ export const ensureUseDagVersionServiceGetDagVersionData = (queryClient: QueryCl * @param data.versionNumber * @param data.bundleName * @param data.bundleVersion -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, version_number, bundle_name, bundle_version` * @returns DAGVersionCollectionResponse Successful Response * @throws ApiError */ @@ -1341,60 +1402,6 @@ export const ensureUseDagVersionServiceGetDagVersionsData = (queryClient: QueryC versionNumber?: number; }) => queryClient.ensureQueryData({ queryKey: Common.UseDagVersionServiceGetDagVersionsKeyFn({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }), queryFn: () => DagVersionService.getDagVersions({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }) }); /** -* Get Hitl Detail -* Get a Human-in-the-loop detail of a specific task instance. -* @param data The data for the request. -* @param data.dagId -* @param data.dagRunId -* @param data.taskId -* @param data.mapIndex -* @returns HITLDetail Successful Response -* @throws ApiError -*/ -export const ensureUseHumanInTheLoopServiceGetHitlDetailData = (queryClient: QueryClient, { dagId, dagRunId, mapIndex, taskId }: { - dagId: string; - dagRunId: string; - mapIndex?: number; - taskId: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }), queryFn: () => HumanInTheLoopService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) }); -/** -* Get Hitl Details -* Get Human-in-the-loop details. -* @param data The data for the request. -* @param data.limit -* @param data.offset -* @param data.orderBy -* @param data.dagId -* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.dagRunId -* @param data.taskId -* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.state -* @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName -* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @returns HITLDetailCollection Successful Response -* @throws ApiError -*/ -export const ensureUseHumanInTheLoopServiceGetHitlDetailsData = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { - bodySearch?: string; - dagId?: string; - dagIdPattern?: string; - dagRunId?: string; - limit?: number; - offset?: number; - orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; - responseReceived?: boolean; - state?: string[]; - subjectSearch?: string; - taskId?: string; - taskIdPattern?: string; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); -/** * Get Health * @returns HealthInfoResponse Successful Response * @throws ApiError @@ -1421,25 +1428,10 @@ export const ensureUseLoginServiceLoginData = (queryClient: QueryClient, { next /** * Logout * Logout the user. -* @param data The data for the request. -* @param data.next -* @returns unknown Successful Response -* @throws ApiError -*/ -export const ensureUseLoginServiceLogoutData = (queryClient: QueryClient, { next }: { - next?: string; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseLoginServiceLogoutKeyFn({ next }), queryFn: () => LoginService.logout({ next }) }); -/** -* Refresh -* Refresh the authentication token. -* @param data The data for the request. -* @param data.next * @returns unknown Successful Response * @throws ApiError */ -export const ensureUseLoginServiceRefreshData = (queryClient: QueryClient, { next }: { - next?: string; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseLoginServiceRefreshKeyFn({ next }), queryFn: () => LoginService.refresh({ next }) }); +export const ensureUseLoginServiceLogoutData = (queryClient: QueryClient) => queryClient.ensureQueryData({ queryKey: Common.UseLoginServiceLogoutKeyFn(), queryFn: () => LoginService.logout() }); /** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response @@ -1505,7 +1497,7 @@ export const ensureUseStructureServiceStructureDataData = (queryClient: QueryCli * @param data.dagId * @param data.offset * @param data.limit -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` * @param data.runAfterGte * @param data.runAfterGt * @param data.runAfterLte @@ -1534,7 +1526,7 @@ export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient * @param data.dagId * @param data.offset * @param data.limit -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` * @param data.runAfterGte * @param data.runAfterGt * @param data.runAfterLte diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts index 180eb82621082..fbb00335e4d0a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1,7 +1,7 @@ // generated with @7nohe/openapi-react-query-codegen@1.6.2 import { type QueryClient } from "@tanstack/react-query"; -import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagReportService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, HumanInTheLoopService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; +import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; import { DagRunState, DagWarningType } from "../requests/types.gen"; import * as Common from "./common"; /** @@ -14,7 +14,7 @@ import * as Common from "./common"; * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.dagIds * @param data.onlyActive -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name, uri, created_at, updated_at` * @returns AssetCollectionResponse Successful Response * @throws ApiError */ @@ -34,7 +34,7 @@ export const prefetchUseAssetServiceGetAssets = (queryClient: QueryClient, { dag * @param data.limit * @param data.offset * @param data.namePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name` * @returns AssetAliasCollectionResponse Successful Response * @throws ApiError */ @@ -61,7 +61,7 @@ export const prefetchUseAssetServiceGetAssetAlias = (queryClient: QueryClient, { * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `source_task_id, source_dag_id, source_run_id, source_map_index, timestamp` * @param data.assetId * @param data.sourceDagId * @param data.sourceTaskId @@ -156,7 +156,7 @@ export const prefetchUseAssetServiceNextRunAssets = (queryClient: QueryClient, { * @param data.dagId * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @returns BackfillCollectionResponse Successful Response * @throws ApiError */ @@ -181,7 +181,7 @@ export const prefetchUseBackfillServiceGetBackfill = (queryClient: QueryClient, * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @param data.dagId * @param data.active * @returns BackfillCollectionResponse Successful Response @@ -211,7 +211,7 @@ export const prefetchUseConnectionServiceGetConnection = (queryClient: QueryClie * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `conn_id, conn_type, description, host, port, id, connection_id` * @param data.connectionIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ConnectionCollectionResponse Successful Response * @throws ApiError @@ -286,7 +286,7 @@ export const prefetchUseDagRunServiceGetUpstreamAssetEvents = (queryClient: Quer * @param data.runType * @param data.state * @param data.dagVersion -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, conf, duration, dag_run_id` * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGRunCollectionResponse Successful Response @@ -325,7 +325,7 @@ export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { d }) => queryClient.prefetchQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -342,7 +342,7 @@ export const prefetchUseDagRunServiceWaitDagRunUntilFinished = (queryClient: Que }) => queryClient.prefetchQuery({ queryKey: Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, interval, result }), queryFn: () => DagRunService.waitDagRunUntilFinished({ dagId, dagRunId, interval, result }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -384,17 +384,6 @@ export const prefetchUseDagStatsServiceGetDagStats = (queryClient: QueryClient, dagIds?: string[]; } = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDagStatsServiceGetDagStatsKeyFn({ dagIds }), queryFn: () => DagStatsService.getDagStats({ dagIds }) }); /** -* Get Dag Reports -* Get DAG report. -* @param data The data for the request. -* @param data.subdir -* @returns unknown Successful Response -* @throws ApiError -*/ -export const prefetchUseDagReportServiceGetDagReports = (queryClient: QueryClient, { subdir }: { - subdir: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseDagReportServiceGetDagReportsKeyFn({ subdir }), queryFn: () => DagReportService.getDagReports({ subdir }) }); -/** * Get Config * @param data The data for the request. * @param data.section @@ -435,7 +424,7 @@ export const prefetchUseConfigServiceGetConfigs = (queryClient: QueryClient) => * @param data.warningType * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, warning_type, message, timestamp` * @returns DAGWarningCollectionResponse Successful Response * @throws ApiError */ @@ -474,7 +463,7 @@ export const prefetchUseDagWarningServiceListDagWarnings = (queryClient: QueryCl * @param data.dagRunEndDateLte * @param data.dagRunEndDateLt * @param data.dagRunState -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` * @param data.isFavorite * @returns DAGCollectionResponse Successful Response * @throws ApiError @@ -535,7 +524,7 @@ export const prefetchUseDagServiceGetDagDetails = (queryClient: QueryClient, { d * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `name` * @param data.tagNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGTagCollectionResponse Successful Response * @throws ApiError @@ -565,7 +554,7 @@ export const prefetchUseDagServiceGetDagTags = (queryClient: QueryClient, { limi * @param data.lastDagRunState * @param data.bundleName * @param data.bundleVersion -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` * @param data.isFavorite * @param data.hasAssetSchedule Filter Dags with asset-based scheduling * @param data.assetDependency Filter Dags by asset dependency (name or URI) @@ -622,7 +611,7 @@ export const prefetchUseEventLogServiceGetEventLog = (queryClient: QueryClient, * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dttm, dag_id, task_id, run_id, event, logical_date, owner, extra, when, event_log_id` * @param data.dagId * @param data.taskId * @param data.runId @@ -750,13 +739,14 @@ export const prefetchUseTaskInstanceServiceGetTaskInstance = (queryClient: Query * @param data.versionNumber * @param data.tryNumber * @param data.operator +* @param data.mapIndex * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end` * @returns TaskInstanceCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseTaskInstanceServiceGetMappedTaskInstances = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const prefetchUseTaskInstanceServiceGetMappedTaskInstances = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -773,6 +763,7 @@ export const prefetchUseTaskInstanceServiceGetMappedTaskInstances = (queryClient logicalDateGte?: string; logicalDateLt?: string; logicalDateLte?: string; + mapIndex?: number[]; offset?: number; operator?: string[]; orderBy?: string[]; @@ -794,7 +785,7 @@ export const prefetchUseTaskInstanceServiceGetMappedTaskInstances = (queryClient updatedAtLt?: string; updatedAtLte?: string; versionNumber?: number[]; -}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); /** * Get Task Instance Dependencies * Get dependencies blocking task from getting scheduled. @@ -914,6 +905,7 @@ export const prefetchUseTaskInstanceServiceGetMappedTaskInstance = (queryClient: * @param data.durationLte * @param data.durationLt * @param data.taskDisplayNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.taskGroupId Filter by exact task group ID. Returns all tasks within the specified task group. * @param data.state * @param data.pool * @param data.queue @@ -921,13 +913,14 @@ export const prefetchUseTaskInstanceServiceGetMappedTaskInstance = (queryClient: * @param data.versionNumber * @param data.tryNumber * @param data.operator +* @param data.mapIndex * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end` * @returns TaskInstanceCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseTaskInstanceServiceGetTaskInstances = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const prefetchUseTaskInstanceServiceGetTaskInstances = (queryClient: QueryClient, { dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -944,6 +937,7 @@ export const prefetchUseTaskInstanceServiceGetTaskInstances = (queryClient: Quer logicalDateGte?: string; logicalDateLt?: string; logicalDateLte?: string; + mapIndex?: number[]; offset?: number; operator?: string[]; orderBy?: string[]; @@ -959,6 +953,7 @@ export const prefetchUseTaskInstanceServiceGetTaskInstances = (queryClient: Quer startDateLte?: string; state?: string[]; taskDisplayNamePattern?: string; + taskGroupId?: string; taskId?: string; tryNumber?: number[]; updatedAtGt?: string; @@ -966,7 +961,7 @@ export const prefetchUseTaskInstanceServiceGetTaskInstances = (queryClient: Quer updatedAtLt?: string; updatedAtLte?: string; versionNumber?: number[]; -}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) }); /** * Get Task Instance Try Details * Get task instance details by try number. @@ -1049,6 +1044,70 @@ export const prefetchUseTaskInstanceServiceGetExternalLogUrl = (queryClient: Que tryNumber: number; }) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetExternalLogUrlKeyFn({ dagId, dagRunId, mapIndex, taskId, tryNumber }), queryFn: () => TaskInstanceService.getExternalLogUrl({ dagId, dagRunId, mapIndex, taskId, tryNumber }) }); /** +* Get Hitl Detail +* Get a Human-in-the-loop detail of a specific task instance. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.taskId +* @param data.mapIndex +* @returns HITLDetail Successful Response +* @throws ApiError +*/ +export const prefetchUseTaskInstanceServiceGetHitlDetail = (queryClient: QueryClient, { dagId, dagRunId, mapIndex, taskId }: { + dagId: string; + dagRunId: string; + mapIndex: number; + taskId: string; +}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }), queryFn: () => TaskInstanceService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) }); +/** +* Get Hitl Details +* Get Human-in-the-loop details. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.limit +* @param data.offset +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, run_after, rendered_map_index, task_instance_operator, task_instance_state` +* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.taskId +* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.mapIndex +* @param data.state +* @param data.responseReceived +* @param data.respondedByUserId +* @param data.respondedByUserName +* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt +* @returns HITLDetailCollection Successful Response +* @throws ApiError +*/ +export const prefetchUseTaskInstanceServiceGetHitlDetails = (queryClient: QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { + bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; + dagId: string; + dagIdPattern?: string; + dagRunId: string; + limit?: number; + mapIndex?: number; + offset?: number; + orderBy?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; + responseReceived?: boolean; + state?: string[]; + subjectSearch?: string; + taskId?: string; + taskIdPattern?: string; +}) => queryClient.prefetchQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); +/** * Get Import Error * Get an import error. * @param data The data for the request. @@ -1065,15 +1124,17 @@ export const prefetchUseImportErrorServiceGetImportError = (queryClient: QueryCl * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, timestamp, filename, bundle_name, stacktrace, import_error_id` +* @param data.filenamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ImportErrorCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: QueryClient, { limit, offset, orderBy }: { +export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: QueryClient, { filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) }); +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) }); /** * Get Jobs * Get all jobs. @@ -1089,7 +1150,7 @@ export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: QueryC * @param data.endDateLt * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dag_id, state, job_type, start_date, end_date, latest_heartbeat, executor_class, hostname, unixname` * @param data.jobState * @param data.jobType * @param data.hostname @@ -1150,7 +1211,7 @@ export const prefetchUsePoolServiceGetPool = (queryClient: QueryClient, { poolNa * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, pool, name` * @param data.poolNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns PoolCollectionResponse Successful Response * @throws ApiError @@ -1291,7 +1352,7 @@ export const prefetchUseVariableServiceGetVariable = (queryClient: QueryClient, * @param data The data for the request. * @param data.limit * @param data.offset -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `key, id, _val, description, is_encrypted` * @param data.variableKeyPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns VariableCollectionResponse Successful Response * @throws ApiError @@ -1327,7 +1388,7 @@ export const prefetchUseDagVersionServiceGetDagVersion = (queryClient: QueryClie * @param data.versionNumber * @param data.bundleName * @param data.bundleVersion -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, version_number, bundle_name, bundle_version` * @returns DAGVersionCollectionResponse Successful Response * @throws ApiError */ @@ -1341,60 +1402,6 @@ export const prefetchUseDagVersionServiceGetDagVersions = (queryClient: QueryCli versionNumber?: number; }) => queryClient.prefetchQuery({ queryKey: Common.UseDagVersionServiceGetDagVersionsKeyFn({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }), queryFn: () => DagVersionService.getDagVersions({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }) }); /** -* Get Hitl Detail -* Get a Human-in-the-loop detail of a specific task instance. -* @param data The data for the request. -* @param data.dagId -* @param data.dagRunId -* @param data.taskId -* @param data.mapIndex -* @returns HITLDetail Successful Response -* @throws ApiError -*/ -export const prefetchUseHumanInTheLoopServiceGetHitlDetail = (queryClient: QueryClient, { dagId, dagRunId, mapIndex, taskId }: { - dagId: string; - dagRunId: string; - mapIndex?: number; - taskId: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }), queryFn: () => HumanInTheLoopService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) }); -/** -* Get Hitl Details -* Get Human-in-the-loop details. -* @param data The data for the request. -* @param data.limit -* @param data.offset -* @param data.orderBy -* @param data.dagId -* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.dagRunId -* @param data.taskId -* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.state -* @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName -* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @returns HITLDetailCollection Successful Response -* @throws ApiError -*/ -export const prefetchUseHumanInTheLoopServiceGetHitlDetails = (queryClient: QueryClient, { bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { - bodySearch?: string; - dagId?: string; - dagIdPattern?: string; - dagRunId?: string; - limit?: number; - offset?: number; - orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; - responseReceived?: boolean; - state?: string[]; - subjectSearch?: string; - taskId?: string; - taskIdPattern?: string; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) }); -/** * Get Health * @returns HealthInfoResponse Successful Response * @throws ApiError @@ -1421,25 +1428,10 @@ export const prefetchUseLoginServiceLogin = (queryClient: QueryClient, { next }: /** * Logout * Logout the user. -* @param data The data for the request. -* @param data.next -* @returns unknown Successful Response -* @throws ApiError -*/ -export const prefetchUseLoginServiceLogout = (queryClient: QueryClient, { next }: { - next?: string; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn({ next }), queryFn: () => LoginService.logout({ next }) }); -/** -* Refresh -* Refresh the authentication token. -* @param data The data for the request. -* @param data.next * @returns unknown Successful Response * @throws ApiError */ -export const prefetchUseLoginServiceRefresh = (queryClient: QueryClient, { next }: { - next?: string; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseLoginServiceRefreshKeyFn({ next }), queryFn: () => LoginService.refresh({ next }) }); +export const prefetchUseLoginServiceLogout = (queryClient: QueryClient) => queryClient.prefetchQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn(), queryFn: () => LoginService.logout() }); /** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response @@ -1505,7 +1497,7 @@ export const prefetchUseStructureServiceStructureData = (queryClient: QueryClien * @param data.dagId * @param data.offset * @param data.limit -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` * @param data.runAfterGte * @param data.runAfterGt * @param data.runAfterLte @@ -1534,7 +1526,7 @@ export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, * @param data.dagId * @param data.offset * @param data.limit -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` * @param data.runAfterGte * @param data.runAfterGt * @param data.runAfterLte diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts index 1b133994fecf5..cd9c9f0e99b20 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1,7 +1,7 @@ // generated with @7nohe/openapi-react-query-codegen@1.6.2 import { UseMutationOptions, UseQueryOptions, useMutation, useQuery } from "@tanstack/react-query"; -import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagParsingService, DagReportService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, HumanInTheLoopService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; +import { AssetService, AuthLinksService, BackfillService, CalendarService, ConfigService, ConnectionService, DagParsingService, DagRunService, DagService, DagSourceService, DagStatsService, DagVersionService, DagWarningService, DashboardService, DependenciesService, EventLogService, ExperimentalService, ExtraLinksService, GridService, ImportErrorService, JobService, LoginService, MonitorService, PluginService, PoolService, ProviderService, StructureService, TaskInstanceService, TaskService, VariableService, VersionService, XcomService } from "../requests/services.gen"; import { BackfillPostBody, BulkBody_BulkTaskInstanceBody_, BulkBody_ConnectionBody_, BulkBody_PoolBody_, BulkBody_VariableBody_, ClearTaskInstancesBody, ConnectionBody, CreateAssetEventsBody, DAGPatchBody, DAGRunClearBody, DAGRunPatchBody, DAGRunsBatchBody, DagRunState, DagWarningType, PatchTaskInstanceBody, PoolBody, PoolPatchBody, TaskInstancesBatchBody, TriggerDAGRunPostBody, UpdateHITLDetailPayload, VariableBody, XComCreateBody, XComUpdateBody } from "../requests/types.gen"; import * as Common from "./common"; /** @@ -14,7 +14,7 @@ import * as Common from "./common"; * @param data.uriPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @param data.dagIds * @param data.onlyActive -* @param data.orderBy +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name, uri, created_at, updated_at` * @returns AssetCollectionResponse Successful Response * @throws ApiError */ @@ -34,7 +34,7 @@ export const useAssetServiceGetAssets = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -342,7 +342,7 @@ export const useDagRunServiceWaitDagRunUntilFinished = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, interval, result }, queryKey), queryFn: () => DagRunService.waitDagRunUntilFinished({ dagId, dagRunId, interval, result }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -384,17 +384,6 @@ export const useDagStatsServiceGetDagStats = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagStatsServiceGetDagStatsKeyFn({ dagIds }, queryKey), queryFn: () => DagStatsService.getDagStats({ dagIds }) as TData, ...options }); /** -* Get Dag Reports -* Get DAG report. -* @param data The data for the request. -* @param data.subdir -* @returns unknown Successful Response -* @throws ApiError -*/ -export const useDagReportServiceGetDagReports = = unknown[]>({ subdir }: { - subdir: string; -}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagReportServiceGetDagReportsKeyFn({ subdir }, queryKey), queryFn: () => DagReportService.getDagReports({ subdir }) as TData, ...options }); -/** * Get Config * @param data The data for the request. * @param data.section @@ -435,7 +424,7 @@ export const useConfigServiceGetConfigs = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const useTaskInstanceServiceGetMappedTaskInstances = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -773,6 +763,7 @@ export const useTaskInstanceServiceGetMappedTaskInstances = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); /** * Get Task Instance Dependencies * Get dependencies blocking task from getting scheduled. @@ -914,6 +905,7 @@ export const useTaskInstanceServiceGetMappedTaskInstance = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const useTaskInstanceServiceGetTaskInstances = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -944,6 +937,7 @@ export const useTaskInstanceServiceGetTaskInstances = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); /** * Get Task Instance Try Details * Get task instance details by try number. @@ -1049,6 +1044,70 @@ export const useTaskInstanceServiceGetExternalLogUrl = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetExternalLogUrlKeyFn({ dagId, dagRunId, mapIndex, taskId, tryNumber }, queryKey), queryFn: () => TaskInstanceService.getExternalLogUrl({ dagId, dagRunId, mapIndex, taskId, tryNumber }) as TData, ...options }); /** +* Get Hitl Detail +* Get a Human-in-the-loop detail of a specific task instance. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.taskId +* @param data.mapIndex +* @returns HITLDetail Successful Response +* @throws ApiError +*/ +export const useTaskInstanceServiceGetHitlDetail = = unknown[]>({ dagId, dagRunId, mapIndex, taskId }: { + dagId: string; + dagRunId: string; + mapIndex: number; + taskId: string; +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }, queryKey), queryFn: () => TaskInstanceService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) as TData, ...options }); +/** +* Get Hitl Details +* Get Human-in-the-loop details. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.limit +* @param data.offset +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, run_after, rendered_map_index, task_instance_operator, task_instance_state` +* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.taskId +* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.mapIndex +* @param data.state +* @param data.responseReceived +* @param data.respondedByUserId +* @param data.respondedByUserName +* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt +* @returns HITLDetailCollection Successful Response +* @throws ApiError +*/ +export const useTaskInstanceServiceGetHitlDetails = = unknown[]>({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { + bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; + dagId: string; + dagIdPattern?: string; + dagRunId: string; + limit?: number; + mapIndex?: number; + offset?: number; + orderBy?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; + responseReceived?: boolean; + state?: string[]; + subjectSearch?: string; + taskId?: string; + taskIdPattern?: string; +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) as TData, ...options }); +/** * Get Import Error * Get an import error. * @param data The data for the request. @@ -1065,15 +1124,17 @@ export const useImportErrorServiceGetImportError = = unknown[]>({ limit, offset, orderBy }: { +export const useImportErrorServiceGetImportErrors = = unknown[]>({ filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) as TData, ...options }); +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) as TData, ...options }); /** * Get Jobs * Get all jobs. @@ -1089,7 +1150,7 @@ export const useImportErrorServiceGetImportErrors = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagVersionServiceGetDagVersionsKeyFn({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }, queryKey), queryFn: () => DagVersionService.getDagVersions({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }) as TData, ...options }); /** -* Get Hitl Detail -* Get a Human-in-the-loop detail of a specific task instance. -* @param data The data for the request. -* @param data.dagId -* @param data.dagRunId -* @param data.taskId -* @param data.mapIndex -* @returns HITLDetail Successful Response -* @throws ApiError -*/ -export const useHumanInTheLoopServiceGetHitlDetail = = unknown[]>({ dagId, dagRunId, mapIndex, taskId }: { - dagId: string; - dagRunId: string; - mapIndex?: number; - taskId: string; -}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) as TData, ...options }); -/** -* Get Hitl Details -* Get Human-in-the-loop details. -* @param data The data for the request. -* @param data.limit -* @param data.offset -* @param data.orderBy -* @param data.dagId -* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.dagRunId -* @param data.taskId -* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.state -* @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName -* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @returns HITLDetailCollection Successful Response -* @throws ApiError -*/ -export const useHumanInTheLoopServiceGetHitlDetails = = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { - bodySearch?: string; - dagId?: string; - dagIdPattern?: string; - dagRunId?: string; - limit?: number; - offset?: number; - orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; - responseReceived?: boolean; - state?: string[]; - subjectSearch?: string; - taskId?: string; - taskIdPattern?: string; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) as TData, ...options }); -/** * Get Health * @returns HealthInfoResponse Successful Response * @throws ApiError @@ -1421,25 +1428,10 @@ export const useLoginServiceLogin = = unknown[]>({ next }: { - next?: string; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn({ next }, queryKey), queryFn: () => LoginService.logout({ next }) as TData, ...options }); -/** -* Refresh -* Refresh the authentication token. -* @param data The data for the request. -* @param data.next * @returns unknown Successful Response * @throws ApiError */ -export const useLoginServiceRefresh = = unknown[]>({ next }: { - next?: string; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseLoginServiceRefreshKeyFn({ next }, queryKey), queryFn: () => LoginService.refresh({ next }) as TData, ...options }); +export const useLoginServiceLogout = = unknown[]>(queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn(queryKey), queryFn: () => LoginService.logout() as TData, ...options }); /** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response @@ -1505,7 +1497,7 @@ export const useStructureServiceStructureData = (options?: Omit({ mutationFn: ({ dagId, dagRunId, mapIndex, requestBody, taskId, updateMask }) => TaskInstanceService.patchTaskInstanceDryRun({ dagId, dagRunId, mapIndex, requestBody, taskId, updateMask }) as unknown as Promise, ...options }); /** +* Update Hitl Detail +* Update a Human-in-the-loop detail. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.taskId +* @param data.mapIndex +* @param data.requestBody +* @returns HITLDetailResponse Successful Response +* @throws ApiError +*/ +export const useTaskInstanceServiceUpdateHitlDetail = (options?: Omit, "mutationFn">) => useMutation({ mutationFn: ({ dagId, dagRunId, mapIndex, requestBody, taskId }) => TaskInstanceService.updateHitlDetail({ dagId, dagRunId, mapIndex, requestBody, taskId }) as unknown as Promise, ...options }); +/** * Patch Pool * Update a Pool. * @param data The data for the request. @@ -2230,31 +2247,6 @@ export const useVariableServiceBulkVariables = ({ mutationFn: ({ requestBody }) => VariableService.bulkVariables({ requestBody }) as unknown as Promise, ...options }); /** -* Update Hitl Detail -* Update a Human-in-the-loop detail. -* @param data The data for the request. -* @param data.dagId -* @param data.dagRunId -* @param data.taskId -* @param data.requestBody -* @param data.mapIndex -* @returns HITLDetailResponse Successful Response -* @throws ApiError -*/ -export const useHumanInTheLoopServiceUpdateHitlDetail = (options?: Omit, "mutationFn">) => useMutation({ mutationFn: ({ dagId, dagRunId, mapIndex, requestBody, taskId }) => HumanInTheLoopService.updateHitlDetail({ dagId, dagRunId, mapIndex, requestBody, taskId }) as unknown as Promise, ...options }); -/** * Delete Asset Queued Events * Delete queued asset events for an asset. * @param data The data for the request. @@ -2354,7 +2346,7 @@ export const useDagServiceDeleteDag = (options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ dagId, dagVersion, endDateGt, endDateGte, endDateLt, endDateLte, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runType, startDateGt, startDateGte, startDateLt, startDateLte, state, triggeringUserNamePattern, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -342,7 +342,7 @@ export const useDagRunServiceWaitDagRunUntilFinishedSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, interval, result }, queryKey), queryFn: () => DagRunService.waitDagRunUntilFinished({ dagId, dagRunId, interval, result }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. -* 🚧 This is an experimental endpoint and may change or be removed without notice. +* 🚧 This is an experimental endpoint and may change or be removed without notice.Successful response are streamed as newline-delimited JSON (NDJSON). Each line is a JSON object representing the DAG run state. * @param data The data for the request. * @param data.dagId * @param data.dagRunId @@ -384,17 +384,6 @@ export const useDagStatsServiceGetDagStatsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagStatsServiceGetDagStatsKeyFn({ dagIds }, queryKey), queryFn: () => DagStatsService.getDagStats({ dagIds }) as TData, ...options }); /** -* Get Dag Reports -* Get DAG report. -* @param data The data for the request. -* @param data.subdir -* @returns unknown Successful Response -* @throws ApiError -*/ -export const useDagReportServiceGetDagReportsSuspense = = unknown[]>({ subdir }: { - subdir: string; -}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagReportServiceGetDagReportsKeyFn({ subdir }, queryKey), queryFn: () => DagReportService.getDagReports({ subdir }) as TData, ...options }); -/** * Get Config * @param data The data for the request. * @param data.section @@ -435,7 +424,7 @@ export const useConfigServiceGetConfigsSuspense = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const useTaskInstanceServiceGetMappedTaskInstancesSuspense = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -773,6 +763,7 @@ export const useTaskInstanceServiceGetMappedTaskInstancesSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetMappedTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getMappedTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); /** * Get Task Instance Dependencies * Get dependencies blocking task from getting scheduled. @@ -914,6 +905,7 @@ export const useTaskInstanceServiceGetMappedTaskInstanceSuspense = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { +export const useTaskInstanceServiceGetTaskInstancesSuspense = = unknown[]>({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }: { dagId: string; dagRunId: string; durationGt?: number; @@ -944,6 +937,7 @@ export const useTaskInstanceServiceGetTaskInstancesSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }, queryKey), queryFn: () => TaskInstanceService.getTaskInstances({ dagId, dagRunId, durationGt, durationGte, durationLt, durationLte, endDateGt, endDateGte, endDateLt, endDateLte, executor, limit, logicalDateGt, logicalDateGte, logicalDateLt, logicalDateLte, mapIndex, offset, operator, orderBy, pool, queue, runAfterGt, runAfterGte, runAfterLt, runAfterLte, startDateGt, startDateGte, startDateLt, startDateLte, state, taskDisplayNamePattern, taskGroupId, taskId, tryNumber, updatedAtGt, updatedAtGte, updatedAtLt, updatedAtLte, versionNumber }) as TData, ...options }); /** * Get Task Instance Try Details * Get task instance details by try number. @@ -1049,6 +1044,70 @@ export const useTaskInstanceServiceGetExternalLogUrlSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetExternalLogUrlKeyFn({ dagId, dagRunId, mapIndex, taskId, tryNumber }, queryKey), queryFn: () => TaskInstanceService.getExternalLogUrl({ dagId, dagRunId, mapIndex, taskId, tryNumber }) as TData, ...options }); /** +* Get Hitl Detail +* Get a Human-in-the-loop detail of a specific task instance. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.taskId +* @param data.mapIndex +* @returns HITLDetail Successful Response +* @throws ApiError +*/ +export const useTaskInstanceServiceGetHitlDetailSuspense = = unknown[]>({ dagId, dagRunId, mapIndex, taskId }: { + dagId: string; + dagRunId: string; + mapIndex: number; + taskId: string; +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }, queryKey), queryFn: () => TaskInstanceService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) as TData, ...options }); +/** +* Get Hitl Details +* Get Human-in-the-loop details. +* @param data The data for the request. +* @param data.dagId +* @param data.dagRunId +* @param data.limit +* @param data.offset +* @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, run_after, rendered_map_index, task_instance_operator, task_instance_state` +* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.taskId +* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.mapIndex +* @param data.state +* @param data.responseReceived +* @param data.respondedByUserId +* @param data.respondedByUserName +* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt +* @returns HITLDetailCollection Successful Response +* @throws ApiError +*/ +export const useTaskInstanceServiceGetHitlDetailsSuspense = = unknown[]>({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { + bodySearch?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; + dagId: string; + dagIdPattern?: string; + dagRunId: string; + limit?: number; + mapIndex?: number; + offset?: number; + orderBy?: string[]; + respondedByUserId?: string[]; + respondedByUserName?: string[]; + responseReceived?: boolean; + state?: string[]; + subjectSearch?: string; + taskId?: string; + taskIdPattern?: string; +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, dagRunId, limit, mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) as TData, ...options }); +/** * Get Import Error * Get an import error. * @param data The data for the request. @@ -1065,15 +1124,17 @@ export const useImportErrorServiceGetImportErrorSuspense = = unknown[]>({ limit, offset, orderBy }: { +export const useImportErrorServiceGetImportErrorsSuspense = = unknown[]>({ filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) as TData, ...options }); +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) as TData, ...options }); /** * Get Jobs * Get all jobs. @@ -1089,7 +1150,7 @@ export const useImportErrorServiceGetImportErrorsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagVersionServiceGetDagVersionsKeyFn({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }, queryKey), queryFn: () => DagVersionService.getDagVersions({ bundleName, bundleVersion, dagId, limit, offset, orderBy, versionNumber }) as TData, ...options }); /** -* Get Hitl Detail -* Get a Human-in-the-loop detail of a specific task instance. -* @param data The data for the request. -* @param data.dagId -* @param data.dagRunId -* @param data.taskId -* @param data.mapIndex -* @returns HITLDetail Successful Response -* @throws ApiError -*/ -export const useHumanInTheLoopServiceGetHitlDetailSuspense = = unknown[]>({ dagId, dagRunId, mapIndex, taskId }: { - dagId: string; - dagRunId: string; - mapIndex?: number; - taskId: string; -}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailKeyFn({ dagId, dagRunId, mapIndex, taskId }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetail({ dagId, dagRunId, mapIndex, taskId }) as TData, ...options }); -/** -* Get Hitl Details -* Get Human-in-the-loop details. -* @param data The data for the request. -* @param data.limit -* @param data.offset -* @param data.orderBy -* @param data.dagId -* @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.dagRunId -* @param data.taskId -* @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.state -* @param data.responseReceived -* @param data.respondedUserId -* @param data.respondedUserName -* @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. -* @returns HITLDetailCollection Successful Response -* @throws ApiError -*/ -export const useHumanInTheLoopServiceGetHitlDetailsSuspense = = unknown[]>({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }: { - bodySearch?: string; - dagId?: string; - dagIdPattern?: string; - dagRunId?: string; - limit?: number; - offset?: number; - orderBy?: string[]; - respondedUserId?: string[]; - respondedUserName?: string[]; - responseReceived?: boolean; - state?: string[]; - subjectSearch?: string; - taskId?: string; - taskIdPattern?: string; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseHumanInTheLoopServiceGetHitlDetailsKeyFn({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }, queryKey), queryFn: () => HumanInTheLoopService.getHitlDetails({ bodySearch, dagId, dagIdPattern, dagRunId, limit, offset, orderBy, respondedUserId, respondedUserName, responseReceived, state, subjectSearch, taskId, taskIdPattern }) as TData, ...options }); -/** * Get Health * @returns HealthInfoResponse Successful Response * @throws ApiError @@ -1421,25 +1428,10 @@ export const useLoginServiceLoginSuspense = = unknown[]>({ next }: { - next?: string; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn({ next }, queryKey), queryFn: () => LoginService.logout({ next }) as TData, ...options }); -/** -* Refresh -* Refresh the authentication token. -* @param data The data for the request. -* @param data.next * @returns unknown Successful Response * @throws ApiError */ -export const useLoginServiceRefreshSuspense = = unknown[]>({ next }: { - next?: string; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseLoginServiceRefreshKeyFn({ next }, queryKey), queryFn: () => LoginService.refresh({ next }) as TData, ...options }); +export const useLoginServiceLogoutSuspense = = unknown[]>(queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn(queryKey), queryFn: () => LoginService.logout() as TData, ...options }); /** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response @@ -1505,7 +1497,7 @@ export const useStructureServiceStructureDataSuspense = { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v2/dagReports', - query: { - subdir: data.subdir - }, - errors: { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Validation Error' - } - }); - } - -} - export class ConfigService { /** * Get Config @@ -1352,7 +1325,7 @@ export class DagWarningService { * @param data.warningType * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, warning_type, message, timestamp` * @returns DAGWarningCollectionResponse Successful Response * @throws ApiError */ @@ -1406,7 +1379,7 @@ export class DagService { * @param data.dagRunEndDateLte * @param data.dagRunEndDateLt * @param data.dagRunState - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` * @param data.isFavorite * @returns DAGCollectionResponse Successful Response * @throws ApiError @@ -1657,7 +1630,7 @@ export class DagService { * @param data The data for the request. * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `name` * @param data.tagNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGTagCollectionResponse Successful Response * @throws ApiError @@ -1699,7 +1672,7 @@ export class DagService { * @param data.lastDagRunState * @param data.bundleName * @param data.bundleVersion - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` * @param data.isFavorite * @param data.hasAssetSchedule Filter Dags with asset-based scheduling * @param data.assetDependency Filter Dags by asset dependency (name or URI) @@ -1793,7 +1766,7 @@ export class EventLogService { * @param data The data for the request. * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dttm, dag_id, task_id, run_id, event, logical_date, owner, extra, when, event_log_id` * @param data.dagId * @param data.taskId * @param data.runId @@ -1991,7 +1964,7 @@ export class TaskInstanceService { * @param data.dagRunId * @param data.taskId * @param data.mapIndex - * @returns null Successful Response + * @returns unknown Successful Response * @throws ApiError */ public static deleteTaskInstance(data: DeleteTaskInstanceData): CancelablePromise { @@ -2053,9 +2026,10 @@ export class TaskInstanceService { * @param data.versionNumber * @param data.tryNumber * @param data.operator + * @param data.mapIndex * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end` * @returns TaskInstanceCollectionResponse Successful Response * @throws ApiError */ @@ -2100,6 +2074,7 @@ export class TaskInstanceService { version_number: data.versionNumber, try_number: data.tryNumber, operator: data.operator, + map_index: data.mapIndex, limit: data.limit, offset: data.offset, order_by: data.orderBy @@ -2340,6 +2315,7 @@ export class TaskInstanceService { * @param data.durationLte * @param data.durationLt * @param data.taskDisplayNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.taskGroupId Filter by exact task group ID. Returns all tasks within the specified task group. * @param data.state * @param data.pool * @param data.queue @@ -2347,9 +2323,10 @@ export class TaskInstanceService { * @param data.versionNumber * @param data.tryNumber * @param data.operator + * @param data.mapIndex * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end` * @returns TaskInstanceCollectionResponse Successful Response * @throws ApiError */ @@ -2388,6 +2365,7 @@ export class TaskInstanceService { duration_lte: data.durationLte, duration_lt: data.durationLt, task_display_name_pattern: data.taskDisplayNamePattern, + task_group_id: data.taskGroupId, state: data.state, pool: data.pool, queue: data.queue, @@ -2395,6 +2373,7 @@ export class TaskInstanceService { version_number: data.versionNumber, try_number: data.tryNumber, operator: data.operator, + map_index: data.mapIndex, limit: data.limit, offset: data.offset, order_by: data.orderBy @@ -2710,6 +2689,131 @@ export class TaskInstanceService { }); } + /** + * Update Hitl Detail + * Update a Human-in-the-loop detail. + * @param data The data for the request. + * @param data.dagId + * @param data.dagRunId + * @param data.taskId + * @param data.mapIndex + * @param data.requestBody + * @returns HITLDetailResponse Successful Response + * @throws ApiError + */ + public static updateHitlDetail(data: UpdateHitlDetailData): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails', + path: { + dag_id: data.dagId, + dag_run_id: data.dagRunId, + task_id: data.taskId, + map_index: data.mapIndex + }, + body: data.requestBody, + mediaType: 'application/json', + errors: { + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 409: 'Conflict', + 422: 'Validation Error' + } + }); + } + + /** + * Get Hitl Detail + * Get a Human-in-the-loop detail of a specific task instance. + * @param data The data for the request. + * @param data.dagId + * @param data.dagRunId + * @param data.taskId + * @param data.mapIndex + * @returns HITLDetail Successful Response + * @throws ApiError + */ + public static getHitlDetail(data: GetHitlDetailData): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails', + path: { + dag_id: data.dagId, + dag_run_id: data.dagRunId, + task_id: data.taskId, + map_index: data.mapIndex + }, + errors: { + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 422: 'Validation Error' + } + }); + } + + /** + * Get Hitl Details + * Get Human-in-the-loop details. + * @param data The data for the request. + * @param data.dagId + * @param data.dagRunId + * @param data.limit + * @param data.offset + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, run_after, rendered_map_index, task_instance_operator, task_instance_state` + * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.taskId + * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.mapIndex + * @param data.state + * @param data.responseReceived + * @param data.respondedByUserId + * @param data.respondedByUserName + * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.createdAtGte + * @param data.createdAtGt + * @param data.createdAtLte + * @param data.createdAtLt + * @returns HITLDetailCollection Successful Response + * @throws ApiError + */ + public static getHitlDetails(data: GetHitlDetailsData): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/hitlDetails', + path: { + dag_id: data.dagId, + dag_run_id: data.dagRunId + }, + query: { + limit: data.limit, + offset: data.offset, + order_by: data.orderBy, + dag_id_pattern: data.dagIdPattern, + task_id: data.taskId, + task_id_pattern: data.taskIdPattern, + map_index: data.mapIndex, + state: data.state, + response_received: data.responseReceived, + responded_by_user_id: data.respondedByUserId, + responded_by_user_name: data.respondedByUserName, + subject_search: data.subjectSearch, + body_search: data.bodySearch, + created_at_gte: data.createdAtGte, + created_at_gt: data.createdAtGt, + created_at_lte: data.createdAtLte, + created_at_lt: data.createdAtLt + }, + errors: { + 401: 'Unauthorized', + 403: 'Forbidden', + 422: 'Validation Error' + } + }); + } + } export class ImportErrorService { @@ -2743,7 +2847,8 @@ export class ImportErrorService { * @param data The data for the request. * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, timestamp, filename, bundle_name, stacktrace, import_error_id` + * @param data.filenamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ImportErrorCollectionResponse Successful Response * @throws ApiError */ @@ -2754,7 +2859,8 @@ export class ImportErrorService { query: { limit: data.limit, offset: data.offset, - order_by: data.orderBy + order_by: data.orderBy, + filename_pattern: data.filenamePattern }, errors: { 401: 'Unauthorized', @@ -2782,7 +2888,7 @@ export class JobService { * @param data.endDateLt * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dag_id, state, job_type, start_date, end_date, latest_heartbeat, executor_class, hostname, unixname` * @param data.jobState * @param data.jobType * @param data.hostname @@ -2954,7 +3060,7 @@ export class PoolService { * @param data The data for the request. * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, pool, name` * @param data.poolNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns PoolCollectionResponse Successful Response * @throws ApiError @@ -3374,7 +3480,7 @@ export class VariableService { * @param data The data for the request. * @param data.limit * @param data.offset - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `key, id, _val, description, is_encrypted` * @param data.variableKeyPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns VariableCollectionResponse Successful Response * @throws ApiError @@ -3450,7 +3556,7 @@ export class DagParsingService { * Request re-parsing a DAG file. * @param data The data for the request. * @param data.fileToken - * @returns null Successful Response + * @returns unknown Successful Response * @throws ApiError */ public static reparseDagFile(data: ReparseDagFileData): CancelablePromise { @@ -3510,7 +3616,7 @@ export class DagVersionService { * @param data.versionNumber * @param data.bundleName * @param data.bundleVersion - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, version_number, bundle_name, bundle_version` * @returns DAGVersionCollectionResponse Successful Response * @throws ApiError */ @@ -3540,126 +3646,6 @@ export class DagVersionService { } -export class HumanInTheLoopService { - /** - * Update Hitl Detail - * Update a Human-in-the-loop detail. - * @param data The data for the request. - * @param data.dagId - * @param data.dagRunId - * @param data.taskId - * @param data.requestBody - * @param data.mapIndex - * @returns HITLDetailResponse Successful Response - * @throws ApiError - */ - public static updateHitlDetail(data: UpdateHitlDetailData): CancelablePromise { - return __request(OpenAPI, { - method: 'PATCH', - url: '/api/v2/hitlDetails/{dag_id}/{dag_run_id}/{task_id}', - path: { - dag_id: data.dagId, - dag_run_id: data.dagRunId, - task_id: data.taskId - }, - query: { - map_index: data.mapIndex - }, - body: data.requestBody, - mediaType: 'application/json', - errors: { - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 409: 'Conflict', - 422: 'Validation Error' - } - }); - } - - /** - * Get Hitl Detail - * Get a Human-in-the-loop detail of a specific task instance. - * @param data The data for the request. - * @param data.dagId - * @param data.dagRunId - * @param data.taskId - * @param data.mapIndex - * @returns HITLDetail Successful Response - * @throws ApiError - */ - public static getHitlDetail(data: GetHitlDetailData): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v2/hitlDetails/{dag_id}/{dag_run_id}/{task_id}', - path: { - dag_id: data.dagId, - dag_run_id: data.dagRunId, - task_id: data.taskId - }, - query: { - map_index: data.mapIndex - }, - errors: { - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 422: 'Validation Error' - } - }); - } - - /** - * Get Hitl Details - * Get Human-in-the-loop details. - * @param data The data for the request. - * @param data.limit - * @param data.offset - * @param data.orderBy - * @param data.dagId - * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - * @param data.dagRunId - * @param data.taskId - * @param data.taskIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - * @param data.state - * @param data.responseReceived - * @param data.respondedUserId - * @param data.respondedUserName - * @param data.subjectSearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - * @param data.bodySearch SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - * @returns HITLDetailCollection Successful Response - * @throws ApiError - */ - public static getHitlDetails(data: GetHitlDetailsData = {}): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v2/hitlDetails/', - query: { - limit: data.limit, - offset: data.offset, - order_by: data.orderBy, - dag_id: data.dagId, - dag_id_pattern: data.dagIdPattern, - dag_run_id: data.dagRunId, - task_id: data.taskId, - task_id_pattern: data.taskIdPattern, - state: data.state, - response_received: data.responseReceived, - responded_user_id: data.respondedUserId, - responded_user_name: data.respondedUserName, - subject_search: data.subjectSearch, - body_search: data.bodySearch - }, - errors: { - 401: 'Unauthorized', - 403: 'Forbidden', - 422: 'Validation Error' - } - }); - } - -} - export class MonitorService { /** * Get Health @@ -3717,43 +3703,15 @@ export class LoginService { /** * Logout * Logout the user. - * @param data The data for the request. - * @param data.next * @returns unknown Successful Response * @throws ApiError */ - public static logout(data: LogoutData = {}): CancelablePromise { + public static logout(): CancelablePromise { return __request(OpenAPI, { method: 'GET', url: '/api/v2/auth/logout', - query: { - next: data.next - }, - errors: { - 307: 'Temporary Redirect', - 422: 'Validation Error' - } - }); - } - - /** - * Refresh - * Refresh the authentication token. - * @param data The data for the request. - * @param data.next - * @returns unknown Successful Response - * @throws ApiError - */ - public static refresh(data: RefreshData = {}): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v2/auth/refresh', - query: { - next: data.next - }, errors: { - 307: 'Temporary Redirect', - 422: 'Validation Error' + 307: 'Temporary Redirect' } }); } @@ -3883,7 +3841,7 @@ export class GridService { * @param data.dagId * @param data.offset * @param data.limit - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` * @param data.runAfterGte * @param data.runAfterGt * @param data.runAfterLte @@ -3926,7 +3884,7 @@ export class GridService { * @param data.dagId * @param data.offset * @param data.limit - * @param data.orderBy + * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` * @param data.runAfterGte * @param data.runAfterGt * @param data.runAfterLte diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index c1320c1c63512..33cc7992bad27 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -64,7 +64,7 @@ export type AssetEventResponse = { name?: string | null; group?: string | null; extra?: { - [key: string]: unknown; + [key: string]: JsonValue; } | null; source_task_id?: string | null; source_dag_id?: string | null; @@ -83,7 +83,7 @@ export type AssetResponse = { uri: string; group: string; extra?: { - [key: string]: unknown; + [key: string]: JsonValue; } | null; created_at: string; updated_at: string; @@ -115,6 +115,7 @@ export type BackfillPostBody = { }; reprocess_behavior?: ReprocessBehavior; max_active_runs?: number; + run_on_latest_version?: boolean; }; /** @@ -380,6 +381,9 @@ export type ClearTaskInstancesBody = { only_failed?: boolean; only_running?: boolean; reset_dag_runs?: boolean; + /** + * A list of `task_id` or [`task_id`, `map_index`]. If only the `task_id` is provided for a mapped task, all of its map indices will be targeted. + */ task_ids?: Array<(string | [ string, number @@ -536,6 +540,7 @@ export type DAGDetailsResponse = { owner_links?: { [key: string]: (string); } | null; + is_favorite?: boolean; /** * Return file token. */ @@ -823,7 +828,7 @@ export type DagVersionResponse = { bundle_version: string | null; created_at: string; dag_display_name: string; - readonly bundle_url: string | null; + bundle_url: string | null; }; /** @@ -873,6 +878,7 @@ export type EventLogResponse = { owner: string | null; extra: string | null; dag_display_name?: string | null; + task_display_name?: string | null; }; /** @@ -940,10 +946,10 @@ export type HITLDetail = { params?: { [key: string]: unknown; }; - respondents?: Array<(string)> | null; - responded_user_id?: string | null; - responded_user_name?: string | null; - response_at?: string | null; + assigned_users?: Array; + created_at: string; + responded_by_user?: HITLUser | null; + responded_at?: string | null; chosen_options?: Array<(string)> | null; params_input?: { [key: string]: unknown; @@ -963,15 +969,22 @@ export type HITLDetailCollection = { * Response of updating a Human-in-the-loop detail. */ export type HITLDetailResponse = { - responded_user_id: string; - responded_user_name: string; - response_at: string; + responded_by: HITLUser; + responded_at: string; chosen_options: Array<(string)>; params_input?: { [key: string]: unknown; }; }; +/** + * Schema for a Human-in-the-loop users. + */ +export type HITLUser = { + id: string; + name: string; +}; + /** * HTTPException Model used for error response. */ @@ -1145,7 +1158,7 @@ export type PoolPatchBody = { export type PoolResponse = { name: string; slots: number; - description: string | null; + description?: string | null; include_deferred: boolean; occupied_slots: number; running_slots: number; @@ -1170,6 +1183,7 @@ export type ProviderResponse = { package_name: string; description: string; version: string; + documentation_url: string | null; }; /** @@ -1731,6 +1745,7 @@ export type DAGRunLightResponse = { start_date: string | null; end_date: string | null; state: DagRunState; + readonly duration: number | null; }; /** @@ -1793,8 +1808,9 @@ export type DAGWithLatestDagRunsResponse = { asset_expression: { [key: string]: unknown; } | null; - latest_dag_runs: Array; + latest_dag_runs: Array; pending_actions: Array; + is_favorite: boolean; /** * Return file token. */ @@ -1970,6 +1986,9 @@ export type GetAssetsData = { namePattern?: string | null; offset?: number; onlyActive?: boolean; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name, uri, created_at, updated_at` + */ orderBy?: Array<(string)>; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. @@ -1986,6 +2005,9 @@ export type GetAssetAliasesData = { */ namePattern?: string | null; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, name` + */ orderBy?: Array<(string)>; }; @@ -2001,6 +2023,9 @@ export type GetAssetEventsData = { assetId?: number | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `source_task_id, source_dag_id, source_run_id, source_map_index, timestamp` + */ orderBy?: Array<(string)>; sourceDagId?: string | null; sourceMapIndex?: number | null; @@ -2088,6 +2113,9 @@ export type ListBackfillsData = { dagId: string; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` + */ orderBy?: Array<(string)>; }; @@ -2134,6 +2162,9 @@ export type ListBackfillsUiData = { dagId?: string | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` + */ orderBy?: Array<(string)>; }; @@ -2166,6 +2197,9 @@ export type GetConnectionsData = { connectionIdPattern?: string | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `conn_id, conn_type, description, host, port, id, connection_id` + */ orderBy?: Array<(string)>; }; @@ -2244,6 +2278,9 @@ export type GetDagRunsData = { logicalDateLt?: string | null; logicalDateLte?: string | null; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, dag_id, run_id, logical_date, run_after, start_date, end_date, updated_at, conf, duration, dag_run_id` + */ orderBy?: Array<(string)>; runAfterGt?: string | null; runAfterGte?: string | null; @@ -2314,12 +2351,6 @@ export type GetDagStatsData = { export type GetDagStatsResponse = DagStatsCollectionResponse; -export type GetDagReportsData = { - subdir: string; -}; - -export type GetDagReportsResponse = unknown; - export type GetConfigData = { accept?: 'application/json' | 'text/plain' | '*/*'; section?: string | null; @@ -2341,6 +2372,9 @@ export type ListDagWarningsData = { dagId?: string | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, warning_type, message, timestamp` + */ orderBy?: Array<(string)>; warningType?: DagWarningType | null; }; @@ -2384,6 +2418,9 @@ export type GetDagsData = { lastDagRunState?: DagRunState | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` + */ orderBy?: Array<(string)>; owners?: Array<(string)>; paused?: boolean | null; @@ -2452,6 +2489,9 @@ export type UnfavoriteDagResponse = void; export type GetDagTagsData = { limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `name` + */ orderBy?: Array<(string)>; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. @@ -2492,6 +2532,9 @@ export type GetDagsUiData = { lastDagRunState?: DagRunState | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `dag_id, dag_display_name, next_dagrun, state, start_date, last_run_state, last_run_start_date` + */ orderBy?: Array<(string)>; owners?: Array<(string)>; paused?: boolean | null; @@ -2531,6 +2574,9 @@ export type GetEventLogsData = { limit?: number; mapIndex?: number | null; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dttm, dag_id, task_id, run_id, event, logical_date, owner, extra, when, event_log_id` + */ orderBy?: Array<(string)>; owner?: string | null; /** @@ -2587,7 +2633,7 @@ export type DeleteTaskInstanceData = { taskId: string; }; -export type DeleteTaskInstanceResponse = null; +export type DeleteTaskInstanceResponse = unknown; export type GetMappedTaskInstancesData = { dagId: string; @@ -2606,8 +2652,12 @@ export type GetMappedTaskInstancesData = { logicalDateGte?: string | null; logicalDateLt?: string | null; logicalDateLte?: string | null; + mapIndex?: Array<(number)>; offset?: number; operator?: Array<(string)>; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, run_after, logical_date, data_interval_start, data_interval_end` + */ orderBy?: Array<(string)>; pool?: Array<(string)>; queue?: Array<(string)>; @@ -2704,8 +2754,12 @@ export type GetTaskInstancesData = { logicalDateGte?: string | null; logicalDateLt?: string | null; logicalDateLte?: string | null; + mapIndex?: Array<(number)>; offset?: number; operator?: Array<(string)>; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, state, duration, start_date, end_date, map_index, try_number, logical_date, run_after, data_interval_start, data_interval_end, rendered_map_index, operator, logical_date, run_after, data_interval_start, data_interval_end` + */ orderBy?: Array<(string)>; pool?: Array<(string)>; queue?: Array<(string)>; @@ -2722,6 +2776,10 @@ export type GetTaskInstancesData = { * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. */ taskDisplayNamePattern?: string | null; + /** + * Filter by exact task group ID. Returns all tasks within the specified task group. + */ + taskGroupId?: string | null; taskId?: string | null; tryNumber?: Array<(number)>; updatedAtGt?: string | null; @@ -2821,6 +2879,64 @@ export type GetExternalLogUrlData = { export type GetExternalLogUrlResponse = ExternalLogUrlResponse; +export type UpdateHitlDetailData = { + dagId: string; + dagRunId: string; + mapIndex: number; + requestBody: UpdateHITLDetailPayload; + taskId: string; +}; + +export type UpdateHitlDetailResponse = HITLDetailResponse; + +export type GetHitlDetailData = { + dagId: string; + dagRunId: string; + mapIndex: number; + taskId: string; +}; + +export type GetHitlDetailResponse = HITLDetail; + +export type GetHitlDetailsData = { + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + bodySearch?: string | null; + createdAtGt?: string | null; + createdAtGte?: string | null; + createdAtLt?: string | null; + createdAtLte?: string | null; + dagId: string; + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + dagIdPattern?: string | null; + dagRunId: string; + limit?: number; + mapIndex?: number | null; + offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `ti_id, subject, responded_at, created_at, responded_by_user_id, responded_by_user_name, dag_id, run_id, run_after, rendered_map_index, task_instance_operator, task_instance_state` + */ + orderBy?: Array<(string)>; + respondedByUserId?: Array<(string)>; + respondedByUserName?: Array<(string)>; + responseReceived?: boolean | null; + state?: Array<(string)>; + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + subjectSearch?: string | null; + taskId?: string | null; + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + taskIdPattern?: string | null; +}; + +export type GetHitlDetailsResponse = HITLDetailCollection; + export type GetImportErrorData = { importErrorId: number; }; @@ -2828,8 +2944,15 @@ export type GetImportErrorData = { export type GetImportErrorResponse = ImportErrorResponse; export type GetImportErrorsData = { + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + filenamePattern?: string | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, timestamp, filename, bundle_name, stacktrace, import_error_id` + */ orderBy?: Array<(string)>; }; @@ -2847,6 +2970,9 @@ export type GetJobsData = { jobType?: string | null; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, dag_id, state, job_type, start_date, end_date, latest_heartbeat, executor_class, hostname, unixname` + */ orderBy?: Array<(string)>; startDateGt?: string | null; startDateGte?: string | null; @@ -2888,6 +3014,9 @@ export type PatchPoolResponse = PoolResponse; export type GetPoolsData = { limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, pool, name` + */ orderBy?: Array<(string)>; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. @@ -3021,6 +3150,9 @@ export type PatchVariableResponse = VariableResponse; export type GetVariablesData = { limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `key, id, _val, description, is_encrypted` + */ orderBy?: Array<(string)>; /** * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. @@ -3046,7 +3178,7 @@ export type ReparseDagFileData = { fileToken: string; }; -export type ReparseDagFileResponse = null; +export type ReparseDagFileResponse = unknown; export type GetDagVersionData = { dagId: string; @@ -3061,61 +3193,14 @@ export type GetDagVersionsData = { dagId: string; limit?: number; offset?: number; - orderBy?: Array<(string)>; - versionNumber?: number; -}; - -export type GetDagVersionsResponse = DAGVersionCollectionResponse; - -export type UpdateHitlDetailData = { - dagId: string; - dagRunId: string; - mapIndex?: number; - requestBody: UpdateHITLDetailPayload; - taskId: string; -}; - -export type UpdateHitlDetailResponse = HITLDetailResponse; - -export type GetHitlDetailData = { - dagId: string; - dagRunId: string; - mapIndex?: number; - taskId: string; -}; - -export type GetHitlDetailResponse = HITLDetail; - -export type GetHitlDetailsData = { - /** - * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - */ - bodySearch?: string | null; - dagId?: string | null; /** - * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id, version_number, bundle_name, bundle_version` */ - dagIdPattern?: string | null; - dagRunId?: string; - limit?: number; - offset?: number; orderBy?: Array<(string)>; - respondedUserId?: Array<(string)>; - respondedUserName?: Array<(string)>; - responseReceived?: boolean | null; - state?: Array<(string)>; - /** - * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - */ - subjectSearch?: string | null; - taskId?: string | null; - /** - * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. - */ - taskIdPattern?: string | null; + versionNumber?: number; }; -export type GetHitlDetailsResponse = HITLDetailCollection; +export type GetDagVersionsResponse = DAGVersionCollectionResponse; export type GetHealthResponse = HealthInfoResponse; @@ -3127,18 +3212,8 @@ export type LoginData = { export type LoginResponse = unknown; -export type LogoutData = { - next?: string | null; -}; - export type LogoutResponse = unknown; -export type RefreshData = { - next?: string | null; -}; - -export type RefreshResponse = unknown; - export type GetAuthMenusResponse = MenuItemCollectionResponse; export type GetDependenciesData = { @@ -3171,6 +3246,9 @@ export type GetDagStructureData = { dagId: string; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` + */ orderBy?: Array<(string)>; runAfterGt?: string | null; runAfterGte?: string | null; @@ -3189,6 +3267,9 @@ export type GetGridRunsData = { dagId: string; limit?: number; offset?: number; + /** + * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `run_after, logical_date, start_date, end_date` + */ orderBy?: Array<(string)>; runAfterGt?: string | null; runAfterGte?: string | null; @@ -4340,33 +4421,6 @@ export type $OpenApiTs = { }; }; }; - '/api/v2/dagReports': { - get: { - req: GetDagReportsData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Bad Request - */ - 400: HTTPExceptionResponse; - /** - * Unauthorized - */ - 401: HTTPExceptionResponse; - /** - * Forbidden - */ - 403: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; '/api/v2/config': { get: { req: GetConfigData; @@ -4895,7 +4949,7 @@ export type $OpenApiTs = { /** * Successful Response */ - 200: null; + 200: unknown; /** * Unauthorized */ @@ -5386,6 +5440,85 @@ export type $OpenApiTs = { }; }; }; + '/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/hitlDetails': { + patch: { + req: UpdateHitlDetailData; + res: { + /** + * Successful Response + */ + 200: HITLDetailResponse; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Not Found + */ + 404: HTTPExceptionResponse; + /** + * Conflict + */ + 409: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + get: { + req: GetHitlDetailData; + res: { + /** + * Successful Response + */ + 200: HITLDetail; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Not Found + */ + 404: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; + '/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/hitlDetails': { + get: { + req: GetHitlDetailsData; + res: { + /** + * Successful Response + */ + 200: HITLDetailCollection; + /** + * Unauthorized + */ + 401: HTTPExceptionResponse; + /** + * Forbidden + */ + 403: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; '/api/v2/importErrors/{import_error_id}': { get: { req: GetImportErrorData; @@ -6024,7 +6157,7 @@ export type $OpenApiTs = { /** * Successful Response */ - 201: null; + 201: unknown; /** * Unauthorized */ @@ -6098,85 +6231,6 @@ export type $OpenApiTs = { }; }; }; - '/api/v2/hitlDetails/{dag_id}/{dag_run_id}/{task_id}': { - patch: { - req: UpdateHitlDetailData; - res: { - /** - * Successful Response - */ - 200: HITLDetailResponse; - /** - * Unauthorized - */ - 401: HTTPExceptionResponse; - /** - * Forbidden - */ - 403: HTTPExceptionResponse; - /** - * Not Found - */ - 404: HTTPExceptionResponse; - /** - * Conflict - */ - 409: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - get: { - req: GetHitlDetailData; - res: { - /** - * Successful Response - */ - 200: HITLDetail; - /** - * Unauthorized - */ - 401: HTTPExceptionResponse; - /** - * Forbidden - */ - 403: HTTPExceptionResponse; - /** - * Not Found - */ - 404: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - '/api/v2/hitlDetails/': { - get: { - req: GetHitlDetailsData; - res: { - /** - * Successful Response - */ - 200: HITLDetailCollection; - /** - * Unauthorized - */ - 401: HTTPExceptionResponse; - /** - * Forbidden - */ - 403: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; '/api/v2/monitor/health': { get: { res: { @@ -6218,7 +6272,6 @@ export type $OpenApiTs = { }; '/api/v2/auth/logout': { get: { - req: LogoutData; res: { /** * Successful Response @@ -6228,29 +6281,6 @@ export type $OpenApiTs = { * Temporary Redirect */ 307: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - '/api/v2/auth/refresh': { - get: { - req: RefreshData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Temporary Redirect - */ - 307: HTTPExceptionResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; }; }; }; diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 7981915048101..e955b6f4afa8b 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -24,12 +24,14 @@ "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.8", "@types/debounce-promise": "^3.1.9", + "@types/react-resizable": "^3.0.8", "@uiw/codemirror-themes-all": "^4.23.12", "@uiw/react-codemirror": "^4.23.12", "@visx/group": "^3.12.0", "@visx/shape": "^3.12.0", "@xyflow/react": "^12.4.4", - "axios": "^1.11.0", + "anser": "^2.3.2", + "axios": "^1.12.0", "chakra-react-select": "6.1.0", "chart.js": "^4.4.9", "chartjs-adapter-dayjs-4": "^1.0.4", @@ -41,11 +43,11 @@ "i18next": "^25.1.2", "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", - "next-themes": "^0.3.0", + "next-themes": "^0.4.6", "node-sql-parser": "^5.3.10", - "react": "^18.3.1", + "react": "^19.1.1", "react-chartjs-2": "^5.3.0", - "react-dom": "^18.3.1", + "react-dom": "^19.1.1", "react-hook-form": "^7.56.1", "react-hotkeys-hook": "^4.6.1", "react-i18next": "^15.5.1", @@ -53,8 +55,9 @@ "react-innertext": "^1.1.5", "react-json-view": "^1.21.3", "react-markdown": "^9.1.0", + "react-resizable": "^3.0.5", "react-resizable-panels": "^2.1.7", - "react-router-dom": "^6.30.0", + "react-router-dom": "^7.12.0", "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.1", "use-debounce": "^10.0.4", @@ -75,6 +78,9 @@ "@types/react": "^18.3.19", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "^15.5.13", + "@typescript-eslint/eslint-plugin": "^8.50.0", + "@typescript-eslint/parser": "^8.50.0", + "@typescript-eslint/utils": "^8.50.0", "@vitejs/plugin-react-swc": "^3.9.0", "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.25.1", @@ -85,7 +91,7 @@ "eslint-plugin-perfectionist": "^4.12.3", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-unicorn": "^55.0.0", "globals": "^15.15.0", diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index 6f7806f784bee..661968e3da727 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -13,46 +13,52 @@ importers: version: 2.3.4 '@chakra-ui/react': specifier: ^3.20.0 - version: 3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@codemirror/lang-json': specifier: ^6.0.1 version: 6.0.1 '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@18.3.19)(react@18.3.1) + version: 11.14.0(@types/react@18.3.19)(react@19.1.1) '@tanstack/react-query': specifier: ^5.75.1 - version: 5.75.4(react@18.3.1) + version: 5.75.4(react@19.1.1) '@tanstack/react-table': specifier: ^8.21.3 - version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/react-virtual': specifier: ^3.13.8 - version: 3.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.13.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@types/debounce-promise': specifier: ^3.1.9 version: 3.1.9 + '@types/react-resizable': + specifier: ^3.0.8 + version: 3.0.8 '@uiw/codemirror-themes-all': specifier: ^4.23.12 version: 4.23.12(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4) '@uiw/react-codemirror': specifier: ^4.23.12 - version: 4.23.12(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3))(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1(@lezer/common@1.2.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.23.12(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0))(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1(@lezer/common@1.5.0))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@visx/group': specifier: ^3.12.0 - version: 3.12.0(react@18.3.1) + version: 3.12.0(react@19.1.1) '@visx/shape': specifier: ^3.12.0 - version: 3.12.0(react@18.3.1) + version: 3.12.0(react@19.1.1) '@xyflow/react': specifier: ^12.4.4 - version: 12.4.4(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 12.4.4(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + anser: + specifier: ^2.3.2 + version: 2.3.2 axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.12.0 + version: 1.12.0 chakra-react-select: specifier: 6.1.0 - version: 6.1.0(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.19)(next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 6.1.0(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@18.3.19)(next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) chart.js: specifier: ^4.4.9 version: 4.4.9 @@ -84,69 +90,72 @@ importers: specifier: ^3.0.2 version: 3.0.2 next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) node-sql-parser: specifier: ^5.3.10 version: 5.3.10 react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.1.1 + version: 19.1.1 react-chartjs-2: specifier: ^5.3.0 - version: 5.3.0(chart.js@4.4.9)(react@18.3.1) + version: 5.3.0(chart.js@4.4.9)(react@19.1.1) react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) react-hook-form: specifier: ^7.56.1 - version: 7.56.2(react@18.3.1) + version: 7.56.2(react@19.1.1) react-hotkeys-hook: specifier: ^4.6.1 - version: 4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.6.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-i18next: specifier: ^15.5.1 - version: 15.5.1(i18next@25.1.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + version: 15.5.1(i18next@25.1.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3) react-icons: specifier: ^5.5.0 - version: 5.5.0(react@18.3.1) + version: 5.5.0(react@19.1.1) react-innertext: specifier: ^1.1.5 - version: 1.1.5(@types/react@18.3.19)(react@18.3.1) + version: 1.1.5(@types/react@18.3.19)(react@19.1.1) react-json-view: specifier: ^1.21.3 - version: 1.21.3(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.21.3(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-markdown: specifier: ^9.1.0 - version: 9.1.0(@types/react@18.3.19)(react@18.3.1) + version: 9.1.0(@types/react@18.3.19)(react@19.1.1) + react-resizable: + specifier: ^3.0.5 + version: 3.0.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-resizable-panels: specifier: ^2.1.7 - version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-router-dom: - specifier: ^6.30.0 - version: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.12.0 + version: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-syntax-highlighter: specifier: ^15.6.1 - version: 15.6.1(react@18.3.1) + version: 15.6.1(react@19.1.1) remark-gfm: specifier: ^4.0.1 version: 4.0.1 use-debounce: specifier: ^10.0.4 - version: 10.0.4(react@18.3.1) + version: 10.0.4(react@19.1.1) usehooks-ts: specifier: ^3.1.1 - version: 3.1.1(react@18.3.1) + version: 3.1.1(react@19.1.1) yaml: specifier: ^2.6.1 version: 2.8.0 zustand: specifier: ^5.0.4 - version: 5.0.4(@types/react@18.3.19)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + version: 5.0.4(@types/react@18.3.19)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)) devDependencies: '@7nohe/openapi-react-query-codegen': specifier: ^1.6.2 - version: 1.6.2(commander@12.1.0)(glob@11.0.0)(magicast@0.3.5)(ts-morph@23.0.0)(typescript@5.8.3) + version: 1.6.2(commander@12.1.0)(glob@11.1.0)(magicast@0.3.5)(ts-morph@23.0.0)(typescript@5.8.3) '@eslint/compat': specifier: ^1.2.9 version: 1.2.9(eslint@9.26.0(jiti@1.21.7)) @@ -164,7 +173,7 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.19))(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.19))(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@trivago/prettier-plugin-sort-imports': specifier: ^4.3.0 version: 4.3.0(prettier@3.5.3) @@ -180,6 +189,15 @@ importers: '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 + '@typescript-eslint/eslint-plugin': + specifier: ^8.50.0 + version: 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.50.0 + version: 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': + specifier: ^8.50.0 + version: 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) '@vitejs/plugin-react-swc': specifier: ^3.9.0 version: 3.9.0(vite@5.4.19(@types/node@22.15.14)) @@ -211,8 +229,8 @@ importers: specifier: ^7.37.5 version: 7.37.5(eslint@9.26.0(jiti@1.21.7)) eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@9.26.0(jiti@1.21.7)) + specifier: ^5.2.0 + version: 5.2.0(eslint@9.26.0(jiti@1.21.7)) eslint-plugin-react-refresh: specifier: ^0.4.20 version: 0.4.20(eslint@9.26.0(jiti@1.21.7)) @@ -289,8 +307,8 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} '@babel/generator@7.17.7': @@ -329,8 +347,8 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/parser@7.26.10': @@ -342,6 +360,10 @@ packages: resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.26.9': resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} engines: {node: '>=6.9.0'} @@ -392,6 +414,9 @@ packages: '@codemirror/view': ^6.0.0 '@lezer/common': ^1.0.0 + '@codemirror/commands@6.10.1': + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} + '@codemirror/commands@6.8.1': resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} @@ -773,9 +798,15 @@ packages: '@lezer/common@1.2.3': resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} + '@lezer/highlight@1.2.1': resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + '@lezer/json@1.0.3': resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} @@ -825,10 +856,6 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@remix-run/router@1.23.0': - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} - engines: {node: '>=14.0.0'} - '@rollup/rollup-android-arm-eabi@4.40.1': resolution: {integrity: sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==} cpu: [arm] @@ -1165,8 +1192,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/lodash@4.17.16': - resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1194,6 +1221,9 @@ packages: peerDependencies: '@types/react': ^18.0.0 + '@types/react-resizable@3.0.8': + resolution: {integrity: sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==} + '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} @@ -1225,6 +1255,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/eslint-plugin@8.50.0': + resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.32.0': resolution: {integrity: sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1232,18 +1270,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.27.0': - resolution: {integrity: sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==} + '@typescript-eslint/parser@8.50.0': + resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.28.0': - resolution: {integrity: sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==} + '@typescript-eslint/project-service@8.50.0': + resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/scope-manager@8.32.0': resolution: {integrity: sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.50.0': + resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.0': + resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.32.0': resolution: {integrity: sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1251,29 +1304,20 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.27.0': - resolution: {integrity: sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.28.0': - resolution: {integrity: sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==} + '@typescript-eslint/type-utils@8.50.0': + resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/types@8.32.0': resolution: {integrity: sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.27.0': - resolution: {integrity: sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/typescript-estree@8.28.0': - resolution: {integrity: sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==} + '@typescript-eslint/types@8.50.0': + resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/typescript-estree@8.32.0': resolution: {integrity: sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==} @@ -1281,19 +1325,11 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.27.0': - resolution: {integrity: sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==} + '@typescript-eslint/typescript-estree@8.50.0': + resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/utils@8.28.0': - resolution: {integrity: sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/utils@8.32.0': resolution: {integrity: sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==} @@ -1302,18 +1338,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.27.0': - resolution: {integrity: sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.28.0': - resolution: {integrity: sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==} + '@typescript-eslint/utils@8.50.0': + resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/visitor-keys@8.32.0': resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.50.0': + resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@uiw/codemirror-extensions-basic-setup@4.23.12': resolution: {integrity: sha512-l9vuiXOTFDBetYrRLDmz3jDxQHDsrVAZ2Y6dVfmrqi2AsulsDu+y7csW0JsvaMqo79rYkaIZg8yeqmDgMb7VyQ==} peerDependencies: @@ -1749,9 +1788,17 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + anser@2.3.2: + resolution: {integrity: sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1840,6 +1887,7 @@ packages: atlassian-openapi@1.0.21: resolution: {integrity: sha512-1OnnoY2CQYHgXrce/06BltL7fox+uVY7brHUInyFbMpTURjTNIGXfLQxVDRo/2On7ryyKzkX7FfNApYhXw7f+w==} + deprecated: 'DEPRECATED: atlassian-openapi has moved to @atlassian/atlassian-openapi. The latest version is 1.0.6. Please update your dependency.' available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} @@ -1849,8 +1897,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.11.0: - resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.12.0: + resolution: {integrity: sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -2035,6 +2083,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} @@ -2095,6 +2147,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.41.0: resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} @@ -2263,8 +2319,8 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destr@2.0.3: - resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2282,8 +2338,8 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dotenv@16.4.7: - resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -2436,11 +2492,11 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint-plugin-react-refresh@0.4.20: resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} @@ -2471,6 +2527,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.26.0: resolution: {integrity: sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2579,6 +2639,15 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2614,8 +2683,8 @@ packages: peerDependencies: react: ^15.0.2 || ^16.0.0 || ^17.0.0 - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2694,12 +2763,12 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true - glob@11.0.0: - resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} hasBin: true @@ -2831,6 +2900,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3154,8 +3227,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.1.0: - resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} lz-string@1.5.0: @@ -3347,8 +3420,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -3387,8 +3460,8 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.7.4: - resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3426,14 +3499,14 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next-themes@0.3.0: - resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: - react: ^16.8 || ^17 || ^18 - react-dom: ^16.8 || ^17 || ^18 + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - node-fetch-native@1.6.6: - resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -3578,8 +3651,8 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} path-to-regexp@6.3.0: @@ -3620,6 +3693,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pkce-challenge@5.0.0: resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} engines: {node: '>=16.20.0'} @@ -3729,10 +3806,16 @@ packages: chart.js: ^4.1.1 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: - react: ^18.3.1 + react: ^19.1.1 + + react-draggable@4.5.0: + resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' react-hook-form@7.56.2: resolution: {integrity: sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==} @@ -3800,18 +3883,27 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-router-dom@6.30.0: - resolution: {integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==} - engines: {node: '>=14.0.0'} + react-resizable@3.0.5: + resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==} peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react: '>= 16.3' - react-router@6.30.0: - resolution: {integrity: sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==} - engines: {node: '>=14.0.0'} + react-router-dom@7.12.0: + resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' + react: '>=18' + react-dom: '>=18' + + react-router@7.12.0: + resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true react-select@5.10.1: resolution: {integrity: sha512-roPEZUL4aRZDx6DcsD+ZNreVl+fM8VsKn0Wtex1v4IazH60ILp5xhdlp464IsEAlJdXeD+BhDAFsBVMfvLQueA==} @@ -3824,8 +3916,8 @@ packages: peerDependencies: react: '>= 0.14.0' - react-textarea-autosize@8.5.8: - resolution: {integrity: sha512-iUiIj70JefrTuSJ4LbVFiSqWiHHss5L63L717bqaWHMgkm9sz6eEvro4vZ3uQfGJbevzwT6rHOszHKA8RkhRMg==} + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3836,8 +3928,8 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} read-pkg-up@7.0.1: @@ -3951,8 +4043,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} @@ -3975,6 +4067,9 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4162,6 +4257,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4266,12 +4365,12 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@1.0.40: - resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true - ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} @@ -4353,6 +4452,15 @@ packages: '@types/react': optional: true + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-latest@1.3.0: resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} peerDependencies: @@ -4609,11 +4717,11 @@ packages: snapshots: - '@7nohe/openapi-react-query-codegen@1.6.2(commander@12.1.0)(glob@11.0.0)(magicast@0.3.5)(ts-morph@23.0.0)(typescript@5.8.3)': + '@7nohe/openapi-react-query-codegen@1.6.2(commander@12.1.0)(glob@11.1.0)(magicast@0.3.5)(ts-morph@23.0.0)(typescript@5.8.3)': dependencies: '@hey-api/openapi-ts': 0.52.0(magicast@0.3.5)(typescript@5.8.3) commander: 12.1.0 - glob: 11.0.0 + glob: 11.1.0 ts-morph: 23.0.0 typescript: 5.8.3 transitivePeerDependencies: @@ -4632,7 +4740,7 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.0 - '@ark-ui/react@5.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@ark-ui/react@5.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@internationalized/date': 3.8.1 '@zag-js/accordion': 1.15.0 @@ -4673,7 +4781,7 @@ snapshots: '@zag-js/qr-code': 1.15.0 '@zag-js/radio-group': 1.15.0 '@zag-js/rating-group': 1.15.0 - '@zag-js/react': 1.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@zag-js/react': 1.15.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@zag-js/select': 1.15.0 '@zag-js/signature-pad': 1.15.0 '@zag-js/slider': 1.15.0 @@ -4692,8 +4800,8 @@ snapshots: '@zag-js/tree-view': 1.15.0 '@zag-js/types': 1.15.0 '@zag-js/utils': 1.15.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) '@babel/code-frame@7.26.2': dependencies: @@ -4701,9 +4809,9 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.28.6': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -4749,7 +4857,7 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/parser@7.26.10': dependencies: @@ -4759,6 +4867,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.4': {} + '@babel/template@7.26.9': dependencies: '@babel/code-frame': 7.26.2 @@ -4819,26 +4929,33 @@ snapshots: '@chakra-ui/anatomy@2.3.4': {} - '@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@ark-ui/react': 5.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@ark-ui/react': 5.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@18.3.19)(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.19)(react@19.1.1) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) '@emotion/utils': 1.4.2 '@pandacss/is-valid-prop': 0.53.6 csstype: 3.1.3 fast-safe-stringify: 2.1.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3)': + '@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0)': dependencies: '@codemirror/language': 6.11.0 '@codemirror/state': 6.5.2 '@codemirror/view': 6.36.4 - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 + + '@codemirror/commands@6.10.1': + dependencies: + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.4 + '@lezer/common': 1.5.0 '@codemirror/commands@6.8.1': dependencies: @@ -4882,7 +4999,7 @@ snapshots: '@codemirror/language': 6.11.0 '@codemirror/state': 6.5.2 '@codemirror/view': 6.36.4 - '@lezer/highlight': 1.2.1 + '@lezer/highlight': 1.2.3 '@codemirror/view@6.36.4': dependencies: @@ -4922,17 +5039,17 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@18.3.19)(react@18.3.1)': + '@emotion/react@11.14.0(@types/react@18.3.19)(react@19.1.1)': dependencies: '@babel/runtime': 7.26.10 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 18.3.1 + react: 19.1.1 optionalDependencies: '@types/react': 18.3.19 transitivePeerDependencies: @@ -4950,9 +5067,9 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.1)': dependencies: - react: 18.3.1 + react: 19.1.1 '@emotion/utils@1.4.2': {} @@ -5198,10 +5315,16 @@ snapshots: '@lezer/common@1.2.3': {} + '@lezer/common@1.5.0': {} + '@lezer/highlight@1.2.1': dependencies: '@lezer/common': 1.2.3 + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/json@1.0.3': dependencies: '@lezer/common': 1.2.3 @@ -5266,8 +5389,6 @@ snapshots: '@pkgr/core@0.2.4': {} - '@remix-run/router@1.23.0': {} - '@rollup/rollup-android-arm-eabi@4.40.1': optional: true @@ -5330,7 +5451,7 @@ snapshots: '@stylistic/eslint-plugin@2.13.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.27.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.26.0(jiti@1.21.7) eslint-visitor-keys: 4.2.0 espree: 10.3.0 @@ -5398,7 +5519,7 @@ snapshots: '@tanstack/eslint-plugin-query@5.74.7(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.28.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.26.0(jiti@1.21.7) transitivePeerDependencies: - supports-color @@ -5406,22 +5527,22 @@ snapshots: '@tanstack/query-core@5.75.4': {} - '@tanstack/react-query@5.75.4(react@18.3.1)': + '@tanstack/react-query@5.75.4(react@19.1.1)': dependencies: '@tanstack/query-core': 5.75.4 - react: 18.3.1 + react: 19.1.1 - '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-table@8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@tanstack/table-core': 8.21.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - '@tanstack/react-virtual@3.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-virtual@3.13.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@tanstack/virtual-core': 3.13.8 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) '@tanstack/table-core@8.21.3': {} @@ -5429,8 +5550,8 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.26.10 + '@babel/code-frame': 7.28.6 + '@babel/runtime': 7.28.4 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -5448,12 +5569,12 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.19))(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.19))(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.26.10 '@testing-library/dom': 10.4.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) optionalDependencies: '@types/react': 18.3.19 '@types/react-dom': 18.3.5(@types/react@18.3.19) @@ -5558,7 +5679,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/lodash@4.17.16': {} + '@types/lodash@4.17.20': {} '@types/mdast@4.0.4': dependencies: @@ -5582,6 +5703,10 @@ snapshots: dependencies: '@types/react': 18.3.19 + '@types/react-resizable@3.0.8': + dependencies: + '@types/react': 18.3.19 + '@types/react-syntax-highlighter@15.5.13': dependencies: '@types/react': 18.3.19 @@ -5620,6 +5745,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/type-utils': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.50.0 + eslint: 9.26.0(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.32.0 @@ -5632,21 +5773,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.27.0': + '@typescript-eslint/parser@8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.27.0 - '@typescript-eslint/visitor-keys': 8.27.0 + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.0 + eslint: 9.26.0(jiti@1.21.7) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/scope-manager@8.28.0': + '@typescript-eslint/project-service@8.50.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.28.0 - '@typescript-eslint/visitor-keys': 8.28.0 + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.8.3) + '@typescript-eslint/types': 8.50.0 + debug: 4.4.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/scope-manager@8.32.0': dependencies: '@typescript-eslint/types': 8.32.0 '@typescript-eslint/visitor-keys': 8.32.0 + '@typescript-eslint/scope-manager@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + + '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + '@typescript-eslint/type-utils@8.32.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) @@ -5658,39 +5819,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.27.0': {} - - '@typescript-eslint/types@8.28.0': {} - - '@typescript-eslint/types@8.32.0': {} - - '@typescript-eslint/typescript-estree@8.27.0(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.27.0 - '@typescript-eslint/visitor-keys': 8.27.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 + eslint: 9.26.0(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.28.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/types': 8.28.0 - '@typescript-eslint/visitor-keys': 8.28.0 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types@8.32.0': {} + + '@typescript-eslint/types@8.50.0': {} '@typescript-eslint/typescript-estree@8.32.0(typescript@5.8.3)': dependencies: @@ -5706,24 +5849,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.27.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.50.0(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.26.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.27.0 - '@typescript-eslint/types': 8.27.0 - '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.3) - eslint: 9.26.0(jiti@1.21.7) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.28.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.26.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.28.0 - '@typescript-eslint/types': 8.28.0 - '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.3) - eslint: 9.26.0(jiti@1.21.7) + '@typescript-eslint/project-service': 8.50.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.8.3) + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/visitor-keys': 8.50.0 + debug: 4.4.0 + minimatch: 9.0.5 + semver: 7.7.1 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -5739,24 +5875,30 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.27.0': + '@typescript-eslint/utils@8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.27.0 - eslint-visitor-keys: 4.2.0 - - '@typescript-eslint/visitor-keys@8.28.0': - dependencies: - '@typescript-eslint/types': 8.28.0 - eslint-visitor-keys: 4.2.0 + '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.50.0 + '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.8.3) + eslint: 9.26.0(jiti@1.21.7) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color '@typescript-eslint/visitor-keys@8.32.0': dependencies: '@typescript-eslint/types': 8.32.0 - eslint-visitor-keys: 4.2.0 + eslint-visitor-keys: 4.2.1 + + '@typescript-eslint/visitor-keys@8.50.0': + dependencies: + '@typescript-eslint/types': 8.50.0 + eslint-visitor-keys: 4.2.1 - '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3))(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)': + '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0))(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)': dependencies: - '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0) '@codemirror/commands': 6.8.1 '@codemirror/language': 6.11.0 '@codemirror/lint': 6.8.2 @@ -6093,17 +6235,17 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.36.4 - '@uiw/react-codemirror@4.23.12(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3))(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1(@lezer/common@1.2.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.23.12(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0))(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1(@lezer/common@1.5.0))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.28.4 '@codemirror/commands': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/theme-one-dark': 6.1.2 '@codemirror/view': 6.36.4 - '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3))(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4) - codemirror: 6.0.1(@lezer/common@1.2.3) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0))(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4) + codemirror: 6.0.1(@lezer/common@1.5.0) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' @@ -6117,32 +6259,32 @@ snapshots: '@types/d3-shape': 1.3.12 d3-shape: 1.3.7 - '@visx/group@3.12.0(react@18.3.1)': + '@visx/group@3.12.0(react@19.1.1)': dependencies: '@types/react': 18.3.19 classnames: 2.5.1 prop-types: 15.8.1 - react: 18.3.1 + react: 19.1.1 '@visx/scale@3.12.0': dependencies: '@visx/vendor': 3.12.0 - '@visx/shape@3.12.0(react@18.3.1)': + '@visx/shape@3.12.0(react@19.1.1)': dependencies: '@types/d3-path': 1.0.11 '@types/d3-shape': 1.3.12 - '@types/lodash': 4.17.16 + '@types/lodash': 4.17.20 '@types/react': 18.3.19 '@visx/curve': 3.12.0 - '@visx/group': 3.12.0(react@18.3.1) + '@visx/group': 3.12.0(react@19.1.1) '@visx/scale': 3.12.0 classnames: 2.5.1 d3-path: 1.0.9 d3-shape: 1.3.7 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.3.1 + react: 19.1.1 '@visx/vendor@3.12.0': dependencies: @@ -6232,13 +6374,13 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - '@xyflow/react@12.4.4(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@xyflow/react@12.4.4(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@xyflow/system': 0.0.52 classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.19)(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + zustand: 4.5.6(@types/react@18.3.19)(react@19.1.1) transitivePeerDependencies: - '@types/react' - immer @@ -6575,14 +6717,14 @@ snapshots: '@zag-js/types': 1.15.0 '@zag-js/utils': 1.15.0 - '@zag-js/react@1.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@zag-js/react@1.15.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@zag-js/core': 1.15.0 '@zag-js/store': 1.15.0 '@zag-js/types': 1.15.0 '@zag-js/utils': 1.15.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) '@zag-js/rect-utils@1.15.0': {} @@ -6763,6 +6905,8 @@ snapshots: acorn@8.14.1: {} + acorn@8.15.0: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6770,6 +6914,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + anser@2.3.2: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -6879,9 +7025,9 @@ snapshots: axe-core@4.10.3: {} - axios@1.11.0: + axios@1.12.0: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.11 form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -6948,10 +7094,10 @@ snapshots: chokidar: 3.6.0 confbox: 0.1.8 defu: 6.1.4 - dotenv: 16.4.7 + dotenv: 16.6.1 giget: 1.2.5 jiti: 1.21.7 - mlly: 1.7.4 + mlly: 1.8.0 ohash: 1.1.6 pathe: 1.1.2 perfect-debounce: 1.0.0 @@ -6995,12 +7141,12 @@ snapshots: loupe: 3.1.3 pathval: 2.0.0 - chakra-react-select@6.1.0(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.19)(next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + chakra-react-select@6.1.0(@chakra-ui/react@3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@18.3.19)(next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@chakra-ui/react': 3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-themes: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-select: 5.10.1(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@chakra-ui/react': 3.20.0(@emotion/react@11.14.0(@types/react@18.3.19)(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next-themes: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-select: 5.10.1(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) transitivePeerDependencies: - '@types/react' - react-dom @@ -7081,12 +7227,14 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clsx@2.1.1: {} + code-block-writer@13.0.3: {} - codemirror@6.0.1(@lezer/common@1.2.3): + codemirror@6.0.1(@lezer/common@1.5.0): dependencies: - '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3) - '@codemirror/commands': 6.8.1 + '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.5.0) + '@codemirror/commands': 6.10.1 '@codemirror/language': 6.11.0 '@codemirror/lint': 6.8.2 '@codemirror/search': 6.5.6 @@ -7131,6 +7279,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + core-js-compat@3.41.0: dependencies: browserslist: 4.24.4 @@ -7304,7 +7454,7 @@ snapshots: dequal@2.0.3: {} - destr@2.0.3: {} + destr@2.0.5: {} devlop@1.1.0: dependencies: @@ -7323,7 +7473,7 @@ snapshots: '@babel/runtime': 7.26.10 csstype: 3.1.3 - dotenv@16.4.7: {} + dotenv@16.6.1: {} dunder-proto@1.0.1: dependencies: @@ -7543,7 +7693,7 @@ snapshots: eslint-plugin-perfectionist@4.12.3(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3): dependencies: '@typescript-eslint/types': 8.32.0 - '@typescript-eslint/utils': 8.32.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.26.0(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -7559,7 +7709,7 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.2(eslint@9.26.0(jiti@1.21.7)) - eslint-plugin-react-hooks@4.6.2(eslint@9.26.0(jiti@1.21.7)): + eslint-plugin-react-hooks@5.2.0(eslint@9.26.0(jiti@1.21.7)): dependencies: eslint: 9.26.0(jiti@1.21.7) @@ -7618,6 +7768,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.26.0(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.26.0(jiti@1.21.7)) @@ -7784,10 +7936,14 @@ snapshots: object-assign: 4.1.1 promise: 7.3.1 setimmediate: 1.0.5 - ua-parser-js: 1.0.40 + ua-parser-js: 1.0.41 transitivePeerDependencies: - encoding + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7826,15 +7982,15 @@ snapshots: flatted@3.3.3: {} - flux@4.0.4(react@18.3.1): + flux@4.0.4(react@19.1.1): dependencies: fbemitter: 3.0.0 fbjs: 3.0.5 - react: 18.3.1 + react: 19.1.1 transitivePeerDependencies: - encoding - follow-redirects@1.15.9: {} + follow-redirects@1.15.11: {} for-each@0.3.5: dependencies: @@ -7910,7 +8066,7 @@ snapshots: citty: 0.1.6 consola: 3.4.2 defu: 6.1.4 - node-fetch-native: 1.6.6 + node-fetch-native: 1.6.7 nypm: 0.5.4 pathe: 2.0.3 tar: 6.2.1 @@ -7923,7 +8079,7 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 @@ -7932,14 +8088,14 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.0.0: + glob@11.1.0: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.0.3 + minimatch: 10.1.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 - path-scurry: 2.0.0 + path-scurry: 2.0.1 globals@11.12.0: {} @@ -8080,6 +8236,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8394,7 +8552,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.1.0: {} + lru-cache@11.2.4: {} lz-string@1.5.0: {} @@ -8787,7 +8945,7 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.0.3: + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -8818,12 +8976,12 @@ snapshots: mkdirp@3.0.1: {} - mlly@1.7.4: + mlly@1.8.0: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.5.4 + ufo: 1.6.1 ms@2.1.3: {} @@ -8864,12 +9022,12 @@ snapshots: neo-async@2.6.2: {} - next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - node-fetch-native@1.6.6: {} + node-fetch-native@1.6.7: {} node-fetch@2.7.0: dependencies: @@ -8898,7 +9056,7 @@ snapshots: pathe: 2.0.3 pkg-types: 1.3.1 tinyexec: 0.3.2 - ufo: 1.5.4 + ufo: 1.6.1 object-assign@4.1.1: {} @@ -9045,9 +9203,9 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.0: + path-scurry@2.0.1: dependencies: - lru-cache: 11.1.0 + lru-cache: 11.2.4 minipass: 7.1.2 path-to-regexp@6.3.0: {} @@ -9072,12 +9230,14 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pkce-challenge@5.0.0: {} pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 pluralize@8.0.0: {} @@ -9165,7 +9325,7 @@ snapshots: rc9@2.1.2: dependencies: defu: 6.1.4 - destr: 2.0.3 + destr: 2.0.5 react-base16-styling@0.6.0: dependencies: @@ -9174,64 +9334,70 @@ snapshots: lodash.flow: 3.5.0 pure-color: 1.3.0 - react-chartjs-2@5.3.0(chart.js@4.4.9)(react@18.3.1): + react-chartjs-2@5.3.0(chart.js@4.4.9)(react@19.1.1): dependencies: chart.js: 4.4.9 - react: 18.3.1 + react: 19.1.1 - react-dom@18.3.1(react@18.3.1): + react-dom@19.1.1(react@19.1.1): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.1.1 + scheduler: 0.26.0 - react-hook-form@7.56.2(react@18.3.1): + react-draggable@4.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 18.3.1 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - react-hotkeys-hook@4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-hook-form@7.56.2(react@19.1.1): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 - react-i18next@15.5.1(i18next@25.1.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + react-hotkeys-hook@4.6.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + + react-i18next@15.5.1(i18next@25.1.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3): dependencies: '@babel/runtime': 7.26.10 html-parse-stringify: 3.0.1 i18next: 25.1.2(typescript@5.8.3) - react: 18.3.1 + react: 19.1.1 optionalDependencies: - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.1.1(react@19.1.1) typescript: 5.8.3 - react-icons@5.5.0(react@18.3.1): + react-icons@5.5.0(react@19.1.1): dependencies: - react: 18.3.1 + react: 19.1.1 - react-innertext@1.1.5(@types/react@18.3.19)(react@18.3.1): + react-innertext@1.1.5(@types/react@18.3.19)(react@19.1.1): dependencies: '@types/react': 18.3.19 - react: 18.3.1 + react: 19.1.1 react-is@16.13.1: {} react-is@17.0.2: {} - react-json-view@1.21.3(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-json-view@1.21.3(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - flux: 4.0.4(react@18.3.1) - react: 18.3.1 + flux: 4.0.4(react@19.1.1) + react: 19.1.1 react-base16-styling: 0.6.0 - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.1.1(react@19.1.1) react-lifecycles-compat: 3.0.4 - react-textarea-autosize: 8.5.8(@types/react@18.3.19)(react@18.3.1) + react-textarea-autosize: 8.5.9(@types/react@18.3.19)(react@19.1.1) transitivePeerDependencies: - '@types/react' - encoding react-lifecycles-compat@3.0.4: {} - react-markdown@9.1.0(@types/react@18.3.19)(react@18.3.1): + react-markdown@9.1.0(@types/react@18.3.19)(react@19.1.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -9240,7 +9406,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 - react: 18.3.1 + react: 19.1.1 remark-parse: 11.0.0 remark-rehype: 11.1.1 unified: 11.0.5 @@ -9249,71 +9415,79 @@ snapshots: transitivePeerDependencies: - supports-color - react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-resizable-panels@2.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + + react-resizable@3.0.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + prop-types: 15.8.1 + react: 19.1.1 + react-draggable: 4.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + transitivePeerDependencies: + - react-dom - react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router-dom@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@remix-run/router': 1.23.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-router: 6.30.0(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-router: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react-router@6.30.0(react@18.3.1): + react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@remix-run/router': 1.23.0 - react: 18.3.1 + cookie: 1.1.1 + react: 19.1.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.1.1(react@19.1.1) - react-select@5.10.1(@types/react@18.3.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-select@5.10.1(@types/react@18.3.19)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@babel/runtime': 7.26.10 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@18.3.19)(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.19)(react@19.1.1) '@floating-ui/dom': 1.6.13 '@types/react-transition-group': 4.4.12(@types/react@18.3.19) memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.19)(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.19)(react@19.1.1) transitivePeerDependencies: - '@types/react' - supports-color - react-syntax-highlighter@15.6.1(react@18.3.1): + react-syntax-highlighter@15.6.1(react@19.1.1): dependencies: '@babel/runtime': 7.26.10 highlight.js: 10.7.3 highlightjs-vue: 1.0.0 lowlight: 1.20.0 prismjs: 1.30.0 - react: 18.3.1 + react: 19.1.1 refractor: 3.6.0 - react-textarea-autosize@8.5.8(@types/react@18.3.19)(react@18.3.1): + react-textarea-autosize@8.5.9(@types/react@18.3.19)(react@19.1.1): dependencies: - '@babel/runtime': 7.26.10 - react: 18.3.1 - use-composed-ref: 1.4.0(@types/react@18.3.19)(react@18.3.1) - use-latest: 1.3.0(@types/react@18.3.19)(react@18.3.1) + '@babel/runtime': 7.28.4 + react: 19.1.1 + use-composed-ref: 1.4.0(@types/react@18.3.19)(react@19.1.1) + use-latest: 1.3.0(@types/react@18.3.19)(react@19.1.1) transitivePeerDependencies: - '@types/react' - react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-transition-group@4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@babel/runtime': 7.26.10 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.1.1: {} read-pkg-up@7.0.1: dependencies: @@ -9492,9 +9666,7 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.26.0: {} semver@5.7.2: {} @@ -9527,6 +9699,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9745,13 +9919,18 @@ snapshots: test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 - glob: 10.4.5 + glob: 10.5.0 minimatch: 9.0.5 tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -9855,9 +10034,9 @@ snapshots: typescript@5.8.3: {} - ua-parser-js@1.0.40: {} + ua-parser-js@1.0.41: {} - ufo@1.5.4: {} + ufo@1.6.1: {} uglify-js@3.19.3: optional: true @@ -9927,37 +10106,43 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-composed-ref@1.4.0(@types/react@18.3.19)(react@18.3.1): + use-composed-ref@1.4.0(@types/react@18.3.19)(react@19.1.1): dependencies: - react: 18.3.1 + react: 19.1.1 optionalDependencies: '@types/react': 18.3.19 - use-debounce@10.0.4(react@18.3.1): + use-debounce@10.0.4(react@19.1.1): + dependencies: + react: 19.1.1 + + use-isomorphic-layout-effect@1.2.0(@types/react@18.3.19)(react@19.1.1): dependencies: - react: 18.3.1 + react: 19.1.1 + optionalDependencies: + '@types/react': 18.3.19 - use-isomorphic-layout-effect@1.2.0(@types/react@18.3.19)(react@18.3.1): + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.19)(react@19.1.1): dependencies: - react: 18.3.1 + react: 19.1.1 optionalDependencies: '@types/react': 18.3.19 - use-latest@1.3.0(@types/react@18.3.19)(react@18.3.1): + use-latest@1.3.0(@types/react@18.3.19)(react@19.1.1): dependencies: - react: 18.3.1 - use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.19)(react@18.3.1) + react: 19.1.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.19)(react@19.1.1) optionalDependencies: '@types/react': 18.3.19 - use-sync-external-store@1.4.0(react@18.3.1): + use-sync-external-store@1.4.0(react@19.1.1): dependencies: - react: 18.3.1 + react: 19.1.1 - usehooks-ts@3.1.1(react@18.3.1): + usehooks-ts@3.1.1(react@19.1.1): dependencies: lodash.debounce: 4.0.8 - react: 18.3.1 + react: 19.1.1 validate-npm-package-license@3.0.4: dependencies: @@ -10168,17 +10353,17 @@ snapshots: zod@3.24.4: {} - zustand@4.5.6(@types/react@18.3.19)(react@18.3.1): + zustand@4.5.6(@types/react@18.3.19)(react@19.1.1): dependencies: - use-sync-external-store: 1.4.0(react@18.3.1) + use-sync-external-store: 1.4.0(react@19.1.1) optionalDependencies: '@types/react': 18.3.19 - react: 18.3.1 + react: 19.1.1 - zustand@5.0.4(@types/react@18.3.19)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + zustand@5.0.4(@types/react@18.3.19)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)): optionalDependencies: '@types/react': 18.3.19 - react: 18.3.1 - use-sync-external-store: 1.4.0(react@18.3.1) + react: 19.1.1 + use-sync-external-store: 1.4.0(react@19.1.1) zwitch@2.0.4: {} diff --git a/airflow-core/src/airflow/ui/public/i18n/README.md b/airflow-core/src/airflow/ui/public/i18n/README.md index 18e4d44284dd9..e219db4e8302e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/README.md +++ b/airflow-core/src/airflow/ui/public/i18n/README.md @@ -61,11 +61,15 @@ communicating in the dev list or merging Pull Requests on their behalf). **Engaged translator** - Active contributor participating in translation without formal ownership. -**Inactive translation/code owner** — A translation/code owner is considered inactive if they meet either of +**Complete translation** - A supported locale is considered complete when it covers at least 90% of the terms in the default locale. + +**Inactive owner** — Either a translation owner or a code owner might be considered inactive if they meet any of the following criteria: - The locale under their responsibility has remained incomplete for at least 2 consecutive releases. -- They have not participated in the Apache Airflow project for more than 12 months. +- They have not contributed to Apache Airflow for more than 12 months. +- Code owners specifically might be considered inactive according to any other terms mentioned in the + ["Committers and PMC Members"](../../../../../../COMMITTERS.rst#inactive-committers) document. **Dev list** - The Apache Airflow development mailing list: dev@airflow.apache.org. @@ -101,6 +105,8 @@ the following criteria: - Code owners who act as translation sponsors are also responsible for: - Ensuring that the translation owner is active and able to maintain the translation. - Act according to section 6.4 when the translation owner relinquishes their role or become inactive. + - When they sponsor a single translation owner, without additional translation owners/engaged translators involved, + they SHALL also review the language aspects of translation-related PRs using a trusted third-party opinion (e.g., LLM). ### 4.3. Engaged translator @@ -128,9 +134,11 @@ the following criteria: owner. - When the above is not met, steps mentioned in section 6.4 SHOULD be taken by the appropriate roles. -> [!NOTE] -> It is welcomed and desired to have more than one translation owner to enable peer reviews and provide -> coverage during absences. + > [!WARNING] + > It is preferred to have at least two translation owners, or at least one translation owner and another engaged translator, + > to allow peer reviews and provide coverage during absences. + > Specifically, when translation is sponsored and there's only a single translation owner, without additional proficient people involved, the code owner becomes responsible for reviewing language aspects of PRs + > using a third-party opinion, which could risk quality and timeliness of reviews. ### 5.2. Adding new locales @@ -200,9 +208,10 @@ Translation conflicts MUST be resolved according to the procedures outlined in s ### 6.1. Approval of ownership candidates -- The designated code owner, should post a thread to the dev list, requesting the approval of: - - Introducing a new locale (including a link to the PR) - - Translation owner(s) in the suggested locale for non committer candidates. (sponsored) +- The designated code owner should post a thread to the dev list that includes the following details: + - The locale being suggested, including a link to the PR. + - Designated code owner(s) and translation owner(s) in the suggested locale. + - If the code owner is sponsored, they should indicate this as well. Specifically, if there is only one translation owner, the code owner should also declare how they plan to approve the language aspects of PRs (e.g., an engaged translator, LLM, etc.). - Within the thread, the code owner should demonstrate that the translation owner is suitable for the role, according to the requirements in section 5.3. - Approval of any translation owner who is not a committer requires at least one binding vote of 1 PMC member, @@ -215,6 +224,8 @@ The following steps outline the process for approving a new locale to be added t - Creating a PR for adding the suggested locale to the codebase ([see example](https://github.com/apache/airflow/pull/51258/files)), which includes: + - Adding the plural form rules for the suggested locale under `PLURAL_SUFFIXES` constant in + `dev/breeze/commands/ui_commands.py`. - The locale files (translated according to the guidelines) in the `airflow-core/src/airflow/ui/public/i18n/locales/` directory, where `` is the code of the language according to ISO 639-1 standard (e.g., `fr` for French). Languages with regional @@ -283,7 +294,8 @@ Language proficiency for translation owners can be demonstrated through any of t (e.g., Portuguese vs. Spanish), the other language might be used as a reference, but still the default language (English) should be the primary source for translations. - Translations should be accurate, maintaining original meaning and intent. -- Translations should be complete, covering all terms and phrases in the default language. +- Translations should be complete, covering all terms and phrases in the default language up to the defined + completeness threshold. - Translation of technical terminology should be consistent (for example: Dag, Task, Operator, etc.). - Language should be polite and neutral in tone. - Local conventions should be considered (e.g., date formats, number formatting, formal vs. informal tone, @@ -299,13 +311,17 @@ Language proficiency for translation owners can be demonstrated through any of t All files: ```bash -uv run dev/i18n/check_translations_completeness.py +breeze ui check-translation-completeness ``` +> [!NOTE] +> When announcing a freeze time, copy the output of the table showing completeness of all languages +> to the mail body. + Files for specific languages: ```bash -uv run dev/i18n/check_translations_completeness.py --language +breeze ui check-translation-completeness --language ``` Where `` is the code of the language you want to check, e.g., `en`, `fr`, `de`, etc. @@ -313,13 +329,25 @@ Where `` is the code of the language you want to check, e.g., `en Adding missing translations (with `TODO: translate` prefix): ```bash -uv run dev/i18n/check_translations_completeness.py --language --add-missing +breeze ui check-translation-completeness --language --add-missing +``` + +You can also remove extra translations from the language of your choice: + +```bash +breeze ui check-translation-completeness --language --remove-extra +``` + +Or from all languages: + +```bash +breeze ui check-translation-completeness --remove-extra ``` The script is also added as a prek hook (manual) so that it can be run from within `prek` and CI: ```bash -prek run --hook-stage manual check-translations-completeness --verbose --all-files +breeze ui check-translation-completeness --verbose --all-files ``` @@ -350,13 +378,27 @@ FAIL_WHEN_ENGLISH_TRANSLATION_CHANGED = True ``` This fails any attempt to change English translation files in a PR unless `allow translation change` -label is applied to the PR. This allows to still fix critical issues in English translation files and make -deliberate updates to it but avoids accidental changes. +label is applied to the PR. This still allows issues in the English translation files to be fixed and +deliberate updates to be made, while avoiding accidental changes. -Any change in such English translation files during freeze time MUST be communicated in the -`#18n` Slack channel - so that translators can be informed as early as possible about those translations +Any change in the English translation files during freeze time MUST be communicated in the +[#18n](https://app.slack.com/client/TCQ18L22Z/C09D0A7FESJ?) Slack channel and MUST be approved by at least 1 PMC member - so that translators can be informed as early as possible about those translations being added. +> [!NOTE] +> The definition of completeness takes into account that some terms might be added during freeze time and remain untranslated. + +### 11.1 Guidelines for approving freeze exemptions + +The following questions should be considered before approving exemptions for changes to the English translation files during freeze time: + +- Are the changes necessary for a critical fix or feature? +- Do the changes only introduce minor fixes to existing terms? (such modifications are usually less disruptive and OK to approve) +- Do the changes introduce new terms, remove terms, or significantly alter already translated terms? (if so, it may be better to wait until the next release) +- Is it feasible to complete the translations in all locales before the release? (the fewer changes, the more feasible) +- If not all translations are completed before the release, will it significantly affect the user experience? (if so, it might be better to wait until the next release). +- If not all translations are completed before the release, will any locales be left in an incomplete state? (if so, it might be better to wait until the next release). + ## 12. Exceptions If any exceptions to this policy are needed, they MUST be discussed and approved by voting in the dev list diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/admin.json index 21e002ba0cd6a..30af3a023c0c4 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/admin.json @@ -61,6 +61,12 @@ "searchPlaceholder": "البحث عن موَّصلات", "test": "اختبار الموَّصل", "testDisabled": "اختبار الموَّصل معطل. تواصل مع المسؤول لتفعيله.", + "testError": { + "title": "فشل اختبار الموَّصل" + }, + "testSuccess": { + "title": "نجح اختبار الموَّصل" + }, "typeMeta": { "error": "فشل في استرداد نوع الموَّصل", "standardFields": { @@ -84,7 +90,6 @@ "tooltip": "حذف الموَّصلات المحددة" }, "formActions": { - "reset": "إعادة تعيين", "save": "حفظ" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/assets.json index 2e1cff6cff333..12b9eba643987 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/assets.json @@ -1,5 +1,13 @@ { + "additional_data": "بيانات إضافية", + "asset_few": "أصول", + "asset_many": "أصول", + "asset_one": "أصل", + "asset_other": "أصول", + "asset_two": "أصلان", + "asset_zero": "لا يوجد أي أصل", "consumingDags": "Dags المستهلكة", + "consumingTasks": "المهام المستهلكة", "createEvent": { "button": "إنشاء حدث", "manual": { @@ -21,6 +29,7 @@ }, "title": "إنشاء حدث أصل لـ {{name}}" }, + "extra": "إضافي", "group": "المجموعة", "lastAssetEvent": "آخر حدث أصل", "name": "الاسم", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/browse.json index f283c52b35a83..988bd57c9d778 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "طي جميع الJSON الإضافية", - "expandAllExtra": "توسيع جميع الJSON الإضافية" - }, "columns": { "event": "اصل", "extra": "إضافي", @@ -16,11 +12,35 @@ "title": "سجل المراجعة" }, "xcom": { + "add": { + "error": "فشل في إضافة XCom", + "errorTitle": "خطأ", + "success": "تمت إضافة XCom بنجاح", + "successTitle": "تمت إضافة XCom", + "title": "إضافة XCom" + }, "columns": { "dag": "Dag", "key": "مفتاح", "value": "قيمة" }, - "title": "(XCom) إكس كوم" + "delete": { + "error": "فشل في حذف XCom", + "errorTitle": "خطأ", + "success": "تم حذف XCom بنجاح", + "successTitle": "تم حذف XCom", + "title": "حذف XCom", + "warning": "هل أنت متأكد أنك تريد حذف هذا XCom؟ لا يمكن التراجع عن هذا الإجراء." + }, + "edit": { + "error": "فشل في تحديث XCom", + "errorTitle": "خطأ", + "success": "تم تحديث XCom بنجاح", + "successTitle": "تم تحديث XCom", + "title": "تعديل XCom" + }, + "key": "مفتاح", + "title": "(XCom) إكس كوم", + "value": "قيمة" } } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json index 813a4487cba11..8cde4e9adcb6c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json @@ -37,6 +37,7 @@ "requiredActions": "إجراءات مطلوبة", "xcoms": "(XComs) إكس كوم" }, + "collapseAllExtra": "طي جميع الJSON الإضافية", "collapseDetailsPanel": "طي لوحة التفاصيل", "createdAssetEvent_few": "تم إنشاء واقعات أصل", "createdAssetEvent_many": "تم إنشاء واقعات أصل", @@ -79,6 +80,7 @@ "dataIntervalEnd": "نهاية فترة البيانات", "dataIntervalStart": "بداية فترة البيانات", "lastSchedulingDecision": "آخر قرار جدولة", + "partitionKey": "مفتاح التقسيم", "queuedAt": "في الطابور في", "runAfter": "تشغيل بعد", "runType": "نوع التشغيل", @@ -96,18 +98,30 @@ "dagWarnings": "تحذيرات الDag", "defaultToGraphView": "عرض الرسم البياني تلقائياً", "defaultToGridView": "عرض الشبكة تلقائياً", + "delete": "حذف", + "diff": "الفرق", + "diffCompareWith": "قارن مع", + "diffExit": "إنهاء المقارنة", + "diffSelectVersionToCompare": "اختر إصدارًا للمقارنة", "direction": "الاتجاه", "docs": { "documentation": "توثيق", "githubRepo": "مستودع GitHub", "restApiReference": "مرجع REST API" }, + "download": { + "download": "تنزيل", + "hotkey": "d", + "tooltip": "اضغط على {{hotkey}} لتنزيل السجلات" + }, "duration": "المدة", + "edit": "تعديل", "endDate": "تاريخ الانتهاء", "error": { "back": "رجوع", "defaultMessage": "حدث خطأ غير متوقع", "home": "الصفحة الرئيسية", + "invalidUrl": "الصفحة غير موجودة. يرجى التحقق من الرابط والمحاولة مرة أخرى.", "notFound": "غير موجود", "title": "خطأ" }, @@ -117,34 +131,38 @@ "hotkey": "e", "tooltip": "اضغط {{hotkey}} للتوسيع" }, + "expandAllExtra": "توسيع جميع الJSON الإضافية", "expression": { "all": "الكل", "and": "و", "any": "أي", "or": "أو" }, + "filter": "فلتر", "filters": { - "dagDisplayNamePlaceholder": "تصفية حسب الDag", - "keyPlaceholder": "تصفية حسب مفتاح XCom", - "logicalDateFromPlaceholder": "من التاريخ المنطقي", - "logicalDateToPlaceholder": "إلى التاريخ المنطقي", - "mapIndexPlaceholder": "تصفية حسب فهرس الخريطة", - "runAfterFromPlaceholder": "من تشغيل بعد", - "runAfterToPlaceholder": "إلى تشغيل بعد", - "runIdPlaceholder": "تصفية حسب معرف التشغيل", - "taskIdPlaceholder": "تصفية حسب معرف المهمة" + "durationFrom": "المدة من", + "durationTo": "المدة إلى", + "endTime": "وقت الانتهاء", + "logicalDateFrom": "من التاريخ المنطقي", + "logicalDateTo": "إلى التاريخ المنطقي", + "runAfterFrom": "من تشغيل بعد", + "runAfterTo": "إلى تشغيل بعد", + "selectDateRange": "اختر نطاق التاريخ", + "startTime": "وقت البدء" }, "logicalDate": "التاريخ المنطقي", "logout": "تسجيل الخروج", "logoutConfirmation": "أنت على وشك تسجيل الخروج.", "mapIndex": "فهرس الخريطة", "modal": { + "add": "إضافة", "cancel": "إلغاء", "confirm": "تأكيد", "delete": { "button": "حذف", "confirmation": "هل أنت متأكد من أنك تريد حذف {{resourceName}}؟ لا يمكن التراجع عن هذا الإجراء." - } + }, + "save": "حفظ" }, "nav": { "admin": "إدارة", @@ -165,19 +183,7 @@ "placeholder": "أضف ملاحظة...", "taskInstance": "مثيل المهمة" }, - "pools": { - "deferred": "مؤجل", - "open": "مفتوح", - "pools_few": "مجمعات", - "pools_many": "مجمعات", - "pools_one": "مجمع", - "pools_other": "مجمعات", - "pools_two": "مجمعان", - "pools_zero": "لا يوجد أي مجمع", - "queued": "في الانتظار", - "running": "قيد التشغيل", - "scheduled": "مجدول" - }, + "reset": "إعادة تعيين", "runId": "معرف التشغيل", "runTypes": { "asset_triggered": "مُشغل بواسطة الأصل", @@ -192,7 +198,6 @@ }, "tooltip": "اضغط {{hotkey}} للتمرير إلى {{direction}}" }, - "seconds": "ثواني", "security": { "actions": "إجراءات", "permissions": "صلاحيات", @@ -202,6 +207,7 @@ }, "selectLanguage": "اختيار اللغة", "showDetailsPanel": "إظهار لوحة التفاصيل", + "signedInAs": "تم تسجيل الدخول باسم", "source": { "hide": "إخفاء المصدر", "hotkey": "s", @@ -220,6 +226,7 @@ "failed": "فشِل", "no_status": "بلا حالة", "none": "لا شيء", + "open": "مفتوح", "planned": "مخطط", "queued": "في الانتظار", "removed": "محذوف", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json index 4975e2114b793..11aa3dbd783da 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/components.json @@ -10,12 +10,11 @@ "allRuns": "جميع التشغيلات", "backwards": "تشغيل رجعي", "dateRange": "نطاق التاريخ", - "dateRangeFrom": "من", - "dateRangeTo": "إلى", "errorStartDateBeforeEndDate": "يجب أن يكون تاريخ البدء قبل تاريخ الانتهاء", "maxRuns": "الحد الأقصى للتشغيلات النشطة", "missingAndErroredRuns": "تشغيلات مفقودة وخاطئة", "missingRuns": "تشغيلات مفقودة", + "permissionDenied": "فشل التشغيل التجريبي (Dry Run): ليس لدى المستخدم صلاحية لإنشاء تعبئات رجعية.", "reprocessBehavior": "اعادة معالجة السلوك", "run": "تشغيل التعبئة الرجعية", "selectDescription": "تشغيل هذا Dag لنطاق من التواريخ", @@ -64,6 +63,13 @@ "warning_two": "تحذيران", "warning_zero": "لا يوجد أي تحذير" }, + "dateRangeFilter": { + "validation": { + "invalidDateFormat": "تنسيق التاريخ غير صالح.", + "invalidTimeFormat": "تنسيق الوقت غير صالح.", + "startBeforeEnd": "يجب أن يكون تاريخ/وقت البدء قبل تاريخ/وقت الانتهاء" + } + }, "durationChart": { "duration": "المدة (بالثواني)", "lastDagRun_few": "آخر {{count}} تشغيلات Dag", @@ -118,6 +124,22 @@ "taskGroup": "مجموعة المهام" }, "limitedList": "+{{count}} المزيد", + "limitedList.allItems": "جميع العناصر {{count}}:", + "limitedList.allTags_few": "كل العلامات ({{count}})", + "limitedList.allTags_many": "كل العلامات ({{count}})", + "limitedList.allTags_one": "كل العلامات (1)", + "limitedList.allTags_other": "كل العلامات ({{count}})", + "limitedList.allTags_two": "كل العلامات (2)", + "limitedList.allTags_zero": "كل العلامات (0)", + "limitedList.clickToInteract": "انقر على علامة لتصفية Dags", + "limitedList.clickToOpenFull": "انقر \"+{{count}} المزيد\" لعرض القائمة الكاملة", + "limitedList.copyPasteText": "يمكنك نسخ ولصق النص أعلاه", + "limitedList.showingItems_few": "عرض {{count}} عناصر", + "limitedList.showingItems_many": "عرض {{count}} عنصر", + "limitedList.showingItems_one": "عرض عنصر واحد", + "limitedList.showingItems_other": "عرض {{count}} عنصر", + "limitedList.showingItems_two": "عرض عنصرين", + "limitedList.showingItems_zero": "عرض 0 عناصر", "logs": { "file": "ملف", "location": "سطر {{line}} في {{name}}" @@ -127,10 +149,23 @@ "sortedDescending": "الترتيب تنازلي", "sortedUnsorted": "غير مرتب", "taskTries": "محاولات المهمة", + "taskTryPlaceholder": "محاولة المهمة", + "team": { + "selector": { + "helperText": "اختياري. قصر الاستخدام على فريق محدد.", + "label": "فريق", + "placeHolder": "اختر فريقًا" + } + }, "toggleCardView": "عرض البطاقة", "toggleTableView": "عرض الجدول", "triggerDag": { "button": "تشغيل", + "dataInterval": "فترة البيانات", + "dataIntervalAuto": "مستنتجة من التاريخ المنطقي والجدول الزمني", + "dataIntervalManual": "تحديد يدويًا", + "intervalEnd": "النهاية", + "intervalStart": "البداية", "loading": "جارٍ تحميل معلومات Dag...", "loadingFailed": "فشل تحميل معلومات Dag. يرجى المحاولة مرة أخرى.", "runIdHelp": "اختياري - سيتم توليده تلقائيًا إذا لم يتم توفيره.", @@ -138,11 +173,15 @@ "selectLabel": "تشغيلة واحدة", "title": "تشغيل Dag", "toaster": { + "error": { + "title": "فشل تشغيل Dag" + }, "success": { "description": "تم تشغيل عملية Dag بنجاح.", "title": "تم تشغيل Dag" } }, + "triggerAgainWithConfig": "تشغيل مرة أخرى باستخدام هذا التكوين", "unpause": "إلغاء إيقاف {{dagDisplayName}} عند التشغيل" }, "trimText": { @@ -158,6 +197,7 @@ "versionId": "معرف الإصدار" }, "versionSelect": { + "allVersions": "كل الإصدارات", "dagVersion": "إصدار Dag", "versionCode": "v{{versionCode}}" } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json index 54936a0d51301..4ae2dd63600f3 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json @@ -10,6 +10,7 @@ "hourly": "ساعي", "legend": { "less": "أقل", + "mixed": "مختلط", "more": "أكثر" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "السنة السابقة" }, "noData": "لا توجد بيانات متاحة", + "noFailedRuns": "لا توجد تشغيلات فاشلة", "noRuns": "لا توجد تشغيلات", "totalRuns": "إجمالي التشغيلات", "week": "الأسبوع {{weekNumber}}", @@ -128,6 +130,18 @@ }, "graphDirection": { "label": "اتجاه الرسم البياني" + }, + "taskStreamFilter": { + "activeFilter": "التصفية النشطة", + "clearFilter": "مسح التصفية", + "clickTask": "انقر على مهمة لاختيارها كجذر للتصفية", + "label": "تصفية", + "options": { + "both": "المهام السابقة واللاحقة", + "downstream": "المهام اللاحقة", + "upstream": "المهام السابقة" + }, + "selectedTask": "المهمة المحددة" } }, "paramsFailed": "فشل في تحميل المعلمات", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json index b7f8e60bbd8b5..ef1170957654c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dags.json @@ -20,8 +20,7 @@ "all": "الكل", "paused": "متوقف" }, - "runIdPatternFilter": "بحث تشغيلات الDag", - "triggeringUserNameFilter": "البحث حسب المستخدم صاحب الاطلاق" + "runIdPatternFilter": "بحث تشغيلات الDag" }, "ownerLink": "رابط المالك لـ{{owner}}", "runAndTaskActions": { @@ -35,6 +34,10 @@ "error": "حدث خطأ أثناء المسح {{type}}", "title": "مسح {{type}}" }, + "confirmationDialog": { + "description": "المهمة حاليًا في حالة {{state}} وقد بدأها المستخدم {{user}} في {{time}}. \nلا يمكن للمستخدم مسح هذه المهمة حتى تنتهي من التشغيل أو يقوم مستخدم بإلغاء تحديد خيار \"منع إعادة التشغيل إذا كانت المهمة قيد التشغيل\" في مربع حوار مسح المهمة.", + "title": "لا يمكن مسح مثيل المهمة" + }, "delete": { "button": "حذف {{type}}", "dialog": { @@ -62,6 +65,7 @@ "future": "المستقبل", "onlyFailed": "مسح المهام الفاشلة فقط", "past": "الماضي", + "preventRunningTasks": "منع إعادة التشغيل إذا كانت المهمة قيد التشغيل", "queueNew": "إضافة مهام جديدة للانتظار", "runOnLatestVersion": "تشغيل بآخر إصدار الحزمة", "upstream": "المهام السابقة" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dashboard.json index e6fc009d0bc66..62d51e3062543 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dashboard.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dashboard.json @@ -37,7 +37,8 @@ "poolSlots": "حصص مجموعة الموارد", "sortBy": { "newestFirst": "الأحدث أولاً", - "oldestFirst": "الأقدم أولاً" + "oldestFirst": "الأقدم أولاً", + "placeholder": "الفرز حسب" }, "source": "المصدر", "stats": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/ar/hitl.json index c971f4525512a..24127decf4ecf 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/hitl.json @@ -1,5 +1,7 @@ { "filters": { + "body": "المحتوى", + "createdAt": "تم الإنشاء في", "response": { "all": "الكل", "pending": "قيد الانتظار", @@ -20,11 +22,13 @@ "requiredActionCount_zero": "لا توجد إجراءات مطلوبة", "requiredActionState": "حالة الإجراء المطلوب", "response": { + "created": "تم إنشاء الاستجابة في ", "error": "فشل في الاستجابة", "optionsDescription": "اختر الخيارات لهذا مثيل المهمة", "optionsLabel": "الخيارات", "received": "تم استلام الاستجابة في ", "respond": "الرد", + "responded_by_user_name": "أجاب بواسطة (اسم المستخدم)", "success": "تمت الاستجابة بنجاح ل{{taskId}}", "title": "مثيل مهمة يدوي - {{taskId}}" }, diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/admin.json index a8a6a26c8e950..21fda3f14aa8c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/admin.json @@ -49,6 +49,12 @@ "searchPlaceholder": "Cercar connexions", "test": "Provar connexió", "testDisabled": "La funció de prova de connexió està desactivada. Si us plau, contacteu amb un administrador per activar-la.", + "testError": { + "title": "Prova de connexió fallida" + }, + "testSuccess": { + "title": "Prova de connexió exitosa" + }, "typeMeta": { "error": "No s'ha pogut recuperar la informació del tipus de connexió", "standardFields": { @@ -72,7 +78,6 @@ "tooltip": "Eliminar connexions seleccionades" }, "formActions": { - "reset": "Restablir", "save": "Desar" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/assets.json index 0538c04501ddf..399abaee90777 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "Dags consumidors", + "consumingTasks": "Tasques consumidores", "createEvent": { "button": "Crear esdeveniment", "manual": { @@ -21,6 +22,7 @@ }, "title": "Crear esdeveniment d'Asset per {{name}}" }, + "extra": "Extra", "group": "Grup", "lastAssetEvent": "Darrer esdeveniment d'Asset", "name": "Nom", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/browse.json index 217f330c56597..5bee6083c0d46 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Col·lapsar tots els camps extra json", - "expandAllExtra": "Expandir tots els camps extra json" - }, "columns": { "event": "Esdeveniment", "extra": "Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json index 9330bc9aafbbf..14b256967cbea 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json @@ -25,6 +25,7 @@ "requiredActions": "Accions requerides", "xcoms": "XComs" }, + "collapseAllExtra": "Col·lapsar tots els camps extra json", "collapseDetailsPanel": "Col·lapsar el panell de detalls", "createdAssetEvent_one": "Esdeveniment d'Asset creat", "createdAssetEvent_other": "Esdeveniments d'Asset creats", @@ -40,7 +41,7 @@ "hasTaskConcurrencyLimits": "Té límits de concurrència de tasques", "lastExpired": "Últim caducat", "lastParseDuration": "Duració de l'últim anàlisi", - "lastParsed": "Últim anàlitzat", + "lastParsed": "Últim analitzat", "latestDagVersion": "Última versió del Dag", "latestRun": "Última execució", "maxActiveRuns": "Màxim d'execucions actives", @@ -78,12 +79,18 @@ "githubRepo": "Repositori de GitHub", "restApiReference": "Referència de l'API REST" }, + "download": { + "download": "Descarregar", + "hotkey": "d", + "tooltip": "Prem {{hotkey}} per descarregar els registres" + }, "duration": "Duració", "endDate": "Data de finalització", "error": { "back": "Tornar", "defaultMessage": "S'ha produït un error inesperat", "home": "Inici", + "invalidUrl": "Pàgina no trobada. Si us plau, comprova la URL i torna-ho a provar.", "notFound": "Pàgina no trobada", "title": "Error" }, @@ -93,22 +100,19 @@ "hotkey": "e", "tooltip": "Prem {{hotkey}} per alternar l'expansió" }, + "expandAllExtra": "Expandir tots els camps extra json", "expression": { "all": "Tots", "and": "I", "any": "Qualsevol", "or": "O" }, + "filter": "Filtre", "filters": { - "dagDisplayNamePlaceholder": "Filtrar per Dag", - "keyPlaceholder": "Filtrar per clau XCom", - "logicalDateFromPlaceholder": "Data Lògica Des de", - "logicalDateToPlaceholder": "Data Lògica Fins a", - "mapIndexPlaceholder": "Filtrar per Índex del Mapa", - "runAfterFromPlaceholder": "Executar Després de", - "runAfterToPlaceholder": "Executar Després de", - "runIdPlaceholder": "Filtrar per ID d'Execució", - "taskIdPlaceholder": "Filtrar per ID de Tasca" + "logicalDateFrom": "Data Lògica Des de", + "logicalDateTo": "Data Lògica Fins a", + "runAfterFrom": "Executar Després de", + "runAfterTo": "Executar Després de" }, "logicalDate": "Data Lògica", "logout": "Tancar sessió", @@ -150,6 +154,7 @@ "running": "Executant-se", "scheduled": "Programat" }, + "reset": "Restablir", "runId": "ID de l'execució", "runTypes": { "asset_triggered": "Executat per Asset", @@ -164,7 +169,6 @@ }, "tooltip": "Prem {{hotkey}} per desplaçar-te cap a {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Accions", "permissions": "Permisos", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json index 295f9c0877887..088b40a5e3db2 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/components.json @@ -6,8 +6,6 @@ "allRuns": "Totes les execucions", "backwards": "Executar enrere", "dateRange": "Interval de dates", - "dateRangeFrom": "Des de", - "dateRangeTo": "Fins", "errorStartDateBeforeEndDate": "La data d'inici ha de ser anterior a la data de finalització", "maxRuns": "Màxim d'execucions actives", "missingAndErroredRuns": "Execucions absents i amb errors", @@ -90,6 +88,14 @@ "taskGroup": "Grup de tasques" }, "limitedList": "+{{count}} més", + "limitedList.allItems": "Tots els {{count}} elements:", + "limitedList.allTags_one": "Tots els tags ({{count}})", + "limitedList.allTags_other": "Tots els tags ({{count}})", + "limitedList.clickToInteract": "Fes clic en una etiqueta per filtrar els Dags", + "limitedList.clickToOpenFull": "Fes clic a \"+{{count}} més\" per veure la llista completa", + "limitedList.copyPasteText": "Pots copiar i enganxar el text de dalt", + "limitedList.showingItems_one": "Mostrant {{count}} element", + "limitedList.showingItems_other": "Mostrant {{count}} elements", "logs": { "file": "Fitxer", "location": "línia {{line}} a {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json index 25392a7877a3a..506bfd36dce1b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json @@ -10,6 +10,7 @@ "hourly": "Horari", "legend": { "less": "Menys", + "mixed": "Barrejat", "more": "Més" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Any anterior" }, "noData": "No hi ha dades disponibles", + "noFailedRuns": "No hi ha execucions fallides", "noRuns": "No hi ha execucions", "totalRuns": "Total d'execucions", "week": "Setmana {{weekNumber}}", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/README.md b/airflow-core/src/airflow/ui/public/i18n/locales/de/README.md index 836c711c359cc..04ba20273b74c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/README.md +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/README.md @@ -61,7 +61,8 @@ Für die Deutsche Übersetzung wurden die folgenden Terme wie folgt übersetzt Da der Begriff in Airflow 3 neu eingeführt wurde steht er derzeit nicht fest. Daher eignet er sich zu der inhaltlich passenden Übersetzung. Um neue Benutzer nicht zu verwirren wird der durch Airflow definierte Originalterm in - Klammern wenn möglich mitgeführt. + Klammern wenn möglich mitgeführt. Einzig in der Navigationsleiste ist der Begriff + ohne die englische Referenz um den Text kurz zu halten. - `Asset Event` --> `Ereignis zu Datenset (Asset)`: Logische Konsequenz der Übersetzung von -->"Asset" ohne einen sperrigen Begriff wie "Datenssatz-Ereignis" zu erzeugen. diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/admin.json index a5299dc13a9d7..df6a88af9481d 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/admin.json @@ -49,6 +49,12 @@ "searchPlaceholder": "Verbindungen suchen", "test": "Verbindung testen", "testDisabled": "Das Testen von Verbindungen ist deaktiviert. Der Administrator kann via Konfiguration das Testen freischalten.", + "testError": { + "title": "Verbindungstest fehlgeschlagen" + }, + "testSuccess": { + "title": "Verbindungstest erfolgreich" + }, "typeMeta": { "error": "Fehler beim Abrufen der Liste der Verbindungstypen", "standardFields": { @@ -72,7 +78,6 @@ "tooltip": "Ausgewählte Verbindungen löschen" }, "formActions": { - "reset": "Zurücksetzen", "save": "Speichern" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/assets.json index 6804f32ffe598..66f5287afc8bc 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "Konsumierende Dags", + "consumingTasks": "Konsumierende Tasks", "createEvent": { "button": "Ereignis erstellen", "manual": { @@ -21,6 +22,7 @@ }, "title": "Ereignis zu Datenset (Asset) {{name}} erstellen" }, + "extra": "Extra", "group": "Gruppe", "lastAssetEvent": "Letztes Ereignis zu Datenset (Asset)", "name": "Name", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/browse.json index 14ac6c58501dd..1f8a816ea80b7 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Alle Extra JSON Daten einklappen", - "expandAllExtra": "Alle Extra JSON Daten ausklappen" - }, "columns": { "event": "Ereignis", "extra": "Extra JSON Daten", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json index ea9788de85b66..1f7e179dc82fc 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json @@ -25,6 +25,7 @@ "requiredActions": "Erforderliche Interaktionen", "xcoms": "Task Kommunikation (XComs)" }, + "collapseAllExtra": "Alle JSON Details ausblenden", "collapseDetailsPanel": "Detailansicht ausblenden", "createdAssetEvent_one": "Erstelltes Ereignis zu Datenset (Asset)", "createdAssetEvent_other": "Erstellte Ereignisse zu Datensets (Assets)", @@ -78,12 +79,18 @@ "githubRepo": "GitHub Ablage", "restApiReference": "REST API Referenz" }, + "download": { + "download": "Herunterladen", + "hotkey": "d", + "tooltip": "Drücken Sie {{hotkey}}, um Protokolle herunterzuladen" + }, "duration": "Laufzeit", "endDate": "Enddatum", "error": { "back": "Zurück", "defaultMessage": "Ein unerwarteter Fehler ist aufgetreten", "home": "Start", + "invalidUrl": "Seite nicht gefunden. Bitte überprüfen Sie die URL und versuchen Sie es erneut.", "notFound": "Seite nicht gefunden", "title": "Fehler" }, @@ -93,23 +100,19 @@ "hotkey": "e", "tooltip": "Tastenkombination {{hotkey}} zum Ein-/Ausklappen drücken" }, + "expandAllExtra": "Alle JSON Details anzeigen", "expression": { "all": "Alle", "and": "UND", "any": "Jeder", "or": "ODER" }, + "filter": "Filter", "filters": { - "dagDisplayNamePlaceholder": "Nach Dag filtern", - "keyPlaceholder": "Nach Task Kommunikation (XCom) Schlüssel filtern", - "logicalDateFromPlaceholder": "Logisches Datum Von", - "logicalDateToPlaceholder": "Logisches Datum Bis", - "mapIndexPlaceholder": "Nach Planungs-Index filtern", - "runAfterFromPlaceholder": "Gelaufen-nach von filtern", - "runAfterToPlaceholder": "Gelaufen-nach bis filtern", - "runIdPlaceholder": "Nach Lauf ID filtern", - "taskIdPlaceholder": "Nach Task ID filtern", - "triggeringUserPlaceholder": "Nach auslösendem Benutzer filtern" + "logicalDateFrom": "Logisches Datum Von", + "logicalDateTo": "Logisches Datum Bis", + "runAfterFrom": "Gelaufen-nach von filtern", + "runAfterTo": "Gelaufen-nach bis filtern" }, "logicalDate": "Logisches Datum", "logout": "Abmelden", @@ -125,7 +128,7 @@ }, "nav": { "admin": "Verwaltung", - "assets": "Datensets (Assets)", + "assets": "Datensets", "browse": "Browsen", "dags": "Dags", "docs": "Doku", @@ -151,6 +154,7 @@ "running": "Laufende", "scheduled": "Geplant" }, + "reset": "Zurücksetzen", "runId": "Lauf Id", "runTypes": { "asset_triggered": "Durch Datenset (Asset) ausgelöst", @@ -165,7 +169,6 @@ }, "tooltip": "Tastenkombination {{hotkey}} zum scrollen nach {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Aktionen", "permissions": "Berechtigungen", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/components.json index 95abe63ea6367..85e7d17a1e0e6 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/components.json @@ -6,8 +6,6 @@ "allRuns": "Alle Läufe", "backwards": "Verarbeitung in Rückwärtiger Reihenfolge", "dateRange": "Datumsbereich", - "dateRangeFrom": "Von", - "dateRangeTo": "Bis", "errorStartDateBeforeEndDate": "Das Startdatum muss vor dem Enddatum liegen.", "maxRuns": "Anzahl aktiver paralleler Läufe", "missingAndErroredRuns": "Fehlende und fehlgeschlagene Läufe", @@ -90,21 +88,29 @@ "taskGroup": "Task Gruppe" }, "limitedList": "+{{count}} mehr", + "limitedList.allItems": "Alle {{count}} Einträge:", + "limitedList.allTags_one": "Alle Markierungen ({{count}})", + "limitedList.allTags_other": "Alle Markierungen ({{count}})", + "limitedList.clickToInteract": "Klicken Sie auf eine Markierung, um Dags zu filtern", + "limitedList.clickToOpenFull": "Klicken Sie auf \"+{{count}} mehr\" für die vollständige Ansicht", + "limitedList.copyPasteText": "Sie können den obigen Text kopieren und einfügen", + "limitedList.showingItems_one": "{{count}} Eintrag wird angezeigt", + "limitedList.showingItems_other": "{{count}} Einträge werden angezeigt", "logs": { "file": "Datei", "location": "Zeile {{line}} in {{name}}" }, "reparseDag": "Dag neu parsen", - "sortedAscending": "aufsteigend sortier", - "sortedDescending": "absteigend sortier", + "sortedAscending": "aufsteigend sortiert", + "sortedDescending": "absteigend sortiert", "sortedUnsorted": "unsortiert", "taskTries": "Versuch des Tasks", "toggleCardView": "Kachelansicht anzeigen", "toggleTableView": "Tabellenansicht anzeigen", "triggerDag": { "button": "Auslösen", - "loading": "Lade DAG Information...", - "loadingFailed": "Das Laden der DAG Information fehlgeschlagen. Bitt versuchen Sie es noch einmal.", + "loading": "Lade Dag Information...", + "loadingFailed": "Das Laden der Dag Information fehlgeschlagen. Bitt versuchen Sie es noch einmal.", "runIdHelp": "Optional - wird automatisch erzeugt wenn nicht angegeben", "selectDescription": "Einen einzelnen Lauf dieses Dag auslösen", "selectLabel": "Einzelner Lauf", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json index 7f0b2c603bb99..1803e2ea630aa 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json @@ -10,6 +10,7 @@ "hourly": "Stündlich", "legend": { "less": "Weniger", + "mixed": "Mittel", "more": "Mehr" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Vorheriges Jahr" }, "noData": "Keine Daten verfügbar", + "noFailedRuns": "Keine fehlgeschlagenen Läufe", "noRuns": "Keine Läufe", "totalRuns": "Gesamtzahl der Läufe", "week": "Kalenderwoche {{weekNumber}}", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/de/dags.json index a81349c6e17a6..99c0a3e4129c9 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/de/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/dags.json @@ -21,7 +21,7 @@ "paused": "Pausiert" }, "runIdPatternFilter": "Dag Läufe suchen", - "triggeringUserNameFilter": "Suche Läufe ausgelöst von..." + "triggeringUserNameFilter": "Suchen nach Benutzer der ausgelöst hat" }, "ownerLink": "Besitzer Verlinkungen zu {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/admin.json new file mode 100644 index 0000000000000..6358cf5e4e5c3 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/admin.json @@ -0,0 +1,166 @@ +{ + "columns": { + "description": "Περιγραφή", + "key": "Κλειδί", + "name": "Όνομα", + "value": "Τιμή" + }, + "config": { + "columns": { + "section": "Ενότητα" + }, + "title": "Ρυθμίσεις Airflow" + }, + "connections": { + "add": "Προσθήκη Σύνδεσης", + "columns": { + "connectionId": "ID Σύνδεσης", + "connectionType": "Τύπος Σύνδεσης", + "host": "Ξενιστής", + "port": "Θύρα" + }, + "connection_one": "Σύνδεση", + "connection_other": "Συνδέσεις", + "delete": { + "deleteConnection_one": "Διαγραφή 1 σύνδεσης", + "deleteConnection_other": "Διαγραφή {{count}} συνδέσεων", + "firstConfirmMessage_one": "Πρόκειται να διαγράψετε την ακόλουθη σύνδεση:", + "firstConfirmMessage_other": "Πρόκειται να διαγράψετε τις ακόλουθες συνδέσεις:", + "title": "Διαγραφή Σύνδεσης" + }, + "edit": "Επεξεργασία Σύνδεσης", + "form": { + "connectionIdRequired": "Το ID Σύνδεσης είναι υποχρεωτικό", + "connectionIdRequirement": "Το ID Σύνδεσης δεν μπορεί να περιέχει μόνο κενά", + "connectionTypeRequired": "Ο Τύπος Σύνδεσης είναι υποχρεωτικός", + "extraFields": "Επιπλέον Πεδία", + "extraFieldsJson": "Επιπλέον Πεδία JSON", + "helperText": "Λείπει ο Τύπος Σύνδεσης; Βεβαιωθείτε ότι έχετε εγκαταστήσει το ανάλογο πακέτο Airflow Παρόχων.", + "helperTextForRedactedFields": "Τα πεδία που έχουν αποκρυφθεί ('***') θα παραμείνουν αμετάβλητα αν δεν τροποποιηθούν.", + "selectConnectionType": "Επιλέξτε Τύπο Σύνδεσης", + "standardFields": "Βασικά Πεδία" + }, + "nothingFound": { + "description": "Οι συνδέσεις που ορίζονται μέσω μεταβλητών περιβάλλοντος ή διαχειριστών μυστικών δεν εμφανίζονται εδώ.", + "documentationLink": "Μάθετε περισσότερα στην τεκμηρίωση του Airflow.", + "learnMore": "Αυτές επιλύονται κατά το χρόνο εκτέλεσης και δεν είναι ορατές στο UI.", + "title": "Δεν βρέθηκε σύνδεση!" + }, + "searchPlaceholder": "Αναζήτηση Συνδέσεων", + "test": "Δοκιμή Σύνδεσης", + "testDisabled": "Η λειτουργία δοκιμής σύνδεσης είναι απενεργοποιημένη. Επικοινωνήστε με τον διαχειριστή για να την ενεργοποιήσει.", + "typeMeta": { + "error": "Αποτυχία ανάκτησης μεταδεδομένων τύπου σύνδεσης", + "standardFields": { + "description": "Περιγραφή", + "host": "Ξενιστής", + "login": "Όνομα Χρήστη", + "password": "Κωδικός", + "port": "Θύρα", + "url_schema": "Σχήμα" + } + } + }, + "deleteActions": { + "button": "Διαγραφή", + "modal": { + "confirmButton": "Ναι, Διαγραφή", + "secondConfirmMessage": "Αυτή η ενέργεια είναι μόνιμη και δεν μπορεί να αναιρεθεί.", + "thirdConfirmMessage": " Είστε σίγουροι ότι θέλετε να συνεχίσετε;" + }, + "selected": "Επιλεγμένα", + "tooltip": "Διαγραφή επιλεγμένων συνδέσεων" + }, + "formActions": { + "save": "Αποθήκευση" + }, + "plugins": { + "columns": { + "source": "Πηγή" + }, + "importError_one": "Σφάλμα Εισαγωγής Πρόσθετου", + "importError_other": "Σφάλματα Εισαγωγής Πρόσθετων", + "searchPlaceholder": "Αναζήτηση κατά αρχείο" + }, + "pools": { + "add": "Προσθήκη Pool", + "deferredSlotsIncluded": "Συμπεριλαμβάνονται Αναβληθέντα Slots", + "delete": { + "title": "Διαγραφή Pool", + "warning": "Αυτό θα αφαιρέσει όλα τα μεταδεδομένα που σχετίζονται με το pool και μπορεί να επηρεάσει εργασίες που το χρησιμοποιούν." + }, + "edit": "Επεξεργασία Pool", + "form": { + "checkbox": "Επιλέξτε για να συμπεριλάβετε αναβληθείσες εργασίες στον υπολογισμό των διαθέσιμων slots του pool", + "description": "Περιγραφή", + "includeDeferred": "Συμπερίληψη Αναβληθέντων", + "nameMaxLength": "Το όνομα μπορεί να περιέχει έως 256 χαρακτήρες", + "nameRequired": "Το όνομα είναι υποχρεωτικό", + "slots": "Slots" + }, + "noPoolsFound": "Δεν βρέθηκαν pools", + "pool_one": "Pool", + "pool_other": "Pools", + "searchPlaceholder": "Αναζήτηση Pools", + "sort": { + "asc": "Όνομα (A-Z)", + "desc": "Όνομα (Ω-Α)", + "placeholder": "Ταξινόμηση κατά" + } + }, + "providers": { + "columns": { + "packageName": "Όνομα Πακέτου", + "version": "Έκδοση" + } + }, + "variables": { + "add": "Προσθήκη Μεταβλητής", + "columns": { + "isEncrypted": "Είναι Κρυπτογραφημένη" + }, + "delete": { + "deleteVariable_one": "Διαγραφή 1 μεταβλητής", + "deleteVariable_other": "Διαγραφή {{count}} μεταβλητών", + "firstConfirmMessage_one": "Πρόκειται να διαγράψετε την ακόλουθη μεταβλητή:", + "firstConfirmMessage_other": "Πρόκειται να διαγράψετε τις ακόλουθες μεταβλητές:", + "title": "Διαγραφή Μεταβλητής", + "tooltip": "Διαγραφή επιλεγμένων μεταβλητών" + }, + "edit": "Επεξεργασία Μεταβλητής", + "export": "Εξαγωγή", + "exportTooltip": "Εξαγωγή επιλεγμένων μεταβλητών", + "form": { + "invalidJson": "Μη έγκυρο JSON", + "keyMaxLength": "Το κλειδί μπορεί να περιέχει έως 250 χαρακτήρες", + "keyRequired": "Το κλειδί είναι υποχρεωτικό", + "valueRequired": "Η τιμή είναι υποχρεωτική" + }, + "import": { + "button": "Εισαγωγή", + "conflictResolution": "Επιλέξτε Τρόπο Επίλυσης Συγκρούσεων Μεταβλητών", + "errorParsingJsonFile": "Σφάλμα Ανάλυσης Αρχείου JSON: Ανεβάστε ένα αρχείο JSON που περιέχει μεταβλητές (π.χ., {\"key\": \"value\", ...}).", + "options": { + "fail": { + "description": "Η εισαγωγή αποτυγχάνει αν εντοπιστούν υπάρχουσες μεταβλητές.", + "title": "Αποτυχία" + }, + "overwrite": { + "description": "Αντικαθιστά τη μεταβλητή σε περίπτωση σύγκρουσης.", + "title": "Αντικατάσταση" + }, + "skip": { + "description": "Παραλείπει την εισαγωγή μεταβλητών που υπάρχουν ήδη.", + "title": "Παράλειψη" + } + }, + "title": "Εισαγωγή Μεταβλητών", + "upload": "Ανέβασμα Αρχείου JSON", + "uploadPlaceholder": "Ανεβάστε ένα αρχείο JSON που περιέχει μεταβλητές (π.χ., {\"key\": \"value\", ...})" + }, + "noRowsMessage": "Δεν βρέθηκαν μεταβλητές", + "searchPlaceholder": "Αναζήτηση Κλειδιών", + "variable_one": "Μεταβλητή", + "variable_other": "Μεταβλητές" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/assets.json new file mode 100644 index 0000000000000..27d565581af85 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/assets.json @@ -0,0 +1,30 @@ +{ + "consumingDags": "Καταναλωτικά Dags", + "createEvent": { + "button": "Δημιουργία Συμβάντος", + "manual": { + "description": "Χειροκίνητη δημιουργία Συμβάντος Οντότητας", + "extra": "Επιπλέον Πληροφορίες Συμβάντος Οντότητας", + "label": "Χειροκίνητα" + }, + "materialize": { + "description": "Εκκίνηση του Dag ανάντη αυτής της οντότητας", + "descriptionWithDag": "Εκκίνηση του Dag ανάντη αυτής της οντότητας: {{dagName}}", + "label": "Υλοποίηση", + "unpauseDag": "Αναίρεση παύσης του {{dagName}} κατά την εκκίνηση" + }, + "success": { + "manualDescription": "Η χειροκίνητη δημιουργία συμβάντος οντότητας ήταν επιτυχής.", + "manualTitle": "Το Συμβάν Οντότητας Δημιουργήθηκε", + "materializeDescription": "Το ανάντη Dag {{dagId}} εκκινήθηκε με επιτυχία.", + "materializeTitle": "Υλοποίηση Οντότητας" + }, + "title": "Δημιουργία Συμβάντος Οντότητας για {{name}}" + }, + "group": "Ομάδα", + "lastAssetEvent": "Τελευταίο Συμβάν Οντότητας", + "name": "Όνομα", + "producingTasks": "Παραγωγή Εργασιών", + "scheduledDags": "Προγραμματισμένα Dags", + "searchPlaceholder": "Αναζήτηση Οντοτήτων" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/browse.json new file mode 100644 index 0000000000000..75c155eddb290 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/browse.json @@ -0,0 +1,22 @@ +{ + "auditLog": { + "columns": { + "event": "Συμβάν", + "extra": "Επιπλέον", + "user": "Χρήστης", + "when": "Πότε" + }, + "filters": { + "eventType": "Τύπος Συμβάντος" + }, + "title": "Καταγραφή Ελέγχου" + }, + "xcom": { + "columns": { + "dag": "Dag", + "key": "Κλειδί", + "value": "Τιμή" + }, + "title": "XCom" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json new file mode 100644 index 0000000000000..88e40898dc9f2 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json @@ -0,0 +1,318 @@ +{ + "admin": { + "Config": "Ρυθμίσεις", + "Connections": "Συνδέσεις", + "Plugins": "Πρόσθετα", + "Pools": "Pools", + "Providers": "Πάροχοι", + "Variables": "Μεταβλητές" + }, + "allOperators": "Όλοι οι Τελεστές", + "appearance": { + "appearance": "Εμφάνιση", + "darkMode": "Σκούρο Θέμα", + "lightMode": "Φωτεινό Θέμα", + "systemMode": "Σύμφωνα με το σύστημα" + }, + "asset_one": "Οντότητα", + "asset_other": "Οντότητες", + "assetEvent_one": "Συμβάν Οντότητας", + "assetEvent_other": "Συμβάντα Οντοτήτων", + "backfill_one": "Backfill", + "backfill_other": "Backfills", + "browse": { + "auditLog": "Καταγραφή Ελέγχου", + "requiredActions": "Απαιτούμενες Ενέργειες", + "xcoms": "XComs" + }, + "collapseAllExtra": "Σύμπτυξη όλων των επιπλέον json", + "collapseDetailsPanel": "Σύμπτυξη Πίνακα Λεπτομερειών", + "createdAssetEvent_one": "Δημιουργήθηκε Συμβάν Οντότητας", + "createdAssetEvent_other": "Δημιουργήθηκαν Συμβάντα Οντοτήτων", + "dag_one": "Dag", + "dag_other": "Dags", + "dagDetails": { + "catchup": "Catchup", + "dagRunTimeout": "Χρονικό Όριο Εκτέλεσης Dag", + "defaultArgs": "Προεπιλεγμένες Παράμετροι", + "description": "Περιγραφή", + "documentation": "Τεκμηρίωση Dag", + "fileLocation": "Τοποθεσία Αρχείου", + "hasTaskConcurrencyLimits": "Περιορισμοί ταυτόχρονων Εργασιών", + "lastExpired": "Τελευταία Λήξη", + "lastParseDuration": "Διάρκεια Τελευταίας Ανάλυσης", + "lastParsed": "Τελευταία Ανάλυση", + "latestDagVersion": "Τελευταία Έκδοση Dag", + "latestRun": "Τελευταία Εκτέλεση", + "maxActiveRuns": "Μέγιστες Ενεργές Εκτελέσεις", + "maxActiveTasks": "Μέγιστες Ενεργές Εργασίες", + "maxConsecutiveFailedDagRuns": "Μέγιστες Διαδοχικές Αποτυχημένες Εκτελέσεις Dag", + "nextRun": "Επόμενη Εκτέλεση", + "owner": "Ιδιοκτήτης", + "params": "Παράμετροι", + "schedule": "Πρόγραμμα", + "tags": "Ετικέτες" + }, + "dagId": "Dag ID", + "dagRun": { + "conf": "Ρυθμίσεις", + "dagVersions": "Έκδοση(εις) Dag", + "dataIntervalEnd": "Τέλος Διαστήματος Δεδομένων", + "dataIntervalStart": "Έναρξη Διαστήματος Δεδομένων", + "lastSchedulingDecision": "Τελευταία Απόφαση Προγραμματισμού", + "queuedAt": "Σε Ουρά Στις", + "runAfter": "Εκτέλεση Μετά", + "runType": "Τύπος Εκτέλεσης", + "sourceAssetEvent": "Συμβάν Πηγής Οντότητας", + "triggeredBy": "Ενεργοποιήθηκε Από", + "triggeringUser": "Όνομα Χρήστη Ενεργοποίησης" + }, + "dagRun_one": "Εκτέλεση Dag", + "dagRun_other": "Εκτελέσεις Dag", + "dagRunId": "ID Εκτέλεσης Dag", + "dagWarnings": "Προειδοποιήσεις/Σφάλματα Dag", + "defaultToGraphView": "Προεπιλογή σε γραφική προβολή", + "defaultToGridView": "Προεπιλογή σε πλέγμα", + "direction": "Κατεύθυνση", + "docs": { + "documentation": "Τεκμηρίωση", + "githubRepo": "Αποθετήριο GitHub", + "restApiReference": "REST API Αναφορά" + }, + "duration": "Διάρκεια", + "endDate": "Ημερομηνία Λήξης", + "error": { + "back": "Πίσω", + "defaultMessage": "Παρουσιάστηκε απρόσμενο σφάλμα", + "home": "Αρχική", + "notFound": "Η σελίδα δεν βρέθηκε", + "title": "Σφάλμα" + }, + "expand": { + "collapse": "Σύμπτυξη", + "expand": "Ανάπτυξη", + "hotkey": "e", + "tooltip": "Πατήστε {{hotkey}} για εναλλαγή ανάπτυξης" + }, + "expandAllExtra": "Ανάπτυξη όλων των επιπλέον json", + "expression": { + "all": "Όλα", + "and": "ΚΑΙ", + "any": "Οποιοδήποτε", + "or": "Ή" + }, + "filter": "Φίλτρο", + "filters": { + "logicalDateFrom": "Λογική Ημερομηνία Από", + "logicalDateTo": "Λογική Ημερομηνία Έως", + "runAfterFrom": "Εκτέλεση Μετά Από", + "runAfterTo": "Εκτέλεση Μετά Έως" + }, + "logicalDate": "Λογική Ημερομηνία", + "logout": "Αποσύνδεση", + "logoutConfirmation": "Πρόκειται να αποσυνδεθείτε από την εφαρμογή.", + "mapIndex": "Δείκτης Χάρτη", + "modal": { + "cancel": "Ακύρωση", + "confirm": "Επιβεβαίωση", + "delete": { + "button": "Διαγραφή", + "confirmation": "Είστε σίγουροι ότι θέλετε να διαγράψετε το {{resourceName}}; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί." + } + }, + "nav": { + "admin": "Διαχείριση", + "assets": "Οντότητες", + "browse": "Περιήγηση", + "dags": "Dags", + "docs": "Τεκμηρίωση", + "home": "Αρχική", + "legacyFabViews": "Παλιές Προβολές", + "plugins": "Πρόσθετα", + "security": "Ασφάλεια" + }, + "noItemsFound": "Δεν βρέθηκε {{modelName}}", + "note": { + "add": "Προσθήκη σημείωσης", + "dagRun": "Σημείωση Εκτέλεσης Dag", + "label": "Σημείωση", + "placeholder": "Προσθέστε μια σημείωση...", + "taskInstance": "Σημείωση Εκτέλεσης Εργασίας" + }, + "pools": { + "deferred": "Αναβληθέν", + "open": "Ανοιχτό", + "pools_one": "pool", + "pools_other": "pools", + "queued": "Σε Ουρά", + "running": "Εκτελείται", + "scheduled": "Προγραμματισμένο" + }, + "reset": "Επαναφορά", + "runId": "ID Εκτέλεσης", + "runTypes": { + "asset_triggered": "Ενεργοποιήθηκε από Οντότητα", + "backfill": "Backfill", + "manual": "Χειροκίνητο", + "scheduled": "Προγραμματισμένο" + }, + "scroll": { + "direction": { + "bottom": "κάτω", + "top": "πάνω" + }, + "tooltip": "Πατήστε {{hotkey}} για κύλιση προς τα {{direction}}" + }, + "security": { + "actions": "Ενέργειες", + "permissions": "Δικαιώματα", + "resources": "Πόροι", + "roles": "Ρόλοι", + "users": "Χρήστες" + }, + "selectLanguage": "Επιλογή Γλώσσας", + "showDetailsPanel": "Εμφάνιση Πίνακα Λεπτομερειών", + "source": { + "hide": "Απόκρυψη Πηγαίου Κώδικα", + "hotkey": "s", + "show": "Εμφάνιση Πηγαίου Κώδικα" + }, + "sourceAssetEvent_one": "Συμβάν Πηγής Οντότητας", + "sourceAssetEvent_other": "Συμβάντα Πηγής Οντοτήτων", + "startDate": "Ημερομηνία Έναρξης", + "state": "Κατάσταση", + "states": { + "deferred": "Αναβληθέν", + "failed": "Αποτυχία", + "no_status": "Χωρίς Κατάσταση", + "none": "Χωρίς Κατάσταση", + "planned": "Προγραμματισμένο", + "queued": "Σε Ουρά", + "removed": "Αφαιρέθηκε", + "restarting": "Επανεκκίνηση", + "running": "Εκτελείται", + "scheduled": "Προγραμματισμένο", + "skipped": "Παραλείφθηκε", + "success": "Επιτυχία", + "up_for_reschedule": "Προς Επαναπρογραμματισμό", + "up_for_retry": "Προς Επανάληψη", + "upstream_failed": "Αποτυχία Ανάντη" + }, + "table": { + "completedAt": "Ολοκληρώθηκε στις", + "createdAt": "Δημιουργήθηκε στις", + "filterByTag": "Φιλτράρισμα Dags κατά ετικέτα", + "filterColumns": "Φιλτράρισμα στηλών πίνακα", + "filterReset_one": "Επαναφορά φίλτρου", + "filterReset_other": "Επαναφορά φίλτρων", + "from": "Από", + "maxActiveRuns": "Μέγιστες Ενεργές Εκτελέσεις", + "noTagsFound": "Δεν βρέθηκαν ετικέτες", + "tagMode": { + "all": "Όλα", + "any": "Οποιοδήποτε" + }, + "tagPlaceholder": "Φιλτράρισμα κατά ετικέτα", + "to": "Έως" + }, + "task": { + "documentation": "Τεκμηρίωση Εργασίας", + "lastInstance": "Τελευταία Εκτέλεση", + "operator": "Τελεστής", + "triggerRule": "Κανόνας Ενεργοποίησης" + }, + "task_one": "Εργασία", + "task_other": "Εργασίες", + "taskGroup": "Ομάδα Εργασιών", + "taskId": "ID Εργασίας", + "taskInstance": { + "dagVersion": "Έκδοση Dag", + "executor": "Εκτελεστής", + "executorConfig": "Ρυθμίσεις Εκτελεστή", + "hostname": "Όνομα Υπολογιστή", + "maxTries": "Μέγιστες Προσπάθειες", + "pid": "PID", + "pool": "Pool", + "poolSlots": "Θέσεις Pool", + "priorityWeight": "Βάρος Προτεραιότητας", + "queue": "Ουρά", + "queuedWhen": "Σε Ουρά Στις", + "scheduledWhen": "Προγραμματίστηκε Στις", + "triggerer": { + "assigned": "Ανατεθειμένος triggerer", + "class": "Κλάση Trigger", + "createdAt": "Χρόνος δημιουργίας trigger", + "id": "ID Trigger", + "latestHeartbeat": "Τελευταίος παλμός triggerer", + "title": "Πληροφορίες Triggerer" + }, + "unixname": "Όνομα Unix" + }, + "taskInstance_one": "Εκτέλεση Εργασίας", + "taskInstance_other": "Εκτελέσεις Εργασίας", + "timeRange": { + "last12Hours": "Τελευταίες 12 Ώρες", + "last24Hours": "Τελευταίες 24 Ώρες", + "lastHour": "Τελευταία Ώρα", + "pastWeek": "Προηγούμενη Εβδομάδα" + }, + "timestamp": { + "hide": "Απόκρυψη Χρονικών Σημάνσεων", + "hotkey": "t", + "show": "Εμφάνιση Χρονικών Σημάνσεων" + }, + "timezone": "Ζώνη Ώρας", + "timezoneModal": { + "current-timezone": "Τρέχουσα ώρα σε", + "placeholder": "Επιλέξτε ζώνη ώρας", + "title": "Επιλογή Ζώνης Ώρας", + "utc": "UTC (Συντονισμένη Παγκόσμια Ώρα)" + }, + "toaster": { + "bulkDelete": { + "error": "Αποτυχία μαζικής διαγραφής {{resourceName}}", + "success": { + "description": "{{count}} {{resourceName}} διαγράφηκαν με επιτυχία. Κλειδιά: {{keys}}", + "title": "Υποβλήθηκε αίτημα μαζικής διαγραφής {{resourceName}}" + } + }, + "create": { + "error": "Αποτυχία δημιουργίας {{resourceName}}", + "success": { + "description": "Το {{resourceName}} δημιουργήθηκε με επιτυχία.", + "title": "Υποβλήθηκε αίτημα δημιουργίας {{resourceName}}" + } + }, + "delete": { + "error": "Αποτυχία διαγραφής {{resourceName}}", + "success": { + "description": "Το {{resourceName}} διαγράφηκε με επιτυχία.", + "title": "Υποβλήθηκε αίτημα διαγραφής {{resourceName}}" + } + }, + "import": { + "error": "Αποτυχία εισαγωγής {{resourceName}}", + "success": { + "description": "{{count}} {{resourceName}} εισήχθησαν με επιτυχία.", + "title": "Υποβλήθηκε αίτημα εισαγωγής {{resourceName}}" + } + }, + "update": { + "error": "Αποτυχία ενημέρωσης {{resourceName}}", + "success": { + "description": "Το {{resourceName}} ενημερώθηκε με επιτυχία.", + "title": "Υποβλήθηκε αίτημα ενημέρωσης {{resourceName}}" + } + } + }, + "total": "Σύνολο {{state}}", + "triggered": "Ενεργοποιήθηκε", + "tryNumber": "Αριθμός Προσπάθειας", + "user": "Χρήστης", + "wrap": { + "hotkey": "w", + "tooltip": "Πατήστε {{hotkey}} για εναλλαγή αναδίπλωσης", + "unwrap": "Ξεδίπλωμα", + "wrap": "Αναδίπλωση" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/components.json new file mode 100644 index 0000000000000..36399b813fdfc --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/components.json @@ -0,0 +1,134 @@ +{ + "backfill": { + "affected_one": "1 εκτέλεση θα ενεργοποιηθεί.", + "affected_other": "{{count}} εκτελέσεις θα ενεργοποιηθούν.", + "affectedNone": "Δεν βρέθηκαν εκτελέσεις που να ταιριάζουν με τα κριτήρια.", + "allRuns": "Όλες οι Εκτελέσεις", + "backwards": "Αντίστροφη Εκτέλεση", + "dateRange": "Εύρος Ημερομηνιών", + "errorStartDateBeforeEndDate": "Η ημερομηνία έναρξης πρέπει να είναι πριν την ημερομηνία λήξης", + "maxRuns": "Μέγιστες Ενεργές Εκτελέσεις", + "missingAndErroredRuns": "Εκτελέσεις που λείπουν ή απέτυχαν", + "missingRuns": "Εκτελέσεις που λείπουν", + "reprocessBehavior": "Συμπεριφορά Επανεπεξεργασίας", + "run": "Εκτέλεση Backfill", + "selectDescription": "Εκτελέστε αυτό το Dag για ένα εύρος ημερομηνιών", + "selectLabel": "Backfill", + "title": "Εκτέλεση Backfill", + "toaster": { + "success": { + "description": "Οι εργασίες backfill ενεργοποιήθηκαν με επιτυχία.", + "title": "Το Backfill δημιουργήθηκε" + } + }, + "tooltip": "Το Backfill απαιτεί πρόγραμμα", + "unpause": "Αναίρεση παύσης του {{dag_display_name}} κατά την ενεργοποίηση", + "validation": { + "datesRequired": "Πρέπει να δοθούν και η ημερομηνία έναρξης και λήξης του διαστήματος δεδομένων.", + "startBeforeEnd": "Η ημερομηνία έναρξης πρέπει να είναι μικρότερη ή ίση με την ημερομηνία λήξης." + } + }, + "banner": { + "backfillInProgress": "Το Backfill είναι σε εξέλιξη", + "cancel": "Ακύρωση backfill", + "pause": "Παύση backfill", + "unpause": "Αναίρεση παύσης backfill" + }, + "clipboard": { + "copy": "Αντιγραφή" + }, + "close": "Κλείσιμο", + "configForm": { + "advancedOptions": "Προχωρημένες Επιλογές", + "configJson": "Ρυθμίσεις JSON", + "invalidJson": "Μη έγκυρη μορφή JSON: {{errorMessage}}" + }, + "dagWarnings": { + "error_one": "1 Σφάλμα", + "error_other": "{{count}} Σφάλματα", + "errorAndWarning": "1 Σφάλμα και {{warningText}}", + "warning_one": "1 Προειδοποίηση", + "warning_other": "{{count}} Προειδοποιήσεις" + }, + "durationChart": { + "duration": "Διάρκεια (δευτερόλεπτα)", + "lastDagRun_one": "Τελευταία Εκτέλεση Dag", + "lastDagRun_other": "Τελευταίες {{count}} Εκτελέσεις Dag", + "lastTaskInstance_one": "Τελευταία Εκτέλεση Εργασίας", + "lastTaskInstance_other": "Τελευταίες {{count}} Εκτελέσεις Εργασίας", + "queuedDuration": "Διάρκεια σε Ουρά", + "runAfter": "Εκτέλεση Μετά", + "runDuration": "Διάρκεια Εκτέλεσης" + }, + "fileUpload": { + "files_one": "{{count}} αρχείο", + "files_other": "{{count}} αρχεία" + }, + "flexibleForm": { + "placeholder": "Επιλέξτε Τιμή", + "placeholderArray": "Εισάγετε κάθε συμβολοσειρά σε νέα γραμμή", + "placeholderExamples": "Ξεκινήστε να πληκτρολογείτε για να δείτε επιλογές", + "placeholderMulti": "Επιλέξτε μία ή περισσότερες τιμές", + "validationErrorArrayNotArray": "Η τιμή πρέπει να είναι πίνακας.", + "validationErrorArrayNotNumbers": "Όλα τα στοιχεία του πίνακα πρέπει να είναι αριθμοί.", + "validationErrorArrayNotObject": "Όλα τα στοιχεία του πίνακα πρέπει να είναι αντικείμενα.", + "validationErrorRequired": "Αυτό το πεδίο είναι υποχρεωτικό" + }, + "graph": { + "directionDown": "Από Πάνω προς Κάτω", + "directionLeft": "Από Δεξιά προς Αριστερά", + "directionRight": "Από Αριστερά προς Δεξιά", + "directionUp": "Από Κάτω προς Πάνω", + "downloadImage": "Λήψη εικόνας γραφήματος", + "downloadImageError": "Αποτυχία λήψης εικόνας γραφήματος.", + "downloadImageErrorTitle": "Αποτυχία Λήψης", + "otherDagRuns": "+Άλλες Εκτελέσεις Dag", + "taskCount_one": "{{count}} Εργασία", + "taskCount_other": "{{count}} Εργασίες", + "taskGroup": "Ομάδα Εργασιών" + }, + "limitedList": "+{{count}} ακόμα", + "logs": { + "file": "Αρχείο", + "location": "γραμμή {{line}} στο {{name}}" + }, + "reparseDag": "Επαναανάλυση Dag", + "sortedAscending": "αύξουσα ταξινόμηση", + "sortedDescending": "φθίνουσα ταξινόμηση", + "sortedUnsorted": "μη ταξινομημένο", + "taskTries": "Προσπάθειες Εργασίας", + "toggleCardView": "Εμφάνιση κάρτας", + "toggleTableView": "Εμφάνιση πίνακα", + "triggerDag": { + "button": "Ενεργοποίηση", + "loading": "Φόρτωση πληροφοριών Dag...", + "loadingFailed": "Αποτυχία φόρτωσης πληροφοριών Dag. Παρακαλώ δοκιμάστε ξανά.", + "runIdHelp": "Προαιρετικό - θα δημιουργηθεί αν δεν δοθεί", + "selectDescription": "Ενεργοποιήστε μία εκτέλεση αυτού του Dag", + "selectLabel": "Μονή Εκτέλεση", + "title": "Ενεργοποίηση Dag", + "toaster": { + "success": { + "description": "Η εκτέλεση Dag ενεργοποιήθηκε με επιτυχία.", + "title": "Η Εκτέλεση Dag Ενεργοποιήθηκε" + } + }, + "unpause": "Αναίρεση παύσης του {{dagDisplayName}} κατά την ενεργοποίηση" + }, + "trimText": { + "details": "Λεπτομέρειες", + "empty": "Κενό", + "noContent": "Δεν υπάρχουν διαθέσιμα δεδομένα." + }, + "versionDetails": { + "bundleLink": "Σύνδεσμος Πακέτου", + "bundleName": "Όνομα Πακέτου", + "bundleVersion": "Έκδοση Πακέτου", + "createdAt": "Δημιουργήθηκε στις", + "versionId": "ID Έκδοσης" + }, + "versionSelect": { + "dagVersion": "Έκδοση Dag", + "versionCode": "v{{versionCode}}" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json new file mode 100644 index 0000000000000..12f472a19473b --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json @@ -0,0 +1,154 @@ +{ + "allRuns": "Όλες οι Εκτελέσεις", + "blockingDeps": { + "dependency": "Εξάρτηση", + "reason": "Αιτία", + "title": "Εξαρτήσεις που εμποδίζουν τον προγραμματισμό της εργασίας" + }, + "calendar": { + "daily": "Καθημερινά", + "hourly": "Ωριαία", + "legend": { + "less": "Λιγότερο", + "mixed": "Μικτό", + "more": "Περισσότερο" + }, + "navigation": { + "nextMonth": "Επόμενος μήνας", + "nextYear": "Επόμενο έτος", + "previousMonth": "Προηγούμενος μήνας", + "previousYear": "Προηγούμενο έτος" + }, + "noData": "Δεν υπάρχουν διαθέσιμα δεδομένα", + "noFailedRuns": "No failed runs", + "noRuns": "Δεν υπάρχουν εκτελέσεις", + "totalRuns": "Σύνολο Εκτελέσεων", + "week": "Εβδομάδα {{weekNumber}}", + "weekdays": { + "friday": "Παρ", + "monday": "Δευ", + "saturday": "Σαβ", + "sunday": "Κυρ", + "thursday": "Πεμ", + "tuesday": "Τρι", + "wednesday": "Τετ" + } + }, + "code": { + "bundleUrl": "Σύνδεσμος Πακέτου", + "noCode": "Δεν βρέθηκε Κώδικας", + "parseDuration": "Διάρκεια Ανάλυσης:", + "parsedAt": "Αναλύθηκε στις:" + }, + "extraLinks": "Επιπλέον Σύνδεσμοι", + "grid": { + "buttons": { + "resetToLatest": "Επαναφορά στα πιο πρόσφατα", + "toggleGroup": "Εναλλαγή ομάδας" + } + }, + "header": { + "buttons": { + "advanced": "Για προχωρημένους", + "dagDocs": "Τεκμήρια Dag" + } + }, + "logs": { + "allLevels": "Όλα τα Επίπεδα Καταγραφής", + "allSources": "Όλες οι Πηγές", + "critical": "ΚΡΙΣΙΜΟ", + "debug": "DEBUG", + "error": "ΣΦΑΛΜΑ", + "fullscreen": { + "button": "Πλήρης οθόνη", + "tooltip": "Πατήστε {{hotkey}} για πλήρη οθόνη" + }, + "info": "INFO", + "noTryNumber": "Χωρίς αριθμό προσπάθειας", + "settings": "Ρυθμίσεις Καταγραφής", + "viewInExternal": "Προβολή καταγραφών στο {{name}} (προσπάθεια {{attempt}})", + "warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ" + }, + "navigation": { + "navigation": "Πλοήγηση: Shift+{{arrow}}", + "toggleGroup": "Εναλλαγή ομάδας: Space" + }, + "overview": { + "buttons": { + "failedRun_one": "Αποτυχημένη Εκτέλεση", + "failedRun_other": "Αποτυχημένες Εκτελέσεις", + "failedTask_one": "Αποτυχημένη Εργασία", + "failedTask_other": "Αποτυχημένες Εργασίες", + "failedTaskInstance_one": "Αποτυχημένη Στιγμιαία Κατάσταση Εργασίας", + "failedTaskInstance_other": "Αποτυχημένες Στιγμιαίες Καταστάσεις Εργασίας" + }, + "charts": { + "assetEvent_one": "Δημιουργήθηκε Συμβάν Οντότητας", + "assetEvent_other": "Δημιουργήθηκαν Συμβάντα Οντοτήτων" + }, + "failedLogs": { + "hideLogs": "Απόκρυψη Καταγραφών", + "showLogs": "Εμφάνιση Καταγραφών", + "title": "Πρόσφατες Καταγραφές Αποτυχημένων Εργασιών", + "viewFullLogs": "Προβολή πλήρων καταγραφών" + } + }, + "panel": { + "buttons": { + "options": "Επιλογές", + "showGantt": "Εμφάνιση Gantt", + "showGraphShortcut": "Εμφάνιση Γραφήματος (Πατήστε g)", + "showGridShortcut": "Εμφάνιση Πλέγματος (Πατήστε g)" + }, + "dagRuns": { + "label": "Αριθμός Εκτελέσεων Dag" + }, + "dependencies": { + "label": "Εξαρτήσεις", + "options": { + "allDagDependencies": "Όλες οι Εξαρτήσεις Dag", + "externalConditions": "Εξωτερικές συνθήκες", + "onlyTasks": "Μόνο εργασίες" + }, + "placeholder": "Εξαρτήσεις" + }, + "graphDirection": { + "label": "Κατεύθυνση Γραφήματος" + } + }, + "paramsFailed": "Αποτυχία φόρτωσης παραμέτρων", + "parse": { + "toaster": { + "error": { + "description": "Το αίτημα ανάλυσης Dag απέτυχε. Ενδέχεται να υπάρχουν εκκρεμή αιτήματα ανάλυσης προς επεξεργασία.", + "title": "Αποτυχία Επανανάλυσης Dag" + }, + "success": { + "description": "Το Dag θα επαναναλυθεί σύντομα.", + "title": "Το αίτημα επανανάλυσης υποβλήθηκε επιτυχώς" + } + } + }, + "tabs": { + "assetEvents": "Συμβάντα Οντοτήτων", + "auditLog": "Καταγραφή Ελέγχου", + "backfills": "Backfills", + "calendar": "Ημερολόγιο", + "code": "Κώδικας", + "details": "Λεπτομέρειες", + "logs": "Καταγραφές", + "mappedTaskInstances_one": "Στιγμιαία Κατάσταση Εργασίας [{{count}}]", + "mappedTaskInstances_other": "Στιγμιαίες Καταστάσεις Εργασίας [{{count}}]", + "overview": "Επισκόπηση", + "renderedTemplates": "Αποδοθέντα Πρότυπα", + "requiredActions": "Απαιτούμενες Ενέργειες", + "runs": "Εκτελέσεις", + "taskInstances": "Στιγμιαίες Καταστάσεις Εργασίας", + "tasks": "Εργασίες", + "xcom": "XCom" + }, + "taskGroups": { + "collapseAll": "Σύμπτυξη όλων των ομάδων εργασιών", + "expandAll": "Ανάπτυξη όλων των ομάδων εργασιών" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/dags.json new file mode 100644 index 0000000000000..7b127ac0af95d --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/dags.json @@ -0,0 +1,96 @@ +{ + "assetSchedule": "{{count}} από {{total}} οντότητες ενημερώθηκαν", + "dagActions": { + "delete": { + "button": "Διαγραφή Dag", + "warning": "Αυτό θα αφαιρέσει όλα τα μεταδεδομένα που σχετίζονται με το Dag, συμπεριλαμβανομένων των Εκτελέσεων και των Εργασιών." + } + }, + "favoriteDag": "Αγαπημένο Dag", + "filters": { + "allRunTypes": "Όλοι οι τύποι Εκτελέσεων", + "allStates": "Όλες οι καταστάσεις", + "favorite": { + "all": "Όλα", + "favorite": "Αγαπημένο", + "unfavorite": "Μη αγαπημένα" + }, + "paused": { + "active": "Ενεργό", + "all": "Όλα", + "paused": "Σε παύση" + }, + "runIdPatternFilter": "Αναζήτηση Εκτελέσεων Dag" + }, + "ownerLink": "Σύνδεσμος κατόχου για {{owner}}", + "runAndTaskActions": { + "affectedTasks": { + "noItemsFound": "Δεν βρέθηκαν εργασίες.", + "title": "Επηρεαζόμενες Εργασίες: {{count}}" + }, + "clear": { + "button": "Εκκαθάριση {{type}}", + "buttonTooltip": "Πατήστε shift+c για εκκαθάριση", + "error": "Αποτυχία εκκαθάρισης {{type}}", + "title": "Εκκαθάριση {{type}}" + }, + "delete": { + "button": "Διαγραφή {{type}}", + "dialog": { + "resourceName": "{{type}} {{id}}", + "title": "Διαγραφή {{type}}", + "warning": "Αυτό θα αφαιρέσει όλα τα μεταδεδομένα που σχετίζονται με το {{type}}." + }, + "error": "Σφάλμα κατά τη διαγραφή του {{type}}", + "success": { + "description": "Το αίτημα διαγραφής του {{type}} ολοκληρώθηκε με επιτυχία.", + "title": "Το {{type}} διαγράφηκε με επιτυχία" + } + }, + "markAs": { + "button": "Σήμανση {{type}} ως...", + "buttonTooltip": { + "failed": "Πατήστε shift+f για σήμανση ως αποτυχία", + "success": "Πατήστε shift+s για σήμανση ως επιτυχία" + }, + "title": "Σήμανση {{type}} ως {{state}}" + }, + "options": { + "downstream": "Kάθοδος", + "existingTasks": "Εκκαθάριση υπαρχουσών εργασιών", + "future": "Μελλοντικές", + "onlyFailed": "Εκκαθάριση μόνο αποτυχημένων εργασιών", + "past": "Παρελθόν", + "queueNew": "Προσθήκη νέων εργασιών στην ουρά", + "runOnLatestVersion": "Εκτέλεση με την τελευταία έκδοση του bundle", + "upstream": "Άνοδος" + } + }, + "search": { + "advanced": "Σύνθετη Αναζήτηση", + "clear": "Εκκαθάριση αναζήτησης", + "dags": "Αναζήτηση Dags", + "hotkey": "+K", + "tasks": "Αναζήτηση Εργασιών" + }, + "sort": { + "displayName": { + "asc": "Ταξινόμηση κατά Όνομα (A-Z)", + "desc": "Ταξινόμηση κατά Όνομα (Z-A)" + }, + "lastRunStartDate": { + "asc": "Ταξινόμηση κατά Ημερομηνία Έναρξης Τελευταίας Εκτέλεσης (Παλαιότερο-Νεότερο)", + "desc": "Ταξινόμηση κατά Ημερομηνία Έναρξης Τελευταίας Εκτέλεσης (Νεότερο-Παλαιότερο)" + }, + "lastRunState": { + "asc": "Ταξινόμηση κατά Κατάσταση Τελευταίας Εκτέλεσης (A-Z)", + "desc": "Ταξινόμηση κατά Κατάσταση Τελευταίας Εκτέλεσης (Z-A)" + }, + "nextDagRun": { + "asc": "Ταξινόμηση κατά Επόμενη Εκτέλεση Dag (Παλαιότερο-Νεότερο)", + "desc": "Ταξινόμηση κατά Επόμενη Εκτέλεση Dag (Νεότερο-Παλαιότερο)" + }, + "placeholder": "Ταξινόμηση κατά" + }, + "unfavoriteDag": "Αφαίρεση από τα αγαπημένα Dag" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/dashboard.json new file mode 100644 index 0000000000000..1ff8e42424699 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/dashboard.json @@ -0,0 +1,45 @@ +{ + "favorite": { + "favoriteDags_one": "Πρώτο αγαπημένο Dag ({{count}})", + "favoriteDags_other": "Πρώτα αγαπημένα Dag ({{count}})", + "noDagRuns": "Δεν υπάρχει ακόμη DagRun για αυτό το dag.", + "noFavoriteDags": "Δεν υπάρχουν αγαπημένα ακόμα. Κάντε κλικ στο εικονίδιο αστεριού δίπλα σε ένα Dag στη λίστα για να το προσθέσετε στα αγαπημένα σας." + }, + "group": "Ομάδα", + "health": { + "dagProcessor": "Επεξεργαστής Dag", + "health": "Υγεία", + "healthy": "Υγιές", + "lastHeartbeat": "Τελευταίος Παλμός", + "metaDatabase": "Μετα-Βάση Δεδομένων", + "scheduler": "Προγραμματιστής", + "status": "Κατάσταση", + "triggerer": "Ενεργοποιητής", + "unhealthy": "Μη υγιές" + }, + "history": "Ιστορικό", + "importErrors": { + "dagImportError_one": "Σφάλμα Εισαγωγής Dag", + "dagImportError_other": "Σφάλματα Εισαγωγής Dag", + "searchByFile": "Αναζήτηση κατά αρχείο", + "timestamp": "Χρονοσήμανση" + }, + "managePools": "Διαχείριση Pools", + "noAssetEvents": "Δεν βρέθηκαν Συμβάντα Οντοτήτων.", + "poolSlots": "Θέσεις Pool", + "sortBy": { + "newestFirst": "Νεότερα Πρώτα", + "oldestFirst": "Παλαιότερα Πρώτα" + }, + "source": "Πηγή", + "stats": { + "activeDags": "Ενεργά Dag", + "failedDags": "Αποτυχημένα Dag", + "queuedDags": "Dag σε Ουρά", + "requiredActions": "Απαιτούμενες Ενέργειες", + "runningDags": "Εκτελούμενα Dag", + "stats": "Στατιστικά" + }, + "uri": "URI", + "welcome": "Καλώς ήρθατε" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/hitl.json new file mode 100644 index 0000000000000..d93ea3c216fe8 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/hitl.json @@ -0,0 +1,34 @@ +{ + "filters": { + "response": { + "all": "Όλα", + "pending": "Σε εκκρεμότητα", + "received": "Εξετασμένα" + } + }, + "requiredAction_one": "Απαιτούμενη Ενέργεια", + "requiredAction_other": "Απαιτούμενες Ενέργειες", + "requiredActionCount_one": "Απαιτούμενη Ενέργεια ({{count}})", + "requiredActionCount_other": "Απαιτούμενες Ενέργειες ({{count}})", + "requiredActionState": "Κατάσταση Απαιτούμενης Ενέργειας", + "response": { + "error": "Αποτυχημένη απάντηση", + "optionsDescription": "Επιλέξτε τις επιλογές σας για αυτήν την εκτέλεση εργασίας", + "optionsLabel": "Επιλογές", + "received": "Η απάντηση ελήφθη στις ", + "respond": "Απάντηση", + "success": "Η απάντηση για το {{taskId}} ήταν επιτυχής", + "title": "Εκτέλεση Εργασίας με Παρέμβαση Χρήστη - {{taskId}}" + }, + "state": { + "approvalReceived": "Έγκριση Ελήφθη", + "approvalRequired": "Απαιτείται Έγκριση", + "choiceReceived": "Επιλογή Ελήφθη", + "choiceRequired": "Απαιτείται Επιλογή", + "noResponseReceived": "Δεν Ελήφθη Απάντηση", + "rejectionReceived": "Απόρριψη Ελήφθη", + "responseReceived": "Απάντηση Ελήφθη", + "responseRequired": "Απαιτείται Απάντηση" + }, + "subject": "Θέμα" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/el/tasks.json new file mode 100644 index 0000000000000..8067fd67ad5a3 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "Χαρτογραφημένο", + "notMapped": "Μη χαρτογραφημένο", + "retries": "Επαναλήψεις", + "searchTasks": "Αναζήτηση εργασιών", + "selectMapped": "Επιλογή χαρτογραφημένων", + "selectOperator": "Επιλογή τελεστή", + "selectRetryValues": "Επιλογή τιμών επανάληψης", + "selectTriggerRules": "Επιλογή κανόνων ενεργοποίησης" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json index 0eb5df5b8a22d..82fb302b34393 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json @@ -49,6 +49,12 @@ "searchPlaceholder": "Search Connections", "test": "Test Connection", "testDisabled": "Test connection feature is disabled. Please contact an administrator to enable it.", + "testError": { + "title": "Test Connection Failed" + }, + "testSuccess": { + "title": "Test Connection Successful" + }, "typeMeta": { "error": "Failed to retrieve Connection Type Meta", "standardFields": { @@ -72,7 +78,6 @@ "tooltip": "Delete selected connections" }, "formActions": { - "reset": "Reset", "save": "Save" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json index f9af5aa8e5da8..1a0bc7bf86b01 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "Consuming Dags", + "consumingTasks": "Consuming Tasks", "createEvent": { "button": "Create Event", "manual": { @@ -21,6 +22,7 @@ }, "title": "Create Asset Event for {{name}}" }, + "extra": "Extra", "group": "Group", "lastAssetEvent": "Last Asset Event", "name": "Name", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json index 07259fd1e5825..235c3ef4aa2a0 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Collapse all extra json", - "expandAllExtra": "Expand all extra json" - }, "columns": { "event": "Event", "extra": "Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index defe64cce63c1..0a59f20347358 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -25,6 +25,7 @@ "requiredActions": "Required Actions", "xcoms": "XComs" }, + "collapseAllExtra": "Collapse all extra JSON", "collapseDetailsPanel": "Collapse Details Panel", "createdAssetEvent_one": "Created Asset Event", "createdAssetEvent_other": "Created Asset Events", @@ -78,12 +79,18 @@ "githubRepo": "GitHub Repo", "restApiReference": "REST API Reference" }, + "download": { + "download": "Download", + "hotkey": "d", + "tooltip": "Press {{hotkey}} to download logs" + }, "duration": "Duration", "endDate": "End Date", "error": { "back": "Back", "defaultMessage": "An unexpected error occurred", "home": "Home", + "invalidUrl": "Page Not Found. Please check the URL and try again.", "notFound": "Page Not Found", "title": "Error" }, @@ -93,23 +100,19 @@ "hotkey": "e", "tooltip": "Press {{hotkey}} to toggle expand" }, + "expandAllExtra": "Expand all extra JSON", "expression": { "all": "All", "and": "AND", "any": "Any", "or": "OR" }, + "filter": "Filter", "filters": { - "dagDisplayNamePlaceholder": "Filter by Dag", - "keyPlaceholder": "Filter by XCom key", - "logicalDateFromPlaceholder": "Logical Date From", - "logicalDateToPlaceholder": "Logical Date To", - "mapIndexPlaceholder": "Filter by Map Index", - "runAfterFromPlaceholder": "Run After From", - "runAfterToPlaceholder": "Run After To", - "runIdPlaceholder": "Filter by Run ID", - "taskIdPlaceholder": "Filter by Task ID", - "triggeringUserPlaceholder": "Filter by triggering user" + "logicalDateFrom": "Logical Date From", + "logicalDateTo": "Logical Date To", + "runAfterFrom": "Run After From", + "runAfterTo": "Run After To" }, "logicalDate": "Logical Date", "logout": "Logout", @@ -142,15 +145,7 @@ "placeholder": "Add a note...", "taskInstance": "Task Instance Note" }, - "pools": { - "deferred": "Deferred", - "open": "Open", - "pools_one": "pool", - "pools_other": "pools", - "queued": "Queued", - "running": "Running", - "scheduled": "Scheduled" - }, + "reset": "Reset", "runId": "Run ID", "runTypes": { "asset_triggered": "Asset Triggered", @@ -165,7 +160,6 @@ }, "tooltip": "Press {{hotkey}} to scroll to {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Actions", "permissions": "Permissions", @@ -189,6 +183,7 @@ "failed": "Failed", "no_status": "No Status", "none": "No Status", + "open": "Open", "planned": "Planned", "queued": "Queued", "removed": "Removed", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json index 3a56657187a36..8da7c64929968 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json @@ -6,12 +6,11 @@ "allRuns": "All Runs", "backwards": "Run Backwards", "dateRange": "Date Range", - "dateRangeFrom": "From", - "dateRangeTo": "To", "errorStartDateBeforeEndDate": "Start Date must be before the End Date", "maxRuns": "Max Active Runs", "missingAndErroredRuns": "Missing and Errored Runs", "missingRuns": "Missing Runs", + "permissionDenied": "Dry Run Failed: User does not have permission to create backfills.", "reprocessBehavior": "Reprocess Behavior", "run": "Run Backfill", "selectDescription": "Run this Dag for a range of dates", @@ -88,6 +87,14 @@ "taskGroup": "Task Group" }, "limitedList": "+{{count}} more", + "limitedList.allItems": "All {{count}} items:", + "limitedList.allTags_one": "All Tags (1)", + "limitedList.allTags_other": "All Tags ({{count}})", + "limitedList.clickToInteract": "Click a tag to filter Dags", + "limitedList.clickToOpenFull": "Click \"+{{count}} more\" to open full view", + "limitedList.copyPasteText": "You can copy and paste the text above", + "limitedList.showingItems_one": "Showing 1 item", + "limitedList.showingItems_other": "Showing {{count}} items", "logs": { "file": "File", "location": "line {{line}} in {{name}}" @@ -108,6 +115,9 @@ "selectLabel": "Single Run", "title": "Trigger Dag", "toaster": { + "error": { + "title": "Failed to Trigger DAG" + }, "success": { "description": "Dag run has been successfully triggered.", "title": "Dag Run Triggered" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json index fd1eef88f9518..8085fcb942d81 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json @@ -10,6 +10,7 @@ "hourly": "Hourly", "legend": { "less": "Less", + "mixed": "Mixed", "more": "More" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Previous year" }, "noData": "No data available", + "noFailedRuns": "No failed runs", "noRuns": "No runs", "totalRuns": "Total Runs", "week": "Week {{weekNumber}}", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/admin.json index 48e33e4fb28c3..92b0a632dad9b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/admin.json @@ -75,7 +75,6 @@ "tooltip": "Eliminar conexiones seleccionadas" }, "formActions": { - "reset": "Restablecer", "save": "Guardar" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/browse.json index b967b5b4ab6bd..5e117af466d12 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Colapsar todos los extra json", - "expandAllExtra": "Expandir todos los extra json" - }, "columns": { "event": "Evento", "extra": "Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json index 4c6784f9ef614..e9b901a401164 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json @@ -28,6 +28,7 @@ "requiredActions": "Acciones Requeridas", "xcoms": "XComs" }, + "collapseAllExtra": "Colapsar todos los extra json", "collapseDetailsPanel": "Colapsar Detalles del Panel", "createdAssetEvent_many": "Eventos de Asset Creados", "createdAssetEvent_one": "Evento de Asset Creado", @@ -44,6 +45,7 @@ "fileLocation": "Ubicación del Archivo", "hasTaskConcurrencyLimits": "Tiene límites de concurrencia de áreas", "lastExpired": "Último Expirado", + "lastParseDuration": "Duración del último parseo", "lastParsed": "Último Parseado", "latestDagVersion": "Última Versión del Dag", "latestRun": "Última Ejecución", @@ -83,6 +85,11 @@ "githubRepo": "Repositorio de GitHub", "restApiReference": "Referencia de REST API" }, + "download": { + "download": "Descargar", + "hotkey": "d", + "tooltip": "Presiona {{hotkey}} para descargar los registros" + }, "duration": "Duración", "endDate": "Fecha Final", "error": { @@ -98,22 +105,21 @@ "hotkey": "e", "tooltip": "Presiona {{hotkey}} para alternar expandir" }, + "expandAllExtra": "Expandir todos los extra json", "expression": { "all": "Todos", "and": "Y", "any": "Cualquiera", "or": "O" }, + "filter": "Filtro", "filters": { - "dagDisplayNamePlaceholder": "Filtrar por Dag", - "keyPlaceholder": "Filtrar por Clave de XCom", - "logicalDateFromPlaceholder": "Fecha Lógica Desde", - "logicalDateToPlaceholder": "Fecha Lógica Hasta", - "mapIndexPlaceholder": "Filtrar por Índice de Mapa", - "runAfterFromPlaceholder": "Ejecutar Después Desde", - "runAfterToPlaceholder": "Ejecutar Después Hasta", - "runIdPlaceholder": "Filtrar por ID de Ejecución", - "taskIdPlaceholder": "Filtrar por ID de Tarea" + "durationFrom": "Duración Desde", + "durationTo": "Duración Hasta", + "logicalDateFrom": "Fecha Lógica Desde", + "logicalDateTo": "Fecha Lógica Hasta", + "runAfterFrom": "Ejecutar Después Desde", + "runAfterTo": "Ejecutar Después Hasta" }, "logicalDate": "Fecha Lógica", "logout": "Cerrar Sesión", @@ -156,7 +162,8 @@ "running": "En Ejecución", "scheduled": "Programado" }, - "runId": "ID de la corrida", + "reset": "Restablecer", + "runId": "ID de la ejecución", "runTypes": { "asset_triggered": "Asset Activado", "backfill": "Backfill", @@ -170,7 +177,6 @@ }, "tooltip": "Presiona {{hotkey}} para desplazarte a {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Acciones", "permissions": "Permisos", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json index 52d40f4b79f84..89d241bc4479e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/components.json @@ -7,8 +7,6 @@ "allRuns": "Todas las Ejecuciones", "backwards": "Ejecutar Hacia Atrás", "dateRange": "Rango de Fechas", - "dateRangeFrom": "Desde", - "dateRangeTo": "Hasta", "errorStartDateBeforeEndDate": "La Fecha Inicial debe ser antes de la Fecha Final", "maxRuns": "Máximo de Ejecuciones Activas", "missingAndErroredRuns": "Ejecutaciones Faltantes y con Errores", @@ -97,6 +95,16 @@ "taskGroup": "Grupo de Tareas" }, "limitedList": "+{{count}} más", + "limitedList.allItems": "Todos los {{count}} elementos:", + "limitedList.allTags_many": "Todas las etiquetas ({{count}})", + "limitedList.allTags_one": "Todas las etiquetas (1)", + "limitedList.allTags_other": "Todas las etiquetas ({{count}})", + "limitedList.clickToInteract": "Haz clic en una etiqueta para filtrar Dags", + "limitedList.clickToOpenFull": "Haz clic en \"+{{count}} más\" para ver la lista completa", + "limitedList.copyPasteText": "Puedes copiar y pegar el texto de arriba", + "limitedList.showingItems_many": "Mostrando {{count}} elementos", + "limitedList.showingItems_one": "Mostrando 1 elemento", + "limitedList.showingItems_other": "Mostrando {{count}} elementos", "logs": { "file": "Archivo", "location": "línea {{line}} en {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/dag.json index eb7f127a60ac9..9a4f6549d36bf 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/dag.json @@ -10,6 +10,7 @@ "hourly": "Cada hora", "legend": { "less": "Menos", + "mixed": "Mixto", "more": "Más" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Año anterior" }, "noData": "No hay datos disponibles", + "noFailedRuns": "Sin ejecuciones fallidas", "noRuns": "No hay ejecuciones", "totalRuns": "Total de Ejecuciones", "week": "Semana {{weekNumber}}", @@ -35,6 +37,7 @@ "code": { "bundleUrl": "URL del Bundle", "noCode": "No se encontró código", + "parseDuration": "Duración del parseo:", "parsedAt": "Parseado en:" }, "extraLinks": "Enlaces Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json index 7d93addccfc72..353ae56534a77 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/dags.json @@ -20,8 +20,7 @@ "all": "Todos", "paused": "Pausado" }, - "runIdPatternFilter": "Buscar Ejecuciones de Dag", - "triggeringUserNameFilter": "Buscar por usuario que activó" + "runIdPatternFilter": "Buscar Ejecuciones de Dag" }, "ownerLink": "Enlace de Propietario para {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/hitl.json index 823d1f5bacf25..028a59e1be9f1 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/es/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/hitl.json @@ -1,5 +1,8 @@ { "filters": { + "body": "Cuerpo", + "createdAtFrom": "Creado Desde", + "createdAtTo": "Creado Hasta", "response": { "all": "Todas", "pending": "Pendientes", @@ -14,11 +17,13 @@ "requiredActionCount_other": "Acciones Requeridas ({{count}})", "requiredActionState": "Estado de Acción Requerida", "response": { + "created": "Respuesta creada el ", "error": "Error en la Respuesta", "optionsDescription": "Elige tus opciones para esta instancia de tarea", "optionsLabel": "Opciones", "received": "Respuesta recibida en ", "respond": "Responder", + "responded_by_user_name": "Respondido por (Nombre de usuario)", "success": "Respuesta de {{taskId}} exitosa", "title": "Instancia de Tarea Humana - {{taskId}}" }, diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/es/tasks.json new file mode 100644 index 0000000000000..391e18c578d6f --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "Mapeado", + "notMapped": "No mapeado", + "retries": "Reintentos", + "searchTasks": "Buscar tareas", + "selectMapped": "Seleccionar mapeados", + "selectOperator": "Seleccionar operadores", + "selectRetryValues": "Seleccionar valores de reintento", + "selectTriggerRules": "Seleccionar reglas de Trigger" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/admin.json index a926dc7a07737..d998e2d0832b3 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/admin.json @@ -19,11 +19,14 @@ "host": "Hôte", "port": "Port" }, + "connection_many": "Connexions", "connection_one": "Connexion", "connection_other": "Connexions", "delete": { + "deleteConnection_many": "Supprimer {{count}} connexions", "deleteConnection_one": "Supprimer 1 connexion", "deleteConnection_other": "Supprimer {{count}} connexions", + "firstConfirmMessage_many": "Vous êtes sur le point de supprimer les connexions suivantes :", "firstConfirmMessage_one": "Vous êtes sur le point de supprimer la connexion suivante :", "firstConfirmMessage_other": "Vous êtes sur le point de supprimer les connexions suivantes :", "title": "Supprimer la Connexion" @@ -49,6 +52,12 @@ "searchPlaceholder": "Rechercher les connexions", "test": "Tester la connexion", "testDisabled": "Le test de connexion est désactivé. Veuillez contacter un administrateur pour l'activer.", + "testError": { + "title": "Échec du test de connexion" + }, + "testSuccess": { + "title": "Test de connexion réussi" + }, "typeMeta": { "error": "Échec de la récupération des métadonnées du type de connexion", "standardFields": { @@ -72,13 +81,13 @@ "tooltip": "Supprimer les connexions sélectionnées" }, "formActions": { - "reset": "Réinitialiser", "save": "Sauvegarder" }, "plugins": { "columns": { "source": "Source" }, + "importError_many": "Erreurs d'importation de plugin", "importError_one": "Erreur d'importation de plugin", "importError_other": "Erreurs d'importation de plugins", "searchPlaceholder": "Rechercher par fichier" @@ -100,6 +109,7 @@ "slots": "Slots" }, "noPoolsFound": "Aucun pool trouvé", + "pool_many": "Pools", "pool_one": "Pool", "pool_other": "Pools", "searchPlaceholder": "Rechercher des Pools", @@ -121,8 +131,10 @@ "isEncrypted": "Est chiffrée" }, "delete": { + "deleteVariable_many": "Supprimer {{count}} Variables", "deleteVariable_one": "Supprimer 1 Variable", "deleteVariable_other": "Supprimer {{count}} Variables", + "firstConfirmMessage_many": "Vous êtes sur le point de supprimer les variables suivantes :", "firstConfirmMessage_one": "Vous êtes sur le point de supprimer la variable suivante :", "firstConfirmMessage_other": "Vous êtes sur le point de supprimer les variables suivantes :", "title": "Supprimer la Variable", @@ -161,6 +173,7 @@ }, "noRowsMessage": "Aucune variable trouvée", "searchPlaceholder": "Rechercher des Variables", + "variable_many": "Variables", "variable_one": "Variable", "variable_other": "Variables" } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/assets.json index f6dc9d824cdf9..2a2d6090e5115 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "Dags consomatteurs", + "consumingTasks": "Tâches consommatrices", "createEvent": { "button": "Créer un événement", "manual": { @@ -21,6 +22,7 @@ }, "title": "Créer un événement d'Asset pour {{name}}" }, + "extra": "Extra", "group": "Group", "lastAssetEvent": "Dernier événement d'Asset", "name": "Nom", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/browse.json index 125689d85fafc..f2a8705521571 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Réduire tous les extra json", - "expandAllExtra": "Ouvrir tous les extra json" - }, "columns": { "event": "Événement", "extra": "Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json index e118dbf035839..96650ff940026 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json @@ -7,10 +7,20 @@ "Providers": "Providers", "Variables": "Variables" }, + "allOperators": "Tous les opérateurs", + "appearance": { + "appearance": "Apparence", + "darkMode": "Mode sombre", + "lightMode": "Mode clair", + "systemMode": "Suivre les paramètres du système" + }, + "asset_many": "Assets", "asset_one": "Asset", "asset_other": "Assets", + "assetEvent_many": "Événements d'Asset", "assetEvent_one": "Événement d'Asset", "assetEvent_other": "Événements d'Asset", + "backfill_many": "Rattrapages", "backfill_one": "Rattrapage", "backfill_other": "Rattrapages", "browse": { @@ -18,9 +28,12 @@ "requiredActions": "Actions requises", "xcoms": "XComs" }, + "collapseAllExtra": "Réduire tous les extra json", "collapseDetailsPanel": "Replier le panneau des détails", + "createdAssetEvent_many": "Événements d'Asset créés", "createdAssetEvent_one": "Événement d'Asset créé", "createdAssetEvent_other": "Événements d'Asset créés", + "dag_many": "Dags", "dag_one": "Dag", "dag_other": "Dags", "dagDetails": { @@ -32,6 +45,7 @@ "fileLocation": "Emplacement du fichier", "hasTaskConcurrencyLimits": "Limites de concurrence par tâche", "lastExpired": "Date d'expiration", + "lastParseDuration": "Dernière durée d'analyse", "lastParsed": "Dernière analyse", "latestDagVersion": "Dernière version du Dag", "latestRun": "Dernière exécution", @@ -58,6 +72,7 @@ "triggeredBy": "Déclenché par", "triggeringUser": "Nom de l'utilisateur déclencheur" }, + "dagRun_many": "Exécutions de Dag", "dagRun_one": "Exécution de Dag", "dagRun_other": "Exécutions de Dag", "dagRunId": "ID d'exécution du Dag", @@ -70,12 +85,18 @@ "githubRepo": "Repo GitHub", "restApiReference": "Référence API REST" }, + "download": { + "download": "Télécharger", + "hotkey": "d", + "tooltip": "Appuyez sur {{hotkey}} pour télécharger les journaux" + }, "duration": "Durée", "endDate": "Date de fin", "error": { "back": "Retour", "defaultMessage": "Une erreur inattendue est survenue", "home": "Accueil", + "invalidUrl": "Page introuvable. Veuillez vérifier l'URL et réessayer.", "notFound": "Page introuvable", "title": "Erreur" }, @@ -85,22 +106,19 @@ "hotkey": "e", "tooltip": "Appuyez sur {{hotkey}} pour développer/réduire" }, + "expandAllExtra": "Ouvrir tous les extra json", "expression": { "all": "Tous", "and": "Et", "any": "N'importe lequel", "or": "Ou" }, + "filter": "Filtre", "filters": { - "dagDisplayNamePlaceholder": "Filtrer par Dag", - "keyPlaceholder": "Filtrer par clé XCom", - "logicalDateFromPlaceholder": "Date logique de début", - "logicalDateToPlaceholder": "Date logique de fin", - "mapIndexPlaceholder": "Filtrer par Map Index", - "runAfterFromPlaceholder": "Exécuté après - de", - "runAfterToPlaceholder": "Exécuté après - à", - "runIdPlaceholder": "Filtrer par ID d'exécution", - "taskIdPlaceholder": "Filtrer par ID de tâche" + "logicalDateFrom": "Date logique de début", + "logicalDateTo": "Date logique de fin", + "runAfterFrom": "Exécuté après - de", + "runAfterTo": "Exécuté après - à" }, "logicalDate": "Date logique", "logout": "Déconnexion", @@ -136,12 +154,14 @@ "pools": { "deferred": "Différé", "open": "Libre", + "pools_many": "Pools", "pools_one": "Pool", "pools_other": "Pools", "queued": "En file", "running": "En cours", "scheduled": "Planifié" }, + "reset": "Réinitialiser", "runId": "ID d'exécution", "runTypes": { "asset_triggered": "Déclenché par Asset", @@ -156,7 +176,6 @@ }, "tooltip": "Appuyez sur {{hotkey}} pour faire défiler vers le {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Actions", "permissions": "Permissions", @@ -171,6 +190,7 @@ "hotkey": "s", "show": "Afficher la source" }, + "sourceAssetEvent_many": "Événements sources", "sourceAssetEvent_one": "Événement source", "sourceAssetEvent_other": "Événements sources", "startDate": "Date de début", @@ -180,6 +200,7 @@ "failed": "Échoué", "no_status": "Aucun statut", "none": "Aucun statut", + "planned": "Planifié", "queued": "En file", "removed": "Supprimé", "restarting": "Redémarrage", @@ -196,6 +217,7 @@ "createdAt": "Créé à", "filterByTag": "Filtrer les Dags par tag", "filterColumns": "Filtrer les colonnes du tableau", + "filterReset_many": "Réinitialiser les filtres", "filterReset_one": "Réinitialiser le filtre", "filterReset_other": "Réinitialiser les filtres", "from": "De", @@ -214,6 +236,7 @@ "operator": "Opérateur", "triggerRule": "Règle de déclenchement" }, + "task_many": "Tâches", "task_one": "Tâche", "task_other": "Tâches", "taskGroup": "Groupe de tâches", @@ -241,6 +264,7 @@ }, "unixname": "Nom Unix" }, + "taskInstance_many": "Instances de tâche", "taskInstance_one": "Instance de tâche", "taskInstance_other": "Instances de tâche", "timeRange": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/components.json index abd0ec1a816ec..93645eb7d71e7 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/components.json @@ -1,13 +1,12 @@ { "backfill": { + "affected_many": "{{count}} exécutions seront déclenchées.", "affected_one": "1 exécution sera déclenchée.", "affected_other": "{{count}} exécutions seront déclenchées.", "affectedNone": "Aucune exécution ne correspond aux critères sélectionnés.", "allRuns": "Toutes les exécutions", "backwards": "Exécuter à rebours", "dateRange": "Plage de dates", - "dateRangeFrom": "De", - "dateRangeTo": "À", "errorStartDateBeforeEndDate": "La date de début doit être antérieure à la date de fin", "maxRuns": "Nombre maximum d'exécutions actives", "missingAndErroredRuns": "Exécutions manquantes et en erreur", @@ -46,16 +45,20 @@ "invalidJson": "Format JSON invalide : {{errorMessage}}" }, "dagWarnings": { + "error_many": "{{count}} erreurs", "error_one": "1 erreur", "error_other": "{{count}} erreurs", "errorAndWarning": "1 erreur et {{warningText}}", + "warning_many": "{{count}} avertissements", "warning_one": "1 avertissement", "warning_other": "{{count}} avertissements" }, "durationChart": { "duration": "Durée (secondes)", + "lastDagRun_many": "Dernières {{count}} exécutions du Dag", "lastDagRun_one": "Dernière exécution du Dag", "lastDagRun_other": "Dernières {{count}} exécutions du Dag", + "lastTaskInstance_many": "Dernières {{count}} Task Instances", "lastTaskInstance_one": "Dernière Task Instance", "lastTaskInstance_other": "Dernières {{count}} Task Instances", "queuedDuration": "Durée en file d'attente", @@ -63,6 +66,7 @@ "runDuration": "Durée d'exécution" }, "fileUpload": { + "files_many": "{{count}} fichiers", "files_one": "{{count}} fichier", "files_other": "{{count}} fichiers" }, @@ -85,11 +89,22 @@ "downloadImageError": "Échec du téléchargement de l'image du graphe.", "downloadImageErrorTitle": "Échec du téléchargement", "otherDagRuns": "+Autres exécutions du Dag", + "taskCount_many": "{{count}} tâches", "taskCount_one": "{{count}} tâche", "taskCount_other": "{{count}} tâches", "taskGroup": "Groupe de tâches" }, "limitedList": "+{{count}} supplémentaires", + "limitedList.allItems": "Tous les {{count}} éléments :", + "limitedList.allTags_many": "Tous les éléments ({{count}}) :", + "limitedList.allTags_one": "Tous les éléments (1) :", + "limitedList.allTags_other": "Tous les éléments ({{count}}) :", + "limitedList.clickToInteract": "Cliquez sur une étiquette pour filtrer les Dags", + "limitedList.clickToOpenFull": "Cliquez sur \"+{{count}} supplémentaires\" pour ouvrir la vue complète", + "limitedList.copyPasteText": "Vous pouvez copier et coller le texte ci-dessus", + "limitedList.showingItems_many": "Affichage de {{count}} éléments", + "limitedList.showingItems_one": "Affichage de 1 élément", + "limitedList.showingItems_other": "Affichage de {{count}} éléments", "logs": { "file": "Fichier", "location": "ligne {{line}} dans {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json index 37b7835696106..b1ae1f5d28f02 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json @@ -5,9 +5,39 @@ "reason": "Raison", "title": "Dépendances bloquant la planification de la tâche" }, + "calendar": { + "daily": "Quotidien", + "hourly": "Toutes les heures", + "legend": { + "less": "Moins", + "mixed": "Mixte", + "more": "Plus" + }, + "navigation": { + "nextMonth": "Mois suivant", + "nextYear": "Année suivante", + "previousMonth": "Mois précédent", + "previousYear": "Année précédente" + }, + "noData": "Aucune donnée disponible", + "noFailedRuns": "Aucun Run échoué", + "noRuns": "Aucun Run", + "totalRuns": "Total des Runs", + "week": "Semaine {{weekNumber}}", + "weekdays": { + "friday": "Ven", + "monday": "Lun", + "saturday": "Sam", + "sunday": "Dim", + "thursday": "Jeu", + "tuesday": "Mar", + "wednesday": "Mer" + } + }, "code": { "bundleUrl": "URL du bundle", "noCode": "Aucun code trouvé", + "parseDuration": "Durée d'analyse :", "parsedAt": "Analysé le :" }, "extraLinks": "Liens supplémentaires", @@ -40,24 +70,29 @@ "warning": "AVERTISSEMENT" }, "navigation": { - "jump": "Sauter : Maj+{{arrow}}", "navigation": "Navigation : {{arrow}}", "toggleGroup": "Basculer le groupe : Espace" }, "overview": { "buttons": { + "failedRun_many": "Runs échoués", "failedRun_one": "Run échoué", "failedRun_other": "Runs échoués", + "failedTask_many": "Tâches échouées", "failedTask_one": "Tâche échouée", "failedTask_other": "Tâches échouées", + "failedTaskInstance_many": "Task Instances échouées", "failedTaskInstance_one": "Task Instance échouée", "failedTaskInstance_other": "Task Instances échouées" }, "charts": { + "assetEvent_many": "Événements d'actif créés", "assetEvent_one": "Événement d'actif créé", "assetEvent_other": "Événements d'actif créés" }, "failedLogs": { + "hideLogs": "Masquer les journaux", + "showLogs": "Afficher les journaux", "title": "Journaux des tâches échouées récemment", "viewFullLogs": "Voir les journaux complets" } @@ -66,8 +101,8 @@ "buttons": { "options": "Options", "showGantt": "Afficher le Gantt", - "showGraph": "Afficher le graphe", - "showGrid": "Afficher la grille" + "showGraphShortcut": "Afficher le graphe (Appuyez sur g)", + "showGridShortcut": "Afficher la grille (Appuyez sur g)" }, "dagRuns": { "label": "Nombre de Runs du Dag" @@ -102,9 +137,11 @@ "assetEvents": "Événements d'actifs", "auditLog": "Journal d'audit", "backfills": "Rattrappages", + "calendar": "Calendrier", "code": "Code", "details": "Détails", "logs": "Journaux", + "mappedTaskInstances_many": "Task Instances [{{count}}]", "mappedTaskInstances_one": "Task Instance [{{count}}]", "mappedTaskInstances_other": "Task Instances [{{count}}]", "overview": "Aperçu", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dashboard.json index 67289ee983f8d..fa60bff6a234c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dashboard.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dashboard.json @@ -1,5 +1,6 @@ { "favorite": { + "favoriteDags_many": "{{count}} premiers Dags favoris", "favoriteDags_one": "{{count}} premier Dag favori", "favoriteDags_other": "{{count}} premiers Dags favoris", "noDagRuns": "Il n'y a pas encore d'exécution pour ce Dag.", @@ -19,6 +20,7 @@ }, "history": "Historique", "importErrors": { + "dagImportError_many": "Erreurs d'importation de Dag", "dagImportError_one": "Erreur d'importation de Dag", "dagImportError_other": "Erreurs d'importation de Dag", "searchByFile": "Rechercher par fichier", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/hitl.json index 3c03176bc69df..1efd0752cc261 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/hitl.json @@ -1,6 +1,17 @@ { + "filters": { + "response": { + "all": "Tous", + "pending": "En attente", + "received": "Reçue" + } + }, + "requiredAction_many": "Actions requises", "requiredAction_one": "Actions requises", "requiredAction_other": "Actions requises", + "requiredActionCount_many": "Actions requises ({{count}})", + "requiredActionCount_one": "Action requise ({{count}})", + "requiredActionCount_other": "Actions requises ({{count}})", "requiredActionState": "État de l'action requise", "response": { "error": "Échec de la réponse", @@ -16,6 +27,7 @@ "approvalRequired": "Approbation requise", "choiceReceived": "Choix reçu", "choiceRequired": "Choix requis", + "noResponseReceived": "Aucune réponse reçue", "rejectionReceived": "Rejet reçu", "responseReceived": "Réponse reçue", "responseRequired": "Réponse requise" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/fr/tasks.json new file mode 100644 index 0000000000000..2971c37e527b7 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "Mappé", + "notMapped": "Non mappé", + "retries": "Éssaies", + "searchTasks": "Rechercher des tâches", + "selectMapped": "Sélectionner mappé", + "selectOperator": "Sélectionner des opérateurs", + "selectRetryValues": "Sélectionner des valeurs des essaies", + "selectTriggerRules": "Sélectionner des règles de déclenchement" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/admin.json index 5024088b35c28..73d5c55951156 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/admin.json @@ -52,6 +52,12 @@ "searchPlaceholder": "חפש חיבורים", "test": "בדוק חיבור", "testDisabled": "אפשרות זו אינה זמינה. פנו למנהל המערכת להפעלתה", + "testError": { + "title": "בדיקת החיבור נכשלה" + }, + "testSuccess": { + "title": "בדיקת החיבור הצליחה" + }, "typeMeta": { "error": "נכשל באחזור מטא סוג חיבור", "standardFields": { @@ -75,7 +81,6 @@ "tooltip": "מחק חיבורים נבחרים" }, "formActions": { - "reset": "אתחל", "save": "שמור" }, "plugins": { @@ -147,7 +152,7 @@ "import": { "button": "ייבא", "conflictResolution": "בחר מנגנון ליישוב קונפליקט משתנים", - "errorParsingJsonFile": "שגיאה בפירסור קובץ JSON: העלה קובץ JSON המכיל משתנים (לדוגמה, {\"key\": \"value\", ...}).", + "errorParsingJsonFile": "שגיאה בניתוח קובץ JSON: העלה קובץ JSON המכיל משתנים (לדוגמה, {\"key\": \"value\", ...}).", "options": { "fail": { "description": "הכשלת הייבוא אם מזוהים משתנים קיימים.", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/assets.json index 64c57f2029ce6..235417b603734 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/assets.json @@ -1,9 +1,15 @@ { + "additional_data": "נתונים נוספים", + "asset_many": "נכסים", + "asset_one": "נכס", + "asset_other": "נכסים", + "asset_two": "שני נכסים", "consumingDags": "צורכים Dags", + "consumingTasks": "משימות צורכות", "createEvent": { "button": "יצירת אירוע", "manual": { - "description": "יצירה ידנית של אירוע בכנס", + "description": "יצירה ידנית של אירוע בנכס", "extra": "תוכן אירוע בנכס", "label": "ידני" }, @@ -21,6 +27,7 @@ }, "title": "יצירת אירוע בנכס עבור {{name}}" }, + "extra": "תוכן", "group": "קבוצה", "lastAssetEvent": "אירוע אחרון בנכס", "name": "שם", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/browse.json index 5f36bba83ac35..00d3aff624591 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "כווץ את כל ה-JSON הנוסף", - "expandAllExtra": "הרחב את כל ה-JSON הנוסף" - }, "columns": { "event": "אירוע", "extra": "תוכן", @@ -16,11 +12,35 @@ "title": "יומן מערכת" }, "xcom": { + "add": { + "error": "הוספת XCom נכשלה", + "errorTitle": "שגיאה", + "success": "XCom נוסף בהצלחה", + "successTitle": "XCom נוסף", + "title": "הוסף XCom" + }, "columns": { "dag": "Dag", "key": "מפתח", "value": "ערך" }, - "title": "XCom" + "delete": { + "error": "מחיקת XCom נכשלה", + "errorTitle": "שגיאה", + "success": "XCom נמחק בהצלחה", + "successTitle": "XCom נמחק", + "title": "מחק XCom", + "warning": "האם אתה בטוח שברצונך למחוק את ה-XCom הזה? פעולה זו אינה הפיכה." + }, + "edit": { + "error": "עדכון XCom נכשל", + "errorTitle": "שגיאה", + "success": "XCom עודכן בהצלחה", + "successTitle": "XCom עודכן", + "title": "ערוך XCom" + }, + "key": "מפתח", + "title": "XCom", + "value": "ערך" } } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json index 85a434bc5b993..ee65c0c902a67 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json @@ -28,6 +28,7 @@ "requiredActions": "פעולות נדרשות", "xcoms": "XComs" }, + "collapseAllExtra": "כווץ תוכן JSON", "collapseDetailsPanel": "כווץ לוח פרטים", "createdAssetEvent_one": "אירוע נכס שנוצר", "createdAssetEvent_other": "אירועי נכס שנוצרו", @@ -44,6 +45,7 @@ "fileLocation": "מיקום קובץ", "hasTaskConcurrencyLimits": "מגבלות על משימות במקביל", "lastExpired": "פג תוקף לאחרונה", + "lastParseDuration": "משך זמן הניתוח האחרון", "lastParsed": "ניתוח אחרון", "latestDagVersion": "גרסת Dag אחרונה", "latestRun": "ריצה אחרונה", @@ -63,6 +65,7 @@ "dataIntervalEnd": "סיום מקטע נתונים", "dataIntervalStart": "תחילת מקטע נתונים", "lastSchedulingDecision": "החלטת תזמון אחרונה", + "partitionKey": "מפתח חלוקה", "queuedAt": "זמן כניסה לתור", "runAfter": "ריצה לאחר", "runType": "סוג ריצה", @@ -77,18 +80,30 @@ "dagWarnings": "אזהרות/שגיאות Dag", "defaultToGraphView": "תצוגת גרף כברירת מחדל", "defaultToGridView": "תצוגת רשת כברירת מחדל", + "delete": "מחק", + "diff": "השוואה", + "diffCompareWith": "השווה עם", + "diffExit": "צא מהשוואה", + "diffSelectVersionToCompare": "בחר גרסה להשוואה", "direction": "כיוון", "docs": { "documentation": "תיעוד", "githubRepo": "מאגר GitHub", "restApiReference": "תיעוד REST API" }, + "download": { + "download": "הורד", + "hotkey": "d", + "tooltip": "לחץ {{hotkey}} להורדת רישומים" + }, "duration": "משך זמן", + "edit": "ערוך", "endDate": "תאריך סיום", "error": { "back": "חזור", "defaultMessage": "התרחשה שגיאה בלתי צפויה", "home": "דף הבית", + "invalidUrl": "הדף לא נמצא. בדוק את הכתובת ונסה שוב.", "notFound": "הדף לא נמצא", "title": "שגיאה" }, @@ -98,34 +113,38 @@ "hotkey": "e", "tooltip": "לחץ {{hotkey}} לכיווץ או הרחבה" }, + "expandAllExtra": "הרחב תוכן JSON", "expression": { "all": "הכל", "and": "וגם", "any": "כלשהו", "or": "או" }, + "filter": "מסנן", "filters": { - "dagDisplayNamePlaceholder": "סנן לפי Dag", - "keyPlaceholder": "סנן לפי מפתח XCom", - "logicalDateFromPlaceholder": "תאריך לוגי מ-", - "logicalDateToPlaceholder": "תאריך לוגי עד", - "mapIndexPlaceholder": "סנן לפי אינדקס מיפוי", - "runAfterFromPlaceholder": "ריצה לאחר (מתאריך)", - "runAfterToPlaceholder": "ריצה לאחר (עד תאריך)", - "runIdPlaceholder": "סנן לפי מזהה ריצה", - "taskIdPlaceholder": "סנן לפי מזהה משימה" + "durationFrom": "משך זמן מ-", + "durationTo": "משך זמן עד", + "endTime": "זמן סיום", + "logicalDateFrom": "תאריך לוגי מ-", + "logicalDateTo": "תאריך לוגי עד", + "runAfterFrom": "ריצה לאחר (מתאריך)", + "runAfterTo": "ריצה לאחר (עד תאריך)", + "selectDateRange": "בחר טווח תאריכים", + "startTime": "זמן התחלה" }, "logicalDate": "תאריך לוגי", "logout": "התנתק", "logoutConfirmation": "האם אתה בטוח שברצונך להתנתק?", "mapIndex": "אינדקס מיפוי", "modal": { + "add": "הוסף", "cancel": "ביטול", "confirm": "אישור", "delete": { "button": "מחק", "confirmation": "האם אתה בטוח שברצונך למחוק את {{resourceName}}? פעולה זו אינה הפיכה" - } + }, + "save": "שמור" }, "nav": { "admin": "ניהול", @@ -146,16 +165,7 @@ "placeholder": "הוסף הערה...", "taskInstance": "הערת משימה" }, - "pools": { - "deferred": "נדחה לתזמון עתידי", - "open": "פתוח", - "pools_one": "מאגר משאבים", - "pools_other": "מאגרי משאבים", - "pools_two": "מאגרי משאבים", - "queued": "בתור", - "running": "בביצוע", - "scheduled": "מתוזמן" - }, + "reset": "אתחל", "runId": "מזהה ריצה", "runTypes": { "asset_triggered": "הופעל על-ידי נכס", @@ -170,7 +180,6 @@ }, "tooltip": "לחץ {{hotkey}} לגלילה ל{{direction}}" }, - "seconds": "{{count}} שניות", "security": { "actions": "פעולות", "permissions": "הרשאות", @@ -180,6 +189,7 @@ }, "selectLanguage": "בחר שפה", "showDetailsPanel": "הצג לוח פרטים", + "signedInAs": "מחובר כ", "source": { "hide": "הסתר מקור", "hotkey": "s", @@ -191,20 +201,21 @@ "startDate": "תאריך התחלה", "state": "מצב", "states": { - "deferred": "נדחה", - "failed": "נכשל", + "deferred": "בהשהייה", + "failed": "נכשלו", "no_status": "ללא סטטוס", "none": "ללא סטטוס", - "planned": "מתוכנן", + "open": "פנוי", + "planned": "בתכנון", "queued": "בתור", - "removed": "הוסר", - "restarting": "מופעל מחדש", - "running": "בביצוע", - "scheduled": "מתוזמן", - "skipped": "דולג", - "success": "הצליח", - "up_for_reschedule": "ממתין לתזמון מחדש", - "up_for_retry": "ממתין לניסיון חוזר", + "removed": "הוסרו", + "restarting": "בהפעלה מחדש", + "running": "בריצה", + "scheduled": "בתזמון", + "skipped": "דולגו", + "success": "הצליחו", + "up_for_reschedule": "בהמתנה לתזמון מחדש", + "up_for_retry": "בהמתנה לניסיון חוזר", "upstream_failed": "משימות קודמות נכשלו" }, "table": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json index 43dd3a9964aaf..23f23faf2aa30 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/components.json @@ -7,12 +7,11 @@ "allRuns": "כל ההרצות", "backwards": "הרץ לאחור", "dateRange": "טווח תאריכים", - "dateRangeFrom": "מתאריך", - "dateRangeTo": "עד תאריך", "errorStartDateBeforeEndDate": "תאריך ההתחלה חייב להיות לפני תאריך הסיום", "maxRuns": "מספר ריצות מקבילות מירבי", "missingAndErroredRuns": "הרצות חסרות ושגויות", "missingRuns": "הרצות חסרות", + "permissionDenied": "הרצה יבשה (Dry Run) נכשלה: למשתמש אין הרשאה ליצור Backfills.", "reprocessBehavior": "התנהגות עיבוד מחדש", "run": "הרץ Backfill", "selectDescription": "הרץ Dag זה עבור טווח תאריכים", @@ -55,6 +54,13 @@ "warning_other": "{{count}} אזהרות", "warning_two": "שתי אזהרות" }, + "dateRangeFilter": { + "validation": { + "invalidDateFormat": "פורמט התאריך אינו תקין.", + "invalidTimeFormat": "פורמט הזמן אינו תקין.", + "startBeforeEnd": "תאריך/זמן התחלה חייב להיות לפני תאריך/זמן סיום" + } + }, "durationChart": { "duration": "משך זמן (שניות)", "lastDagRun_one": "ריצת Dag אחרונה", @@ -97,6 +103,16 @@ "taskGroup": "קבוצת משימות" }, "limitedList": "+ {{count}} נוספים", + "limitedList.allItems": "כל {{count}} הפריטים:", + "limitedList.allTags_one": "כל התגיות (1)", + "limitedList.allTags_other": "כל התגיות ({{count}})", + "limitedList.allTags_two": "כל התגיות (2)", + "limitedList.clickToInteract": "לחץ על תגית כדי לסנן Dags", + "limitedList.clickToOpenFull": "לחץ על \"+{{count}} נוספים\" כדי לפתוח את התצוגה המלאה", + "limitedList.copyPasteText": "ניתן להעתיק ולהדביק את הטקסט שמעל", + "limitedList.showingItems_one": "מציג פריט אחד", + "limitedList.showingItems_other": "מציג {{count}} פריטים", + "limitedList.showingItems_two": "מציג שני פריטים", "logs": { "file": "קובץ", "location": "שורה {{line}} ב{{name}}" @@ -106,10 +122,23 @@ "sortedDescending": "ממוין בסדר יורד", "sortedUnsorted": "לא ממוין", "taskTries": "ניסיונות משימה", + "taskTryPlaceholder": "ניסיון משימה", + "team": { + "selector": { + "helperText": "אופציונלי. הגבל שימוש לצוות מסוים.", + "label": "צוות", + "placeHolder": "בחר צוות" + } + }, "toggleCardView": "הצג תצוגת כרטיסים", "toggleTableView": "הצג תצוגת טבלה", "triggerDag": { "button": "הפעל", + "dataInterval": "מרווח נתונים", + "dataIntervalAuto": "מוסק מתוך התאריך הלוגי וטבלת הזמנים", + "dataIntervalManual": "הגדר ידנית", + "intervalEnd": "סיום", + "intervalStart": "התחלה", "loading": "טוען מידע Dag...", "loadingFailed": "טעינת מידע ה-Dag נכשלה. אנא נסו שוב.", "runIdHelp": "אופציונלי - ייווצר אוטומטית אם לא יסופק", @@ -117,11 +146,15 @@ "selectLabel": "ריצה בודדת", "title": "הפעל Dag", "toaster": { + "error": { + "title": "הפעלת Dag נכשלה" + }, "success": { "description": "ריצת ה-Dag הופעלה בהצלחה.", "title": "ריצת Dag הופעלה" } }, + "triggerAgainWithConfig": "הפעל שוב עם התצורה הזו", "unpause": "הפעל את {{dagDisplayName}} בעת ההרצה" }, "trimText": { @@ -137,6 +170,7 @@ "versionId": "מזהה גרסה" }, "versionSelect": { + "allVersions": "כל הגרסאות", "dagVersion": "גרסת Dag", "versionCode": "גרסה {{versionCode}}" } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json index e110a798f6bb7..b7fbb38cc7f91 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json @@ -10,6 +10,7 @@ "hourly": "שעתי", "legend": { "less": "פחות", + "mixed": "מעורב", "more": "יותר" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "השנה הקודמת" }, "noData": "אין נתונים זמינים", + "noFailedRuns": "אין הרצות שנכשלו", "noRuns": "אין הרצות", "totalRuns": "סה״כ הרצות", "week": "שבוע {{weekNumber}}", @@ -35,7 +37,8 @@ "code": { "bundleUrl": "כתובת החבילה", "noCode": "לא נמצא קוד", - "parsedAt": "נקרא בשעה:" + "parseDuration": "משך זמן הניתוח:", + "parsedAt": "נותח בשעה:" }, "extraLinks": "קישורים נוספים", "grid": { @@ -115,6 +118,18 @@ }, "graphDirection": { "label": "כיוון התרשים" + }, + "taskStreamFilter": { + "activeFilter": "מסנן פעיל", + "clearFilter": "נקה מסנן", + "clickTask": "לחץ על משימה כדי לבחור אותה כבסיס המסנן", + "label": "מסנן", + "options": { + "both": "גם במעלה וגם במורד הזרם", + "downstream": "במורד הזרם", + "upstream": "במעלה הזרם" + }, + "selectedTask": "משימה נבחרת" } }, "paramsFailed": "טעינת הפרמטרים נכשלה", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json index 15729c5f1e866..76ad2416cba54 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dags.json @@ -35,6 +35,10 @@ "error": "נכשל לנקות {{type}}", "title": "נקה {{type}}" }, + "confirmationDialog": { + "description": "המשימה נמצאת כרגע במצב {{state}} שהופעל על ידי המשתמש {{user}} ב-{{time}}.\nלא ניתן לנקות את המשימה עד שהיא תסיים לרוץ או שמשתמש יבטל את הסימון \"מנע הרצה חוזרת אם המשימה כבר רצה\" בדיאלוג ניקוי המשימה.", + "title": "לא ניתן לנקות מופע משימה" + }, "delete": { "button": "מחק {{type}}", "dialog": { @@ -62,6 +66,7 @@ "future": "בעתיד", "onlyFailed": "נקה משימות שנכשלו בלבד", "past": "בעבר", + "preventRunningTasks": "מנע הרצה חוזרת אם המשימה כבר רצה", "queueNew": "תזמן משימות חדשות", "runOnLatestVersion": "הרץ על גרסת החבילה האחרונה", "upstream": "במעלה הזרם" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/dashboard.json index 7f962b05cebe3..6bb41f58dfcfb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dashboard.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dashboard.json @@ -8,7 +8,7 @@ }, "group": "קבוצה", "health": { - "dagProcessor": "Dag מנתח", + "dagProcessor": "מנתח Dags", "health": "תקינות", "healthy": "תקין", "lastHeartbeat": "פעימה אחרונה", @@ -31,12 +31,13 @@ "poolSlots": "סטאטוס מאגרים", "sortBy": { "newestFirst": "חדש קודם", - "oldestFirst": "ישן קודם" + "oldestFirst": "ישן קודם", + "placeholder": "סדר לפי" }, "source": "מקור", "stats": { - "activeDags": "Dags פעיל", - "failedDags": "Dags בכשלון", + "activeDags": "Dags פעילים", + "failedDags": "Dags נכשלו", "queuedDags": "Dags בתור", "requiredActions": "פעולות נדרשות", "runningDags": "Dags בריצה", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/he/hitl.json index 36aae61a389f0..9d450c86b9b12 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/he/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/hitl.json @@ -1,5 +1,7 @@ { "filters": { + "body": "גוף", + "createdAt": "נוצר בתאריך", "response": { "all": "הכל", "pending": "ממתין", @@ -14,11 +16,13 @@ "requiredActionCount_two": "פעולות נדרשות ({{count}})", "requiredActionState": "מצב פעולה נדרשת", "response": { + "created": "התגובה נוצרה ב", "error": "התגובה נכשלה", "optionsDescription": "בחר את האפשרויות למופע המשימה הזה", "optionsLabel": "אפשרויות", "received": "תגובה התקבלה ב", "respond": "הגב", + "responded_by_user_name": "נענה על ידי (שם משתמש)", "success": "התגובה של {{taskId}} הצליחה", "title": "מופע משימה ידנית - {{taskId}}" }, diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/admin.json index 8a12c76d4d218..590ad3eac82ba 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/admin.json @@ -72,7 +72,6 @@ "tooltip": "चयनित कनेक्शन्स हटाएं" }, "formActions": { - "reset": "रीसेट करें", "save": "सेव करें" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/browse.json index 4ee78a7e0aef9..1fc984b27a912 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "सभी अतिरिक्त JSON को संकुचित करें", - "expandAllExtra": "सभी अतिरिक्त JSON को विस्तृत करें" - }, "columns": { "event": "इवेंट", "extra": "अतिरिक्त", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json index 53cc2469791e0..69ecd84e7bb5f 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json @@ -18,6 +18,7 @@ "requiredActions": "आवश्यक क्रियाएं", "xcoms": "XComs" }, + "collapseAllExtra": "सभी अतिरिक्त JSON को संकुचित करें", "collapseDetailsPanel": "विवरण पैनल को छुपाएं", "createdAssetEvent_one": "बनाया गया एसेट इवेंट", "createdAssetEvent_other": "बनाए गए एसेट इवेंट्स", @@ -25,7 +26,6 @@ "dag_other": "डैग्स", "dagDetails": { "catchup": "पकड़ना", - "concurrency": "समानांतरता", "dagRunTimeout": "डैग रन टाइमआउट", "defaultArgs": "डिफ़ॉल्ट तर्क", "description": "विवरण", @@ -86,22 +86,19 @@ "hotkey": "e", "tooltip": "विस्तार को टॉगल करने के लिए {{hotkey}} दबाएं" }, + "expandAllExtra": "सभी अतिरिक्त JSON को विस्तृत करें", "expression": { "all": "सभी", "and": "और", "any": "कोई भी", "or": "या" }, + "filter": "फ़िल्टर", "filters": { - "dagDisplayNamePlaceholder": "डैग द्वारा फ़िल्टर करें", - "keyPlaceholder": "XCom कुंजी द्वारा फ़िल्टर करें", - "logicalDateFromPlaceholder": "तार्किक तिथि से", - "logicalDateToPlaceholder": "तार्किक तिथि तक", - "mapIndexPlaceholder": "मैप इंडेक्स द्वारा फ़िल्टर करें", - "runAfterFromPlaceholder": "रन आफ़्टर से", - "runAfterToPlaceholder": "रन आफ़्टर तक", - "runIdPlaceholder": "रन ID द्वारा फ़िल्टर करें", - "taskIdPlaceholder": "टास्क ID द्वारा फ़िल्टर करें" + "logicalDateFrom": "तार्किक तिथि से", + "logicalDateTo": "तार्किक तिथि तक", + "runAfterFrom": "रन आफ़्टर से", + "runAfterTo": "रन आफ़्टर तक" }, "logicalDate": "तार्किक तिथि", "logout": "लॉग आउट", @@ -143,6 +140,7 @@ "running": "चल रहा", "scheduled": "निर्धारित" }, + "reset": "रीसेट करें", "runId": "रन ID", "runTypes": { "asset_triggered": "एसेट द्वारा ट्रिगर", @@ -157,7 +155,6 @@ }, "tooltip": "{{direction}} तक स्क्रॉल करने के लिए {{hotkey}} दबाएं" }, - "seconds": "{{count}}सेकंड", "security": { "actions": "क्रियाएं", "permissions": "अनुमतियां", @@ -192,8 +189,6 @@ "up_for_retry": "पुनः प्रयास के लिए", "upstream_failed": "अपस्ट्रीम विफल" }, - "switchToDarkMode": "डार्क मोड में स्विच करें", - "switchToLightMode": "लाइट मोड में स्विच करें", "table": { "completedAt": "पर पूर्ण", "createdAt": "पर बनाया", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json index e84d1da7745b3..d3caf8c34c5eb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/components.json @@ -6,8 +6,6 @@ "allRuns": "सभी रन्स", "backwards": "पीछे की ओर चलाएं", "dateRange": "तिथि रेंज", - "dateRangeFrom": "से", - "dateRangeTo": "तक", "errorStartDateBeforeEndDate": "प्रारंभ तिथि समाप्ति तिथि से पहले होनी चाहिए", "maxRuns": "अधिकतम सक्रिय रन्स", "missingAndErroredRuns": "गुम और त्रुटि वाले रन्स", @@ -90,6 +88,10 @@ "taskGroup": "टास्क ग्रुप" }, "limitedList": "+{{count}} और", + "limitedList.allItems": "सभी {{count}} आइटम:", + "limitedList.clickToInteract": "टैग पर क्लिक करके Dags फ़िल्टर करें", + "limitedList.clickToOpenFull": "\"+{{count}} और\" पर क्लिक करके पूरी सूची खोलें", + "limitedList.copyPasteText": "आप ऊपर दिए गए पाठ को कॉपी और पेस्ट कर सकते हैं", "logs": { "file": "फ़ाइल", "location": "{{name}} में लाइन {{line}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json index 253ed9585407c..36e5d3adfca97 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dag.json @@ -40,7 +40,6 @@ "warning": "WARNING" }, "navigation": { - "jump": "जंप: Shift+{{arrow}}", "navigation": "नेवीगेशन: {{arrow}}", "toggleGroup": "ग्रुप टॉगल करें: Space" }, @@ -64,9 +63,7 @@ }, "panel": { "buttons": { - "options": "विकल्प", - "showGraph": "ग्राफ़ दिखाएं", - "showGrid": "ग्रिड दिखाएं" + "options": "विकल्प" }, "dagRuns": { "label": "डैग रन्स की संख्या" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json index 54cac11533683..74c112d0dfbcb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/dags.json @@ -20,8 +20,7 @@ "all": "सभी", "paused": "रोका गया" }, - "runIdPatternFilter": "डैग रन्स खोजें", - "triggeringUserNameFilter": "ट्रिगर करने वाले उपयोगकर्ता द्वारा खोजें" + "runIdPatternFilter": "डैग रन्स खोजें" }, "ownerLink": "{{owner}} के लिए स्वामी लिंक", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/admin.json index eaea0f27ecff9..4cf8aa1536994 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/admin.json @@ -72,7 +72,6 @@ "tooltip": "Kijelölt kapcsolatok törlése" }, "formActions": { - "reset": "Alaphelyzetbe állítás", "save": "Mentés" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/browse.json index c4520b3cb24b8..403fbac6cce26 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Összes extra JSON összecsukása", - "expandAllExtra": "Összes extra JSON kibontása" - }, "columns": { "event": "Esemény", "extra": "Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json index 2c777a339ecf2..bd48068c9de79 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json @@ -8,6 +8,12 @@ "Variables": "Változók" }, "allOperators": "Összes adatkészlet", + "appearance": { + "appearance": "Megjelenés", + "darkMode": "Sötét mód", + "lightMode": "Világos mód", + "systemMode": "Automatikus (rendszer szerint)" + }, "asset_one": "Adatkészlet", "asset_other": "Adatkészletek", "assetEvent_one": "Adatkészlet esemény", @@ -19,6 +25,7 @@ "requiredActions": "Szükséges műveletek", "xcoms": "XComok" }, + "collapseAllExtra": "Összes extra JSON összecsukása", "collapseDetailsPanel": "Részletek panel összecsukása", "createdAssetEvent_one": "Létrehozott adatkészlet esemény", "createdAssetEvent_other": "Létrehozott adatkészlet események", @@ -33,6 +40,7 @@ "fileLocation": "Fájl helye", "hasTaskConcurrencyLimits": "Van feladat párhuzamossági korlát", "lastExpired": "Legutóbb lejárt", + "lastParseDuration": "Utolsó feldolgozás időtartama", "lastParsed": "Legutóbb feldolgozva", "latestDagVersion": "Legújabb Dag verzió", "latestRun": "Legutóbbi futás", @@ -71,6 +79,11 @@ "githubRepo": "GitHub repo", "restApiReference": "REST API referencia" }, + "download": { + "download": "Letöltés", + "hotkey": "d", + "tooltip": "Nyomja meg a(z) {{hotkey}} billentyűt a naplók letöltéséhez" + }, "duration": "Időtartam", "endDate": "Befejezés dátuma", "error": { @@ -86,22 +99,19 @@ "hotkey": "e", "tooltip": "Nyomd meg a(z) {{hotkey}} gombot a kibontáshoz" }, + "expandAllExtra": "Összes extra JSON kibontása", "expression": { "all": "Mind", "and": "ÉS", "any": "Bármely", "or": "VAGY" }, + "filter": "Szűrő", "filters": { - "dagDisplayNamePlaceholder": "Szűrés Dag szerint", - "keyPlaceholder": "Szűrés XCom kulcs szerint", - "logicalDateFromPlaceholder": "Logikai dátum -tól", - "logicalDateToPlaceholder": "Logikai dátum -ig", - "mapIndexPlaceholder": "Szűrés Map Index szerint", - "runAfterFromPlaceholder": "Futás ekkortól", - "runAfterToPlaceholder": "Futás eddig", - "runIdPlaceholder": "Szűrés futás azonosító szerint", - "taskIdPlaceholder": "Szűrés feladat azonosító szerint" + "logicalDateFrom": "Logikai dátum -tól", + "logicalDateTo": "Logikai dátum -ig", + "runAfterFrom": "Futás ekkortól", + "runAfterTo": "Futás eddig" }, "logicalDate": "Logikai dátum", "logout": "Kijelentkezés", @@ -143,6 +153,7 @@ "running": "Fut", "scheduled": "Ütemezett" }, + "reset": "Alaphelyzetbe állítás", "runId": "Futás azonosító", "runTypes": { "asset_triggered": "Adatkészlet által indított", @@ -157,7 +168,6 @@ }, "tooltip": "Nyomd meg a(z) {{hotkey}} gombot a görgetéshez ide: {{direction}}" }, - "seconds": "{{count}} mp", "security": { "actions": "Műveletek", "permissions": "Jogosultságok", @@ -193,8 +203,6 @@ "up_for_retry": "Újrapróbálkozásra vár", "upstream_failed": "Előfeltétel sikertelen" }, - "switchToDarkMode": "Váltás sötét módra", - "switchToLightMode": "Váltás világos módra", "table": { "completedAt": "Befejezve ekkor", "createdAt": "Létrehozva ekkor", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json index f89188343dc23..8d802e79a5e3a 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json @@ -6,8 +6,6 @@ "allRuns": "Összes futás", "backwards": "Futtatás visszafelé", "dateRange": "Dátumtartomány", - "dateRangeFrom": "Kezdő dátum", - "dateRangeTo": "Befejező dátum", "errorStartDateBeforeEndDate": "A kezdő dátumnak a befejező dátum előtt kell lennie.", "maxRuns": "Maximális aktív futások", "missingAndErroredRuns": "Hiányzó és hibás futások", @@ -84,12 +82,20 @@ "downloadImage": "Gráf kép letöltése", "downloadImageError": "A gráf letöltése sikertelen.", "downloadImageErrorTitle": "Sikertelen letöltés", - "otherDagRuns": "+További DAG futások", + "otherDagRuns": "+További Dag futások", "taskCount_one": "{{count}} feladat", "taskCount_other": "{{count}} feladat", "taskGroup": "Feladatcsoport" }, "limitedList": "+{{count}} további", + "limitedList.allItems": "Összes {{count}} elem:", + "limitedList.allTags_one": "Összes címke ({{count}})", + "limitedList.allTags_other": "Összes címke ({{count}})", + "limitedList.clickToInteract": "Kattintson egy címkére a Dag-ok szűréséhez", + "limitedList.clickToOpenFull": "Kattintson a \"+{{count}} további\" gombra a teljes nézethez", + "limitedList.copyPasteText": "A fenti szöveg másolható és beilleszthető", + "limitedList.showingItems_one": "{{count}} elem megjelenítve", + "limitedList.showingItems_other": "{{count}} elem megjelenítve", "logs": { "file": "Fájl", "location": "{{name}} fájl {{line}}. sora" @@ -104,7 +110,7 @@ "triggerDag": { "button": "Indítás", "loading": "Dag információk betöltése...", - "loadingFailed": "A DAG információk betöltése sikertelen. Kérem, próbálja újra.", + "loadingFailed": "A Dag információk betöltése sikertelen. Kérem, próbálja újra.", "runIdHelp": "Opcionális – ha nincs megadva, automatikusan generálódik", "selectDescription": "Indítsd el ennek a Dag-nek egyetlen futását", "selectLabel": "Egyszeri futás", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json index ad362c9c103df..8f6955079c5b5 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dag.json @@ -10,6 +10,7 @@ "hourly": "Óránkénti", "legend": { "less": "Kevesebb", + "mixed": "Vegyes", "more": "Több" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Előző év" }, "noData": "Nincs elérhető adat", + "noFailedRuns": "Nincs sikertelen futás", "noRuns": "Nincs futás", "totalRuns": "Összes futás", "week": "Hét {{weekNumber}}", @@ -35,6 +37,7 @@ "code": { "bundleUrl": "Csomag URL", "noCode": "Nem található kód", + "parseDuration": "Feldolgozás időtartama:", "parsedAt": "Feldolgozás ideje:" }, "extraLinks": "Extra hivatkozások", @@ -84,6 +87,8 @@ "assetEvent_other": "Létrehozott adatkészlet események" }, "failedLogs": { + "hideLogs": "Napló elrejtése", + "showLogs": "Napló mutatása", "title": "Legutóbbi sikertelen feladatnaplók", "viewFullLogs": "Teljes naplók megtekintése" } @@ -116,10 +121,10 @@ "toaster": { "error": { "description": "A Dag feldolgozási kérelem meghiúsult. Lehetnek még feldolgozásra váró kérelmek.", - "title": "DAG újrafeldolgozása sikertelen" + "title": "Dag újrafeldolgozása sikertelen" }, "success": { - "description": "A DAG újrafeldolgozása hamarosan.", + "description": "A Dag újrafeldolgozása hamarosan.", "title": "Újrafeldolgozási kérelem sikeresen elküldve" } } diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json index 93a925c7c4d7e..f2cf39a602e13 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/dags.json @@ -21,7 +21,7 @@ "paused": "Szüneteltetett" }, "runIdPatternFilter": "Dag futások keresése", - "triggeringUserNameFilter": "Keresés indító felhasználó alapján" + "triggeringUserNameFilter": "Keresés indító felhasználó szerint" }, "ownerLink": "Tulajdonosi hivatkozás: {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json index 7134f0e2709fe..b211e2fa89cb4 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json @@ -8,6 +8,8 @@ }, "requiredAction_one": "Szükséges művelet", "requiredAction_other": "Szükséges műveletek", + "requiredActionCount_one": "Szükséges teendők ({{count}})", + "requiredActionCount_other": "Szükséges teendők ({{count}})", "requiredActionState": "Szükséges művelet állapota", "response": { "error": "Válasz sikertelen", @@ -23,6 +25,7 @@ "approvalRequired": "Jóváhagyás szükséges", "choiceReceived": "Választás megérkezett", "choiceRequired": "Választás szükséges", + "noResponseReceived": "Nem érkezett válasz", "rejectionReceived": "Elutasítás megérkezett", "responseReceived": "Válasz megérkezett", "responseRequired": "Válasz szükséges" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/admin.json new file mode 100644 index 0000000000000..2937ba21fd63a --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/admin.json @@ -0,0 +1,182 @@ +{ + "columns": { + "description": "Descrizione", + "key": "Chiave", + "name": "Nome", + "value": "Valore" + }, + "config": { + "columns": { + "section": "Sezione" + }, + "title": "Configurazione di Airflow" + }, + "connections": { + "add": "Aggiungi Connessione", + "columns": { + "connectionId": "ID Connessione", + "connectionType": "Tipo di Connessione", + "host": "Host", + "port": "Port" + }, + "connection_many": "Connessioni", + "connection_one": "Connessione", + "connection_other": "Connessioni", + "connection_zero": "Nessuna connessione", + "delete": { + "deleteConnection_many": "Elimina {{count}} connessioni", + "deleteConnection_one": "Elimina 1 connessione", + "deleteConnection_other": "Elimina {{count}} connessioni", + "deleteConnection_zero": "Nessuna connessione da eliminare", + "firstConfirmMessage_many": "Stai per eliminare le seguenti connessioni:", + "firstConfirmMessage_one": "Stai per eliminare la seguente connessione:", + "firstConfirmMessage_other": "Stai per eliminare le seguenti connessioni:", + "firstConfirmMessage_zero": "Nessuna connessione da eliminare:", + "title": "Elimina Connessione" + }, + "edit": "Modifica Connessione", + "form": { + "connectionIdRequired": "L'ID della connessione è obbligatorio", + "connectionIdRequirement": "L'ID della connessione non può contenere solo spazi", + "connectionTypeRequired": "Il tipo di connessione è obbligatorio", + "extraFields": "Campi Extra", + "extraFieldsJson": "Campi Extra JSON", + "helperText": "Il tipo di connessione è mancante? Assicurati di aver installato il pacchetto corrispondente di Airflow Providers.", + "helperTextForRedactedFields": "I campi redatti ('***') rimarranno invariati se non modificati.", + "selectConnectionType": "Seleziona Tipo di Connessione", + "standardFields": "Campi Standard" + }, + "nothingFound": { + "description": "Le connessioni definite tramite variabili d'ambiente o gestori di segreti non sono elencate qui.", + "documentationLink": "Scopri di più nella documentazione di Airflow.", + "learnMore": "Queste sono risolte in fase di runtime e non sono visibili nell'interfaccia utente.", + "title": "Nessuna connessione trovata!" + }, + "searchPlaceholder": "Cercare Connessioni", + "test": "Test Connessione", + "testDisabled": "La funzione di test della connessione è disabilitata. Per favore contatta un amministratore per abilitarla.", + "typeMeta": { + "error": "Impossibile recuperare il tipo di connessione", + "standardFields": { + "description": "Descrizione", + "host": "Host", + "login": "Login", + "password": "Password", + "port": "Porta", + "url_schema": "Schema" + } + } + }, + "deleteActions": { + "button": "Elimina", + "modal": { + "confirmButton": "Sì, Elimina", + "secondConfirmMessage": "Questa azione è permanente e non può essere annullata.", + "thirdConfirmMessage": "Confermi di voler procedere?" + }, + "selected": "Selezionato", + "tooltip": "Elimina le connessioni selezionate" + }, + "formActions": { + "save": "Salva" + }, + "plugins": { + "columns": { + "source": "Fonte" + }, + "importError_many": "Errori di Importazione Plugin", + "importError_one": "Errore di Importazione Plugin", + "importError_other": "Errori di Importazione Plugin", + "importError_zero": "Nessun errore di importazione plugin", + "searchPlaceholder": "Cercare per file" + }, + "pools": { + "add": "Aggiungi Pool", + "deferredSlotsIncluded": "Slots Deferiti Inclusi", + "delete": { + "title": "Elimina Pool", + "warning": "Questo rimuoverà tutti i metadati relativi al pool e potrebbe influenzare le tare che lo utilizzano." + }, + "edit": "Modifica Pool", + "form": { + "checkbox": "Seleziona per includere le tare deferite quando si calcolano gli slot liberi del pool", + "description": "Descrizione", + "includeDeferred": "Includi Deferiti", + "nameMaxLength": "Il nome può contenere un massimo di 256 caratteri", + "nameRequired": "Il nome è obbligatorio", + "slots": "Slot" + }, + "noPoolsFound": "Nessun pool trovato", + "pool_many": "Pools", + "pool_one": "Pool", + "pool_other": "Pools", + "pool_zero": "Nessun pool", + "searchPlaceholder": "Cercare Pools", + "sort": { + "asc": "Nome (A-Z)", + "desc": "Nome (Z-A)", + "placeholder": "Ordina per" + } + }, + "providers": { + "columns": { + "packageName": "Nome del Pacchetto", + "version": "Versione" + } + }, + "variables": { + "add": "Aggiungi Variabile", + "columns": { + "isEncrypted": "È Crittografata" + }, + "delete": { + "deleteVariable_many": "Elimina {{count}} Variabili", + "deleteVariable_one": "Elimina 1 Variabile", + "deleteVariable_other": "Elimina {{count}} Variabili", + "deleteVariable_zero": "Nessuna variabile da eliminare", + "firstConfirmMessage_many": "Stai per eliminare le seguenti variabili:", + "firstConfirmMessage_one": "Stai per eliminare la seguente variabile:", + "firstConfirmMessage_other": "Stai per eliminare le seguenti variabili:", + "firstConfirmMessage_zero": "Nessuna variabile da eliminare:", + "title": "Elimina Variabile", + "tooltip": "Elimina le variabili selezionate" + }, + "edit": "Modifica Variabile", + "export": "Esporta", + "exportTooltip": "Esporta le variabili selezionate", + "form": { + "invalidJson": "JSON non valido", + "keyMaxLength": "La chiave può contenere un massimo di 250 caratteri", + "keyRequired": "La chiave è obbligatoria", + "valueRequired": "Il valore è obbligatorio" + }, + "import": { + "button": "Importa", + "conflictResolution": "Seleziona la risoluzione dei conflitti delle variabili", + "errorParsingJsonFile": "Errore nell'analisi del file JSON: Carica un file JSON contenente le variabili (es. {\"key\": \"value\", ...}).", + "options": { + "fail": { + "description": "Fallisce l'importazione se vengono rilevate variabili esistenti.", + "title": "Fallisci" + }, + "overwrite": { + "description": "Sovrascrive la variabile in caso di conflitto.", + "title": "Sovrascrivi" + }, + "skip": { + "description": "Salta l'importazione delle variabili che già esistono.", + "title": "Salta" + } + }, + "title": "Importa Variabili", + "upload": "Carica un File JSON", + "uploadPlaceholder": "Carica un file JSON contenente le variabili (es. {\"key\": \"value\", ...})" + }, + "noRowsMessage": "Nessuna variabile trovata", + "searchPlaceholder": "Cercare Chiavi", + "variable_many": "Variabili", + "variable_one": "Variabile", + "variable_other": "Variabili", + "variable_zero": "Nessuna variabile" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/assets.json new file mode 100644 index 0000000000000..bb94a3cf908f1 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/assets.json @@ -0,0 +1,30 @@ +{ + "consumingDags": "Dag Consumatori", + "createEvent": { + "button": "Creare un Evento", + "manual": { + "description": "Crea manualmente un Evento Asset", + "extra": "Extra dell'Evento Asset", + "label": "Manuale" + }, + "materialize": { + "description": "Attiva il Dag upstream di questo asset", + "descriptionWithDag": "Attiva il Dag upstream di questo asset: {{dagName}}", + "label": "Materializza", + "unpauseDag": "Riattiva {{dagName}} al momento del trigger" + }, + "success": { + "manualDescription": "La creazione manuale dell'evento asset è avvenuta con successo.", + "manualTitle": "Evento Asset Creato", + "materializeDescription": "Il Dag upstream {{dagId}} è stato attivato con successo.", + "materializeTitle": "Materializzazione dell'Asset" + }, + "title": "Creare Evento Asset per {{name}}" + }, + "group": "Gruppo", + "lastAssetEvent": "Ultimo Evento Asset", + "name": "Nome", + "producingTasks": "Task Produttori", + "scheduledDags": "Dag Programmati", + "searchPlaceholder": "Cercare Assets" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/browse.json new file mode 100644 index 0000000000000..ace4e7d408b7e --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/browse.json @@ -0,0 +1,22 @@ +{ + "auditLog": { + "columns": { + "event": "Evento", + "extra": "Extra", + "user": "Utente", + "when": "Quando" + }, + "filters": { + "eventType": "Tipo di Evento" + }, + "title": "Registro di Controllo (Logs)" + }, + "xcom": { + "columns": { + "dag": "Dag", + "key": "Chiave", + "value": "Valore" + }, + "title": "XCom" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json new file mode 100644 index 0000000000000..c504aa7f9425a --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json @@ -0,0 +1,340 @@ +{ + "admin": { + "Config": "Configurazione", + "Connections": "Connessioni", + "Plugins": "Plugins", + "Pools": "Pools", + "Providers": "Provider", + "Variables": "Variabili" + }, + "allOperators": "Tutti gli Operatori", + "appearance": { + "appearance": "Aspetto", + "darkMode": "Modalità scura", + "lightMode": "Modalità chiara", + "systemMode": "Segui impostazioni di sistema" + }, + "asset_many": "Assets", + "asset_one": "Asset", + "asset_other": "Assets", + "asset_zero": "Nessun asset", + "assetEvent_many": "Eventi Asset", + "assetEvent_one": "Evento Asset", + "assetEvent_other": "Eventi Asset", + "assetEvent_zero": "Nessun evento asset", + "backfill_many": "Backfills", + "backfill_one": "Backfill", + "backfill_other": "Backfills", + "backfill_zero": "Nessun backfill", + "browse": { + "auditLog": "Registro di Controllo (Logs)", + "requiredActions": "Azioni Richieste", + "xcoms": "XComs" + }, + "collapseAllExtra": "Collassa tutti i json extra", + "collapseDetailsPanel": "Collassa i Dettagli", + "createdAssetEvent_many": "Eventi Asset Creati", + "createdAssetEvent_one": "Evento Asset Creato", + "createdAssetEvent_other": "Eventi Asset Creati", + "createdAssetEvent_zero": "Nessun evento asset creato", + "dag_many": "Dags", + "dag_one": "Dag", + "dag_other": "Dags", + "dag_zero": "Nessun Dag", + "dagDetails": { + "catchup": "Catchup", + "dagRunTimeout": "Timeout del Run del Dag", + "defaultArgs": "Argomenti di Default", + "description": "Descrizione", + "documentation": "Documentazione del Dag", + "fileLocation": "Percorso del File", + "hasTaskConcurrencyLimits": "Ha Limiti di Concorrenza dei Task", + "lastExpired": "Ultimo Scaduto", + "lastParseDuration": "Durata dell'ultimo parsing", + "lastParsed": "Ultimo Parsed", + "latestDagVersion": "Ultima Versione del Dag", + "latestRun": "Ultimo Run del Dag", + "maxActiveRuns": "Numero massimo di Runs attive", + "maxActiveTasks": "Numero massimo di Tasks attive", + "maxConsecutiveFailedDagRuns": "Numero massimo di Runs falliti consecutivi del Dag", + "nextRun": "Prossimo Run", + "owner": "Proprietario", + "params": "Parametri", + "schedule": "Programmazione", + "tags": "Tag" + }, + "dagId": "ID del Dag", + "dagRun": { + "conf": "Conf", + "dagVersions": "Versioni del Dag", + "dataIntervalEnd": "Data Intervallo Fine", + "dataIntervalStart": "Data Intervallo Inizio", + "lastSchedulingDecision": "Ultima Decisione di Programmazione", + "queuedAt": "In Coda il", + "runAfter": "Esegui dopo", + "runType": "Tipo di Run", + "sourceAssetEvent": "Evento Asset di Origine", + "triggeredBy": "Triggered da", + "triggeringUser": "Nome Utente che ha Attivato" + }, + "dagRun_many": "Run del Dag", + "dagRun_one": "Run del Dag", + "dagRun_other": "Run del Dag", + "dagRun_zero": "Nessun run del Dag", + "dagRunId": "ID del Run del Dag", + "dagWarnings": "Avvisi/Errori del Dag", + "defaultToGraphView": "Default to vista grafica", + "defaultToGridView": "Default to vista griglia", + "direction": "Direzione", + "docs": { + "documentation": "Documentazione", + "githubRepo": "Repository GitHub", + "restApiReference": "Riferimento API REST" + }, + "duration": "Durata", + "endDate": "Data di Fine", + "error": { + "back": "Indietro", + "defaultMessage": "Si è verificato un errore imprevisto", + "home": "Home", + "notFound": "Pagina non trovata", + "title": "Errore" + }, + "expand": { + "collapse": "Collassa", + "expand": "Espandi", + "hotkey": "e", + "tooltip": "Premi {{hotkey}} per attivare/disattivare espansione" + }, + "expandAllExtra": "Espandi tutti i json extra", + "expression": { + "all": "Tutti", + "and": "E", + "any": "Qualunque", + "or": "O" + }, + "filter": "Filtro", + "filters": { + "logicalDateFrom": "Data Logica Da", + "logicalDateTo": "Data Logica A", + "runAfterFrom": "Esegui Dopo Da", + "runAfterTo": "Esegui Dopo A" + }, + "logicalDate": "Data Logica", + "logout": "Esci", + "logoutConfirmation": "Stai per uscire dall'applicazione.", + "mapIndex": "Mappa Index", + "modal": { + "cancel": "Annulla", + "confirm": "Conferma", + "delete": { + "button": "Elimina", + "confirmation": "Confermi di voler eliminare {{resourceName}}? Questa azione non può essere annullata." + } + }, + "nav": { + "admin": "Amministrazione", + "assets": "Asset", + "browse": "Sfoglia", + "dags": "Dag", + "docs": "Documentazione", + "home": "Home", + "legacyFabViews": "Legacy View", + "plugins": "Plugins", + "security": "Sicurezza" + }, + "noItemsFound": "Nessun {{modelName}} trovato", + "note": { + "add": "Aggiungi una nota", + "dagRun": "Nota del Run del Dag", + "label": "Note", + "placeholder": "Aggiungi una nota...", + "taskInstance": "Nota dell'Istanza di Task" + }, + "pools": { + "deferred": "In Attesa", + "open": "Aperto", + "pools_many": "Pools", + "pools_one": "Pool", + "pools_other": "Pools", + "pools_zero": "Nessun pool", + "queued": "In Coda", + "running": "In Esecuzione", + "scheduled": "Programmato" + }, + "reset": "Resetta", + "runId": "ID del Run", + "runTypes": { + "asset_triggered": "Asset Triggered", + "backfill": "Backfill", + "manual": "Manuale", + "scheduled": "Programmato" + }, + "scroll": { + "direction": { + "bottom": "In basso", + "top": "In alto" + }, + "tooltip": "Premi {{hotkey}} per scorrere a {{direction}}" + }, + "security": { + "actions": "Azioni", + "permissions": "Permessi", + "resources": "Risorse", + "roles": "Ruoli", + "users": "Utenti" + }, + "selectLanguage": "Seleziona la Lingua", + "showDetailsPanel": "Mostra il Pannello dei Dettagli", + "source": { + "hide": "Nascondi Sorgente", + "hotkey": "s", + "show": "Mostra Sorgente" + }, + "sourceAssetEvent_many": "Eventi Asset di Origine", + "sourceAssetEvent_one": "Evento Asset di Origine", + "sourceAssetEvent_other": "Eventi Asset di Origine", + "sourceAssetEvent_zero": "Nessun evento asset di origine", + "startDate": "Data di Inizio", + "state": "Stato", + "states": { + "deferred": "In Attesa", + "failed": "Fallito", + "no_status": "Nessun Stato", + "none": "Nessun Stato", + "planned": "Pianificato", + "queued": "In Coda", + "removed": "Rimosso", + "restarting": "Riavviato", + "running": "In Esecuzione", + "scheduled": "Programmato", + "skipped": "Saltato", + "success": "Successo", + "up_for_reschedule": "Da Reschedulare", + "up_for_retry": "Da Riavviare", + "upstream_failed": "Upstream Fallito" + }, + "table": { + "completedAt": "Completato il", + "createdAt": "Creato il", + "filterByTag": "Filtra Dag per tag", + "filterColumns": "Filtra le colonne della tabella", + "filterReset_many": "Resetta filtri", + "filterReset_one": "Resetta filtro", + "filterReset_other": "Resetta filtri", + "filterReset_zero": "Nessun filtro da resettare", + "from": "Da", + "maxActiveRuns": "Numero massimo di Runs attive", + "noTagsFound": "Nessun tag trovato", + "tagMode": { + "all": "Tutti", + "any": "Qualunque" + }, + "tagPlaceholder": "Filtra per tag", + "to": "A" + }, + "task": { + "documentation": "Documentazione del Task", + "lastInstance": "Ultima Istanza", + "operator": "Operatore", + "triggerRule": "Regola di Trigger" + }, + "task_many": "Tasks", + "task_one": "Task", + "task_other": "Tasks", + "task_zero": "Nessun task", + "taskGroup": "Gruppo di Task", + "taskId": "ID del Task", + "taskInstance": { + "dagVersion": "Versione del Dag", + "executor": "Executor", + "executorConfig": "Configurazione dell'Executor", + "hostname": "Nome delHost", + "maxTries": "Numero massimo di Tentativi", + "pid": "PID", + "pool": "Pool", + "poolSlots": "Slot del Pool", + "priorityWeight": "Peso di Priorità", + "queue": "Coda", + "queuedWhen": "In Coda il", + "scheduledWhen": "Programmato il", + "triggerer": { + "assigned": "Triggerer Assegnato", + "class": "Classe del Trigger", + "createdAt": "Creato il", + "id": "ID del Trigger", + "latestHeartbeat": "Ultimo Heartbeat del Trigger", + "title": "Info del Trigger" + }, + "unixname": "Nome Unix" + }, + "taskInstance_many": "Istanze di Task", + "taskInstance_one": "Istanza di Task", + "taskInstance_other": "Istanze di Task", + "taskInstance_zero": "Nessuna istanza di task", + "timeRange": { + "last12Hours": "Ultimi 12 Ore", + "last24Hours": "Ultimi 24 Ore", + "lastHour": "Ultima Ora", + "pastWeek": "Settimana Scorsa" + }, + "timestamp": { + "hide": "Nascondi Timestamp", + "hotkey": "t", + "show": "Mostra Timestamp" + }, + "timezone": "Fuso Orario", + "timezoneModal": { + "current-timezone": "Ora corrente in", + "placeholder": "Seleziona un fuso orario", + "title": "Seleziona il Fuso Orario", + "utc": "UTC (Tempo Coordinato Universale)" + }, + "toaster": { + "bulkDelete": { + "error": "Eliminazione in Massa di {{resourceName}} Fallita", + "success": { + "description": "{{count}} {{resourceName}} sono stati eliminati con successo. Chiavi: {{keys}}", + "title": "Eliminazione in Massa di {{resourceName}} Richiesta Inviata" + } + }, + "create": { + "error": "Creazione di {{resourceName}} Fallita", + "success": { + "description": "{{resourceName}} è stato creato con successo.", + "title": "Creazione di {{resourceName}} Richiesta Inviata" + } + }, + "delete": { + "error": "Eliminazione di {{resourceName}} Fallita", + "success": { + "description": "{{resourceName}} è stato eliminato con successo.", + "title": "Eliminazione di {{resourceName}} Richiesta Inviata" + } + }, + "import": { + "error": "Importazione di {{resourceName}} Fallita", + "success": { + "description": "{{count}} {{resourceName}} sono stati importati con successo.", + "title": "Importazione di {{resourceName}} Richiesta Inviata" + } + }, + "update": { + "error": "Aggiornamento di {{resourceName}} Fallita", + "success": { + "description": "{{resourceName}} è stato aggiornato con successo.", + "title": "Aggiornamento di {{resourceName}} Richiesta Inviata" + } + } + }, + "total": "Totale {{state}}", + "triggered": "Triggered", + "tryNumber": "Numero di Tentativo", + "user": "Utente", + "wrap": { + "hotkey": "w", + "tooltip": "Premi {{hotkey}} per abilitare il wrap", + "unwrap": "Disabilita il wrap", + "wrap": "Abilita il wrap" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json new file mode 100644 index 0000000000000..3978d69905b7e --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/components.json @@ -0,0 +1,152 @@ +{ + "backfill": { + "affected_many": "{{count}} runs verranno attivati.", + "affected_one": "1 run verrà attivato.", + "affected_other": "{{count}} runs verranno attivati.", + "affected_zero": "Nessun run verrà attivato.", + "affectedNone": "Nessun run corrispondente ai criteri selezionati.", + "allRuns": "Tutti i Run", + "backwards": "Run all'indietro", + "dateRange": "Intervallo di Date", + "errorStartDateBeforeEndDate": "La Data di Inizio deve essere precedente alla Data di Fine", + "maxRuns": "Numero massimo di Runs attive", + "missingAndErroredRuns": "Run Mancanti ed Errati", + "missingRuns": "Run Mancanti", + "reprocessBehavior": "Comportamento di Reprocesso", + "run": "Correre Backfill", + "selectDescription": "Esegui questo Dag per un intervallo di date", + "selectLabel": "Backfill", + "title": "Correre Backfill", + "toaster": { + "success": { + "description": "Backfill jobs sono stati attivati con successo.", + "title": "Backfill generato" + } + }, + "tooltip": "Backfill richiede un programma", + "unpause": "Sospendi {{dag_display_name}} al trigger", + "validation": { + "datesRequired": "Devi fornire sia la Data di Inizio che la Data di Fine.", + "startBeforeEnd": "La Data di Inizio deve essere precedente o uguale alla Data di Fine." + } + }, + "banner": { + "backfillInProgress": "Backfill in corso", + "cancel": "Annulla backfill", + "pause": "Sospendi backfill", + "unpause": "Sospendi backfill" + }, + "clipboard": { + "copy": "Copia" + }, + "close": "Chiudi", + "configForm": { + "advancedOptions": "Opzioni Avanzate", + "configJson": "Configurazione JSON", + "invalidJson": "Formato JSON non valido: {{errorMessage}}" + }, + "dagWarnings": { + "error_many": "{{count}} Errori", + "error_one": "1 Errore", + "error_other": "{{count}} Errori", + "error_zero": "Nessun errore", + "errorAndWarning": "1 Errore e {{warningText}}", + "warning_many": "{{count}} Avvisi", + "warning_one": "1 Avviso", + "warning_other": "{{count}} Avvisi", + "warning_zero": "Nessun avviso" + }, + "durationChart": { + "duration": "Durata (secondi)", + "lastDagRun_many": "Ultimi {{count}} Runs del Dag", + "lastDagRun_one": "Ultimo Run del Dag", + "lastDagRun_other": "Ultimi {{count}} Runs del Dag", + "lastDagRun_zero": "Nessun run del Dag", + "lastTaskInstance_many": "Ultime {{count}} Istanze di Task", + "lastTaskInstance_one": "Ultima Istanza di Task", + "lastTaskInstance_other": "Ultime {{count}} Istanze di Task", + "lastTaskInstance_zero": "Nessuna istanza di task", + "queuedDuration": "Durata in coda", + "runAfter": "Esegui dopo", + "runDuration": "Durata del Run" + }, + "fileUpload": { + "files_many": "{{count}} file", + "files_one": "1 file", + "files_other": "{{count}} file", + "files_zero": "Nessun file" + }, + "flexibleForm": { + "placeholder": "Seleziona Valore", + "placeholderArray": "Inserisci ogni stringa su una nuova riga", + "placeholderExamples": "Inizia a digitare per vedere le opzioni", + "placeholderMulti": "Seleziona uno o più valori", + "validationErrorArrayNotArray": "Il valore deve essere un array.", + "validationErrorArrayNotNumbers": "Tutti gli elementi dell'array devono essere numeri.", + "validationErrorArrayNotObject": "Tutti gli elementi dell'array devono essere oggetti.", + "validationErrorRequired": "Questo campo è obbligatorio" + }, + "graph": { + "directionDown": "Da Superiore a Inferiore", + "directionLeft": "Da Destra a Sinistra", + "directionRight": "Da Sinistra a Destra", + "directionUp": "Da Inferiore a Superiore", + "downloadImage": "Scarica l'immagine del grafico", + "downloadImageError": "Impossibile scaricare l'immagine del grafico.", + "downloadImageErrorTitle": "Scaricamento Fallito", + "otherDagRuns": "+Altri Runs del Dag", + "taskCount_many": "{{count}} Tasks", + "taskCount_one": "{{count}} Task", + "taskCount_other": "{{count}} Tasks", + "taskCount_zero": "Nessun task", + "taskGroup": "Task Group" + }, + "limitedList": "+{{count}} altro", + "limitedList.allItems": "Tutti i {{count}} elementi:", + "limitedList.clickToInteract": "Fai clic su un'etichetta per filtrare i Dag", + "limitedList.clickToOpenFull": "Fai clic su \"+{{count}} altro\" per visualizzare tutto", + "limitedList.copyPasteText": "Puoi copiare e incollare il testo sopra", + "logs": { + "file": "File", + "location": "linea {{line}} in {{name}}" + }, + "reparseDag": "Reparse Dag", + "sortedAscending": "ordinato in ordine crescente", + "sortedDescending": "ordinato in ordine decrescente", + "sortedUnsorted": "non ordinato", + "taskTries": "Tentativi di Task", + "toggleCardView": "Mostra la vista in card", + "toggleTableView": "Mostra la vista in tabella", + "triggerDag": { + "button": "Trigger", + "loading": "Caricamento informazioni del Dag...", + "loadingFailed": "Impossibile caricare le informazioni del Dag. Per favore, riprova.", + "runIdHelp": "Opzionale - verrà generato se non fornito", + "selectDescription": "Triggera un singolo run di questo Dag", + "selectLabel": "Singolo Run", + "title": "Trigger del Dag", + "toaster": { + "success": { + "description": "Il run del Dag è stato attivato con successo.", + "title": "Run del Dag attivato" + } + }, + "unpause": "Sospendi {{dagDisplayName}} al trigger" + }, + "trimText": { + "details": "Dettagli", + "empty": "Vuoto", + "noContent": "Nessun contenuto disponibile." + }, + "versionDetails": { + "bundleLink": "Link del Bundle", + "bundleName": "Nome del Bundle", + "bundleVersion": "Versione del Bundle", + "createdAt": "Creato il", + "versionId": "ID della Versione" + }, + "versionSelect": { + "dagVersion": "Versione del Dag", + "versionCode": "v{{versionCode}}" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/dag.json new file mode 100644 index 0000000000000..90d6bdd6dc190 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/dag.json @@ -0,0 +1,162 @@ +{ + "allRuns": "Tutti i Run", + "blockingDeps": { + "dependency": "Dipendenza", + "reason": "Motivo", + "title": "Dipendenze che bloccano la Task da essere pianificata" + }, + "calendar": { + "daily": "Giornaliero", + "hourly": "Orario", + "legend": { + "less": "Meno", + "more": "Più" + }, + "navigation": { + "nextMonth": "Mese successivo", + "nextYear": "Anno successivo", + "previousMonth": "Mese precedente", + "previousYear": "Anno precedente" + }, + "noData": "Nessun dato disponibile", + "noRuns": "Nessun run", + "totalRuns": "Run Totali", + "week": "Settimana {{weekNumber}}", + "weekdays": { + "friday": "Ven", + "monday": "Lun", + "saturday": "Sab", + "sunday": "Dom", + "thursday": "Gio", + "tuesday": "Mar", + "wednesday": "Mer" + } + }, + "code": { + "bundleUrl": "URL del Bundle", + "noCode": "Nessun Codice Trovato", + "parseDuration": "Durata del parsing", + "parsedAt": "Parsato il:" + }, + "extraLinks": "Extra Links", + "grid": { + "buttons": { + "resetToLatest": "Resetta al più recente", + "toggleGroup": "Attiva/Disattiva gruppo" + } + }, + "header": { + "buttons": { + "advanced": "Avanzato", + "dagDocs": "Documentazione sul Dag" + } + }, + "logs": { + "allLevels": "Tutti i Livelli di Log", + "allSources": "Tutte le Fonti", + "critical": "CRITICO", + "debug": "DEBUG", + "error": "ERRORE", + "fullscreen": { + "button": "Schermo Intero", + "tooltip": "Premi {{hotkey}} per schermo intero" + }, + "info": "INFO", + "noTryNumber": "Nessun numero di tentativo", + "settings": "Impostazioni Log", + "viewInExternal": "Visualizza i logs in {{name}} (tentativo {{attempt}})", + "warning": "AVVISO" + }, + "navigation": { + "navigation": "Navigazione: Shift+{{arrow}}", + "toggleGroup": "Attiva/Disattiva gruppo: Spazio" + }, + "overview": { + "buttons": { + "failedRun_many": "Run falliti", + "failedRun_one": "Run fallito", + "failedRun_other": "Run falliti", + "failedRun_zero": "Nessun Run fallito", + "failedTask_many": "Task falliti", + "failedTask_one": "Task fallito", + "failedTask_other": "Task falliti", + "failedTask_zero": "Nessun Task fallito", + "failedTaskInstance_many": "Istanze di Task fallite", + "failedTaskInstance_one": "Istanza di Task fallita", + "failedTaskInstance_other": "Istanze di Task fallite", + "failedTaskInstance_zero": "Nessuna istanza di Task fallita" + }, + "charts": { + "assetEvent_many": "Eventi di Asset Creati", + "assetEvent_one": "Evento di Asset Creato", + "assetEvent_other": "Eventi di Asset Creati", + "assetEvent_zero": "Nessun evento di asset creato" + }, + "failedLogs": { + "hideLogs": "Nascondi Logs", + "showLogs": "Mostra Logs", + "title": "Logs delle Task fallite recenti", + "viewFullLogs": "Visualizza i logs completi" + } + }, + "panel": { + "buttons": { + "options": "Opzioni", + "showGantt": "Mostra Gantt", + "showGraphShortcut": "Mostra Grafico (Premi g)", + "showGridShortcut": "Mostra Griglia (Premi g)" + }, + "dagRuns": { + "label": "Numero di Runs del Dag" + }, + "dependencies": { + "label": "Dipendenze", + "options": { + "allDagDependencies": "Tutte le Dipendenze del Dag", + "externalConditions": "Condizioni Esterne", + "onlyTasks": "Solo le Task" + }, + "placeholder": "Dipendenze" + }, + "graphDirection": { + "label": "Direzione del Grafico" + } + }, + "paramsFailed": "Impossibile caricare i parametri", + "parse": { + "toaster": { + "error": { + "description": "La richiesta di parsing del Dag è fallita. Potrebbero esserci richieste di parsing in sospeso da processare.", + "title": "Parsing del Dag fallito" + }, + "success": { + "description": "Il Dag dovrebbe essere riparsat presto.", + "title": "Richiesta di riparsing inviata con successo" + } + } + }, + "tabs": { + "assetEvents": "Eventi di Asset", + "auditLog": "Registro di Controllo (Logs)", + "backfills": "Backfills", + "calendar": "Calendario", + "code": "Codice", + "details": "Dettagli", + "logs": "Logs", + "mappedTaskInstances_many": "Istanze di Task [{{count}}]", + "mappedTaskInstances_one": "Istanza di Task [{{count}}]", + "mappedTaskInstances_other": "Istanze di Task [{{count}}]", + "mappedTaskInstances_zero": "Nessuna istanza di task mappata", + "overview": "Panoramica", + "renderedTemplates": "Template Renderizzato", + "requiredActions": "Azioni Richieste", + "runs": "Run", + "taskInstances": "Istanze di Task", + "tasks": "Tasks", + "xcom": "XCom" + }, + "taskGroups": { + "collapseAll": "Collassa tutti i gruppi di task", + "expandAll": "Espandi tutti i gruppi di task" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json new file mode 100644 index 0000000000000..6c6ab552ddc9c --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/dags.json @@ -0,0 +1,96 @@ +{ + "assetSchedule": "{{count}} di {{total}} assets aggiornati", + "dagActions": { + "delete": { + "button": "Elimina Dag", + "warning": "Questo rimuoverà tutti i metadati relativi al Dag, compresi i Run e le Task." + } + }, + "favoriteDag": "Dag Preferito", + "filters": { + "allRunTypes": "Tutti i Tipi di Run", + "allStates": "Tutti gli Stati", + "favorite": { + "all": "Tutti", + "favorite": "Preferito", + "unfavorite": "Rimuovi dai preferiti" + }, + "paused": { + "active": "Attivo", + "all": "Tutti", + "paused": "In Pausa" + }, + "runIdPatternFilter": "Cercare Dag Runs" + }, + "ownerLink": "Link dell'Owner per {{owner}}", + "runAndTaskActions": { + "affectedTasks": { + "noItemsFound": "Nessuna task trovata.", + "title": "Task interessati: {{count}}" + }, + "clear": { + "button": "Pulisci {{type}}", + "buttonTooltip": "Premi shift+c per pulire", + "error": "Impossibile pulire {{type}}", + "title": "Pulisci {{type}}" + }, + "delete": { + "button": "Elimina {{type}}", + "dialog": { + "resourceName": "{{type}} {{id}}", + "title": "Elimina {{type}}", + "warning": "Questo rimuoverà tutti i metadati relativi a {{type}}." + }, + "error": "Errore nell'eliminazione di {{type}}", + "success": { + "description": "La richiesta di eliminazione di {{type}} è stata completata con successo.", + "title": "{{type}} Eliminato con Successo" + } + }, + "markAs": { + "button": "Marca {{type}} come...", + "buttonTooltip": { + "failed": "Premi shift+f per marcare come fallito", + "success": "Premi shift+s per marcare come successo" + }, + "title": "Marca {{type}} come {{state}}" + }, + "options": { + "downstream": "Downstream", + "existingTasks": "Pulisci le task esistenti", + "future": "Futuro", + "onlyFailed": "Pulisci solo le task fallite", + "past": "Passato", + "queueNew": "Inserisci nuove task", + "runOnLatestVersion": "Esegui con l'ultima versione del bundle", + "upstream": "Upstream" + } + }, + "search": { + "advanced": "RiCercare Avanzata", + "clear": "Pulisci la ricerca", + "dags": "Cercare Dag", + "hotkey": "+K", + "tasks": "Cercare Task" + }, + "sort": { + "displayName": { + "asc": "Ordina per Nome (A-Z)", + "desc": "Ordina per Nome (Z-A)" + }, + "lastRunStartDate": { + "asc": "Ordina per Data di Inizio del Run più Recente (Prima-Ultimo)", + "desc": "Ordina per Data di Inizio del Run più Recente (Ultimo-Prima)" + }, + "lastRunState": { + "asc": "Ordina per Stato del Run più Recente (A-Z)", + "desc": "Ordina per Stato del Run più Recente (Z-A)" + }, + "nextDagRun": { + "asc": "Ordina per Next Dag Run (Prima-Ultimo)", + "desc": "Ordina per Next Dag Run (Ultimo-Prima)" + }, + "placeholder": "Ordina per" + }, + "unfavoriteDag": "Rimuovi Dag dai Preferiti" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/dashboard.json new file mode 100644 index 0000000000000..8533c24047a49 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/dashboard.json @@ -0,0 +1,49 @@ +{ + "favorite": { + "favoriteDags_many": "{{count}} Dags preferiti", + "favoriteDags_one": "Primo {{count}} Dag preferito", + "favoriteDags_other": "Primi {{count}} Dags preferiti", + "favoriteDags_zero": "Nessun Dag preferito", + "noDagRuns": "Non ci sono ancora DagRun per questo dag.", + "noFavoriteDags": "Nessun preferito ancora. Clicca sull'icona stella accanto a un Dag nella lista per aggiungerlo ai tuoi preferiti." + }, + "group": "Gruppo", + "health": { + "dagProcessor": "Processore del Dag", + "health": "Salute", + "healthy": "Sano", + "lastHeartbeat": "Ultimo Heartbeat", + "metaDatabase": "Database di metadati", + "scheduler": "Pianificatrice", + "status": "Stato", + "triggerer": "Triggerer", + "unhealthy": "Malato" + }, + "history": "Cronologia", + "importErrors": { + "dagImportError_many": "Errori di Importazione Dag", + "dagImportError_one": "Errore di Importazione Dag", + "dagImportError_other": "Errori di Importazione Dag", + "dagImportError_zero": "Nessun errore di importazione Dag", + "searchByFile": "Cercare per file", + "timestamp": "Timestamp" + }, + "managePools": "Gestisci Pools", + "noAssetEvents": "Nessun evento di asset trovato.", + "poolSlots": "Slot del Pool", + "sortBy": { + "newestFirst": "Più Recenti", + "oldestFirst": "Più Vecchi" + }, + "source": "Fonte", + "stats": { + "activeDags": "Dags Attivi", + "failedDags": "Dags Falliti", + "queuedDags": "Dags In Attesa", + "requiredActions": "Azioni Richieste", + "runningDags": "Dags In Esecuzione", + "stats": "Statistiche" + }, + "uri": "Uri", + "welcome": "Benvenuto" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/hitl.json new file mode 100644 index 0000000000000..5a76d6035d7e8 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/hitl.json @@ -0,0 +1,38 @@ +{ + "filters": { + "response": { + "all": "Tutte", + "pending": "In attesa", + "received": "Ricevute" + } + }, + "requiredAction_many": "Azioni richieste", + "requiredAction_one": "Azione richiesta", + "requiredAction_other": "Azioni richieste", + "requiredAction_zero": "Nessuna azione richiesta", + "requiredActionCount_many": "Azioni richieste ({{count}})", + "requiredActionCount_one": "Azione richiesta ({{count}})", + "requiredActionCount_other": "Azioni richieste ({{count}})", + "requiredActionCount_zero": "Nessuna azione richiesta", + "requiredActionState": "Stato dell'Azione richiesta", + "response": { + "error": "Errore nella Risposta", + "optionsDescription": "Scegli le tue opzioni per questa istanza di task", + "optionsLabel": "Opzioni", + "received": "Risposta ricevuta il ", + "respond": "Rispondi", + "success": "Risposta di {{taskId}} riuscita", + "title": "Istanza di Task Umana - {{taskId}}" + }, + "state": { + "approvalReceived": "Approvazione ricevuta", + "approvalRequired": "Approvazione richiesta", + "choiceReceived": "Scelta ricevuta", + "choiceRequired": "Scelta richiesta", + "noResponseReceived": "Nessuna risposta ricevuta", + "rejectionReceived": "Rifiuto ricevuto", + "responseReceived": "Risposta ricevuta", + "responseRequired": "Risposta richiesta" + }, + "subject": "Oggetto" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/it/tasks.json new file mode 100644 index 0000000000000..eff98e212b664 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "Mappato", + "notMapped": "Non mappato", + "retries": "Tentativi", + "searchTasks": "Cercare attività", + "selectMapped": "Seleziona mappato", + "selectOperator": "Seleziona operatore", + "selectRetryValues": "Seleziona valori di Retry", + "selectTriggerRules": "Seleziona regole di Trigger" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/admin.json new file mode 100644 index 0000000000000..3e80a92678106 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/admin.json @@ -0,0 +1,172 @@ +{ + "columns": { + "description": "説明", + "key": "キー", + "name": "名前", + "value": "値" + }, + "config": { + "columns": { + "section": "セクション" + }, + "title": "Airflow 設定" + }, + "connections": { + "add": "接続を追加", + "columns": { + "connectionId": "接続 ID", + "connectionType": "接続タイプ", + "host": "ホスト", + "port": "ポート" + }, + "connection_one": "接続", + "connection_other": "接続", + "delete": { + "deleteConnection_one": "1 件の接続を削除", + "deleteConnection_other": "{{count}} 件の接続を削除", + "firstConfirmMessage_one": "以下の接続を削除します", + "firstConfirmMessage_other": "以下の接続を削除します", + "title": "接続を削除" + }, + "edit": "接続を編集", + "form": { + "connectionIdRequired": "接続 ID は必須です", + "connectionIdRequirement": "空白文字だけの接続 ID は認められません", + "connectionTypeRequired": "接続タイプは必須です", + "extraFields": "追加項目", + "extraFieldsJson": "追加項目 (JSON 形式)", + "helperText": "接続タイプが見つかりませんか?対応する Airflow Providers パッケージがインストールされていることを確認してください", + "helperTextForRedactedFields": "非表示の項目 ('***') は、編集しなければ以前の値がそのまま使われます", + "selectConnectionType": "接続タイプを選ぶ", + "standardFields": "標準項目" + }, + "nothingFound": { + "description": "環境変数やシークレットマネージャによって定義された接続は表示されません", + "documentationLink": "Airflow のドキュメントで詳細を確認する", + "learnMore": "これらは実行時に解決され、UI からは確認できません", + "title": "接続が見つかりません" + }, + "searchPlaceholder": "接続を検索", + "test": "接続をテストする", + "testDisabled": "接続をテストする機能は無効化されています。有効化するには管理者に問い合わせてください", + "testError": { + "title": "接続テストに失敗しました" + }, + "testSuccess": { + "title": "接続テストに成功しました" + }, + "typeMeta": { + "error": "接続タイプの取得に失敗しました", + "standardFields": { + "description": "説明", + "host": "ホスト", + "login": "ログイン名", + "password": "パスワード", + "port": "ポート", + "url_schema": "スキーマ" + } + } + }, + "deleteActions": { + "button": "削除", + "modal": { + "confirmButton": "はい、削除します", + "secondConfirmMessage": "このアクションは永続的で取り消せません", + "thirdConfirmMessage": "本当に実行しますか?" + }, + "selected": "選択済み", + "tooltip": "選択された接続を削除" + }, + "formActions": { + "save": "保存" + }, + "plugins": { + "columns": { + "source": "ソース" + }, + "importError_one": "プラグインのインポートエラー", + "importError_other": "プラグインのインポートエラー", + "searchPlaceholder": "ファイルで検索" + }, + "pools": { + "add": "プールを追加", + "deferredSlotsIncluded": "遅延スロットも含める", + "delete": { + "title": "プールを削除", + "warning": "プールに関連するすべてのメタデータを削除します。プールを使用中のタスクに影響する可能性があります" + }, + "edit": "プールを編集", + "form": { + "checkbox": "空きスロットの計算に遅延タスクも含める", + "description": "説明", + "includeDeferred": "遅延も含める", + "nameMaxLength": "名前の最大長は 256 文字です", + "nameRequired": "名前は必須です", + "slots": "スロット" + }, + "noPoolsFound": "プールが見つかりません", + "pool_one": "プール", + "pool_other": "プール", + "searchPlaceholder": "プールを検索", + "sort": { + "asc": "名前 (A-Z)", + "desc": "名前 (Z-A)", + "placeholder": "以下のキーで並べ替え" + } + }, + "providers": { + "columns": { + "packageName": "パッケージ名", + "version": "バージョン" + } + }, + "variables": { + "add": "変数を追加", + "columns": { + "isEncrypted": "暗号化" + }, + "delete": { + "deleteVariable_one": "1 件の変数を削除", + "deleteVariable_other": "{{count}} 件の変数を削除", + "firstConfirmMessage_one": "以下の変数を削除します", + "firstConfirmMessage_other": "以下の変数を削除します", + "title": "変数を削除", + "tooltip": "選択された変数を削除" + }, + "edit": "変数を編集", + "export": "エキスポート", + "exportTooltip": "選択した変数をエキスポート", + "form": { + "invalidJson": "不正な形式の JSON", + "keyMaxLength": "キーの最大長は 250 文字です", + "keyRequired": "キーは必須です", + "valueRequired": "値は必須です" + }, + "import": { + "button": "インポート", + "conflictResolution": "変数名が衝突した場合の解決方法", + "errorParsingJsonFile": "JSON ファイルの解析に失敗しました。変数を含む JSON ファイルをアップロードしてください (例: {\"key\": \"value\", ...})", + "options": { + "fail": { + "description": "同名の変数が存在した場合はインポートに失敗します", + "title": "失敗" + }, + "overwrite": { + "description": "同名の変数が存在した場合は上書きします", + "title": "上書き" + }, + "skip": { + "description": "同名の変数が存在した場合はスキップします", + "title": "スキップ" + } + }, + "title": "変数をインポート", + "upload": "JSON ファイルをアップロード", + "uploadPlaceholder": "変数を含む JSON ファイルをアップロード (例: {\"key\": \"value\", ...})" + }, + "noRowsMessage": "変数が見つかりません", + "searchPlaceholder": "キーを検索", + "variable_one": "変数", + "variable_other": "変数" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/assets.json new file mode 100644 index 0000000000000..a5ef07f71095b --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/assets.json @@ -0,0 +1,32 @@ +{ + "consumingDags": "Consuming Dags", + "consumingTasks": "Consuming Tasks", + "createEvent": { + "button": "イベントを作成", + "manual": { + "description": "アセットイベントを手動で作成", + "extra": "アセットイベントの追加情報", + "label": "手動" + }, + "materialize": { + "description": "このアセットの上流 Dag をトリガー", + "descriptionWithDag": "このアセットの上流 Dag をトリガー: {{dagName}}", + "label": "実体化", + "unpauseDag": "トリガー時に {{dagName}} の停止を解除" + }, + "success": { + "manualDescription": "アセットイベントの手動作成に成功しました", + "manualTitle": "アセットイベントを作成しました", + "materializeDescription": "上流の Dag {{dagId}} は正常にトリガーされました", + "materializeTitle": "アセットを実体化" + }, + "title": "{{name}} 用のアセットイベントを作成" + }, + "extra": "追加情報", + "group": "グループ", + "lastAssetEvent": "最終アセットイベント", + "name": "名前", + "producingTasks": "タスクを生成", + "scheduledDags": "スケジュール済 Dag", + "searchPlaceholder": "アセットを検索" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/browse.json new file mode 100644 index 0000000000000..86b6d705a3125 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/browse.json @@ -0,0 +1,22 @@ +{ + "auditLog": { + "columns": { + "event": "イベント", + "extra": "追加情報", + "user": "ユーザ", + "when": "日時" + }, + "filters": { + "eventType": "イベントタイプ" + }, + "title": "監査ログ" + }, + "xcom": { + "columns": { + "dag": "Dag", + "key": "キー", + "value": "値" + }, + "title": "XCom" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json new file mode 100644 index 0000000000000..412ba70bfe060 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json @@ -0,0 +1,324 @@ +{ + "admin": { + "Config": "設定", + "Connections": "接続", + "Plugins": "プラグイン", + "Pools": "プール", + "Providers": "Providers", + "Variables": "変数" + }, + "allOperators": "全オペレータ", + "appearance": { + "appearance": "外観モード", + "darkMode": "ダークモード", + "lightMode": "ライトモード", + "systemMode": "OS 設定に合わせる" + }, + "asset_one": "アセット", + "asset_other": "アセット", + "assetEvent_one": "アセットのイベント", + "assetEvent_other": "アセットのイベント", + "backfill_one": "過去分の再実行", + "backfill_other": "過去分の再実行", + "browse": { + "auditLog": "監査ログ", + "requiredActions": "要対応項目", + "xcoms": "XComs" + }, + "collapseAllExtra": "追加情報の JSON を閉じる", + "collapseDetailsPanel": "詳細パネルを閉じる", + "createdAssetEvent_one": "作成したアセットイベント", + "createdAssetEvent_other": "作成したアセットイベント", + "dag_one": "Dag", + "dag_other": "Dags", + "dagDetails": { + "catchup": "遅延分の追跡実行", + "dagRunTimeout": "Dag 実行タイムアウト", + "defaultArgs": "デフォルトパラメータ", + "description": "説明", + "documentation": "Dag ドキュメント", + "fileLocation": "保存先", + "hasTaskConcurrencyLimits": "タスク同時実行制限", + "lastExpired": "最終期限切れ日時", + "lastParseDuration": "最終パース時間", + "lastParsed": "最終パース日時", + "latestDagVersion": "最新の Dag バージョン", + "latestRun": "最終実行日時", + "maxActiveRuns": "最大アクティブ実行数", + "maxActiveTasks": "最大アクティブタスク数", + "maxConsecutiveFailedDagRuns": "連続失敗 Dag 実行の最大回数", + "nextRun": "次の実行", + "owner": "オーナー", + "params": "パラメータ", + "schedule": "スケジュール", + "tags": "タグ" + }, + "dagId": "Dag ID", + "dagRun": { + "conf": "設定", + "dagVersions": "Dag バージョン", + "dataIntervalEnd": "データ期間の終了", + "dataIntervalStart": "データ期間の開始", + "lastSchedulingDecision": "最終スケジューリング決定時刻", + "queuedAt": "キュー登録時刻", + "runAfter": "最早実行可能時間", + "runType": "実行タイプ", + "sourceAssetEvent": "元アセットのイベント", + "triggeredBy": "実行トリガー", + "triggeringUser": "トリガーユーザ" + }, + "dagRun_one": "Dag 実行", + "dagRun_other": "Dag 実行", + "dagRunId": "Dag 実行 ID", + "dagWarnings": "Dag 実行警告/エラー", + "defaultToGraphView": "グラフ表示を既定にします", + "defaultToGridView": "グリッド表示を既定にします", + "direction": "方向", + "docs": { + "documentation": "ドキュメント", + "githubRepo": "GitHub リポジトリ", + "restApiReference": "REST API 仕様書" + }, + "download": { + "download": "ダウンロード", + "hotkey": "d", + "tooltip": "ログをダウンロードするには {{hotkey}} キーを押してください" + }, + "duration": "実行時間", + "endDate": "終了日", + "error": { + "back": "戻る", + "defaultMessage": "予期せぬエラーが発生しました", + "home": "ホーム", + "invalidUrl": "ページが見つかりません。URLを確認して再度お試しください", + "notFound": "ページが見つかりません", + "title": "エラー" + }, + "expand": { + "collapse": "閉じる", + "expand": "展開する", + "hotkey": "e", + "tooltip": "切り替えるには {{hotkey}} を押してください" + }, + "expandAllExtra": "追加情報の JSON を展開", + "expression": { + "all": "すべて", + "and": "かつ", + "any": "いずれか", + "or": "または" + }, + "filter": "フィルタ", + "filters": { + "logicalDateFrom": "論理開始日", + "logicalDateTo": "論理終了日", + "runAfterFrom": "実行開始日時の指定", + "runAfterTo": "実行終了日時の指定" + }, + "logicalDate": "論理日付", + "logout": "ログアウト", + "logoutConfirmation": "ログアウトしてもよろしいですか", + "mapIndex": "Map Index", + "modal": { + "cancel": "キャンセル", + "confirm": "確認", + "delete": { + "button": "削除", + "confirmation": "{{resourceName}} を削除してもよろしいですか。この操作は元に戻せません" + } + }, + "nav": { + "admin": "管理", + "assets": "アセット", + "browse": "閲覧", + "dags": "Dags", + "docs": "ヘルプ", + "home": "ホーム", + "legacyFabViews": "旧式ビュー", + "plugins": "プラグイン", + "security": "セキュリティ" + }, + "noItemsFound": "{{modelName}} が見つかりません", + "note": { + "add": "ノートの追加", + "dagRun": "Dag 実行ノート", + "label": "ノート", + "placeholder": "ノートの追加...", + "taskInstance": "タスクインスタンスノート" + }, + "pools": { + "deferred": "延期済", + "open": "開く", + "pools_one": "プール", + "pools_other": "プール", + "queued": "待機中", + "running": "実行中", + "scheduled": "スケジュール済" + }, + "reset": "リセット", + "runId": "実行 ID", + "runTypes": { + "asset_triggered": "トリガー済アセット", + "backfill": "過去分の再実行", + "manual": "手動", + "scheduled": "スケジュール済" + }, + "scroll": { + "direction": { + "bottom": "下", + "top": "上" + }, + "tooltip": "{{direction}} に動かすには {{hotkey}} を押してください" + }, + "security": { + "actions": "アクション", + "permissions": "権限", + "resources": "リソース", + "roles": "ロール", + "users": "ユーザ" + }, + "selectLanguage": "言語を選択", + "showDetailsPanel": "詳細パネル表示", + "source": { + "hide": "ソースを隠す", + "hotkey": "s", + "show": "ソースを表示" + }, + "sourceAssetEvent_one": "元のアセットイベント", + "sourceAssetEvent_other": "元のアセットイベント", + "startDate": "開始日", + "state": "状態", + "states": { + "deferred": "延期済", + "failed": "失敗した", + "no_status": "ステータスなし", + "none": "ステータスなし", + "planned": "計画済", + "queued": "待機中", + "removed": "削除済", + "restarting": "再起動中", + "running": "実行中", + "scheduled": "スケジュール済", + "skipped": "スキップ済", + "success": "成功", + "up_for_reschedule": "リスケジュール待ち", + "up_for_retry": "再試行待ち", + "upstream_failed": "上流が失敗しました" + }, + "table": { + "completedAt": "完了時刻", + "createdAt": "作成時刻", + "filterByTag": "タグでフィルタします", + "filterColumns": "テーブルの列をフィルタします", + "filterReset_one": "フィルタのリセット", + "filterReset_other": "フィルタのリセット", + "from": "開始時間", + "maxActiveRuns": "最大アクティブ実行", + "noTagsFound": "タグが見つかりません", + "tagMode": { + "all": "すべて", + "any": "いずれか" + }, + "tagPlaceholder": "タグでフィルタします", + "to": "終了時間" + }, + "task": { + "documentation": "タスクドキュメント", + "lastInstance": "最終インスタンス", + "operator": "オペレータ", + "triggerRule": "トリガールール" + }, + "task_one": "タスク", + "task_other": "タスク", + "taskGroup": "タスクグループ", + "taskId": "タスク ID", + "taskInstance": { + "dagVersion": "Dag バージョン", + "executor": "エクゼキュータ", + "executorConfig": "エクゼキュータ設定", + "hostname": "ホスト名", + "maxTries": "最大試行回数", + "pid": "PID", + "pool": "プール", + "poolSlots": "プールスロット", + "priorityWeight": "優先度", + "queue": "キュー", + "queuedWhen": "キューイング時刻", + "scheduledWhen": "スケジュールされた時刻", + "triggerer": { + "assigned": "割り当てられたTriggerer", + "class": "トリガーカテゴリ", + "createdAt": "トリガー作成時刻", + "id": "トリガー ID", + "latestHeartbeat": "最新のTriggererハートビート", + "title": "Triggerer情報" + }, + "unixname": "Unix 名" + }, + "taskInstance_one": "タスクインスタンス", + "taskInstance_other": "タスクインスタンス", + "timeRange": { + "last12Hours": "直近12時間", + "last24Hours": "直近24時間", + "lastHour": "直近の1時間", + "pastWeek": "先週" + }, + "timestamp": { + "hide": "タイムスタンプを非表示にします", + "hotkey": "t", + "show": "タイムスタンプを表示します" + }, + "timezone": "タイムゾーン", + "timezoneModal": { + "current-timezone": "現在のタイムゾーン", + "placeholder": "タイムゾーンを選択", + "title": "タイムゾーンを選択", + "utc": "UTC" + }, + "toaster": { + "bulkDelete": { + "error": "{{resourceName}} の一括削除が失敗しました", + "success": { + "description": "{{count}} {{resourceName}} が削除済。 キー: {{keys}}", + "title": "{{resourceName}} 一括削除リクエストが送信済" + } + }, + "create": { + "error": "{{resourceName}} の作成リクエストが失敗しました", + "success": { + "description": "{{resourceName}} 作成が完了しました", + "title": "{{resourceName}} 作成リクエスト送信済み" + } + }, + "delete": { + "error": "{{resourceName}} 削除リクエストが失敗しました", + "success": { + "description": "{{resourceName}} が削除済", + "title": "{{resourceName}} の削除リクエスト送信済み" + } + }, + "import": { + "error": "{{resourceName}} のインポートに失敗しました", + "success": { + "description": "{{count}} {{resourceName}} のインポートが成功しました", + "title": "{{resourceName}} インポートリクエスト送信済み" + } + }, + "update": { + "error": "{{resourceName}} の更新処理が失敗しました", + "success": { + "description": "{{resourceName}} の更新が成功しました", + "title": "{{resourceName}} の更新リクエスト送信済み" + } + } + }, + "total": "合計 {{state}}", + "triggered": "トリガー済", + "tryNumber": "試行回数", + "user": "ユーザ", + "wrap": { + "hotkey": "w", + "tooltip": "折り返し表示を切替えるには {{hotkey}} を押してください", + "unwrap": "改行なし", + "wrap": "改行" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/components.json new file mode 100644 index 0000000000000..97c9e66ef07cf --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/components.json @@ -0,0 +1,140 @@ +{ + "backfill": { + "affected_one": "1 回の実行をトリガーします", + "affected_other": "{{count}} 回の実行をトリガーします", + "affectedNone": "選択した条件に一致する実行はありません", + "allRuns": "すべての実行", + "backwards": "逆実行", + "dateRange": "日付範囲", + "errorStartDateBeforeEndDate": "開始日が終了日より前にする必要があります", + "maxRuns": "最大アクティブ実行数", + "missingAndErroredRuns": "欠落およびエラー発生した実行", + "missingRuns": "欠落実行", + "reprocessBehavior": "再処理動作", + "run": "過去分再実行の実行", + "selectDescription": "この Dag を指定した日付範囲で実行します", + "selectLabel": "過去分再実行", + "title": "過去分再実行", + "toaster": { + "success": { + "description": "過去分再実行ジョブが正常にトリガーされました", + "title": "過去分再実行処理を生成しました" + } + }, + "tooltip": "過去分再実行処理には Dag にスケジューリングが必要です", + "unpause": "{{dag_display_name}} がトリガーされると一時停止を解除します", + "validation": { + "datesRequired": "データ範囲の開始日と終了日は必須です", + "startBeforeEnd": "データ範囲の開始日は終了日以前である必要があります" + } + }, + "banner": { + "backfillInProgress": "過去分再実行中", + "cancel": "過去分再実行をキャンセルします", + "pause": "過去分再実行を一時停止します", + "unpause": "過去分再実行の停止を解除します" + }, + "clipboard": { + "copy": "コピー" + }, + "close": "閉じる", + "configForm": { + "advancedOptions": "詳細設定", + "configJson": "JSON の設定", + "invalidJson": "無効な JSON 形式: {{errorMessage}}" + }, + "dagWarnings": { + "error_one": "1 件のエラー", + "errorAndWarning": "1 件のエラーと {{warningText}}", + "warning_one": "1 件の警告", + "warning_other": "{{count}} 件の警告" + }, + "durationChart": { + "duration": "期間(秒)", + "lastDagRun_one": "最新の Dag 実行", + "lastDagRun_other": "直近の {{count}} 回の Dag 実行", + "lastTaskInstance_one": "最新のタスクインスタンス", + "lastTaskInstance_other": "直近の {{count}} 個のタスクインスタンス", + "queuedDuration": "キューイングされた時間", + "runAfter": "最も早い実行可能時間", + "runDuration": "実行期間" + }, + "fileUpload": { + "files_other": "{{count}} 個のファイル" + }, + "flexibleForm": { + "placeholder": "値を選択してください", + "placeholderArray": "各文字列を新しい行に入力してください", + "placeholderExamples": "入力を開始するとオプションが表示されます", + "placeholderMulti": "1 つまたは複数の値を選択してください", + "validationErrorArrayNotArray": "値は配列でなければなりません", + "validationErrorArrayNotNumbers": "配列内のすべての要素が数値でなければなりません", + "validationErrorArrayNotObject": "配列内のすべての要素がオブジェクトでなければなりません", + "validationErrorRequired": "この項目は必須です" + }, + "graph": { + "directionDown": "上から下へ", + "directionLeft": "右から左へ", + "directionRight": "左から右へ", + "directionUp": "下から上へ", + "downloadImage": "グラフ画像のダウンロード", + "downloadImageError": "グラフ画像のダウンロードに失敗しました", + "downloadImageErrorTitle": "ダウンロードに失敗しました", + "otherDagRuns": "+その他の Dag 実行", + "taskCount_one": "{{count}} 個のタスク", + "taskCount_other": "{{count}} 個のタスク", + "taskGroup": "タスクグループ" + }, + "limitedList": "その他 +{{count}} 件", + "limitedList.allItems": "全 {{count}} 件:", + "limitedList.allTags_one": "すべてのタグ (1)", + "limitedList.allTags_other": "すべてのタグ ({{count}})", + "limitedList.clickToInteract": "タグをクリックして Dag をフィルタリング", + "limitedList.clickToOpenFull": "\"+{{count}} もっと見る\" をクリックして全体を表示", + "limitedList.copyPasteText": "上記のテキストをコピーして貼り付けることが可能", + "limitedList.showingItems_one": "1 件表示", + "limitedList.showingItems_other": "{{count}} 件の表示", + "logs": { + "file": "ファイル", + "location": "{{name}} の {{line}} 行目" + }, + "reparseDag": "Dag の再パース", + "sortedAscending": "昇順ソート", + "sortedDescending": "降順でソート", + "sortedUnsorted": "未ソート", + "taskTries": "タスクの試行", + "toggleCardView": "カードビューを表示", + "toggleTableView": "テーブルビューを表示", + "triggerDag": { + "button": "トリガー", + "loading": "Dag 情報を読込中...", + "loadingFailed": "情報読込に失敗しました。もう一度試してください", + "runIdHelp": "オプション- 指定しない場合自動的に生成されます", + "selectDescription": "この Dag の単体での実行をトリガーします", + "selectLabel": "単体での実行", + "title": "Dag のトリガー", + "toaster": { + "success": { + "description": "Dag の実行が正常にトリガーされました", + "title": "Dag の実行がトリガーされました" + } + }, + "unpause": "{{dagDisplayName}} トリガーで停止を解除します" + }, + "trimText": { + "details": "詳細", + "empty": "空", + "noContent": "利用可能なコンテンツがありません" + }, + "versionDetails": { + "bundleLink": "バンドルのリンク", + "bundleName": "バンドル名", + "bundleVersion": "バンドルバージョン", + "createdAt": "作成時刻", + "versionId": "バージョン ID" + }, + "versionSelect": { + "dagVersion": "Dag バージョン", + "versionCode": "v{{versionCode}}" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/dag.json new file mode 100644 index 0000000000000..d25ff587ceab1 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/dag.json @@ -0,0 +1,154 @@ +{ + "allRuns": "すべての実行", + "blockingDeps": { + "dependency": "依存関係", + "reason": "理由", + "title": "スケジュール済タスクをブロック中の依存関係" + }, + "calendar": { + "daily": "日単位", + "hourly": "時間単位", + "legend": { + "less": "Less", + "mixed": "Mixed", + "more": "More" + }, + "navigation": { + "nextMonth": "次月", + "nextYear": "次年", + "previousMonth": "前月", + "previousYear": "前年" + }, + "noData": "データが利用できません", + "noFailedRuns": "失敗した実行はありません", + "noRuns": "実行はありません", + "totalRuns": "すべての実行", + "week": "{{weekNumber}} 週", + "weekdays": { + "friday": "金", + "monday": "月", + "saturday": "土", + "sunday": "日", + "thursday": "木", + "tuesday": "火", + "wednesday": "水" + } + }, + "code": { + "bundleUrl": "バンドル URL", + "noCode": "コードが見つかりません", + "parseDuration": "パース時間:", + "parsedAt": "パース日時:" + }, + "extraLinks": "追加リンク", + "grid": { + "buttons": { + "resetToLatest": "Reset to latest", + "toggleGroup": "グループを切り替え" + } + }, + "header": { + "buttons": { + "advanced": "Advanced", + "dagDocs": "Dag ドキュメント" + } + }, + "logs": { + "allLevels": "全ログレベル", + "allSources": "全ソース", + "critical": "CRITICAL", + "debug": "DEBUG", + "error": "ERROR", + "fullscreen": { + "button": "全画面", + "tooltip": "全画面で表示するには {{hotkey}} を押してください" + }, + "info": "INFO", + "noTryNumber": "試行回数なし", + "settings": "ログ設定", + "viewInExternal": "{{name}} でログを見る ({{attempt}} 試行目)", + "warning": "WARNING" + }, + "navigation": { + "navigation": "Navigation: Shift+{{arrow}}", + "toggleGroup": "Toggle group: Space" + }, + "overview": { + "buttons": { + "failedRun_one": "失敗した実行", + "failedRun_other": "失敗した実行", + "failedTask_one": "失敗したタスク", + "failedTask_other": "失敗したタスク", + "failedTaskInstance_one": "失敗したタスクインスタンス", + "failedTaskInstance_other": "失敗したタスクインスタンス" + }, + "charts": { + "assetEvent_one": "作成されたアセットイベント", + "assetEvent_other": "作成されたアセットイベント" + }, + "failedLogs": { + "hideLogs": "ログを隠す", + "showLogs": "ログを表示", + "title": "最近失敗したタスクのログ", + "viewFullLogs": "すべてのログを表示" + } + }, + "panel": { + "buttons": { + "options": "オプション", + "showGantt": "ガントチャート表示", + "showGraphShortcut": "グラフ表示 (g を押す)", + "showGridShortcut": "グリッド表示 (g を押す)" + }, + "dagRuns": { + "label": "Dag 実行数" + }, + "dependencies": { + "label": "依存関係", + "options": { + "allDagDependencies": "すべての Dag 依存関係", + "externalConditions": "外部条件", + "onlyTasks": "タスクのみ" + }, + "placeholder": "依存関係" + }, + "graphDirection": { + "label": "グラフの向き" + } + }, + "paramsFailed": "パラメータのロードに失敗しました", + "parse": { + "toaster": { + "error": { + "description": "Dag のパースリクエストが失敗しました。未完了のパースリクエストが存在する可能性があります", + "title": "Dag の再パースに失敗" + }, + "success": { + "description": "Dag はまもなく再パースされます", + "title": "再パースが正常にリクエストされました" + } + } + }, + "tabs": { + "assetEvents": "アセットイベント", + "auditLog": "監査ログ", + "backfills": "過去分実行", + "calendar": "カレンダー", + "code": "コード", + "details": "詳細", + "logs": "ログ", + "mappedTaskInstances_one": "タスクインスタンス [{{count}}]", + "mappedTaskInstances_other": "タスクインスタンス [{{count}}]", + "overview": "概要", + "renderedTemplates": "生成されたテンプレート", + "requiredActions": "要対応項目", + "runs": "実行", + "taskInstances": "タスクインスタンス", + "tasks": "タスク", + "xcom": "XCom" + }, + "taskGroups": { + "collapseAll": "タスクグループをすべて閉じる", + "expandAll": "タスクグループをすべて展開" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/dags.json new file mode 100644 index 0000000000000..d01127c952ca2 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/dags.json @@ -0,0 +1,97 @@ +{ + "assetSchedule": "{{total}} 件中 {{count}} 件のアセットが更新されました", + "dagActions": { + "delete": { + "button": "Dag を削除", + "warning": "実行履歴やタスクを含め、Dag に関連するメタデータはすべて削除されます" + } + }, + "favoriteDag": "お気に入りの Dag", + "filters": { + "allRunTypes": "すべての実行タイプ", + "allStates": "すべての状態", + "favorite": { + "all": "すべて", + "favorite": "お気に入り", + "unfavorite": "お気に入り以外" + }, + "paused": { + "active": "アクティブ", + "all": "すべて", + "paused": "停止済" + }, + "runIdPatternFilter": "Dag 実行を検索", + "triggeringUserNameFilter": "トリガーユーザで検索" + }, + "ownerLink": "Owner link for {{owner}}", + "runAndTaskActions": { + "affectedTasks": { + "noItemsFound": "タスクが見つかりません", + "title": "影響を受けたタスク: {{count}} 件" + }, + "clear": { + "button": "{{type}} を消去", + "buttonTooltip": "消去するには shift+c を押してください", + "error": "{{type}} の消去に失敗しました", + "title": "{{type}} を消去" + }, + "delete": { + "button": "{{type}} を削除", + "dialog": { + "resourceName": "{{type}} {{id}}", + "title": "{{type}} を削除", + "warning": "{{type}} に関連するすべてのメタデータを削除します" + }, + "error": "{{type}} 削除エラー", + "success": { + "description": "{{type}} の削除に成功しました", + "title": "{{type}} の削除に成功しました" + } + }, + "markAs": { + "button": "{{type}} をマーク", + "buttonTooltip": { + "failed": "失敗とマークするには shift+f を押してください", + "success": "成功とマークするには shift+s を押してください" + }, + "title": "{{type}} を {{state}} とマーク" + }, + "options": { + "downstream": "下流", + "existingTasks": "存在するタスクを消去", + "future": "未来", + "onlyFailed": "失敗したタスクのみを消去", + "past": "過去", + "queueNew": "新しいタスクを準備", + "runOnLatestVersion": "最新のバンドルバージョンで実行", + "upstream": "上流" + } + }, + "search": { + "advanced": "高度な検索", + "clear": "検索条件を消去", + "dags": "Dag を検索", + "hotkey": "+K", + "tasks": "タスクを検索" + }, + "sort": { + "displayName": { + "asc": "表示名で並び替え (A-Z)", + "desc": "表示名で並び替え (Z-A)" + }, + "lastRunStartDate": { + "asc": "最新の実行開始日時で並び替え (旧-新)", + "desc": "最新の実行開始日時で並び替え (新-旧)" + }, + "lastRunState": { + "asc": "最新の実行状態で並び替え (A-Z)", + "desc": "最新の実行状態で並び替え (Z-A)" + }, + "nextDagRun": { + "asc": "次の Dag 実行で並び替え (旧-新)", + "desc": "次の Dag 実行で並び替え (新-旧)" + }, + "placeholder": "以下で並び替え" + }, + "unfavoriteDag": "お気に入り以外の Dag" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/dashboard.json new file mode 100644 index 0000000000000..60e665af9e539 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/dashboard.json @@ -0,0 +1,45 @@ +{ + "favorite": { + "favoriteDags_one": "先頭 {{count}} 件のお気に入り Dag", + "favoriteDags_other": "先頭 {{count}} 件のお気に入り Dag", + "noDagRuns": "この Dag はまだ実行されていません", + "noFavoriteDags": "まだお気に入りがありません。お気に入りに追加するには、Dag の隣にある星型のアイコンをクリックしてください" + }, + "group": "グループ", + "health": { + "dagProcessor": "Dag プロセッサ", + "health": "健康状態", + "healthy": "正常", + "lastHeartbeat": "最終ハートビート", + "metaDatabase": "メタデータベース", + "scheduler": "スケジューラ", + "status": "状態", + "triggerer": "Triggerer", + "unhealthy": "異常" + }, + "history": "履歴", + "importErrors": { + "dagImportError_one": "Dag 取り込みエラー", + "dagImportError_other": "Dag 取り込みエラー", + "searchByFile": "ファイルで検索", + "timestamp": "タイムスタンプ" + }, + "managePools": "プールの管理", + "noAssetEvents": "アセットイベントが見つかりません", + "poolSlots": "プールスロット", + "sortBy": { + "newestFirst": "新しい順", + "oldestFirst": "古い順" + }, + "source": "ソース", + "stats": { + "activeDags": "アクティブな Dag", + "failedDags": "失敗した Dag", + "queuedDags": "待機中の Dag", + "requiredActions": "必要なアクション", + "runningDags": "実行中の Dag", + "stats": "統計" + }, + "uri": "Uri", + "welcome": "ようこそ" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/hitl.json new file mode 100644 index 0000000000000..e05c054feac06 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/hitl.json @@ -0,0 +1,34 @@ +{ + "filters": { + "response": { + "all": "すべての", + "pending": "保留中", + "received": "レビュー済" + } + }, + "requiredAction_one": "要対応項目", + "requiredAction_other": "要対応項目", + "requiredActionCount_one": "要対応項目 ({{count}} 件)", + "requiredActionCount_other": "要対応項目 ({{count}} 件)", + "requiredActionState": "要対応状態", + "response": { + "error": "応答に失敗しました", + "optionsDescription": "このタスクインスタンスに対する選択肢を選んでください", + "optionsLabel": "選択肢", + "received": "以下の日時に応答を受信しました", + "respond": "応答する", + "success": "{{taskId}} の応答に成功しました", + "title": "ヒューマンタスクインスタンス - {{taskId}}" + }, + "state": { + "approvalReceived": "承認済", + "approvalRequired": "要承認", + "choiceReceived": "選択済", + "choiceRequired": "要選択", + "noResponseReceived": "未応答", + "rejectionReceived": "却下済", + "responseReceived": "応答済", + "responseRequired": "要応答" + }, + "subject": "件名" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/ja/tasks.json new file mode 100644 index 0000000000000..1fc0200cc1f7c --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "Mapped", + "notMapped": "Not mapped", + "retries": "再試行", + "searchTasks": "タスクを選択", + "selectMapped": "Select mapped", + "selectOperator": "オペレータを選択", + "selectRetryValues": "再試行数を選択", + "selectTriggerRules": "トリガールールを選択" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/admin.json index 5acf37c678506..9e6be8a1ee554 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/admin.json @@ -72,7 +72,6 @@ "tooltip": "선택된 커넥션들 삭제" }, "formActions": { - "reset": "초기화", "save": "저장" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/browse.json index 35a0600014191..0e1ec65f21475 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "모든 추가 JSON 접기", - "expandAllExtra": "모든 추가 JSON 펼치기" - }, "columns": { "event": "이벤트", "extra": "추가 정보", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json index bd391224d51d2..965ec38f33cff 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json @@ -8,6 +8,12 @@ "Variables": "변수들" }, "allOperators": "모든 오퍼레이터", + "appearance": { + "appearance": "외관", + "darkMode": "다크 모드", + "lightMode": "라이트 모드", + "systemMode": "시스템 설정 따르기" + }, "asset_one": "에셋", "asset_other": "에셋들", "assetEvent_one": "에셋 이벤트", @@ -19,6 +25,7 @@ "requiredActions": "필수 작업", "xcoms": "XComs" }, + "collapseAllExtra": "모든 추가 JSON 접기", "collapseDetailsPanel": "세부 정보 패널 접기", "createdAssetEvent_one": "생성된 에셋 이벤트", "createdAssetEvent_other": "생성된 에셋 이벤트들", @@ -33,6 +40,7 @@ "fileLocation": "파일 위치", "hasTaskConcurrencyLimits": "태스크 동시성 제한 존재 여부", "lastExpired": "마지막 만료 시점", + "lastParseDuration": "마지막 파싱 기간", "lastParsed": "마지막 파싱 시점", "latestDagVersion": "최신 Dag 버전", "latestRun": "최근 실행", @@ -86,22 +94,19 @@ "hotkey": "e", "tooltip": "{{hotkey}}를 눌러 펼치기/접기합니다." }, + "expandAllExtra": "모든 추가 JSON 펼치기", "expression": { "all": "모두", "and": "그리고", "any": "모든", "or": "또는" }, + "filter": "필터", "filters": { - "dagDisplayNamePlaceholder": "Dag로 필터링", - "keyPlaceholder": "XCom 키로 필터링", - "logicalDateFromPlaceholder": "논리적 날짜 (시작)", - "logicalDateToPlaceholder": "논리적 날짜 (종료)", - "mapIndexPlaceholder": "맵 인덱스로 필터링", - "runAfterFromPlaceholder": "실행 이후 (시작)", - "runAfterToPlaceholder": "실행 이후 (종료)", - "runIdPlaceholder": "실행 ID로 필터링", - "taskIdPlaceholder": "작업 ID로 필터링" + "logicalDateFrom": "논리적 날짜 (시작)", + "logicalDateTo": "논리적 날짜 (종료)", + "runAfterFrom": "실행 이후 (시작)", + "runAfterTo": "실행 이후 (종료)" }, "logicalDate": "논리적 날짜", "logout": "로그아웃", @@ -143,6 +148,7 @@ "running": "실행 중", "scheduled": "예약됨" }, + "reset": "초기화", "runId": "실행 ID", "runTypes": { "asset_triggered": "에셋 트리거", @@ -157,7 +163,6 @@ }, "tooltip": "{{hotkey}}를 눌러 {{direction}}로 스크롤" }, - "seconds": "{{count}}초", "security": { "actions": "작업", "permissions": "권한", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json index 3395f29de4452..190a430bd44e9 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/components.json @@ -6,8 +6,6 @@ "allRuns": "모든 실행", "backwards": "거꾸로 실행", "dateRange": "날짜 범위", - "dateRangeFrom": "시작", - "dateRangeTo": "종료", "errorStartDateBeforeEndDate": "시작일은 종료일보다 빨라야 합니다.", "maxRuns": "최대 활성 실행 수", "missingAndErroredRuns": "누락되었거나 오류가 발생한 실행", @@ -88,6 +86,10 @@ "taskGroup": "작업 그룹" }, "limitedList": "+{{count}}개 더 보기", + "limitedList.allItems": "모든 {{count}}개 항목:", + "limitedList.clickToInteract": "태그를 클릭하여 Dag를 필터링하세요", + "limitedList.clickToOpenFull": "\"+{{count}}개 더 보기\"를 클릭하여 전체 보기 열기", + "limitedList.copyPasteText": "위의 텍스트를 복사하여 붙여넣을 수 있습니다", "logs": { "file": "파일", "location": "{{name}}의 {{line}}번째 줄" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json index 2bdcff01f452f..fc722b1f826ae 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json @@ -35,7 +35,8 @@ "code": { "bundleUrl": "번들 URL", "noCode": "코드를 찾을 수 없습니다.", - "parsedAt": "구문 분석 시간:" + "parseDuration": "파싱 기간:", + "parsedAt": "파싱 시간:" }, "extraLinks": "추가 링크", "grid": { @@ -84,6 +85,8 @@ "assetEvent_other": "생성된 에셋 이벤트" }, "failedLogs": { + "hideLogs": "로그 숨기기", + "showLogs": "로그 보기", "title": "최근 실패한 작업 로그", "viewFullLogs": "전체 로그 보기" } @@ -92,9 +95,7 @@ "buttons": { "options": "옵션", "showGantt": "간트 차트 보기", - "showGraph": "그래프 보기", "showGraphShortcut": "그래프 보기 (g 키)", - "showGrid": "그리드 보기", "showGridShortcut": "그리드 보기 (g 키)" }, "dagRuns": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/ko/hitl.json index 488a6504f8e7a..8b78496ce6d9e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/hitl.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/hitl.json @@ -8,6 +8,8 @@ }, "requiredAction_one": "필수 작업", "requiredAction_other": "필수 작업들", + "requiredActionCount_one": "필수 작업 ({{count}})", + "requiredActionCount_other": "필수 작업들 ({{count}})", "requiredActionState": "필수 작업 상태", "response": { "error": "응답 실패", @@ -23,6 +25,7 @@ "approvalRequired": "승인 필요", "choiceReceived": "선택 수신됨", "choiceRequired": "선택 필요", + "noResponseReceived": "응답 없음", "rejectionReceived": "거절 수신됨", "responseReceived": "응답 수신됨", "responseRequired": "응답 필요" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/admin.json index e584a54c77849..39d3b73bdcaa5 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/admin.json @@ -72,8 +72,7 @@ "tooltip": "Verwijder geselecteerde connecties" }, "formActions": { - "reset": "Reset", - "save": "Opsalaan" + "save": "Opslaan" }, "plugins": { "columns": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/browse.json index 4fadaab67595b..379851601dbda 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "All extra JSON inklappen", - "expandAllExtra": "Alle extra JSON uitklappen" - }, "columns": { "event": "Event", "extra": "Extra", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json index 95707d92beb14..d348d8f1a7ad4 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json @@ -25,6 +25,7 @@ "requiredActions": "Vereiste acties", "xcoms": "XComs" }, + "collapseAllExtra": "All extra JSON inklappen", "collapseDetailsPanel": "Details inklappen", "createdAssetEvent_one": "Asset Event aangemaakt", "createdAssetEvent_other": "Asset Events aangemaakt", @@ -39,6 +40,7 @@ "fileLocation": "Bestandslocatie", "hasTaskConcurrencyLimits": "Heeft Task Concurrency limieten", "lastExpired": "Laatst verlopen", + "lastParseDuration": "Laatste parse duur", "lastParsed": "Laatst geparsed", "latestDagVersion": "Laatste Dag versie", "latestRun": "Laatste Run", @@ -92,22 +94,19 @@ "hotkey": "e", "tooltip": "Druk op {{hotkey}} op in- of uit te klappen" }, + "expandAllExtra": "Alle extra JSON uitklappen", "expression": { "all": "Alles", "and": "En", "any": "Elk", "or": "Of" }, + "filter": "Filter", "filters": { - "dagDisplayNamePlaceholder": "Filter op Dag", - "keyPlaceholder": "Filter op XCom sleutel", - "logicalDateFromPlaceholder": "Van logische datum", - "logicalDateToPlaceholder": "Tot logische datum", - "mapIndexPlaceholder": "Filter op Map Index", - "runAfterFromPlaceholder": "Van Run na", - "runAfterToPlaceholder": "Tot Run na", - "runIdPlaceholder": "Filter op Run ID", - "taskIdPlaceholder": "Filter op Task ID" + "logicalDateFrom": "Van logische datum", + "logicalDateTo": "Tot logische datum", + "runAfterFrom": "Van Run na", + "runAfterTo": "Tot Run na" }, "logicalDate": "Logische datum", "logout": "Uitloggen", @@ -149,6 +148,7 @@ "running": "Lopend", "scheduled": "Gepland" }, + "reset": "Reset", "runId": "Run ID", "runTypes": { "asset_triggered": "Asset Triggered", @@ -163,7 +163,6 @@ }, "tooltip": "Druk op {{hotkey}} om te scrollen naar {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Acties", "permissions": "Permissies", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json index b3b6e82d10f6f..0349dc7d989f3 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json @@ -1,13 +1,11 @@ { "backfill": { - "affected_one": "1 run zal getriggered worden.", - "affected_other": "{{count}} Runs zullen getriggered worden.", + "affected_one": "1 run zal getriggerd worden.", + "affected_other": "{{count}} Runs zullen getriggerd worden.", "affectedNone": "Geen Runs komen overeen met de geselecteerde criteria.", "allRuns": "Alle Runs", "backwards": "Run achterstevoren", "dateRange": "Datumbereik", - "dateRangeFrom": "Van", - "dateRangeTo": "Tot", "errorStartDateBeforeEndDate": "Startdatum moet voor de einddatum", "maxRuns": "Maximaal aantal actieve Runs", "missingAndErroredRuns": "Missende en mislukte Runs", @@ -90,6 +88,10 @@ "taskGroup": "Task Group" }, "limitedList": "+{{count}} meer", + "limitedList.allItems": "Alle {{count}} items:", + "limitedList.clickToInteract": "Klik op een label om Dags te filteren", + "limitedList.clickToOpenFull": "Klik op \"+{{count}} meer\" om de volledige lijst te openen", + "limitedList.copyPasteText": "Je kunt de bovenstaande tekst kopiëren en plakken", "logs": { "file": "Bestand", "location": "regel {{line}} in {{name}}" @@ -103,8 +105,8 @@ "toggleTableView": "Toon tabelweergave", "triggerDag": { "button": "Trigger", - "loading": "DAG informatie aan het laden...", - "loadingFailed": "Mislukt om DAG informatie te laden. Probeer het opnieuw.", + "loading": "Dag informatie aan het laden...", + "loadingFailed": "Mislukt om Dag informatie te laden. Probeer het opnieuw.", "runIdHelp": "Optioneel - wordt gegenereerd indien niet opgegeven", "selectDescription": "Trigger een enkele run van deze Dag", "selectLabel": "Enkele Run", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json index 07a30b10bbfe3..ad1a937e6afff 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json @@ -35,7 +35,8 @@ "code": { "bundleUrl": "Bundle URL", "noCode": "Geen code gevonden", - "parsedAt": "Parsed at:" + "parseDuration": "Parse duur:", + "parsedAt": "Geparsed op:" }, "extraLinks": "Extra Links", "grid": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json index b57b2290a9232..23fdb3bc2d480 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dags.json @@ -20,8 +20,7 @@ "all": "Alles", "paused": "Gepauzeerd" }, - "runIdPatternFilter": "Zoek Dag Runs", - "triggeringUserNameFilter": "Zoek op Triggering User" + "runIdPatternFilter": "Zoek Dag Runs" }, "ownerLink": "Eigenaarslink voor {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/admin.json index 3328d86ee87b1..bedc2c9c63f5b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/admin.json @@ -55,6 +55,12 @@ "searchPlaceholder": "Szukaj połączeń", "test": "Test połączenia", "testDisabled": "Testowanie połączeń wyłączone. Skontaktuj się z administratorem, aby je włączyć.", + "testError": { + "title": "Test połączenia nieudany" + }, + "testSuccess": { + "title": "Test połączenia udany" + }, "typeMeta": { "error": "Nie udało się pobrać metadanych typu połączenia", "standardFields": { @@ -78,7 +84,6 @@ "tooltip": "Usuń wybrane połączenia" }, "formActions": { - "reset": "Resetuj", "save": "Zapisz" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/assets.json index 8456890416b2b..ef84d8e4c9862 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "Przetwarzanie Dagów", + "consumingTasks": "Przetwarzanie zadań", "createEvent": { "button": "Utwórz zdarzenie", "manual": { @@ -21,6 +22,7 @@ }, "title": "Utwórz zdarzenie zasobu dla {{name}}" }, + "extra": "Dodatkowe", "group": "Grupa", "lastAssetEvent": "Ostatnie zdarzenie zasobu", "name": "Nazwa", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/browse.json index 2ea27a65a5043..9358f954d889f 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Zwiń wszystkie dodatkowe dane JSON", - "expandAllExtra": "Rozwiń wszystkie dodatkowe dane JSON" - }, "columns": { "event": "Zdarzenie", "extra": "Dodatkowe", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json index f5f657e40c8c5..ba08387658784 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json @@ -31,6 +31,7 @@ "requiredActions": "Wymagane akcje", "xcoms": "XComy" }, + "collapseAllExtra": "Zwiń wszystkie dodatkowe dane JSON", "collapseDetailsPanel": "Zwiń panel szczegółów", "createdAssetEvent_few": "Utworzone zdarzenia zasobów", "createdAssetEvent_many": "Utworzonych zdarzeń zasobów", @@ -90,12 +91,18 @@ "githubRepo": "Repozytorium GitHub", "restApiReference": "Dokuentacja REST API" }, + "download": { + "download": "Pobierz", + "hotkey": "d", + "tooltip": "Naciśnij {{hotkey}}, aby pobrać logi" + }, "duration": "Czas trwania", "endDate": "Data zakończenia", "error": { "back": "Powrót", "defaultMessage": "Wystąpił nieoczekiwany błąd", "home": "Strona główna", + "invalidUrl": "Nie znaleziono strony. Sprawdź adres URL i spróbuj ponownie.", "notFound": "Nie znaleziono strony", "title": "Błąd" }, @@ -105,23 +112,19 @@ "hotkey": "e", "tooltip": "Wybierz {{hotkey}} aby przełączyć rozwijanie" }, + "expandAllExtra": "Rozwiń wszystkie dodatkowe dane JSON", "expression": { "all": "Wszystkie", "and": "ORAZ", "any": "Dowolne", "or": "LUB" }, + "filter": "Filtr", "filters": { - "dagDisplayNamePlaceholder": "Filtruj według Daga", - "keyPlaceholder": "Filtruj według klucza XCom", - "logicalDateFromPlaceholder": "Data logiczna od", - "logicalDateToPlaceholder": "Data logiczna do", - "mapIndexPlaceholder": "Filtruj według indeksu mapowania", - "runAfterFromPlaceholder": "Uruchom po (od)", - "runAfterToPlaceholder": "Uruchom po (do)", - "runIdPlaceholder": "Filtruj według identyfikatora uruchomienia", - "taskIdPlaceholder": "Filtruj według identyfikatora zadania", - "triggeringUserPlaceholder": "Filtruj według uruchamiającego użytkownika" + "logicalDateFrom": "Data logiczna od", + "logicalDateTo": "Data logiczna do", + "runAfterFrom": "Uruchom po (od)", + "runAfterTo": "Uruchom po (do)" }, "logicalDate": "Data logiczna", "logout": "Wyloguj", @@ -165,6 +168,7 @@ "running": "W trakcie", "scheduled": "Zaplanowane" }, + "reset": "Resetuj", "runId": "Identyfikator wykonania", "runTypes": { "asset_triggered": "Uruchomiony przez zasób", @@ -179,7 +183,6 @@ }, "tooltip": "Naciśnij {{hotkey}}, aby przewinąć do {{direction}}" }, - "seconds": "{{count}}s", "security": { "actions": "Akcje", "permissions": "Uprawnienia", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/components.json index 0dc4b8699757d..932d7d87bf7fb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/components.json @@ -8,8 +8,6 @@ "allRuns": "Wszystkie wykonania", "backwards": "Uruchom wstecz", "dateRange": "Zakres dat", - "dateRangeFrom": "Od", - "dateRangeTo": "Do", "errorStartDateBeforeEndDate": "Data początkowa musi być wcześniejsza niż data końcowa", "maxRuns": "Maksymalna liczba aktywnych wykonań", "missingAndErroredRuns": "Brakujące i błędne wykonania", @@ -104,6 +102,18 @@ "taskGroup": "Grupa zadań" }, "limitedList": "+{{count}} więcej", + "limitedList.allItems": "Wszystkie {{count}} elementy:", + "limitedList.allTags_few": "Wszystkie tagi ({{count}})", + "limitedList.allTags_many": "Wszystkie tagi ({{count}})", + "limitedList.allTags_one": "Wszystkie tagi (1)", + "limitedList.allTags_other": "Wszystkie tagi ({{count}})", + "limitedList.clickToInteract": "Kliknij etykietę, aby przefiltrować Dagi", + "limitedList.clickToOpenFull": "Kliknij \"+{{count}} więcej\", aby zobaczyć pełną listę", + "limitedList.copyPasteText": "Możesz skopiować i wkleić powyższy tekst", + "limitedList.showingItems_few": "Wyświetlanie {{count}} elementów", + "limitedList.showingItems_many": "Wyświetlanie {{count}} elementów", + "limitedList.showingItems_one": "Wyświetlanie 1 elementu", + "limitedList.showingItems_other": "Wyświetlanie {{count}} elementów", "logs": { "file": "Plik", "location": "linia {{line}} w {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json index 27276f595f68d..3a55fced65990 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json @@ -10,6 +10,7 @@ "hourly": "Godzinowo", "legend": { "less": "Mniej", + "mixed": "Mieszane", "more": "Więcej" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Poprzedni rok" }, "noData": "Brak danych", + "noFailedRuns": "Brak nieudanych wykonań", "noRuns": "Brak wykonań", "totalRuns": "Łączna liczba wykonań", "week": "Tydzień {{weekNumber}}", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dags.json index 9ca67b21a5be2..76dfb64f269f4 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dags.json @@ -21,7 +21,7 @@ "paused": "Wstrzymane" }, "runIdPatternFilter": "Szukaj Wykonań Dagów", - "triggeringUserNameFilter": "Szukaj według użytkownika wywołującego" + "triggeringUserNameFilter": "Szukaj według użytkownika wyzwalającego" }, "ownerLink": "Link do właściciela {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/admin.json new file mode 100644 index 0000000000000..86b74854eff21 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/admin.json @@ -0,0 +1,182 @@ +{ + "columns": { + "description": "Descrição", + "key": "Chave", + "name": "Nome", + "value": "Valor" + }, + "config": { + "columns": { + "section": "Secção" + }, + "title": "Configuração do Airflow" + }, + "connections": { + "add": "Adicionar Conexão", + "columns": { + "connectionId": "ID da Conexão", + "connectionType": "Tipo de Conexão", + "host": "Host", + "port": "Porta" + }, + "connection_many": "Conexões", + "connection_one": "Conexão", + "connection_other": "Conexões", + "connection_zero": "Nenhuma conexão", + "delete": { + "deleteConnection_many": "Excluir {{count}} conexões", + "deleteConnection_one": "Excluir 1 conexão", + "deleteConnection_other": "Excluir {{count}} conexões", + "deleteConnection_zero": "Nenhuma conexão para excluir", + "firstConfirmMessage_many": "Você está prestes a excluir as seguintes conexões:", + "firstConfirmMessage_one": "Você está prestes a excluir a seguinte conexão:", + "firstConfirmMessage_other": "Você está prestes a excluir as seguintes conexões:", + "firstConfirmMessage_zero": "Nenhuma conexão para excluir", + "title": "Excluir Conexão" + }, + "edit": "Editar Conexão", + "form": { + "connectionIdRequired": "ID da Conexão é obrigatório", + "connectionIdRequirement": "ID da Conexão não pode conter somente espaços", + "connectionTypeRequired": "Tipo de Conexão é obrigatório", + "extraFields": "Campos Extra", + "extraFieldsJson": "Campos Extra JSON", + "helperText": "Tipo de conexão faltando? Certifique-se de ter instalado o pacote do provider correspondente ao Airflow.", + "helperTextForRedactedFields": "Os campos redigidos ('***') permanecerão inalterados se não forem modificados.", + "selectConnectionType": "Selecionar Tipo de Conexão", + "standardFields": "Campos Padrão" + }, + "nothingFound": { + "description": "Conexões definidas via variáveis de ambiente ou gerenciadores de segredos não estão listadas aqui.", + "documentationLink": "Saiba mais na documentação do Airflow.", + "learnMore": "Estas são resolvidas em tempo de execução e não são visíveis na interface do usuário.", + "title": "Nenhuma conexão encontrada!" + }, + "searchPlaceholder": "Pesquisar Conexões", + "test": "Testar Conexão", + "testDisabled": "A funcionalidade de teste de conexão está desativada. Por favor, contate um administrador para ativá-la.", + "typeMeta": { + "error": "Falha ao recuperar Meta do Tipo de Conexão", + "standardFields": { + "description": "Descrição", + "host": "Host", + "login": "Login", + "password": "Senha", + "port": "Porta", + "url_schema": "Esquema" + } + } + }, + "deleteActions": { + "button": "Excluir", + "modal": { + "confirmButton": "Sim, Excluir", + "secondConfirmMessage": "Esta ação é permanente e não pode ser desfeita.", + "thirdConfirmMessage": "Tem certeza que deseja prosseguir?" + }, + "selected": "Selecionado", + "tooltip": "Excluir conexões selecionadas" + }, + "formActions": { + "save": "Salvar" + }, + "plugins": { + "columns": { + "source": "Origem" + }, + "importError_many": "Erros de Importação de Plugins", + "importError_one": "Erro de Importação de Plugin", + "importError_other": "Erros de Importação de Plugins", + "importError_zero": "Nenhum erro de importação de plugin", + "searchPlaceholder": "Pesquisar por arquivo" + }, + "pools": { + "add": "Adicionar Pool", + "deferredSlotsIncluded": "Slots Deferidos Incluídos", + "delete": { + "title": "Excluir Pool", + "warning": "Isso removerá todas as metadados relacionados ao pool e pode afetar as tarefas usando este pool." + }, + "edit": "Editar Pool", + "form": { + "checkbox": "Marcar para incluir tarefas deferidas ao calcular slots abertos do pool", + "description": "Descrição", + "includeDeferred": "Incluir Deferidos", + "nameMaxLength": "Nome pode conter um máximo de 256 caracteres", + "nameRequired": "Nome é obrigatório", + "slots": "Slots" + }, + "noPoolsFound": "Nenhum pool encontrado", + "pool_many": "Pools", + "pool_one": "Pool", + "pool_other": "Pools", + "pool_zero": "Nenhum pool", + "searchPlaceholder": "Pesquisar Pools", + "sort": { + "asc": "Nome (A-Z)", + "desc": "Nome (Z-A)", + "placeholder": "Ordenar por" + } + }, + "providers": { + "columns": { + "packageName": "Nome do Pacote", + "version": "Versão" + } + }, + "variables": { + "add": "Adicionar Variável", + "columns": { + "isEncrypted": "Está Encriptado" + }, + "delete": { + "deleteVariable_many": "Excluir {{count}} Variáveis", + "deleteVariable_one": "Excluir 1 Variável", + "deleteVariable_other": "Excluir {{count}} Variáveis", + "deleteVariable_zero": "Nenhuma variável para excluir", + "firstConfirmMessage_many": "Você está prestes a excluir as seguintes variáveis:", + "firstConfirmMessage_one": "Você está prestes a excluir a seguinte variável:", + "firstConfirmMessage_other": "Você está prestes a excluir as seguintes variáveis:", + "firstConfirmMessage_zero": "Nenhuma variável para excluir", + "title": "Excluir Variável", + "tooltip": "Excluir variáveis selecionadas" + }, + "edit": "Editar Variável", + "export": "Exportar", + "exportTooltip": "Exportar variáveis selecionadas", + "form": { + "invalidJson": "JSON Inválido", + "keyMaxLength": "Chave pode conter um máximo de 250 caracteres", + "keyRequired": "Chave é obrigatória", + "valueRequired": "Valor é obrigatório" + }, + "import": { + "button": "Importar", + "conflictResolution": "Selecionar Resolução de Conflito de Variável", + "errorParsingJsonFile": "Erro ao Analisar Arquivo JSON: Upload um arquivo JSON contendo variáveis (exemplo: {\"key\": \"value\", ...}).", + "options": { + "fail": { + "description": "Falha na importação se forem detetadas variáveis que já existem.", + "title": "Falha" + }, + "overwrite": { + "description": "Sobreescreve a variável em caso de conflito.", + "title": "Sobreescrever" + }, + "skip": { + "description": "Ignora a importação de variáveis que já existem.", + "title": "Ignorar" + } + }, + "title": "Importar Variáveis", + "upload": "Upload um Arquivo JSON", + "uploadPlaceholder": "Upload um arquivo JSON contendo variáveis (exemplo: {\"key\": \"value\", ...})" + }, + "noRowsMessage": "Nenhuma variável encontrada", + "searchPlaceholder": "Pesquisar Chaves", + "variable_many": "Variáveis", + "variable_one": "Variável", + "variable_other": "Variáveis", + "variable_zero": "Nenhuma variável" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/assets.json new file mode 100644 index 0000000000000..ca1a65fc735de --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/assets.json @@ -0,0 +1,30 @@ +{ + "consumingDags": "Consumindo Dags", + "createEvent": { + "button": "Criar Evento", + "manual": { + "description": "Manualmente criar um Evento de Asset", + "extra": "Evento de Asset Extra", + "label": "Manual" + }, + "materialize": { + "description": "Ativar o Dag acima deste Asset", + "descriptionWithDag": "Ativar o Dag acima deste Asset: {{dagName}}", + "label": "Materializar", + "unpauseDag": "Despausar {{dagName}} ao ativar" + }, + "success": { + "manualDescription": "Criação de evento de Asset manual foi bem-sucedida.", + "manualTitle": "Evento de Asset Criado", + "materializeDescription": "Dag acima {{dagId}} foi ativado com sucesso.", + "materializeTitle": "Materializando Asset" + }, + "title": "Criar Evento de Asset para {{name}}" + }, + "group": "Grupo", + "lastAssetEvent": "Último Evento de Asset", + "name": "Nome", + "producingTasks": "Produzindo Tarefas", + "scheduledDags": "Dags Programados", + "searchPlaceholder": "Pesquisar Assets" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/browse.json new file mode 100644 index 0000000000000..867d8494a6c32 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/browse.json @@ -0,0 +1,22 @@ +{ + "auditLog": { + "columns": { + "event": "Evento", + "extra": "Extra", + "user": "Usuário", + "when": "Quando" + }, + "filters": { + "eventType": "Tipo de Evento" + }, + "title": "Log de Auditoria" + }, + "xcom": { + "columns": { + "dag": "Dag", + "key": "Chave", + "value": "Valor" + }, + "title": "XCom" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json new file mode 100644 index 0000000000000..01940bf08617b --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json @@ -0,0 +1,340 @@ +{ + "admin": { + "Config": "Configuração", + "Connections": "Conexões", + "Plugins": "Plugins", + "Pools": "Pools", + "Providers": "Providers", + "Variables": "Variáveis" + }, + "allOperators": "Todos os Operadores", + "appearance": { + "appearance": "Aparência", + "darkMode": "Modo Escuro", + "lightMode": "Modo Claro", + "systemMode": "Seguir Configuração do Sistema" + }, + "asset_many": "Assets", + "asset_one": "Asset", + "asset_other": "Assets", + "asset_zero": "Nenhum asset", + "assetEvent_many": "Eventos de Asset", + "assetEvent_one": "Evento de Asset", + "assetEvent_other": "Eventos de Asset", + "assetEvent_zero": "Nenhum evento de asset", + "backfill_many": "Backfills", + "backfill_one": "Backfill", + "backfill_other": "Backfills", + "backfill_zero": "Nenhum backfill", + "browse": { + "auditLog": "Log de Auditoria", + "requiredActions": "Ações Necessárias", + "xcoms": "XComs" + }, + "collapseAllExtra": "Recolher todos os extra json", + "collapseDetailsPanel": "Recolher Painel de Detalhes", + "createdAssetEvent_many": "Eventos de Asset Criados", + "createdAssetEvent_one": "Evento de Asset Criado", + "createdAssetEvent_other": "Eventos de Asset Criados", + "createdAssetEvent_zero": "Nenhum evento de asset criado", + "dag_many": "Dags", + "dag_one": "Dag", + "dag_other": "Dags", + "dag_zero": "Nenhum Dag", + "dagDetails": { + "catchup": "Catchup", + "dagRunTimeout": "Tempo Limite da Execução do Dag", + "defaultArgs": "Argumentos Padrão", + "description": "Descrição", + "documentation": "Documentação do Dag", + "fileLocation": "Local do Arquivo", + "hasTaskConcurrencyLimits": "Tem Limite de Concorrência de Tarefas", + "lastExpired": "Último Expirado", + "lastParseDuration": "Duração do Último Parse", + "lastParsed": "Último Parseado", + "latestDagVersion": "Última Versão do Dag", + "latestRun": "Última Execução", + "maxActiveRuns": "Máximo de Execuções Ativas", + "maxActiveTasks": "Máximo de Tarefas Ativas", + "maxConsecutiveFailedDagRuns": "Máximo de Execuções Consecutivas Falhadas", + "nextRun": "Próxima Execução", + "owner": "Proprietário", + "params": "Parâmetros", + "schedule": "Agendamento", + "tags": "Etiquetas" + }, + "dagId": "ID do Dag", + "dagRun": { + "conf": "Conf", + "dagVersions": "Versão(s) do Dag", + "dataIntervalEnd": "Fim do Intervalo de Dados", + "dataIntervalStart": "Início do Intervalo de Dados", + "lastSchedulingDecision": "Última Decisão de Agendamento", + "queuedAt": "Enfileirado Em", + "runAfter": "Executar Depois", + "runType": "Tipo de Execução", + "sourceAssetEvent": "Evento de Asset de Origem", + "triggeredBy": "Acionado por", + "triggeringUser": "Nome do Usuário que Disparou" + }, + "dagRun_many": "Execuções do Dag", + "dagRun_one": "Execução do Dag", + "dagRun_other": "Execuções do Dag", + "dagRun_zero": "Nenhuma execução do Dag", + "dagRunId": "ID da Execução do Dag", + "dagWarnings": "Avisos/Erros do Dag", + "defaultToGraphView": "Padrão para visualização gráfica", + "defaultToGridView": "Padrão para visualização em grade", + "direction": "Direção", + "docs": { + "documentation": "Documentação", + "githubRepo": "Repositório GitHub", + "restApiReference": "Referência da API REST" + }, + "duration": "Duração", + "endDate": "Data Final", + "error": { + "back": "Voltar", + "defaultMessage": "Ocorreu um erro inesperado", + "home": "Início", + "notFound": "Página Não Encontrada", + "title": "Erro" + }, + "expand": { + "collapse": "Recolher", + "expand": "Expandir", + "hotkey": "Pressione {{hotkey}} para expandir ou recolher", + "tooltip": "Pressione {{hotkey}} para expandir ou recolher a secção" + }, + "expandAllExtra": "Expandir todos os extra json", + "expression": { + "all": "Todos", + "and": "E", + "any": "Qualquer", + "or": "OU" + }, + "filter": "Filtro", + "filters": { + "logicalDateFrom": "Data Lógica De", + "logicalDateTo": "Data Lógica Para", + "runAfterFrom": "Executar Depois De", + "runAfterTo": "Executar Depois Para" + }, + "logicalDate": "Data Lógica", + "logout": "Sair", + "logoutConfirmation": "Você está prestes a sair do aplicativo.", + "mapIndex": "Índice do Mapa", + "modal": { + "cancel": "Cancelar", + "confirm": "Confirmar", + "delete": { + "button": "Excluir", + "confirmation": "Tem certeza que deseja excluir {{resourceName}}? Esta ação não pode ser desfeita." + } + }, + "nav": { + "admin": "Administração", + "assets": "Assets", + "browse": "Navegar", + "dags": "Dags", + "docs": "Documentação", + "home": "Início", + "legacyFabViews": "Visualizações Legacy FAB", + "plugins": "Plugins", + "security": "Segurança" + }, + "noItemsFound": "Nenhum {{modelName}} encontrado", + "note": { + "add": "Adicionar uma nota", + "dagRun": "Nota da Execução do Dag", + "label": "Nota", + "placeholder": "Adicionar uma nota...", + "taskInstance": "Nota da Instância de Tarefa" + }, + "pools": { + "deferred": "Deferido", + "open": "Aberto", + "pools_many": "pools", + "pools_one": "pool", + "pools_other": "pools", + "pools_zero": "nenhum pool", + "queued": "Enfileirado", + "running": "Executando", + "scheduled": "Agendado" + }, + "reset": "Resetar", + "runId": "ID da Execução", + "runTypes": { + "asset_triggered": "Asset Acionado", + "backfill": "Backfill", + "manual": "Manual", + "scheduled": "Agendado" + }, + "scroll": { + "direction": { + "bottom": "Inferior", + "top": "Superior" + }, + "tooltip": "Pressione {{hotkey}} para rolar para {{direction}}" + }, + "security": { + "actions": "Ações", + "permissions": "Permissões", + "resources": "Recursos", + "roles": "Funções", + "users": "Usuários" + }, + "selectLanguage": "Selecionar Idioma", + "showDetailsPanel": "Mostrar Painel de Detalhes", + "source": { + "hide": "Ocultar", + "hotkey": "Pressione {{hotkey}} para ocultar/mostrar", + "show": "Mostrar" + }, + "sourceAssetEvent_many": "Eventos de Asset de Origem", + "sourceAssetEvent_one": "Evento de Asset de Origem", + "sourceAssetEvent_other": "Eventos de Asset de Origem", + "sourceAssetEvent_zero": "Nenhum evento de asset de origem", + "startDate": "Data Inicial", + "state": "Estado", + "states": { + "deferred": "Deferido", + "failed": "Falha", + "no_status": "Sem Status", + "none": "Sem Status", + "planned": "Planejado", + "queued": "Enfileirado", + "removed": "Removido", + "restarting": "Reiniciando", + "running": "Executando", + "scheduled": "Agendado", + "skipped": "Pulado", + "success": "Sucesso", + "up_for_reschedule": "Pronto para reagendar", + "up_for_retry": "Pronto para tentar novamente", + "upstream_failed": "Falha no upstream" + }, + "table": { + "completedAt": "Concluído em", + "createdAt": "Criado em", + "filterByTag": "Filtrar Dags por tag", + "filterColumns": "Filtrar colunas da tabela", + "filterReset_many": "Resetar filtros", + "filterReset_one": "Resetar filtro", + "filterReset_other": "Resetar filtros", + "filterReset_zero": "Nenhum filtro para resetar", + "from": "De", + "maxActiveRuns": "Máximo de Execuções Ativas", + "noTagsFound": "Nenhuma tag encontrada", + "tagMode": { + "all": "Todos", + "any": "Qualquer" + }, + "tagPlaceholder": "Filtrar por tag", + "to": "Para" + }, + "task": { + "documentation": "Documentação da Tarefa", + "lastInstance": "Última Instância", + "operator": "Operador", + "triggerRule": "Regra de Trigger" + }, + "task_many": "Tarefas", + "task_one": "Tarefa", + "task_other": "Tarefas", + "task_zero": "Nenhuma tarefa", + "taskGroup": "Grupo de Tarefas", + "taskId": "ID da Tarefa", + "taskInstance": { + "dagVersion": "Versão do Dag", + "executor": "Executor", + "executorConfig": "Configuração do Executor", + "hostname": "Hostname", + "maxTries": "Máximo de Tentativas", + "pid": "PID", + "pool": "Pool", + "poolSlots": "Slots do Pool", + "priorityWeight": "Prioridade", + "queue": "Fila", + "queuedWhen": "Enfileirado em", + "scheduledWhen": "Agendado em", + "triggerer": { + "assigned": "Triggerer atribuído", + "class": "Classe do Trigger", + "createdAt": "Tempo de criação do Trigger", + "id": "ID do Trigger", + "latestHeartbeat": "Último heartbeat do Trigger", + "title": "Informações do Trigger" + }, + "unixname": "Nome do Unix" + }, + "taskInstance_many": "Instâncias de Tarefa", + "taskInstance_one": "Instância de Tarefa", + "taskInstance_other": "Instâncias de Tarefa", + "taskInstance_zero": "Nenhuma instância de tarefa", + "timeRange": { + "last12Hours": "Últimas 12 Horas", + "last24Hours": "Últimas 24 Horas", + "lastHour": "Última Hora", + "pastWeek": "Semana Passada" + }, + "timestamp": { + "hide": "Ocultar", + "hotkey": "Pressione {{hotkey}} para ocultar/mostrar", + "show": "Mostrar" + }, + "timezone": "Fuso Horário", + "timezoneModal": { + "current-timezone": "Hora atual em", + "placeholder": "Selecionar um fuso horário", + "title": "Selecionar Fuso Horário", + "utc": "UTC (Tempo Universal Coordenado)" + }, + "toaster": { + "bulkDelete": { + "error": "Exclusão em Massa de {{resourceName}} Falhou", + "success": { + "description": "{{count}} {{resourceName}} foram excluídos com sucesso. Chaves: {{keys}}", + "title": "Exclusão em Massa de {{resourceName}} Submetida" + } + }, + "create": { + "error": "Criação de {{resourceName}} Falhou", + "success": { + "description": "{{resourceName}} foi criado com sucesso.", + "title": "Criação de {{resourceName}} Submetida" + } + }, + "delete": { + "error": "Exclusão de {{resourceName}} Falhou", + "success": { + "description": "{{resourceName}} foi excluído com sucesso.", + "title": "Exclusão de {{resourceName}} Submetida" + } + }, + "import": { + "error": "Importação de {{resourceName}} Falhou", + "success": { + "description": "{{count}} {{resourceName}} foram importados com sucesso.", + "title": "Importação de {{resourceName}} Submetida" + } + }, + "update": { + "error": "Atualização de {{resourceName}} Falhou", + "success": { + "description": "{{resourceName}} foi atualizado com sucesso.", + "title": "Atualização de {{resourceName}} Submetida" + } + } + }, + "total": "Total {{state}}", + "triggered": "Acionado", + "tryNumber": "Número de Tentativas", + "user": "Usuário", + "wrap": { + "hotkey": "Pressione {{hotkey}} para expandir ou recolher", + "tooltip": "Pressione {{hotkey}} para expandir ou recolher a secção", + "unwrap": "Expandir", + "wrap": "Recolher" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json new file mode 100644 index 0000000000000..6649bee37ebc5 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json @@ -0,0 +1,152 @@ +{ + "backfill": { + "affected_many": "{{count}} execuções serão acionadas.", + "affected_one": "1 execução será acionada.", + "affected_other": "{{count}} execuções serão acionadas.", + "affected_zero": "Nenhuma execução será acionada.", + "affectedNone": "Nenhuma execução correspondente aos critérios selecionados.", + "allRuns": "Todas as Execuções", + "backwards": "Executar para trás", + "dateRange": "Intervalo de Data", + "errorStartDateBeforeEndDate": "A Data Inicial deve ser antes da Data Final", + "maxRuns": "Máximo de Execuções Ativas", + "missingAndErroredRuns": "Execuções Faltando e com Erro", + "missingRuns": "Execuções Faltando", + "reprocessBehavior": "Comportamento de Reprocessamento", + "run": "Executar Backfill", + "selectDescription": "Executar este Dag para um intervalo de datas", + "selectLabel": "Backfill", + "title": "Executar Backfill", + "toaster": { + "success": { + "description": "Backfill jobs foram acionados com sucesso.", + "title": "Backfill gerado" + } + }, + "tooltip": "Backfill requer um agendamento", + "unpause": "Despausar {{dag_display_name}} ao acionar", + "validation": { + "datesRequired": "Ambas as datas de início e fim do intervalo devem ser fornecidas.", + "startBeforeEnd": "A Data Inicial do Intervalo deve ser menor ou igual à Data Final do Intervalo." + } + }, + "banner": { + "backfillInProgress": "Backfill em progresso", + "cancel": "Cancelar backfill", + "pause": "Pausar backfill", + "unpause": "Despausar backfill" + }, + "clipboard": { + "copy": "Copiar" + }, + "close": "Fechar", + "configForm": { + "advancedOptions": "Opções Avançadas", + "configJson": "Configuração JSON", + "invalidJson": "Formato JSON inválido: {{errorMessage}}" + }, + "dagWarnings": { + "error_many": "{{count}} Erros", + "error_one": "1 Erro", + "error_other": "{{count}} Erros", + "error_zero": "Nenhum erro", + "errorAndWarning": "1 Erro e {{warningText}}", + "warning_many": "{{count}} Avisos", + "warning_one": "1 Aviso", + "warning_other": "{{count}} Avisos", + "warning_zero": "Nenhum aviso" + }, + "durationChart": { + "duration": "Duração (segundos)", + "lastDagRun_many": "Últimas {{count}} Execuções do Dag", + "lastDagRun_one": "Última Execução do Dag", + "lastDagRun_other": "Últimas {{count}} Execuções do Dag", + "lastDagRun_zero": "Nenhuma execução do Dag", + "lastTaskInstance_many": "Últimas {{count}} Instâncias de Tarefa", + "lastTaskInstance_one": "Última Instância de Tarefa", + "lastTaskInstance_other": "Últimas {{count}} Instâncias de Tarefa", + "lastTaskInstance_zero": "Nenhuma instância de tarefa", + "queuedDuration": "Duração da Fila", + "runAfter": "Executar Depois", + "runDuration": "Duração da Execução" + }, + "fileUpload": { + "files_many": "{{count}} arquivos", + "files_one": "1 arquivo", + "files_other": "{{count}} arquivos", + "files_zero": "Nenhum arquivo" + }, + "flexibleForm": { + "placeholder": "Selecionar Valor", + "placeholderArray": "Digite cada string em uma nova linha", + "placeholderExamples": "Comece a digitar para ver opções", + "placeholderMulti": "Selecione um ou vários valores", + "validationErrorArrayNotArray": "O valor deve ser um array.", + "validationErrorArrayNotNumbers": "Todos os elementos do array devem ser números.", + "validationErrorArrayNotObject": "Todos os elementos do array devem ser objetos.", + "validationErrorRequired": "Este campo é obrigatório" + }, + "graph": { + "directionDown": "Superior para Inferior", + "directionLeft": "Direita para Esquerda", + "directionRight": "Esquerda para Direita", + "directionUp": "Inferior para Superior", + "downloadImage": "Baixar imagem do gráfico", + "downloadImageError": "Falha ao baixar a imagem do gráfico.", + "downloadImageErrorTitle": "Download Falhou", + "otherDagRuns": "+Outras Execuções do Dag", + "taskCount_many": "{{count}} Tarefas", + "taskCount_one": "{{count}} Tarefa", + "taskCount_other": "{{count}} Tarefas", + "taskCount_zero": "Nenhuma tarefa", + "taskGroup": "Grupo de Tarefas" + }, + "limitedList": "+{{count}} mais", + "limitedList.allItems": "Todos os {{count}} itens:", + "limitedList.clickToInteract": "Clique em uma etiqueta para filtrar os Dags", + "limitedList.clickToOpenFull": "Clique em \"+{{count}} mais\" para ver a lista completa", + "limitedList.copyPasteText": "Você pode copiar e colar o texto acima", + "logs": { + "file": "Arquivo", + "location": "linha {{line}} em {{name}}" + }, + "reparseDag": "Reparse Dag", + "sortedAscending": "Ordenado em ordem crescente", + "sortedDescending": "Ordenado em ordem decrescente", + "sortedUnsorted": "Não ordenado", + "taskTries": "Tentativas de Tarefa", + "toggleCardView": "Mostrar visualização de cartão", + "toggleTableView": "Mostrar visualização de tabela", + "triggerDag": { + "button": "Acionar", + "loading": "Carregando informações do Dag...", + "loadingFailed": "Falha ao carregar informações do Dag. Por favor, tente novamente.", + "runIdHelp": "Opcional - será gerado se não for fornecido", + "selectDescription": "Acionar uma única execução deste Dag", + "selectLabel": "Execução Única", + "title": "Acionar Dag", + "toaster": { + "success": { + "description": "A execução do Dag foi acionada com sucesso.", + "title": "Execução do Dag acionada" + } + }, + "unpause": "Despausar {{dagDisplayName}} ao acionar" + }, + "trimText": { + "details": "Detalhes", + "empty": "Vazio", + "noContent": "Nenhum conteúdo disponível." + }, + "versionDetails": { + "bundleLink": "Link do Bundle", + "bundleName": "Nome do Bundle", + "bundleVersion": "Versão do Bundle", + "createdAt": "Criado em", + "versionId": "ID da Versão" + }, + "versionSelect": { + "dagVersion": "Versão do Dag", + "versionCode": "v{{versionCode}}" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dag.json new file mode 100644 index 0000000000000..2c9cb5cdbb4ef --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dag.json @@ -0,0 +1,162 @@ +{ + "allRuns": "Todas as Execuções", + "blockingDeps": { + "dependency": "Dependência", + "reason": "Motivo", + "title": "Dependências Bloqueando Tarefa de Ser Agendada" + }, + "calendar": { + "daily": "Diário", + "hourly": "Por Hora", + "legend": { + "less": "Menos", + "more": "Mais" + }, + "navigation": { + "nextMonth": "Próximo mês", + "nextYear": "Próximo ano", + "previousMonth": "Mês anterior", + "previousYear": "Ano anterior" + }, + "noData": "Nenhum dado disponível", + "noRuns": "Nenhuma execução", + "totalRuns": "Total de Execuções", + "week": "Semana {{weekNumber}}", + "weekdays": { + "friday": "Sex", + "monday": "Seg", + "saturday": "Sáb", + "sunday": "Dom", + "thursday": "Qui", + "tuesday": "Ter", + "wednesday": "Qua" + } + }, + "code": { + "bundleUrl": "URL do Bundle", + "noCode": "Nenhum Código Encontrado", + "parseDuration": "Duração do Processamento:", + "parsedAt": "Processado em:" + }, + "extraLinks": "Links Extra", + "grid": { + "buttons": { + "resetToLatest": "Redefinir para a Última", + "toggleGroup": "Alternar Grupo" + } + }, + "header": { + "buttons": { + "advanced": "Avançado", + "dagDocs": "Documentação do Dag" + } + }, + "logs": { + "allLevels": "Todos os Níveis de Log", + "allSources": "Todas as Fontes", + "critical": "CRÍTICO", + "debug": "DEBUG", + "error": "ERRO", + "fullscreen": { + "button": "Tela cheia", + "tooltip": "Pressione {{hotkey}} para tela cheia" + }, + "info": "INFO", + "noTryNumber": "Nenhum número de tentativa", + "settings": "Configurações", + "viewInExternal": "Ver logs em {{name}} (tentativa {{attempt}})", + "warning": "AVISO" + }, + "navigation": { + "navigation": "Navegação: Shift+{{arrow}}", + "toggleGroup": "Alternar grupo: Espaço" + }, + "overview": { + "buttons": { + "failedRun_many": "Execuções Falhadas", + "failedRun_one": "Execução Falhada", + "failedRun_other": "Execuções Falhadas", + "failedRun_zero": "Nenhuma execução falhada", + "failedTask_many": "Tarefas Falhadas", + "failedTask_one": "Tarefa Falhada", + "failedTask_other": "Tarefas Falhadas", + "failedTask_zero": "Nenhuma tarefa falhada", + "failedTaskInstance_many": "Instâncias de Tarefa Falhadas", + "failedTaskInstance_one": "Instância de Tarefa Falhada", + "failedTaskInstance_other": "Instâncias de Tarefa Falhadas", + "failedTaskInstance_zero": "Nenhuma instância de tarefa falhada" + }, + "charts": { + "assetEvent_many": "Eventos de Asset Criados", + "assetEvent_one": "Evento de Asset Criado", + "assetEvent_other": "Eventos de Asset Criados", + "assetEvent_zero": "Nenhum evento de asset criado" + }, + "failedLogs": { + "hideLogs": "Ocultar Logs", + "showLogs": "Mostrar Logs", + "title": "Logs Recentes de Tarefas Falhadas", + "viewFullLogs": "Ver logs completos" + } + }, + "panel": { + "buttons": { + "options": "Opções", + "showGantt": "Mostrar Gantt", + "showGraphShortcut": "Mostrar Gráfico (Pressione g)", + "showGridShortcut": "Mostrar Grade (Pressione g)" + }, + "dagRuns": { + "label": "Número de Execuções do Dag" + }, + "dependencies": { + "label": "Dependências", + "options": { + "allDagDependencies": "Todas as Dependências do Dag", + "externalConditions": "Condições Externas", + "onlyTasks": "Somente Tarefas" + }, + "placeholder": "Dependências" + }, + "graphDirection": { + "label": "Direção do Gráfico" + } + }, + "paramsFailed": "Falha ao carregar parâmetros", + "parse": { + "toaster": { + "error": { + "description": "Falha ao processar o Dag. Pode haver solicitações de processamento pendentes ainda não processadas.", + "title": "Falha ao Reprocessar o Dag" + }, + "success": { + "description": "O Dag deve ser reprocessado em breve.", + "title": "Solicitação de Reprocessamento Enviada com Sucesso" + } + } + }, + "tabs": { + "assetEvents": "Asset Events", + "auditLog": "Log de Auditoria", + "backfills": "Backfills", + "calendar": "Calendário", + "code": "Código", + "details": "Detalhes", + "logs": "Logs", + "mappedTaskInstances_many": "Instâncias de Tarefa [{{count}}]", + "mappedTaskInstances_one": "Instância de Tarefa [{{count}}]", + "mappedTaskInstances_other": "Instâncias de Tarefa [{{count}}]", + "mappedTaskInstances_zero": "Nenhuma instância de tarefa mapeada", + "overview": "Visão Geral", + "renderedTemplates": "Modelos Renderizados", + "requiredActions": "Ações Necessárias", + "runs": "Execuções", + "taskInstances": "Instâncias de Tarefa", + "tasks": "Tarefas", + "xcom": "XCom" + }, + "taskGroups": { + "collapseAll": "Recolher todos os grupos de tarefas", + "expandAll": "Expandir todos os grupos de tarefas" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json new file mode 100644 index 0000000000000..138a7b9293139 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json @@ -0,0 +1,96 @@ +{ + "assetSchedule": "{{count}} de {{total}} ativos atualizados", + "dagActions": { + "delete": { + "button": "Excluir Dag", + "warning": "Isso removerá todas as metadados relacionados ao Dag, incluindo Execuções e Tarefas." + } + }, + "favoriteDag": "Dag Favorito", + "filters": { + "allRunTypes": "Todos os Tipos de Execução", + "allStates": "Todos os Estados", + "favorite": { + "all": "Todos", + "favorite": "Favorito", + "unfavorite": "Remover dos Favoritos" + }, + "paused": { + "active": "Ativo", + "all": "Todos", + "paused": "Pausado" + }, + "runIdPatternFilter": "Pesquisar Execuções de Dag" + }, + "ownerLink": "Link do Proprietário para {{owner}}", + "runAndTaskActions": { + "affectedTasks": { + "noItemsFound": "Nenhuma tarefa encontrada.", + "title": "Tarefas Afetadas: {{count}}" + }, + "clear": { + "button": "Limpar {{type}}", + "buttonTooltip": "Pressione shift+c para limpar", + "error": "Falha ao limpar {{type}}", + "title": "Limpar {{type}}" + }, + "delete": { + "button": "Excluir {{type}}", + "dialog": { + "resourceName": "{{type}} {{id}}", + "title": "Excluir {{type}}", + "warning": "Isso removerá todas as metadados relacionados ao {{type}}." + }, + "error": "Erro ao excluir {{type}}", + "success": { + "description": "A solicitação de exclusão do {{type}} foi bem-sucedida.", + "title": "{{type}} Excluído com Sucesso" + } + }, + "markAs": { + "button": "Marcar {{type}} como...", + "buttonTooltip": { + "failed": "Pressione shift+f para marcar como falha", + "success": "Pressione shift+s para marcar como sucesso" + }, + "title": "Marcar {{type}} como {{state}}" + }, + "options": { + "downstream": "Downstream", + "existingTasks": "Limpar tarefas existentes", + "future": "Futuro", + "onlyFailed": "Limpar somente tarefas falhadas", + "past": "Passado", + "queueNew": "Enfileirar novas tarefas", + "runOnLatestVersion": "Executar com a versão mais recente do pacote", + "upstream": "Upstream" + } + }, + "search": { + "advanced": "Pesquisa Avançada", + "clear": "Limpar pesquisa", + "dags": "Pesquisar Dags", + "hotkey": "+K", + "tasks": "Pesquisar Tarefas" + }, + "sort": { + "displayName": { + "asc": "Ordenar por Nome (A-Z)", + "desc": "Ordenar por Nome (Z-A)" + }, + "lastRunStartDate": { + "asc": "Ordenar por Data de Início da Última Execução (Mais Antiga-Mais Recente)", + "desc": "Ordenar por Data de Início da Última Execução (Mais Recente-Mais Antiga)" + }, + "lastRunState": { + "asc": "Ordenar por Estado da Última Execução (A-Z)", + "desc": "Ordenar por Estado da Última Execução (Z-A)" + }, + "nextDagRun": { + "asc": "Ordenar por Próxima Execução do Dag (Mais Antiga-Mais Recente)", + "desc": "Ordenar por Próxima Execução do Dag (Mais Recente-Mais Antiga)" + }, + "placeholder": "Ordenar por" + }, + "unfavoriteDag": "Remover Dag dos Favoritos" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dashboard.json new file mode 100644 index 0000000000000..d5245e4b6236e --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dashboard.json @@ -0,0 +1,49 @@ +{ + "favorite": { + "favoriteDags_many": "Primeiros {{count}} Dags favoritos", + "favoriteDags_one": "Primeiro {{count}} Dag favorito", + "favoriteDags_other": "Primeiros {{count}} Dags favoritos", + "favoriteDags_zero": "Nenhum Dag favorito", + "noDagRuns": "Ainda não há DagRun para este dag.", + "noFavoriteDags": "Nenhum favorito ainda. Clique no ícone de estrela ao lado de um Dag na lista para adicioná-lo aos seus favoritos." + }, + "group": "Grupo", + "health": { + "dagProcessor": "Processador de Dag", + "health": "Saúde", + "healthy": "Saúde", + "lastHeartbeat": "Último Heartbeat", + "metaDatabase": "Base de Dados de Metadados", + "scheduler": "Agendador", + "status": "Estado", + "triggerer": "Disparador", + "unhealthy": "Não saudável" + }, + "history": "Histórico", + "importErrors": { + "dagImportError_many": "Erros de Importação de Dag", + "dagImportError_one": "Erro de Importação de Dag", + "dagImportError_other": "Erros de Importação de Dag", + "dagImportError_zero": "Nenhum erro de importação de Dag", + "searchByFile": "Pesquisar por arquivo", + "timestamp": "Timestamp" + }, + "managePools": "Gerenciar Pools", + "noAssetEvents": "Nenhum Evento de Asset encontrado.", + "poolSlots": "Slots de Pool", + "sortBy": { + "newestFirst": "Mais Recentes Primeiro", + "oldestFirst": "Mais Antigos Primeiro" + }, + "source": "Fonte", + "stats": { + "activeDags": "Dags Ativos", + "failedDags": "Dags Falhados", + "queuedDags": "Dags em Fila", + "requiredActions": "Ações Necessárias", + "runningDags": "Dags em Execução", + "stats": "Estatísticas" + }, + "uri": "URI", + "welcome": "Bem-vindo" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/hitl.json new file mode 100644 index 0000000000000..c63027cdea479 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/hitl.json @@ -0,0 +1,38 @@ +{ + "filters": { + "response": { + "all": "Todas", + "pending": "Pendente", + "received": "Revisadas" + } + }, + "requiredAction_many": "Ações Necessárias", + "requiredAction_one": "Ação Necessária", + "requiredAction_other": "Ações Necessárias", + "requiredAction_zero": "Nenhuma ação necessária", + "requiredActionCount_many": "Ações Necessárias ({{count}})", + "requiredActionCount_one": "Ação Necessária ({{count}})", + "requiredActionCount_other": "Ações Necessárias ({{count}})", + "requiredActionCount_zero": "Nenhuma ação necessária", + "requiredActionState": "Estado da Ação Necessária", + "response": { + "error": "Falha na resposta", + "optionsDescription": "Escolha suas opções para esta instância de tarefa", + "optionsLabel": "Opções", + "received": "Resposta recebida em ", + "respond": "Responder", + "success": "Resposta de {{taskId}} bem-sucedida", + "title": "Instância de Tarefa Humana - {{taskId}}" + }, + "state": { + "approvalReceived": "Aprovação Recebida", + "approvalRequired": "Aprovação Necessária", + "choiceReceived": "Escolha Recebida", + "choiceRequired": "Escolha Necessária", + "noResponseReceived": "Nenhuma Resposta Recebida", + "rejectionReceived": "Rejeição Recebida", + "responseReceived": "Resposta Recebida", + "responseRequired": "Resposta Necessária" + }, + "subject": "Assunto" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/tasks.json new file mode 100644 index 0000000000000..d15d2dd10ce28 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "Mapeado", + "notMapped": "Não mapeado", + "retries": "Tentativas", + "searchTasks": "Pesquisar tarefas", + "selectMapped": "Selecionar mapeado", + "selectOperator": "Selecionar operadores", + "selectRetryValues": "Selecionar valores de tentativa", + "selectTriggerRules": "Selecionar regras de gatilho" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/admin.json new file mode 100644 index 0000000000000..cbfa9705abace --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/admin.json @@ -0,0 +1,166 @@ +{ + "columns": { + "description": "คำอธิบาย", + "key": "คีย์", + "name": "ชื่อ", + "value": "ค่า" + }, + "config": { + "columns": { + "section": "ส่วน" + }, + "title": "การตั้งค่า Airflow" + }, + "connections": { + "add": "เพิ่มการเชื่อมต่อ", + "columns": { + "connectionId": "ID การเชื่อมต่อ", + "connectionType": "ประเภทการเชื่อมต่อ", + "host": "โฮสต์", + "port": "พอร์ต" + }, + "connection_one": "การเชื่อมต่อ", + "connection_other": "การเชื่อมต่อทั้งหมด", + "delete": { + "deleteConnection_one": "ลบการเชื่อมต่อ 1 รายการ", + "deleteConnection_other": "ลบการเชื่อมต่อ {{count}} รายการ", + "firstConfirmMessage_one": "คุณกำลังจะลบการเชื่อมต่อดังต่อไปนี้:", + "firstConfirmMessage_other": "คุณกำลังจะลบการเชื่อมต่อดังต่อไปนี้:", + "title": "ลบการเชื่อมต่อ" + }, + "edit": "แก้ไขการเชื่อมต่อ", + "form": { + "connectionIdRequired": "กรุณาระบุ ID การเชื่อมต่อ", + "connectionIdRequirement": "ID การเชื่อมต่อไม่สามารถมีช่องว่างได้", + "connectionTypeRequired": "กรุณาระบุประเภทการเชื่อมต่อ", + "extraFields": "ฟิลด์เพิ่มเติม", + "extraFieldsJson": "ฟิลด์เพิ่มเติมในรูปแบบ JSON", + "helperText": "ไม่พบประเภทการเชื่อมต่อ? โปรดตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง Airflow Providers Package ที่เกี่ยวข้องแล้ว", + "helperTextForRedactedFields": "ฟิลด์ที่ถูกซ่อน ('***') จะไม่ถูกเปลี่ยนแปลงหากไม่ได้แก้ไข", + "selectConnectionType": "เลือกประเภทการเชื่อมต่อ", + "standardFields": "ฟิลด์มาตรฐาน" + }, + "nothingFound": { + "description": "การเชื่อมต่อที่ตั้งค่าผ่าน environment variables หรือ secrets managers จะไม่ปรากฏที่นี่", + "documentationLink": "ดูรายละเอียดเพิ่มเติมได้ในเอกสารของ Airflow", + "learnMore": "ค่าการเชื่อมต่อเหล่านี้จะถูกประมวลผลตอน runtime และจะไม่แสดงในหน้า UI", + "title": "ไม่พบการเชื่อมต่อ!" + }, + "searchPlaceholder": "ค้นหาการเชื่อมต่อ", + "test": "ทดสอบการเชื่อมต่อ", + "testDisabled": "ฟีเจอร์ทดสอบการเชื่อมต่อถูกปิดใช้งาน โปรดติดต่อผู้ดูแลระบบเพื่อเปิดใช้งาน", + "typeMeta": { + "error": "ไม่สามารถดึงข้อมูล Connection Type Meta ได้", + "standardFields": { + "description": "คำอธิบาย", + "host": "โฮสต์", + "login": "เข้าสู่ระบบ", + "password": "รหัสผ่าน", + "port": "พอร์ต", + "url_schema": "สคีมา" + } + } + }, + "deleteActions": { + "button": "ลบ", + "modal": { + "confirmButton": "ใช่, ลบ", + "secondConfirmMessage": "การกระทำนี้เป็นการถาวรและไม่สามารถย้อนกลับได้", + "thirdConfirmMessage": "คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?" + }, + "selected": "ที่เลือกไว้", + "tooltip": "ลบการเชื่อมต่อที่เลือก" + }, + "formActions": { + "save": "บันทึก" + }, + "plugins": { + "columns": { + "source": "แหล่งที่มา" + }, + "importError_one": "เกิดข้อผิดพลาดในการนำเข้าปลั๊กอิน", + "importError_other": "เกิดข้อผิดพลาดในการนำเข้าปลั๊กอิน", + "searchPlaceholder": "ค้นหาตามไฟล์" + }, + "pools": { + "add": "เพิ่มพูล", + "deferredSlotsIncluded": "รวมช่องที่ถูกเลื่อนแล้ว", + "delete": { + "title": "ลบพูล", + "warning": "การกระทำนี้จะลบเมทาดาต้าทั้งหมดที่เกี่ยวข้องกับพูลและอาจส่งผลกระทบต่องานที่ใช้พูลนี้" + }, + "edit": "แก้ไขพูล", + "form": { + "checkbox": "เลือกเพื่อรวมงานที่ถูกเลื่อนเมื่อคำนวณจำนวนช่องว่างในพูล", + "description": "คำอธิบาย", + "includeDeferred": "รวมงานที่ถูกเลื่อน", + "nameMaxLength": "ชื่อยาวได้ไม่เกิน 256 ตัวอักษร", + "nameRequired": "กรุณาระบุชื่อ", + "slots": "จำนวนช่อง" + }, + "noPoolsFound": "ไม่พบพูล", + "pool_one": "พูล", + "pool_other": "พูลทั้งหมด", + "searchPlaceholder": "ค้นหาพูล", + "sort": { + "asc": "ชื่อ (A-Z)", + "desc": "ชื่อ (Z-A)", + "placeholder": "เรียงตาม" + } + }, + "providers": { + "columns": { + "packageName": "ชื่อแพ็กเกจ", + "version": "เวอร์ชัน" + } + }, + "variables": { + "add": "เพิ่มตัวแปร", + "columns": { + "isEncrypted": "ถูกเข้ารหัสหรือไม่" + }, + "delete": { + "deleteVariable_one": "ลบตัวแปร 1 รายการ", + "deleteVariable_other": "ลบตัวแปร {{count}} รายการ", + "firstConfirmMessage_one": "คุณกำลังจะลบตัวแปรดังต่อไปนี้:", + "firstConfirmMessage_other": "คุณกำลังจะลบตัวแปรดังต่อไปนี้:", + "title": "ลบตัวแปร", + "tooltip": "ลบตัวแปรที่เลือก" + }, + "edit": "แก้ไขตัวแปร", + "export": "ส่งออก", + "exportTooltip": "ส่งออกตัวแปรที่เลือก", + "form": { + "invalidJson": "JSON ไม่ถูกต้อง", + "keyMaxLength": "คีย์ยาวได้ไม่เกิน 250 ตัวอักษร", + "keyRequired": "กรุณาระบุคีย์", + "valueRequired": "กรุณาระบุค่า" + }, + "import": { + "button": "นำเข้า", + "conflictResolution": "เลือกวิธีจัดการเมื่อมีตัวแปรซ้ำ", + "errorParsingJsonFile": "เกิดข้อผิดพลาดในการอ่านไฟล์ JSON: โปรดอัปโหลดไฟล์ JSON ที่มีตัวแปร (เช่น {\"key\": \"value\", ...})", + "options": { + "fail": { + "description": "หยุดการนำเข้าหากพบตัวแปรที่มีอยู่แล้ว", + "title": "ล้มเหลว" + }, + "overwrite": { + "description": "เขียนทับตัวแปรในกรณีที่มีความขัดแย้ง", + "title": "เขียนทับ" + }, + "skip": { + "description": "ข้ามการนำเข้าตัวแปรที่มีอยู่แล้ว", + "title": "ข้าม" + } + }, + "title": "นำเข้าตัวแปร", + "upload": "อัปโหลดไฟล์ JSON", + "uploadPlaceholder": "อัปโหลดไฟล์ JSON ที่มีตัวแปร (เช่น {\"key\": \"value\", ...})" + }, + "noRowsMessage": "ไม่พบตัวแปร", + "searchPlaceholder": "ค้นหาคีย์", + "variable_one": "ตัวแปร", + "variable_other": "ตัวแปรทั้งหมด" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/assets.json new file mode 100644 index 0000000000000..7991401f30344 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/assets.json @@ -0,0 +1,30 @@ +{ + "consumingDags": "Dags ที่ใช้", + "createEvent": { + "button": "สร้างอีเวนต์", + "manual": { + "description": "สร้างอีเวนต์ให้กับ Asset ด้วยตนเอง", + "extra": "ข้อมูลเพิ่มเติมของอีเวนต์ Asset", + "label": "สร้างด้วยตนเอง" + }, + "materialize": { + "description": "ทริกเกอร์ Dag ต้นทางของ Asset นี้", + "descriptionWithDag": "ทริกเกอร์ Dag ต้นทางของ Asset นี้: {{dagName}}", + "label": "สร้างจากต้นทาง (Materialize)", + "unpauseDag": "ยกเลิกการหยุดพัก {{dagName}} เมื่อทริกเกอร์" + }, + "success": { + "manualDescription": "สร้างอีเวนต์ให้กับ Asset ด้วยตนเองสำเร็จแล้ว", + "manualTitle": "สร้างอีเวนต์ให้กับ Asset แล้ว", + "materializeDescription": "Dag ต้นทาง {{dagId}} ถูกทริกเกอร์สำเร็จแล้ว", + "materializeTitle": "กำลังสร้าง Asset" + }, + "title": "สร้างอีเวนต์ให้กับ Asset สำหรับ {{name}}" + }, + "group": "กลุ่ม", + "lastAssetEvent": "อีเวนต์ของ Asset ล่าสุด", + "name": "ชื่อ", + "producingTasks": "งานที่ถูกสร้าง", + "scheduledDags": "Dags ที่ถูกตั้งเวลา", + "searchPlaceholder": "ค้นหา Assets" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/browse.json new file mode 100644 index 0000000000000..953dd46d608a9 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/browse.json @@ -0,0 +1,22 @@ +{ + "auditLog": { + "columns": { + "event": "อีเวนต์", + "extra": "เพิ่มเติม", + "user": "ผู้ใช้", + "when": "เมื่อไหร่" + }, + "filters": { + "eventType": "ประเภทของอีเวนต์" + }, + "title": "บันทึกการตรวจสอบ (Audit Log)" + }, + "xcom": { + "columns": { + "dag": "Dag", + "key": "คีย์", + "value": "ค่า" + }, + "title": "XCom" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json new file mode 100644 index 0000000000000..8934d826ced65 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json @@ -0,0 +1,318 @@ +{ + "admin": { + "Config": "การตั้งค่า", + "Connections": "การเชื่อมต่อ", + "Plugins": "ปลั๊กอิน", + "Pools": "พูล", + "Providers": "ผู้ให้บริการ", + "Variables": "ตัวแปร" + }, + "allOperators": "ตัวดำเนินการทั้งหมด", + "appearance": { + "appearance": "การแสดงผล", + "darkMode": "โหมดมืด", + "lightMode": "โหมดสว่าง", + "systemMode": "ตามการตั้งค่าของระบบ" + }, + "asset_one": "Asset", + "asset_other": "Assets", + "assetEvent_one": "อีเวนต์ของ Asset", + "assetEvent_other": "อีเวนต์ของ Assets", + "backfill_one": "Backfill", + "backfill_other": "Backfills", + "browse": { + "auditLog": "บันทึกการตรวจสอบ (Audit Log)", + "requiredActions": "การดำเนินการที่จำเป็น", + "xcoms": "XComs" + }, + "collapseAllExtra": "ย่อข้อมูล JSON เพิ่มเติมทั้งหมด", + "collapseDetailsPanel": "ย่อแผงรายละเอียด", + "createdAssetEvent_one": "อีเวนต์ของ Asset ที่สร้างแล้ว", + "createdAssetEvent_other": "อีเวนต์ของ Assets ที่สร้างแล้ว", + "dag_one": "Dag", + "dag_other": "Dags", + "dagDetails": { + "catchup": "Catchup", + "dagRunTimeout": "ระยะหมดเวลาของ Dag Run", + "defaultArgs": "ค่า Arguments เริ่มต้น", + "description": "คำอธิบาย", + "documentation": "เอกสารประกอบ Dag", + "fileLocation": "ตำแหน่งไฟล์", + "hasTaskConcurrencyLimits": "มีการจำกัดจำนวนงานที่ทำพร้อมกัน", + "lastExpired": "หมดอายุครั้งล่าสุด", + "lastParseDuration": "ระยะเวลาการประมวลผลล่าสุด", + "lastParsed": "การประมวลผลครั้งล่าสุด", + "latestDagVersion": "เวอร์ชัน Dag ล่าสุด", + "latestRun": "การทำงานครั้งล่าสุด", + "maxActiveRuns": "จำนวน Dag Run ที่ทำงานพร้อมกันสูงสุด", + "maxActiveTasks": "จำนวนงานที่ทำงานพร้อมกันสูงสุด", + "maxConsecutiveFailedDagRuns": "จำนวน Dag Run ที่ล้มเหลวต่อเนื่องสูงสุด", + "nextRun": "การทำงานครั้งถัดไป", + "owner": "เจ้าของ", + "params": "พารามิเตอร์", + "schedule": "เวลาทำงาน", + "tags": "แท็ก" + }, + "dagId": "Dag ID", + "dagRun": { + "conf": "การตั้งค่า (Conf)", + "dagVersions": "เวอร์ชันของ Dag", + "dataIntervalEnd": "สิ้นสุดช่วงข้อมูล", + "dataIntervalStart": "เริ่มต้นช่วงข้อมูล", + "lastSchedulingDecision": "การตัดสินใจกำหนดเวลาล่าสุด", + "queuedAt": "เข้าคิวเมื่อ", + "runAfter": "ทำงานหลังจาก", + "runType": "ประเภทการทำงาน", + "sourceAssetEvent": "อีเวนต์ของ Asset ต้นทาง", + "triggeredBy": "ทริกเกอร์โดย", + "triggeringUser": "ชื่อผู้ใช้ที่ทริกเกอร์" + }, + "dagRun_one": "Dag Run", + "dagRun_other": "Dag Runs", + "dagRunId": "Dag Run ID", + "dagWarnings": "คำเตือน/ข้อผิดพลาดของ Dag", + "defaultToGraphView": "ค่าเริ่มต้นเป็นมุมมองกราฟ", + "defaultToGridView": "ค่าเริ่มต้นเป็นมุมมองกริด", + "direction": "ทิศทาง", + "docs": { + "documentation": "เอกสารประกอบ", + "githubRepo": "GitHub Repo", + "restApiReference": "เอกสารอ้างอิง REST API" + }, + "duration": "ระยะเวลา", + "endDate": "วันที่สิ้นสุด", + "error": { + "back": "ย้อนกลับ", + "defaultMessage": "เกิดข้อผิดพลาดที่ไม่คาดคิดขึ้น", + "home": "หน้าแรก", + "notFound": "ไม่พบหน้านี้", + "title": "เกิดข้อผิดพลาด" + }, + "expand": { + "collapse": "ย่อ", + "expand": "ขยาย", + "hotkey": "e", + "tooltip": "กด {{hotkey}} เพื่อสลับการขยาย" + }, + "expandAllExtra": "ขยายข้อมูล JSON เพิ่มเติมทั้งหมด", + "expression": { + "all": "ทั้งหมด", + "and": "และ", + "any": "อย่างใดอย่างหนึ่ง", + "or": "หรือ" + }, + "filter": "ตัวกรอง", + "filters": { + "logicalDateFrom": "วันที่ตามกำหนดทำงานตั้งแต่", + "logicalDateTo": "วันที่ตามกำหนดทำงานถึง", + "runAfterFrom": "ทำงานตั้งแต่", + "runAfterTo": "ทำงานถึง" + }, + "logicalDate": "วันที่ตามกำหนดทำงาน", + "logout": "ออกจากระบบ", + "logoutConfirmation": "คุณกำลังจะออกจากระบบแอปพลิเคชัน", + "mapIndex": "ดัชนีแผนที่", + "modal": { + "cancel": "ยกเลิก", + "confirm": "ยืนยัน", + "delete": { + "button": "ลบ", + "confirmation": "คุณแน่ใจหรือไม่ว่าต้องการลบ {{resourceName}}? การดำเนินการนี้ไม่สามารถย้อนกลับได้" + } + }, + "nav": { + "admin": "ผู้ดูแลระบบ", + "assets": "Assets", + "browse": "เรียกดู", + "dags": "Dags", + "docs": "เอกสาร", + "home": "หน้าแรก", + "legacyFabViews": "มุมมองแบบเก่า", + "plugins": "ปลั๊กอิน", + "security": "ความปลอดภัย" + }, + "noItemsFound": "ไม่พบ {{modelName}}", + "note": { + "add": "เพิ่มโน้ต", + "dagRun": "โน้ตของ Dag Run", + "label": "โน้ต", + "placeholder": "เพิ่มโน้ต...", + "taskInstance": "โน้ตของ Task Instance" + }, + "pools": { + "deferred": "เลื่อนออกไป", + "open": "เปิด", + "pools_one": "พูล", + "pools_other": "พูล", + "queued": "เข้าคิวแล้ว", + "running": "กำลังทำงาน", + "scheduled": "กำหนดเวลาทำงานแล้ว" + }, + "reset": "รีเซ็ต", + "runId": "Run ID", + "runTypes": { + "asset_triggered": "ทริกเกอร์โดย Asset", + "backfill": "Backfill", + "manual": "ทำด้วยตนเอง", + "scheduled": "กำหนดเวลาทำงานแล้ว" + }, + "scroll": { + "direction": { + "bottom": "ล่างสุด", + "top": "บนสุด" + }, + "tooltip": "กด {{hotkey}} เพื่อเลื่อนไปที่ {{direction}}" + }, + "security": { + "actions": "การดำเนินการ", + "permissions": "สิทธิ์", + "resources": "ทรัพยากร", + "roles": "บทบาท", + "users": "ผู้ใช้" + }, + "selectLanguage": "เลือกภาษา", + "showDetailsPanel": "แสดงแผงรายละเอียด", + "source": { + "hide": "ซ่อนแหล่งที่มา", + "hotkey": "s", + "show": "แสดงแหล่งที่มา" + }, + "sourceAssetEvent_one": "อีเวนต์ของ Asset ต้นทาง", + "sourceAssetEvent_other": "อีเวนต์ของ Asset ต้นทาง", + "startDate": "วันที่เริ่มต้น", + "state": "สถานะ", + "states": { + "deferred": "เลื่อนออกไป", + "failed": "ล้มเหลว", + "no_status": "ไม่มีสถานะ", + "none": "ยังไม่กำหนดสถานะ", + "planned": "วางแผนแล้ว", + "queued": "เข้าคิวแล้ว", + "removed": "ถูกลบแล้ว", + "restarting": "กำลังเริ่มใหม่", + "running": "กำลังทำงาน", + "scheduled": "กำหนดเวลาทำงานแล้ว", + "skipped": "ข้าม", + "success": "สำเร็จ", + "up_for_reschedule": "รอการกำหนดเวลาทำงานใหม่", + "up_for_retry": "รอการลองใหม่", + "upstream_failed": "ต้นทางล้มเหลว" + }, + "table": { + "completedAt": "เสร็จสิ้นเมื่อ", + "createdAt": "สร้างเมื่อ", + "filterByTag": "กรอง Dags ตามแท็ก", + "filterColumns": "กรองคอลัมน์ตาราง", + "filterReset_one": "รีเซ็ตตัวกรอง", + "filterReset_other": "รีเซ็ตตัวกรอง", + "from": "จาก", + "maxActiveRuns": "จำนวน Dag Run ที่ทำงานพร้อมกันสูงสุด", + "noTagsFound": "ไม่พบแท็ก", + "tagMode": { + "all": "ทั้งหมด", + "any": "อย่างใดอย่างหนึ่ง" + }, + "tagPlaceholder": "กรองตามแท็ก", + "to": "ถึง" + }, + "task": { + "documentation": "เอกสารประกอบงาน", + "lastInstance": "อินสแตนซ์ล่าสุด", + "operator": "ตัวดำเนินการ", + "triggerRule": "กฎการทริกเกอร์" + }, + "task_one": "งาน", + "task_other": "งาน", + "taskGroup": "กลุ่มงาน", + "taskId": "Task ID", + "taskInstance": { + "dagVersion": "เวอร์ชัน Dag", + "executor": "Executor", + "executorConfig": "การตั้งค่า Executor", + "hostname": "ชื่อโฮสต์", + "maxTries": "จำนวนครั้งสูงสุด", + "pid": "PID", + "pool": "พูล", + "poolSlots": "ช่องในพูล", + "priorityWeight": "น้ำหนักความสำคัญ", + "queue": "คิว", + "queuedWhen": "เข้าคิวเมื่อ", + "scheduledWhen": "กำหนดเวลาทำงานเมื่อ", + "triggerer": { + "assigned": "Triggerer ที่ถูกกำหนด", + "class": "คลาส Trigger", + "createdAt": "เวลาสร้าง Trigger", + "id": "Trigger ID", + "latestHeartbeat": "Heartbeat ล่าสุดของ Triggerer", + "title": "ข้อมูล Triggerer" + }, + "unixname": "ชื่อ Unix" + }, + "taskInstance_one": "Task Instance", + "taskInstance_other": "Task Instances", + "timeRange": { + "last12Hours": "12 ชั่วโมงล่าสุด", + "last24Hours": "24 ชั่วโมงล่าสุด", + "lastHour": "ชั่วโมงล่าสุด", + "pastWeek": "สัปดาห์ที่ผ่านมา" + }, + "timestamp": { + "hide": "ซ่อนเวลา", + "hotkey": "t", + "show": "แสดงเวลา" + }, + "timezone": "เขตเวลา", + "timezoneModal": { + "current-timezone": "เวลาปัจจุบันของ", + "placeholder": "เลือกเขตเวลา", + "title": "เลือกเขตเวลา", + "utc": "UTC (เวลาสากล)" + }, + "toaster": { + "bulkDelete": { + "error": "คำขอลบจำนวนมากของ {{resourceName}} ล้มเหลว", + "success": { + "description": "{{count}} {{resourceName}} ถูกลบเรียบร้อยแล้ว คีย์: {{keys}}", + "title": "ส่งคำขอลบจำนวนมากของ {{resourceName}} แล้ว" + } + }, + "create": { + "error": "คำขอสร้าง {{resourceName}} ล้มเหลว", + "success": { + "description": "{{resourceName}} ถูกสร้างเรียบร้อยแล้ว", + "title": "ส่งคำขอสร้าง {{resourceName}} แล้ว" + } + }, + "delete": { + "error": "คำขอลบ {{resourceName}} ล้มเหลว", + "success": { + "description": "{{resourceName}} ถูกลบเรียบร้อยแล้ว", + "title": "ส่งคำขอลบ {{resourceName}} แล้ว" + } + }, + "import": { + "error": "คำขอนำเข้า {{resourceName}} ล้มเหลว", + "success": { + "description": "{{count}} {{resourceName}} ถูกนำเข้าเรียบร้อยแล้ว", + "title": "ส่งคำขอนำเข้า {{resourceName}} แล้ว" + } + }, + "update": { + "error": "คำขออัปเดต {{resourceName}} ล้มเหลว", + "success": { + "description": "{{resourceName}} ถูกอัปเดตเรียบร้อยแล้ว", + "title": "ส่งคำขออัปเดต {{resourceName}} แล้ว" + } + } + }, + "total": "รวม {{state}}", + "triggered": "ทริกเกอร์แล้ว", + "tryNumber": "จำนวนครั้งที่ลอง", + "user": "ผู้ใช้", + "wrap": { + "hotkey": "w", + "tooltip": "กด {{hotkey}} เพื่อสลับการตัดบรรทัด", + "unwrap": "ไม่ตัดบรรทัด", + "wrap": "ตัดบรรทัด" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/components.json new file mode 100644 index 0000000000000..e696d62493941 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/components.json @@ -0,0 +1,132 @@ +{ + "backfill": { + "affected_one": "จะมีการทริกเกอร์ให้ทำงาน 1 ครั้ง", + "affected_other": "จะมีการทริกเกอร์ให้ทำงาน {{count}} ครั้ง", + "affectedNone": "ไม่มีการทำงานที่ตรงกับเงื่อนไขที่เลือก", + "allRuns": "ทุกการทำงาน", + "backwards": "ทำงานแบบย้อนกลับ", + "dateRange": "ช่วงวันที่", + "errorStartDateBeforeEndDate": "วันที่เริ่มต้นต้องมาก่อนวันที่สิ้นสุด", + "maxRuns": "จำนวนการทำงานที่ทำพร้อมกันสูงสุด", + "missingAndErroredRuns": "การทำงานที่หายไปและเกิดข้อผิดพลาด", + "missingRuns": "การทำงานที่หายไป", + "reprocessBehavior": "พฤติกรรมการประมวลผลซ้ำ", + "run": "สั่ง Backfill", + "selectDescription": "สั่งให้ Dag นี้ทำงานตามช่วงเวลาที่กำหนด", + "selectLabel": "Backfill", + "title": "สั่ง Backfill", + "toaster": { + "success": { + "description": "งาน Backfill ถูกทริกเกอร์สำเร็จแล้ว", + "title": "สร้าง Backfill แล้ว" + } + }, + "tooltip": "Backfill ต้องการการกำหนดเวลาทำงาน (schedule)", + "unpause": "ยกเลิกการหยุดพัก {{dag_display_name}} เมื่อทริกเกอร์", + "validation": { + "datesRequired": "จำเป็นต้องระบุวันเริ่มต้นและวันสิ้นสุดทั้งสองวัน", + "startBeforeEnd": "วันเริ่มต้นต้องน้อยกว่าหรือเท่ากับวันสิ้นสุด" + } + }, + "banner": { + "backfillInProgress": "Backfill กำลังทำงานอยู่", + "cancel": "ยกเลิก Backfill", + "pause": "หยุดพัก Backfill", + "unpause": "ยกเลิกการหยุดพัก Backfill" + }, + "clipboard": { + "copy": "คัดลอก" + }, + "close": "ปิด", + "configForm": { + "advancedOptions": "ตัวเลือกขั้นสูง", + "configJson": "การตั้งค่า JSON", + "invalidJson": "รูปแบบ JSON ไม่ถูกต้อง: {{errorMessage}}" + }, + "dagWarnings": { + "error_one": "1 ข้อผิดพลาด", + "errorAndWarning": "1 ข้อผิดพลาดและ {{warningText}}", + "warning_one": "1 คำเตือน", + "warning_other": "{{count}} คำเตือน" + }, + "durationChart": { + "duration": "ระยะเวลา (วินาที)", + "lastDagRun_one": "Dag Run ล่าสุด", + "lastDagRun_other": "Dag Runs {{count}} ครั้งล่าสุด", + "lastTaskInstance_one": "Task Instance ล่าสุด", + "lastTaskInstance_other": "Task Instances {{count}} ครั้งล่าสุด", + "queuedDuration": "ระยะเวลาที่รอคิว", + "runAfter": "ทำงานหลังจาก", + "runDuration": "ระยะเวลาการทำงาน" + }, + "fileUpload": { + "files_other": "{{count}} ไฟล์" + }, + "flexibleForm": { + "placeholder": "เลือกค่า", + "placeholderArray": "กรอกค่าแยกกันในบรรทัดใหม่", + "placeholderExamples": "เริ่มพิมพ์เพื่อดูตัวเลือก", + "placeholderMulti": "เลือกค่าเดียวหรือหลายค่า", + "validationErrorArrayNotArray": "ค่าต้องเป็น Array", + "validationErrorArrayNotNumbers": "สมาชิกทั้งหมดใน Array ต้องเป็นตัวเลข", + "validationErrorArrayNotObject": "สมาชิกทั้งหมดใน Array ต้องเป็น Object", + "validationErrorRequired": "จำเป็นต้องกรอกช่องนี้" + }, + "graph": { + "directionDown": "จากบนลงล่าง", + "directionLeft": "จากขวาไปซ้าย", + "directionRight": "จากซ้ายไปขวา", + "directionUp": "จากล่างขึ้นบน", + "downloadImage": "ดาวน์โหลดรูปภาพกราฟ", + "downloadImageError": "ไม่สามารถดาวน์โหลดรูปภาพกราฟได้", + "downloadImageErrorTitle": "ดาวน์โหลดล้มเหลว", + "otherDagRuns": "+Dag Runs อื่น ๆ", + "taskCount_one": "{{count}} งาน", + "taskCount_other": "{{count}} งาน", + "taskGroup": "กลุ่มงาน" + }, + "limitedList": "+{{count}} เพิ่มเติม", + "logs": { + "file": "ไฟล์", + "location": "บรรทัด {{line}} ใน {{name}}" + }, + "reparseDag": "ประมวลผล Dag ใหม่", + "sortedAscending": "เรียงจากน้อยไปมาก", + "sortedDescending": "เรียงจากมากไปน้อย", + "sortedUnsorted": "ไม่เรียงลำดับ", + "taskTries": "จำนวนครั้งที่ทำงาน", + "toggleCardView": "แสดงมุมมองแบบการ์ด", + "toggleTableView": "แสดงมุมมองแบบตาราง", + "triggerDag": { + "button": "ทริกเกอร์", + "loading": "กำลังโหลดข้อมูล Dag...", + "loadingFailed": "โหลดข้อมูล Dag ไม่สำเร็จ กรุณาลองใหม่", + "runIdHelp": "ไม่บังคับ - จะถูกสร้างอัตโนมัติหากไม่ระบุ", + "selectDescription": "ทริกเกอร์ Dag นี้สำหรับการทำงานครั้งเดียว", + "selectLabel": "ทำงานครั้งเดียว", + "title": "ทริกเกอร์ Dag", + "toaster": { + "success": { + "description": "Dag Run ถูกทริกเกอร์สำเร็จแล้ว", + "title": "ทริกเกอร์ Dag Run แล้ว" + } + }, + "unpause": "ยกเลิกการหยุดพัก {{dagDisplayName}} เมื่อทริกเกอร์" + }, + "trimText": { + "details": "รายละเอียด", + "empty": "ว่างเปล่า", + "noContent": "ไม่มีเนื้อหา" + }, + "versionDetails": { + "bundleLink": "ลิงก์ Bundle", + "bundleName": "ชื่อ Bundle", + "bundleVersion": "เวอร์ชัน Bundle", + "createdAt": "สร้างเมื่อ", + "versionId": "ID เวอร์ชัน" + }, + "versionSelect": { + "dagVersion": "เวอร์ชัน Dag", + "versionCode": "v{{versionCode}}" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/dag.json new file mode 100644 index 0000000000000..d632937f24dcb --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/dag.json @@ -0,0 +1,154 @@ +{ + "allRuns": "การทำงานทั้งหมด", + "blockingDeps": { + "dependency": "การพึ่งพา", + "reason": "เหตุผล", + "title": "การพึ่งพาที่บล็อกงานไม่ให้ถูกกำหนดเวลาทำงาน" + }, + "calendar": { + "daily": "รายวัน", + "hourly": "รายชั่วโมง", + "legend": { + "less": "น้อย", + "mixed": "ผสม", + "more": "มาก" + }, + "navigation": { + "nextMonth": "เดือนถัดไป", + "nextYear": "ปีถัดไป", + "previousMonth": "เดือนก่อนหน้า", + "previousYear": "ปีก่อนหน้า" + }, + "noData": "ไม่มีข้อมูล", + "noFailedRuns": "ไม่มีการทำงานที่ล้มเหลว", + "noRuns": "ไม่มีการทำงาน", + "totalRuns": "การทำงานทั้งหมด", + "week": "สัปดาห์ที่ {{weekNumber}}", + "weekdays": { + "friday": "ศ.", + "monday": "จ.", + "saturday": "ส.", + "sunday": "อา.", + "thursday": "พฤ.", + "tuesday": "อ.", + "wednesday": "พ." + } + }, + "code": { + "bundleUrl": "URL ของ Bundle", + "noCode": "ไม่พบโค้ด", + "parseDuration": "ระยะเวลาการประมวลผล:", + "parsedAt": "ประมวลผลเมื่อ:" + }, + "extraLinks": "ลิงก์เพิ่มเติม", + "grid": { + "buttons": { + "resetToLatest": "รีเซ็ตเป็นล่าสุด", + "toggleGroup": "สลับกลุ่ม" + } + }, + "header": { + "buttons": { + "advanced": "ขั้นสูง", + "dagDocs": "เอกสาร Dag" + } + }, + "logs": { + "allLevels": "ทุกระดับ Log", + "allSources": "ทุกแหล่งที่มา", + "critical": "ร้ายแรง (CRITICAL)", + "debug": "ดีบัก (DEBUG)", + "error": "ข้อผิดพลาด (ERROR)", + "fullscreen": { + "button": "เต็มจอ", + "tooltip": "กด {{hotkey}} เพื่อเข้าสู่โหมดเต็มจอ" + }, + "info": "ข้อมูล (INFO)", + "noTryNumber": "ไม่มีหมายเลขการลอง", + "settings": "การตั้งค่า Log", + "viewInExternal": "ดู log ใน {{name}} (ความพยายามครั้งที่ {{attempt}})", + "warning": "คำเตือน (WARNING)" + }, + "navigation": { + "navigation": "การนำทาง: Shift+{{arrow}}", + "toggleGroup": "สลับกลุ่ม: Space" + }, + "overview": { + "buttons": { + "failedRun_one": "การทำงานที่ล้มเหลว", + "failedRun_other": "การทำงานที่ล้มเหลว", + "failedTask_one": "งานที่ล้มเหลว", + "failedTask_other": "งานที่ล้มเหลว", + "failedTaskInstance_one": "Task Instance ที่ล้มเหลว", + "failedTaskInstance_other": "Task Instances ที่ล้มเหลว" + }, + "charts": { + "assetEvent_one": "การสร้างอีเวนต์ให้กับ Asset", + "assetEvent_other": "การสร้างอีเวนต์ให้กับ Asset" + }, + "failedLogs": { + "hideLogs": "ซ่อน Logs", + "showLogs": "แสดง Logs", + "title": "Logs ของงานที่ล้มเหลวล่าสุด", + "viewFullLogs": "ดู Logs แบบเต็ม" + } + }, + "panel": { + "buttons": { + "options": "ตัวเลือก", + "showGantt": "แสดง Gantt", + "showGraphShortcut": "แสดงกราฟ (กด g)", + "showGridShortcut": "แสดงกริด (กด g)" + }, + "dagRuns": { + "label": "จำนวน Dag Runs" + }, + "dependencies": { + "label": "การพึ่งพา", + "options": { + "allDagDependencies": "การพึ่งพาของ Dag ทั้งหมด", + "externalConditions": "เงื่อนไขภายนอก", + "onlyTasks": "เฉพาะงาน" + }, + "placeholder": "การพึ่งพา" + }, + "graphDirection": { + "label": "ทิศทางของกราฟ" + } + }, + "paramsFailed": "โหลดพารามิเตอร์ไม่สำเร็จ", + "parse": { + "toaster": { + "error": { + "description": "การร้องขอประมวลผล Dag ล้มเหลว อาจมีคำขอการประมวลผลที่รอดำเนินการอยู่", + "title": "การประมวลผล Dag ล้มเหลว" + }, + "success": { + "description": "Dag จะถูกประมวลผลใหม่ในเร็ว ๆ นี้", + "title": "ส่งคำขอประมวลผลใหม่สำเร็จแล้ว" + } + } + }, + "tabs": { + "assetEvents": "อีเวนต์ของ Asset", + "auditLog": "บันทึกการตรวจสอบ", + "backfills": "Backfills", + "calendar": "ปฏิทิน", + "code": "โค้ด", + "details": "รายละเอียด", + "logs": "Logs", + "mappedTaskInstances_one": "Task Instance [{{count}}]", + "mappedTaskInstances_other": "Task Instances [{{count}}]", + "overview": "ภาพรวม", + "renderedTemplates": "เทมเพลตที่เรนเดอร์แล้ว", + "requiredActions": "การดำเนินการที่จำเป็น", + "runs": "การทำงาน", + "taskInstances": "Task Instances", + "tasks": "งาน", + "xcom": "XCom" + }, + "taskGroups": { + "collapseAll": "ย่อกลุ่มงานทั้งหมด", + "expandAll": "ขยายกลุ่มงานทั้งหมด" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/dags.json new file mode 100644 index 0000000000000..9b1ce043e8f34 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/dags.json @@ -0,0 +1,96 @@ +{ + "assetSchedule": "{{count}} จาก {{total}} รายการ Assets อัปเดตแล้ว", + "dagActions": { + "delete": { + "button": "ลบ Dag", + "warning": "การดำเนินการนี้จะลบเมทาดาต้าทั้งหมดที่เกี่ยวข้องกับ Dag รวมถึงการทำงานและรายละเอียดงานด้วย" + } + }, + "favoriteDag": "ชื่นชอบ Dag", + "filters": { + "allRunTypes": "ประเภทการทำงานทั้งหมด", + "allStates": "สถานะทั้งหมด", + "favorite": { + "all": "ทั้งหมด", + "favorite": "รายการโปรด", + "unfavorite": "ไม่ใช่รายการโปรด" + }, + "paused": { + "active": "ใช้งาน", + "all": "ทั้งหมด", + "paused": "หยุดชั่วคราว" + }, + "runIdPatternFilter": "ค้นหา Dag Runs" + }, + "ownerLink": "ลิงก์เจ้าของสำหรับ {{owner}}", + "runAndTaskActions": { + "affectedTasks": { + "noItemsFound": "ไม่พบงาน", + "title": "งานที่ได้รับผลกระทบ: {{count}}" + }, + "clear": { + "button": "ล้าง {{type}}", + "buttonTooltip": "กด shift+c เพื่อล้าง", + "error": "ล้าง {{type}} ไม่สำเร็จ", + "title": "ล้าง {{type}}" + }, + "delete": { + "button": "ลบ {{type}}", + "dialog": { + "resourceName": "{{type}} {{id}}", + "title": "ลบ {{type}}", + "warning": "การดำเนินการนี้จะลบเมทาดาต้าทั้งหมดที่เกี่ยวข้องกับ {{type}}" + }, + "error": "เกิดข้อผิดพลาดในการลบ {{type}}", + "success": { + "description": "คำขอการลบ {{type}} สำเร็จ", + "title": "ลบ {{type}} สำเร็จแล้ว" + } + }, + "markAs": { + "button": "ทำเครื่องหมาย {{type}} เป็น...", + "buttonTooltip": { + "failed": "กด shift+f เพื่อทำเครื่องหมายว่าล้มเหลว", + "success": "กด shift+s เพื่อทำเครื่องหมายว่าสำเร็จ" + }, + "title": "ทำเครื่องหมาย {{type}} เป็น {{state}}" + }, + "options": { + "downstream": "งานถัดไป", + "existingTasks": "ล้างงานที่มีอยู่แล้ว", + "future": "งานในอนาคต", + "onlyFailed": "ล้างเฉพาะงานที่ล้มเหลว", + "past": "งานในอดีต", + "queueNew": "จัดคิวงานใหม่", + "runOnLatestVersion": "ทำงานด้วยเวอร์ชันล่าสุดของชุดรวม (Bundle)", + "upstream": "งานก่อนหน้า" + } + }, + "search": { + "advanced": "การค้นหาขั้นสูง", + "clear": "ล้างการค้นหา", + "dags": "ค้นหา Dags", + "hotkey": "+K", + "tasks": "ค้นหางาน (Tasks)" + }, + "sort": { + "displayName": { + "asc": "จัดเรียงตามชื่อที่แสดง (A-Z)", + "desc": "จัดเรียงตามชื่อที่แสดง (Z-A)" + }, + "lastRunStartDate": { + "asc": "จัดเรียงตามวันที่เริ่มทำงานครั้งล่าสุด (เก่าสุด-ใหม่สุด)", + "desc": "จัดเรียงตามวันที่เริ่มทำงานครั้งล่าสุด (ใหม่สุด-เก่าสุด)" + }, + "lastRunState": { + "asc": "จัดเรียงตามสถานะการทำงานครั้งล่าสุด (A-Z)", + "desc": "จัดเรียงตามสถานะการทำงานครั้งล่าสุด (Z-A)" + }, + "nextDagRun": { + "asc": "จัดเรียงตาม Dag Run ถัดไป (เก่าสุด-ใหม่สุด)", + "desc": "จัดเรียงตาม Dag Run ถัดไป (ใหม่สุด-เก่าสุด)" + }, + "placeholder": "จัดเรียงตาม" + }, + "unfavoriteDag": "เลิกชื่นชอบ Dag" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/dashboard.json new file mode 100644 index 0000000000000..b06545e12c37b --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/dashboard.json @@ -0,0 +1,45 @@ +{ + "favorite": { + "favoriteDags_one": "Dag รายการโปรด {{count}} รายการแรก", + "favoriteDags_other": "Dags รายการโปรด {{count}} รายการแรก", + "noDagRuns": "ยังไม่มี DagRun สำหรับ Dag นี้", + "noFavoriteDags": "ยังไม่มีรายการโปรด คลิกไอคอนรูปดาวด้านข้าง Dag ในรายการเพื่อเพิ่มเป็นรายการโปรดของคุณ" + }, + "group": "กลุ่ม", + "health": { + "dagProcessor": "Dag Processor", + "health": "สถานะสุขภาพ", + "healthy": "ปกติ", + "lastHeartbeat": "สัญญาณล่าสุด", + "metaDatabase": "MetaDatabase", + "scheduler": "Scheduler", + "status": "สถานะ", + "triggerer": "Triggerer", + "unhealthy": "ไม่ปกติ" + }, + "history": "ประวัติ", + "importErrors": { + "dagImportError_one": "เกิดข้อผิดพลาดในการนำเข้า Dag", + "dagImportError_other": "เกิดข้อผิดพลาดในการนำเข้า Dags", + "searchByFile": "ค้นหาตามไฟล์", + "timestamp": "เวลา" + }, + "managePools": "จัดการพูล", + "noAssetEvents": "ไม่พบอีเวนต์ของ Asset", + "poolSlots": "จำนวนช่องในพูล", + "sortBy": { + "newestFirst": "ใหม่สุดก่อน", + "oldestFirst": "เก่าสุดก่อน" + }, + "source": "แหล่งที่มา", + "stats": { + "activeDags": "Dags ที่ทำงานอยู่", + "failedDags": "Dags ที่ล้มเหลว", + "queuedDags": "Dags ที่ถูกจัดคิว", + "requiredActions": "การดำเนินการที่จำเป็น", + "runningDags": "Dags ที่กำลังทำงาน", + "stats": "สถิติ" + }, + "uri": "Uri", + "welcome": "ยินดีต้อนรับ" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/hitl.json new file mode 100644 index 0000000000000..3d8fb5a847ee3 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/hitl.json @@ -0,0 +1,34 @@ +{ + "filters": { + "response": { + "all": "ทั้งหมด", + "pending": "รอดำเนินการ", + "received": "ตรวจสอบแล้ว" + } + }, + "requiredAction_one": "การดำเนินการที่จำเป็น", + "requiredAction_other": "การดำเนินการที่จำเป็น", + "requiredActionCount_one": "การดำเนินการที่จำเป็น ({{count}})", + "requiredActionCount_other": "การดำเนินการที่จำเป็น ({{count}})", + "requiredActionState": "สถานะการดำเนินการที่จำเป็น", + "response": { + "error": "การตอบสนองล้มเหลว", + "optionsDescription": "เลือกตัวเลือกของคุณสำหรับ Task Instance นี้", + "optionsLabel": "ตัวเลือก", + "received": "ได้รับการตอบสนองเมื่อ ", + "respond": "ตอบสนอง", + "success": "การตอบสนองของ {{taskId}} สำเร็จ", + "title": "Human Task Instance - {{taskId}}" + }, + "state": { + "approvalReceived": "ได้รับการอนุมัติแล้ว", + "approvalRequired": "ต้องการการอนุมัติ", + "choiceReceived": "ได้รับตัวเลือกแล้ว", + "choiceRequired": "ต้องการตัวเลือก", + "noResponseReceived": "ไม่ได้รับการตอบสนอง", + "rejectionReceived": "ได้รับการปฏิเสธแล้ว", + "responseReceived": "ได้รับการตอบสนองแล้ว", + "responseRequired": "ต้องการการตอบสนอง" + }, + "subject": "หัวเรื่อง" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/th/tasks.json new file mode 100644 index 0000000000000..ca5cf8f716bff --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "ถูกแม็ปแล้ว", + "notMapped": "ยังไม่ได้แม็ป", + "retries": "การลองใหม่", + "searchTasks": "ค้นหางาน", + "selectMapped": "เลือกการแม็ป", + "selectOperator": "เลือกตัวดำเนินการ", + "selectRetryValues": "เลือกค่าการลองใหม่", + "selectTriggerRules": "เลือกกฎการทริกเกอร์" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/admin.json index 6298c9f2b295a..bbf9b03a0aba4 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/admin.json @@ -49,6 +49,12 @@ "searchPlaceholder": "Bağlantıları Ara", "test": "Bağlantıyı Test Et", "testDisabled": "Bağlantı test özelliği devre dışı. Etkinleştirmek için lütfen bir yönetici ile iletişime geçin.", + "testError": { + "title": "Bağlantı Testi Başarısız" + }, + "testSuccess": { + "title": "Bağlantı Testi Başarılı" + }, "typeMeta": { "error": "Bağlantı Tipi Meta verileri alınamadı", "standardFields": { @@ -72,7 +78,6 @@ "tooltip": "Seçilen bağlantıları sil" }, "formActions": { - "reset": "Sıfırla", "save": "Kaydet" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/assets.json index 3999d970008f4..8b1b65ebe033f 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "Tüketen Dag'ler", + "consumingTasks": "Tüketen Görevler", "createEvent": { "button": "Olay oluştur", "manual": { @@ -21,6 +22,7 @@ }, "title": "{{name}} için kaynak olayı oluştur" }, + "extra": "Ekstra", "group": "Grup", "lastAssetEvent": "Son kaynak olayı", "name": "Ad", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/browse.json index ef6b5c91852ac..f71904df748b3 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "Tüm ek JSON'ları daralt", - "expandAllExtra": "Tüm ek JSON'ları genişlet" - }, "columns": { "event": "Etkinlik", "extra": "Ek Bilgi", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json index 3b37fa8487990..32c66539b6473 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json @@ -25,6 +25,7 @@ "requiredActions": "Gerekli Eylemler", "xcoms": "XCom'lar" }, + "collapseAllExtra": "Tüm ek JSON'ları daralt", "collapseDetailsPanel": "Detay Panelini Daralt", "createdAssetEvent_one": "Oluşturulan Varlık Etkinliği", "createdAssetEvent_other": "Oluşturulan Varlık Etkinlikleri", @@ -39,6 +40,7 @@ "fileLocation": "Dosya Konumu", "hasTaskConcurrencyLimits": "Görev Eş Zamanlılık Limitleri Var", "lastExpired": "Son Süresi Dolan", + "lastParseDuration": "Son Atrıştırma Süresi", "lastParsed": "Son Ayrıştırılan", "latestDagVersion": "En Son Dag Versiyonu", "latestRun": "En Son Çalışma", @@ -77,12 +79,18 @@ "githubRepo": "GitHub Deposu", "restApiReference": "REST API Referansı" }, + "download": { + "download": "İndir", + "hotkey": "d", + "tooltip": "{{hotkey}} tuşuna basarak indir" + }, "duration": "Süre", "endDate": "Bitiş Tarihi", "error": { "back": "Geri", "defaultMessage": "Beklenmeyen bir hata oluştu", "home": "Ana Sayfa", + "invalidUrl": "Sayfa Bulunamadı. Lütfen URL'yi kontrol edin ve tekrar deneyin.", "notFound": "Sayfa Bulunamadı", "title": "Hata" }, @@ -92,22 +100,19 @@ "hotkey": "e", "tooltip": "{{hotkey}} tuşuna basarak genişletmeyi değiştir" }, + "expandAllExtra": "Tüm ek JSON'ları genişlet", "expression": { "all": "Tümü", "and": "VE", "any": "Herhangi", "or": "VEYA" }, + "filter": "Filtre", "filters": { - "dagDisplayNamePlaceholder": "Dag adına göre filtrele", - "keyPlaceholder": "XCom anahtarına göre filtrele", - "logicalDateFromPlaceholder": "Mantıksal tarih başlangıcı", - "logicalDateToPlaceholder": "Mantıksal tarih bitişi", - "mapIndexPlaceholder": "Harita indeksine göre filtrele", - "runAfterFromPlaceholder": "Şu tarihten sonra çalıştır (başlangıç)", - "runAfterToPlaceholder": "Şu tarihten sonra çalıştır (bitiş)", - "runIdPlaceholder": "Çalıştırma kimliğine göre filtrele", - "taskIdPlaceholder": "Görev kimliğine göre filtrele" + "logicalDateFrom": "Mantıksal tarih başlangıcı", + "logicalDateTo": "Mantıksal tarih bitişi", + "runAfterFrom": "Şu tarihten sonra çalıştır (başlangıç)", + "runAfterTo": "Şu tarihten sonra çalıştır (bitiş)" }, "logicalDate": "Mantıksal Tarih", "logout": "Çıkış", @@ -149,6 +154,7 @@ "running": "Çalışıyor", "scheduled": "Planlanmış" }, + "reset": "Sıfırla", "runId": "Çalıştırma Kimliği", "runTypes": { "asset_triggered": "Varlık Tetiklemeli", @@ -163,7 +169,6 @@ }, "tooltip": "{{direction}} kaydırmak için {{hotkey}} tuşuna basın" }, - "seconds": "{{count}}sn", "security": { "actions": "Eylemler", "permissions": "İzinler", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json index c2733b504e221..b40523e33065e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/components.json @@ -6,8 +6,6 @@ "allRuns": "Bütün Dag Çalıştırmaları", "backwards": "Geriye Dönük Çalıştır", "dateRange": "Tarih Aralığı", - "dateRangeFrom": "Başlangıç", - "dateRangeTo": "Bitiş", "errorStartDateBeforeEndDate": "Başlangıç Tarihi, Bitiş Tarihinden önce olmalıdır", "maxRuns": "Maksimum Aktif Çalıştırma", "missingAndErroredRuns": "Eksik ve Hata Almış Dag Çalıştırmaları", @@ -90,6 +88,14 @@ "taskGroup": "Görev Grubu" }, "limitedList": "+{{count}} daha", + "limitedList.allItems": "Tüm {{count}} öğeler:", + "limitedList.allTags_one": "Tüm Etiketler (1)", + "limitedList.allTags_other": "Tüm Etiketler ({{count}})", + "limitedList.clickToInteract": "Bir etikete tıklayarak Dag'leri filtreleyin", + "limitedList.clickToOpenFull": "\"+{{count}} daha\" tıklayarak tam görünümü açın", + "limitedList.copyPasteText": "Yukarıdaki metni kopyalayıp yapıştırabilirsiniz", + "limitedList.showingItems_one": "1 öğe gösteriliyor", + "limitedList.showingItems_other": "{{count}} öğeler gösteriliyor", "logs": { "file": "Dosya", "location": "{{name}} içinde satır {{line}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json index 2e538ae3e6e74..987391a63da96 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json @@ -10,6 +10,7 @@ "hourly": "Saatlik", "legend": { "less": "Az", + "mixed": "Karışık", "more": "Çok" }, "navigation": { @@ -19,6 +20,7 @@ "previousYear": "Önceki yıl" }, "noData": "Veri yok", + "noFailedRuns": "Başarısız çalışma yok", "noRuns": "Çalıştırma yok", "totalRuns": "Bütün Çalıştırmalar", "week": "Hafta {{weekNumber}}", @@ -35,7 +37,8 @@ "code": { "bundleUrl": "Paket URL'i", "noCode": "Kod Bulunamadı", - "parsedAt": "Ayrıştırılma zamanı:" + "parseDuration": "Ayrıştırılma süresi:", + "parsedAt": "Ayrıştırıldı:" }, "extraLinks": "Ek Bağlantılar", "grid": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json new file mode 100644 index 0000000000000..c8853ac873caf --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/admin.json @@ -0,0 +1,166 @@ +{ + "columns": { + "description": "描述", + "key": "键", + "name": "名称", + "value": "值" + }, + "config": { + "columns": { + "section": "段落" + }, + "title": "Airflow 配置" + }, + "connections": { + "add": "添加连接", + "columns": { + "connectionId": "连接 ID", + "connectionType": "连接类型", + "host": "主机地址", + "port": "端口" + }, + "connection_one": "连接", + "connection_other": "连接", + "delete": { + "deleteConnection_one": "删除 1 个连接", + "deleteConnection_other": "删除 {{count}} 个连接", + "firstConfirmMessage_one": "您即将删除以下连接:", + "firstConfirmMessage_other": "您即将删除以下连接:", + "title": "删除连接" + }, + "edit": "编辑连接", + "form": { + "connectionIdRequired": "连接 ID 是必填的", + "connectionIdRequirement": "连接 ID 不能只包含空格", + "connectionTypeRequired": "连接类型是必填的", + "extraFields": "额外字段", + "extraFieldsJson": "额外字段 JSON", + "helperText": "找不到连接类型?请确保您已安装对应的 Airflow Providers 套件。", + "helperTextForRedactedFields": "已遮蔽的字段 ('***') 若未修改,将保持不变。", + "selectConnectionType": "选择连接类型", + "standardFields": "标准字段" + }, + "nothingFound": { + "description": "通过环境变量或密钥管理器定义的连接不会列在此处。", + "documentationLink": "在 Airflow 文件中了解更多。", + "learnMore": "这些连接会在执行时解析,不会在 UI 显示。", + "title": "找不到连接" + }, + "searchPlaceholder": "搜索连接", + "test": "测试连接", + "testDisabled": "测试连接功能已停用。请联系管理员以启用。", + "typeMeta": { + "error": "获取连接类型元数据失败", + "standardFields": { + "description": "描述", + "host": "主机地址", + "login": "登录", + "password": "密码", + "port": "端口", + "url_schema": "Schema" + } + } + }, + "deleteActions": { + "button": "删除", + "modal": { + "confirmButton": "确定删除", + "secondConfirmMessage": "此操作无法恢复。", + "thirdConfirmMessage": "您确定要继续吗?" + }, + "selected": "已选择", + "tooltip": "删除所选连接" + }, + "formActions": { + "save": "保存" + }, + "plugins": { + "columns": { + "source": "来源" + }, + "importError_one": "插件导入错误", + "importError_other": "插件导入错误", + "searchPlaceholder": "搜索文件" + }, + "pools": { + "add": "新增资源池", + "deferredSlotsIncluded": "包含延迟任务", + "delete": { + "title": "删除资源池", + "warning": "这将删除所有与此资源池相关的系统数据,可能会影响使用此资源池的任务。" + }, + "edit": "编辑资源池", + "form": { + "checkbox": "计算可用资源池配额时,将包含延迟的任务", + "description": "描述", + "includeDeferred": "包含延迟任务", + "nameMaxLength": "名称最多只能包含 256 个字符", + "nameRequired": "名称是必填的", + "slots": "配额" + }, + "noPoolsFound": "找不到资源池", + "pool_one": "资源池", + "pool_other": "资源池", + "searchPlaceholder": "搜索资源池", + "sort": { + "asc": "名称 (A-Z)", + "desc": "名称 (Z-A)", + "placeholder": "排序方式" + } + }, + "providers": { + "columns": { + "packageName": "插件名称", + "version": "版本" + } + }, + "variables": { + "add": "新增变量", + "columns": { + "isEncrypted": "是否加密" + }, + "delete": { + "deleteVariable_one": "删除 1 个变量", + "deleteVariable_other": "删除 {{count}} 个变量", + "firstConfirmMessage_one": "您即将删除以下变量:", + "firstConfirmMessage_other": "您即将删除以下变量:", + "title": "删除变量", + "tooltip": "删除所选变量" + }, + "edit": "编辑变量", + "export": "导出", + "exportTooltip": "导出所选变量", + "form": { + "invalidJson": "无效的 JSON", + "keyMaxLength": "键最多只能包含 250 个字符", + "keyRequired": "键是必填的", + "valueRequired": "值是必填的" + }, + "import": { + "button": "导入", + "conflictResolution": "选择变量冲突解决方式", + "errorParsingJsonFile": "解析 JSON 文件时发生错误:请上传包含变量的 JSON 文件 (例如:{\"key\": \"value\", ...})。", + "options": { + "fail": { + "description": "如果检测到任何已存在的变量,则导入失败。", + "title": "严格模式" + }, + "overwrite": { + "description": "发生冲突时覆盖变量。", + "title": "覆盖模式" + }, + "skip": { + "description": "忽略导入已存在的变量。", + "title": "非严格模式" + } + }, + "title": "导入变量", + "upload": "上传 JSON 文件", + "uploadPlaceholder": "上传包含变量的 JSON 文件 (例如:{\"key\": \"value\", ...})" + }, + "noRowsMessage": "找不到变量", + "searchPlaceholder": "搜索键", + "variable_one": "变量", + "variable_other": "变量" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json new file mode 100644 index 0000000000000..059726de65f7a --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/assets.json @@ -0,0 +1,30 @@ +{ + "consumingDags": "消费者 Dags", + "createEvent": { + "button": "创建事件", + "manual": { + "description": "手动创建资源事件", + "extra": "资源事件额外信息", + "label": "手动" + }, + "materialize": { + "description": "触发此资源上游的 Dag", + "descriptionWithDag": "触发此资源上游的 Dag: {{dagName}}", + "label": "物化", + "unpauseDag": "触发时取消暂停 {{dagName}}" + }, + "success": { + "manualDescription": "已成功手动创建资源事件。", + "manualTitle": "已创建资源事件", + "materializeDescription": "已成功触发上游 Dag {{dagId}}。", + "materializeTitle": "正在物化资源" + }, + "title": "为 {{name}} 创建资源事件" + }, + "group": "分组", + "lastAssetEvent": "最后资源事件", + "name": "名称", + "producingTasks": "生产任务", + "scheduledDags": "已调度的 Dags", + "searchPlaceholder": "搜索资源" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json new file mode 100644 index 0000000000000..8888c37783993 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/browse.json @@ -0,0 +1,22 @@ +{ + "auditLog": { + "columns": { + "event": "事件", + "extra": "额外信息", + "user": "用户", + "when": "时间" + }, + "filters": { + "eventType": "事件类型" + }, + "title": "审计日志" + }, + "xcom": { + "columns": { + "dag": "Dag", + "key": "键", + "value": "值" + }, + "title": "XCom" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json new file mode 100644 index 0000000000000..0c22b45055ce2 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json @@ -0,0 +1,318 @@ +{ + "admin": { + "Config": "配置", + "Connections": "连接", + "Plugins": "插件", + "Pools": "资源池", + "Providers": "Providers", + "Variables": "变量" + }, + "allOperators": "全部操作器", + "appearance": { + "appearance": "外观", + "darkMode": "深色模式", + "lightMode": "浅色模式", + "systemMode": "跟随系统设置" + }, + "asset_one": "资源", + "asset_other": "资源", + "assetEvent_one": "资源事件", + "assetEvent_other": "资源事件", + "backfill_one": "回填", + "backfill_other": "回填", + "browse": { + "auditLog": "审计日志", + "requiredActions": "待响应的任务实例", + "xcoms": "XComs" + }, + "collapseAllExtra": "收起所有额外 JSON", + "collapseDetailsPanel": "收起详细信息", + "createdAssetEvent_one": "已创建资源事件", + "createdAssetEvent_other": "已创建资源事件", + "dag_one": "Dag", + "dag_other": "Dags", + "dagDetails": { + "catchup": "自动回填", + "dagRunTimeout": "Dag 执行超时", + "defaultArgs": "默认参数", + "description": "描述", + "documentation": "Dag 文档", + "fileLocation": "文件位置", + "hasTaskConcurrencyLimits": "有任务并发数限制", + "lastExpired": "最后过期时间", + "lastParseDuration": "最后解析耗时", + "lastParsed": "最后解析时间", + "latestDagVersion": "最新 Dag 版本", + "latestRun": "上次 Dag 执行", + "maxActiveRuns": "活跃执行数上限", + "maxActiveTasks": "活跃任务数上限", + "maxConsecutiveFailedDagRuns": "连续失败执行数上限", + "nextRun": "下次 Dag 执行", + "owner": "拥有者", + "params": "参数", + "schedule": "调度", + "tags": "标签" + }, + "dagId": "Dag ID", + "dagRun": { + "conf": "配置", + "dagVersions": "Dag 版本", + "dataIntervalEnd": "数据区间结束", + "dataIntervalStart": "数据区间起始", + "lastSchedulingDecision": "最后调度决策", + "queuedAt": "开始排队时间", + "runAfter": "最早可执行时间", + "runType": "执行类型", + "sourceAssetEvent": "来源资源事件", + "triggeredBy": "触发者", + "triggeringUser": "触发用户名称" + }, + "dagRun_one": "Dag 执行", + "dagRun_other": "Dag 执行", + "dagRunId": "Dag 执行 ID", + "dagWarnings": "Dag 警告 / 错误", + "defaultToGraphView": "默认使用图形视图", + "defaultToGridView": "默认使用网格视图", + "direction": "书写方向", + "docs": { + "documentation": "文档", + "githubRepo": "GitHub 仓库", + "restApiReference": "REST API 参考" + }, + "duration": "执行时间", + "endDate": "结束日期", + "error": { + "back": "返回", + "defaultMessage": "发生未预期的错误", + "home": "首页", + "notFound": "找不到页面", + "title": "错误" + }, + "expand": { + "collapse": "收起", + "expand": "展开", + "hotkey": "e", + "tooltip": "按下 {{hotkey}} 切换展开" + }, + "expandAllExtra": "展开所有额外 JSON", + "expression": { + "all": "全部", + "and": "且", + "any": "任何", + "or": "或" + }, + "filter": "筛选", + "filters": { + "logicalDateFrom": "逻辑日期起始", + "logicalDateTo": "逻辑结束日期", + "runAfterFrom": "执行时间起始", + "runAfterTo": "执行时间结束" + }, + "logicalDate": "逻辑日期", + "logout": "退出登录", + "logoutConfirmation": "确定要退出登录吗?", + "mapIndex": "映射索引", + "modal": { + "cancel": "取消", + "confirm": "确认", + "delete": { + "button": "删除", + "confirmation": "确定要删除 {{resourceName}} 吗?此操作无法还原。" + } + }, + "nav": { + "admin": "管理", + "assets": "资源", + "browse": "浏览", + "dags": "Dags", + "docs": "文档", + "home": "首页", + "legacyFabViews": "旧版视图", + "plugins": "插件", + "security": "安全" + }, + "noItemsFound": "找不到 {{modelName}}", + "note": { + "add": "添加笔记", + "dagRun": "Dag 执行笔记", + "label": "笔记", + "placeholder": "添加笔记...", + "taskInstance": "任务实例笔记" + }, + "pools": { + "deferred": "已延后", + "open": "开放", + "pools_one": "资源池", + "pools_other": "资源池", + "queued": "排队中", + "running": "执行中", + "scheduled": "已调度" + }, + "reset": "重置", + "runId": "执行 ID", + "runTypes": { + "asset_triggered": "资源触发", + "backfill": "回填", + "manual": "手动触发", + "scheduled": "已调度" + }, + "scroll": { + "direction": { + "bottom": "最下方", + "top": "最上方" + }, + "tooltip": "按 {{hotkey}} 滚动到{{direction}}" + }, + "security": { + "actions": "操作", + "permissions": "权限", + "resources": "资源", + "roles": "角色", + "users": "用户" + }, + "selectLanguage": "选择语言", + "showDetailsPanel": "显示详细信息", + "source": { + "hide": "隐藏来源", + "hotkey": "s", + "show": "显示来源" + }, + "sourceAssetEvent_one": "来源资源事件", + "sourceAssetEvent_other": "来源资源事件", + "startDate": "开始日期", + "state": "状态", + "states": { + "deferred": "已延后", + "failed": "失败", + "no_status": "无状态", + "none": "无状态", + "planned": "已计划", + "queued": "排队中", + "removed": "已移除", + "restarting": "重启中", + "running": "执行中", + "scheduled": "已调度", + "skipped": "已跳过", + "success": "成功", + "up_for_reschedule": "等待重新调度", + "up_for_retry": "等待重试", + "upstream_failed": "上游任务失败" + }, + "table": { + "completedAt": "完成时间", + "createdAt": "创建时间", + "filterByTag": "按标签筛选 Dags", + "filterColumns": "筛选表格列", + "filterReset_one": "重置筛选", + "filterReset_other": "重置筛选", + "from": "开始时间", + "maxActiveRuns": "最大活跃执行数", + "noTagsFound": "找不到标签", + "tagMode": { + "all": "全部", + "any": "任何" + }, + "tagPlaceholder": "按标签筛选", + "to": "结束时间" + }, + "task": { + "documentation": "任务文档", + "lastInstance": "最后实例", + "operator": "任务操作器", + "triggerRule": "触发规则" + }, + "task_one": "任务", + "task_other": "任务", + "taskGroup": "任务分组", + "taskId": "任务 ID", + "taskInstance": { + "dagVersion": "Dag 版本", + "executor": "执行器", + "executorConfig": "执行器配置", + "hostname": "主机名称", + "maxTries": "最大尝试次数", + "pid": "PID", + "pool": "资源池", + "poolSlots": "资源池配额", + "priorityWeight": "优先级权重", + "queue": "排队", + "queuedWhen": "开始排队时间", + "scheduledWhen": "开始调度时间", + "triggerer": { + "assigned": "指派的触发器", + "class": "触发器类别", + "createdAt": "触发器创建时间", + "id": "触发器 ID", + "latestHeartbeat": "最新触发器心跳时间", + "title": "触发器信息" + }, + "unixname": "Unix 名称" + }, + "taskInstance_one": "任务实例", + "taskInstance_other": "任务实例", + "timeRange": { + "last12Hours": "最近 12 小时", + "last24Hours": "最近 24 小时", + "lastHour": "最近 1 小时", + "pastWeek": "过去一周" + }, + "timestamp": { + "hide": "隐藏时间戳", + "hotkey": "t", + "show": "显示时间戳" + }, + "timezone": "时区", + "timezoneModal": { + "current-timezone": "当前时区", + "placeholder": "搜索时区", + "title": "选择时区", + "utc": "UTC" + }, + "toaster": { + "bulkDelete": { + "error": "批量删除 {{resourceName}} 请求失败", + "success": { + "description": "已成功删除 {{count}} 个 {{resourceName}}。键:{{keys}}", + "title": "已提交批量删除 {{resourceName}} 请求" + } + }, + "create": { + "error": "创建 {{resourceName}} 请求失败", + "success": { + "description": "{{resourceName}} 已成功创建。", + "title": "已提交创建 {{resourceName}} 请求" + } + }, + "delete": { + "error": "删除 {{resourceName}} 请求失败", + "success": { + "description": "{{resourceName}} 已成功删除。", + "title": "已提交删除 {{resourceName}} 请求" + } + }, + "import": { + "error": "导入 {{resourceName}} 请求失败", + "success": { + "description": "已成功导入 {{count}} 个 {{resourceName}}。", + "title": "已提交导入 {{resourceName}} 请求" + } + }, + "update": { + "error": "更新 {{resourceName}} 请求失败", + "success": { + "description": "{{resourceName}} 已成功更新。", + "title": "已提交更新 {{resourceName}} 请求" + } + } + }, + "total": "总计 {{state}}", + "triggered": "已触发", + "tryNumber": "尝试次数", + "user": "用户", + "wrap": { + "hotkey": "w", + "tooltip": "按 {{hotkey}} 切换换行", + "unwrap": "不换行", + "wrap": "换行" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json new file mode 100644 index 0000000000000..b52f071f60775 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/components.json @@ -0,0 +1,136 @@ +{ + "backfill": { + "affected_one": "将会触发 1 次执行。", + "affected_other": "将会触发 {{count}} 次执行。", + "affectedNone": "没有符合条件的执行。", + "allRuns": "所有执行", + "backwards": "反向执行", + "dateRange": "日期范围", + "errorStartDateBeforeEndDate": "开始日期必须早于结束日期。", + "maxRuns": "活跃执行数上限", + "missingAndErroredRuns": "遗漏和错误的执行", + "missingRuns": "遗漏的执行", + "reprocessBehavior": "重新处理行为", + "run": "执行回填", + "selectDescription": "为指定的日期范围补上 Dag 执行", + "selectLabel": "回填", + "title": "执行回填", + "toaster": { + "success": { + "description": "已成功触发回填作业。", + "title": "已触发 Dag 执行" + } + }, + "tooltip": "回填功能需要 Dag 具有调度", + "unpause": "触发时取消暂停 {{dag_display_name}}", + "validation": { + "datesRequired": "必须提供数据区间的开始与结束日期。", + "startBeforeEnd": "数据区间起始日期必须早于或等于结束日期。" + } + }, + "banner": { + "backfillInProgress": "回填正在进行中", + "cancel": "取消回填", + "pause": "暂停回填", + "unpause": "取消暂停回填" + }, + "clipboard": { + "copy": "复制" + }, + "close": "关闭", + "configForm": { + "advancedOptions": "高级选项", + "configJson": "配置 JSON", + "invalidJson": "无效的 JSON 格式: {{errorMessage}}" + }, + "dagWarnings": { + "error_one": "1 个错误", + "errorAndWarning": "1 个错误与 {{warningText}}", + "warning_one": "1 个警告", + "warning_other": "{{count}} 个警告" + }, + "durationChart": { + "duration": "持续时间 (秒)", + "lastDagRun_one": "最近 1 次 Dag 执行", + "lastDagRun_other": "最近 {{count}} 次 Dag 执行", + "lastTaskInstance_one": "最近 1 次任务实例", + "lastTaskInstance_other": "最近 {{count}} 次任务实例", + "queuedDuration": "排队等候时间", + "runAfter": "最早可执行时间", + "runDuration": "执行持续时间" + }, + "fileUpload": { + "files_other": "{{count}} 个文件" + }, + "flexibleForm": { + "placeholder": "请选择一个值", + "placeholderArray": "请逐行输入,每行输入一个字符串", + "placeholderExamples": "开始输入以查看选项", + "placeholderMulti": "可选择单一或多个值", + "validationErrorArrayNotArray": "值必须是数组格式。", + "validationErrorArrayNotNumbers": "数组中的所有元素都必须是数字。", + "validationErrorArrayNotObject": "数组中的所有元素都必须是对象。", + "validationErrorRequired": "此为必填字段" + }, + "graph": { + "directionDown": "由上到下", + "directionLeft": "由右到左", + "directionRight": "由左到右", + "directionUp": "由下到上", + "downloadImage": "下载图表图片", + "downloadImageError": "下载图表图片失败。", + "downloadImageErrorTitle": "下载失败", + "otherDagRuns": "+ 其他 Dag 执行", + "taskCount_one": "1 个任务", + "taskCount_other": "{{count}} 个任务", + "taskGroup": "任务分组" + }, + "limitedList": "+ 其他 {{count}} 项", + "limitedList.allItems": "所有 {{count}} 项:", + "limitedList.clickToInteract": "点击标签以筛选 Dags", + "limitedList.clickToOpenFull": "点击 \"+{{count}} 更多\" 打开完整视图", + "limitedList.copyPasteText": "你可以复制并粘贴上方文本", + "logs": { + "file": "文件", + "location": "第 {{line}} 行,位于 {{name}}" + }, + "reparseDag": "重新解析 Dag", + "sortedAscending": "递增排序", + "sortedDescending": "递减排序", + "sortedUnsorted": "未排序", + "taskTries": "任务尝试次数", + "toggleCardView": "显示卡片视图", + "toggleTableView": "显示表格视图", + "triggerDag": { + "button": "触发", + "loading": "正在加载 Dag 信息...", + "loadingFailed": "加载 Dag 信息失败,请重试。", + "runIdHelp": "选填 - 若未提供将会自动生成", + "selectDescription": "触发此 Dag 单次执行", + "selectLabel": "单次执行", + "title": "触发 Dag", + "toaster": { + "success": { + "description": "已成功触发 Dag 执行。", + "title": "已触发 Dag 执行" + } + }, + "unpause": "触发时取消暂停 {{dagDisplayName}}" + }, + "trimText": { + "details": "详细信息", + "empty": "空值", + "noContent": "无可用内容。" + }, + "versionDetails": { + "bundleLink": "套件包链接", + "bundleName": "套件包名称", + "bundleVersion": "套件包版本", + "createdAt": "创建时间", + "versionId": "版本 ID" + }, + "versionSelect": { + "dagVersion": "Dag 版本", + "versionCode": "v{{versionCode}}" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json new file mode 100644 index 0000000000000..6f9a498f7a2c2 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dag.json @@ -0,0 +1,152 @@ +{ + "allRuns": "所有执行", + "blockingDeps": { + "dependency": "依赖 ", + "reason": "原因", + "title": "依赖阻碍任务调度" + }, + "calendar": { + "daily": "每日", + "hourly": "每小时", + "legend": { + "less": "更少", + "more": "更多" + }, + "navigation": { + "nextMonth": "下个月", + "nextYear": "下一年", + "previousMonth": "上个月", + "previousYear": "上一年" + }, + "noData": "无数据", + "noRuns": "未执行", + "totalRuns": "总执行次数", + "week": "第 {{weekNumber}} 周", + "weekdays": { + "friday": "周五", + "monday": "周一", + "saturday": "周六", + "sunday": "周日", + "thursday": "周四", + "tuesday": "周二", + "wednesday": "周三" + } + }, + "code": { + "bundleUrl": "套件包链接", + "noCode": "找不到代码", + "parseDuration": "解析耗时:", + "parsedAt": "解析时间:" + }, + "extraLinks": "额外链接", + "grid": { + "buttons": { + "resetToLatest": "重置为最新", + "toggleGroup": "切换分组状态" + } + }, + "header": { + "buttons": { + "advanced": "高级功能", + "dagDocs": "Dag 文档" + } + }, + "logs": { + "allLevels": "所有日志等级", + "allSources": "所有来源", + "critical": "CRITICAL", + "debug": "DEBUG", + "error": "ERROR", + "fullscreen": { + "button": "全屏", + "tooltip": "按下 {{hotkey}} 进入全屏" + }, + "info": "INFO", + "noTryNumber": "没有重试次数", + "settings": "日志设置", + "viewInExternal": "在 {{name}} 中查看日志(重试 {{attempt}})", + "warning": "WARNING" + }, + "navigation": { + "navigation": "导航: {{arrow}}", + "toggleGroup": "展开/收起分组: 空格键" + }, + "overview": { + "buttons": { + "failedRun_one": "失败的执行", + "failedRun_other": "失败的执行", + "failedTask_one": "失败的任务", + "failedTask_other": "失败的任务", + "failedTaskInstance_one": "失败的任务实例", + "failedTaskInstance_other": "失败的任务实例" + }, + "charts": { + "assetEvent_one": "已创建资源事件", + "assetEvent_other": "已创建资源事件" + }, + "failedLogs": { + "hideLogs": "隐藏日志", + "showLogs": "显示日志", + "title": "最近失败任务的日志", + "viewFullLogs": "查看完整日志" + } + }, + "panel": { + "buttons": { + "options": "选项", + "showGantt": "显示甘特图", + "showGraphShortcut": "显示图表(按 g)", + "showGridShortcut": "显示网格(按 g)" + }, + "dagRuns": { + "label": "Dag 执行次数" + }, + "dependencies": { + "label": "依赖项", + "options": { + "allDagDependencies": "所有 Dag 依赖项", + "externalConditions": "外部条件", + "onlyTasks": "仅限任务" + }, + "placeholder": "依赖项" + }, + "graphDirection": { + "label": "图表方向" + } + }, + "paramsFailed": "加载参数失败", + "parse": { + "toaster": { + "error": { + "description": "Dag 解析请求失败。可能还有待处理的解析请求。", + "title": "Dag 重新解析失败" + }, + "success": { + "description": "Dag 即将重新解析。", + "title": "已成功提交重新解析请求" + } + } + }, + "tabs": { + "assetEvents": "资源事件", + "auditLog": "审计日志", + "backfills": "回填", + "calendar": "日历", + "code": "代码", + "details": "详细信息", + "logs": "日志", + "mappedTaskInstances_one": "任务实例 [{{count}}]", + "mappedTaskInstances_other": "任务实例 [{{count}}]", + "overview": "总览", + "renderedTemplates": "渲染后的模板", + "requiredActions": "待响应的任务实例", + "runs": "执行记录", + "taskInstances": "任务实例", + "tasks": "任务", + "xcom": "XCom" + }, + "taskGroups": { + "collapseAll": "收起所有任务分组", + "expandAll": "展开所有任务分组" + } +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json new file mode 100644 index 0000000000000..90ba4ff9588cd --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dags.json @@ -0,0 +1,96 @@ +{ + "assetSchedule": "{{count}} / {{total}} 个资源事件已更新", + "dagActions": { + "delete": { + "button": "删除 Dag", + "warning": "这将会删除所有与此 Dag 相关的系统数据,包括 Dag 执行与任务。" + } + }, + "favoriteDag": "将 Dag 加入收藏", + "filters": { + "allRunTypes": "全部执行类型", + "allStates": "全部状态", + "favorite": { + "all": "全部", + "favorite": "已加入收藏", + "unfavorite": "未加入收藏" + }, + "paused": { + "active": "已启用", + "all": "全部", + "paused": "暂停" + }, + "runIdPatternFilter": "搜索 Dag 执行" + }, + "ownerLink": "拥有者 {{owner}} 的地址", + "runAndTaskActions": { + "affectedTasks": { + "noItemsFound": "找不到任务。", + "title": "受影响的任务: {{count}}" + }, + "clear": { + "button": "清除 {{type}}", + "buttonTooltip": "按下 shift+c 清除", + "error": "清除 {{type}} 时发生错误", + "title": "清除 {{type}}" + }, + "delete": { + "button": "删除 {{type}}", + "dialog": { + "resourceName": "{{type}} {{id}}", + "title": "删除 {{type}}", + "warning": "这将会删除所有与此 {{type}} 相关的系统数据。" + }, + "error": "删除 {{type}} 时发生错误", + "success": { + "description": "{{type}} 删除请求成功。", + "title": "{{type}} 删除成功" + } + }, + "markAs": { + "button": "标记 {{type}} 为...", + "buttonTooltip": { + "failed": "按下 shift+f 标记为失败", + "success": "按下 shift+s 标记为成功" + }, + "title": "标记为 {{type}} 为 {{state}}" + }, + "options": { + "downstream": "下游", + "existingTasks": "清除现有任务", + "future": "未来", + "onlyFailed": "只清除失败任务", + "past": "过去", + "queueNew": "加入新任务到队列", + "runOnLatestVersion": "执行最新套件包版本", + "upstream": "上游" + } + }, + "search": { + "advanced": "高级搜索", + "clear": "清除搜索", + "dags": "搜索 Dags", + "hotkey": "+K", + "tasks": "搜索任务" + }, + "sort": { + "displayName": { + "asc": "按显示名称排序 (A-Z)", + "desc": "按显示名称排序 (Z-A)" + }, + "lastRunStartDate": { + "asc": "按上次开始执行日期排序 (从新到旧)", + "desc": "按上次开始执行日期排序 (从新到旧)" + }, + "lastRunState": { + "asc": "按上次执行状态排序 (A-Z)", + "desc": "按上次执行状态排序 (Z-A)" + }, + "nextDagRun": { + "asc": "按下次执行时间排序 (由近而远)", + "desc": "按下次执行时间排序 (由远而近)" + }, + "placeholder": "排序方式" + }, + "unfavoriteDag": "将 Dag 移出收藏" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json new file mode 100644 index 0000000000000..89022de3eb740 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/dashboard.json @@ -0,0 +1,45 @@ +{ + "favorite": { + "favoriteDags_one": "第 {{ count }} 个收藏的 Dag", + "favoriteDags_other": "前 {{ count }} 个收藏的 Dag", + "noDagRuns": "此 Dag 尚未被触发过。", + "noFavoriteDags": "尚无收藏的 Dag。请点击 Dag 列表旁的星号图示,将 Dag 加入收藏。" + }, + "group": "分组", + "health": { + "dagProcessor": "Dag 处理器", + "health": "健康状态", + "healthy": "健康", + "lastHeartbeat": "最后心跳", + "metaDatabase": "系统数据库", + "scheduler": "调度器", + "status": "状态", + "triggerer": "触发器", + "unhealthy": "健康状态异常" + }, + "history": "历史记录", + "importErrors": { + "dagImportError_one": "Dag 导入错误", + "dagImportError_other": "Dag 导入错误", + "searchByFile": "依文件搜索", + "timestamp": "时间戳" + }, + "managePools": "管理资源池", + "noAssetEvents": "未找到资源事件", + "poolSlots": "资源池配额", + "sortBy": { + "newestFirst": "由新到旧", + "oldestFirst": "由旧到新" + }, + "source": "来源", + "stats": { + "activeDags": "已启用的 Dags", + "failedDags": "失败的 Dags", + "queuedDags": "排队的 Dags", + "requiredActions": "待操作的任务实例", + "runningDags": "执行中的 Dags", + "stats": "统计" + }, + "uri": "Uri", + "welcome": "欢迎" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json new file mode 100644 index 0000000000000..0c4cc910c60a4 --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/hitl.json @@ -0,0 +1,34 @@ +{ + "filters": { + "response": { + "all": "全部", + "pending": "待响应", + "received": "已响应" + } + }, + "requiredAction_one": "待操作的任务实例", + "requiredAction_other": "待操作的任务实例", + "requiredActionCount_one": "待操作的任务实例 ({{count}})", + "requiredActionCount_other": "待操作的任务实例 ({{count}})", + "requiredActionState": "待操作的任务实例状态", + "response": { + "error": "响应失败", + "optionsDescription": "请为此任务实例选择一个选项", + "optionsLabel": "选项", + "received": "收到响应的时间:", + "respond": "发送响应", + "success": "任务 {{taskId}} 响应成功", + "title": "人类参与流程任务实例 - {{taskId}}" + }, + "state": { + "approvalReceived": "已批准", + "approvalRequired": "需要批准", + "choiceReceived": "已选择", + "choiceRequired": "需要选择", + "noResponseReceived": "未收到响应", + "rejectionReceived": "已拒绝", + "responseReceived": "已响应", + "responseRequired": "需要响应" + }, + "subject": "标题" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/tasks.json new file mode 100644 index 0000000000000..2ba125e591a4d --- /dev/null +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/tasks.json @@ -0,0 +1,10 @@ +{ + "mapped": "已映射", + "notMapped": "未映射", + "retries": "重试", + "searchTasks": "搜索任务", + "selectMapped": "选择已映射", + "selectOperator": "选择操作器", + "selectRetryValues": "选择重试值", + "selectTriggerRules": "选择触发规则" +} diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/admin.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/admin.json index fa2e576915ad1..f9fbb579540fd 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/admin.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/admin.json @@ -49,6 +49,12 @@ "searchPlaceholder": "搜尋連線", "test": "測試連線", "testDisabled": "測試連線功能已停用。請聯繫管理員以啟用。", + "testError": { + "title": "連線測試失敗" + }, + "testSuccess": { + "title": "連線測試成功" + }, "typeMeta": { "error": "取得連線類型中繼資料失敗", "standardFields": { @@ -72,7 +78,6 @@ "tooltip": "刪除所選連線" }, "formActions": { - "reset": "重置", "save": "儲存" }, "plugins": { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/assets.json index bf5daf90e0629..5ff3242a2ba21 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/assets.json @@ -1,5 +1,6 @@ { "consumingDags": "消費者 Dags", + "consumingTasks": "消費者任務", "createEvent": { "button": "建立事件", "manual": { @@ -21,6 +22,7 @@ }, "title": "為 {{name}} 建立資源事件" }, + "extra": "資源額外資訊", "group": "群組", "lastAssetEvent": "最後資源事件", "name": "名稱", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/browse.json index 410ed4dad8b23..324f437bcd019 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/browse.json @@ -1,9 +1,5 @@ { "auditLog": { - "actions": { - "collapseAllExtra": "收合所有額外 JSON", - "expandAllExtra": "展開所有額外 JSON" - }, "columns": { "event": "事件", "extra": "額外資訊", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json index 8f86cac6900e5..e96f96eb10af8 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json @@ -25,6 +25,7 @@ "requiredActions": "待回應的任務實例", "xcoms": "XComs" }, + "collapseAllExtra": "收合所有額外 JSON", "collapseDetailsPanel": "收起詳細資訊", "createdAssetEvent_one": "已建立資源事件", "createdAssetEvent_other": "已建立資源事件", @@ -63,7 +64,7 @@ "runAfter": "最早可執行時間", "runType": "執行類型", "sourceAssetEvent": "來源資源事件", - "triggeredBy": "觸發者", + "triggeredBy": "觸發來源", "triggeringUser": "觸發使用者名稱" }, "dagRun_one": "Dag 執行", @@ -78,12 +79,18 @@ "githubRepo": "GitHub 倉庫", "restApiReference": "REST API 參考" }, + "download": { + "download": "下載", + "hotkey": "d", + "tooltip": "按下 {{hotkey}} 下載日誌" + }, "duration": "執行時間", "endDate": "結束日期", "error": { "back": "返回", "defaultMessage": "發生未預期的錯誤", "home": "首頁", + "invalidUrl": "找不到頁面。請檢查 URL 並重試。", "notFound": "找不到頁面", "title": "錯誤" }, @@ -93,23 +100,19 @@ "hotkey": "e", "tooltip": "按下 {{hotkey}} 切換展開" }, + "expandAllExtra": "展開所有額外 JSON", "expression": { "all": "全部", "and": "且", "any": "任何", "or": "或" }, + "filter": "篩選", "filters": { - "dagDisplayNamePlaceholder": "依 Dag 名稱篩選", - "keyPlaceholder": "依 XCom 鍵篩選", - "logicalDateFromPlaceholder": "邏輯日期起始", - "logicalDateToPlaceholder": "邏輯日期結束", - "mapIndexPlaceholder": "依映射索引篩選", - "runAfterFromPlaceholder": "執行時間起始", - "runAfterToPlaceholder": "執行時間結束", - "runIdPlaceholder": "依執行 ID 篩選", - "taskIdPlaceholder": "依任務 ID 篩選", - "triggeringUserPlaceholder": "依觸發使用者篩選" + "logicalDateFrom": "從邏輯日期", + "logicalDateTo": "到邏輯日期", + "runAfterFrom": "從最早可執行時間", + "runAfterTo": "到最早可執行時間" }, "logicalDate": "邏輯日期", "logout": "登出", @@ -131,7 +134,7 @@ "docs": "文件", "home": "首頁", "legacyFabViews": "舊版檢視", - "plugins": "插件", + "plugins": "外掛", "security": "安全" }, "noItemsFound": "找不到 {{modelName}}", @@ -151,6 +154,7 @@ "running": "執行中", "scheduled": "已排程" }, + "reset": "重置", "runId": "執行 ID", "runTypes": { "asset_triggered": "資源觸發", @@ -165,7 +169,6 @@ }, "tooltip": "按 {{hotkey}} 捲動到{{direction}}" }, - "seconds": "{{count}} 秒", "security": { "actions": "操作", "permissions": "權限", @@ -242,12 +245,12 @@ "queuedWhen": "開始排隊時間", "scheduledWhen": "開始排程時間", "triggerer": { - "assigned": "指派的觸發器", + "assigned": "指派的觸發者", "class": "觸發器類別", "createdAt": "觸發器建立時間", "id": "觸發器 ID", - "latestHeartbeat": "最新觸發器心跳時間", - "title": "觸發器資訊" + "latestHeartbeat": "最新觸發者心跳時間", + "title": "觸發者資訊" }, "unixname": "Unix 名稱" }, diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json index 77075ae59494d..ecdd087178d8b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/components.json @@ -2,12 +2,10 @@ "backfill": { "affected_one": "將會觸發 1 次執行。", "affected_other": "將會觸發 {{count}} 次執行。", - "affectedNone": "沒有符合條件的執行。", + "affectedNone": "無符合條件的執行。", "allRuns": "所有執行", "backwards": "反向執行", "dateRange": "日期範圍", - "dateRangeFrom": "從", - "dateRangeTo": "到", "errorStartDateBeforeEndDate": "開始日期必須早於結束日期。", "maxRuns": "活躍執行數上限", "missingAndErroredRuns": "遺漏和錯誤的執行", @@ -88,6 +86,14 @@ "taskGroup": "任務群組" }, "limitedList": "+ 其他 {{count}} 項", + "limitedList.allItems": "所有 {{count}} 個項目:", + "limitedList.allTags_one": " 所有標籤 (1)", + "limitedList.allTags_other": "所有標籤 ({{count}})", + "limitedList.clickToInteract": "點擊標籤以篩選 Dags", + "limitedList.clickToOpenFull": "點擊 \"+{{count}} 更多\" 以開啟完整檢視", + "limitedList.copyPasteText": "你可以複製並貼上上方文字", + "limitedList.showingItems_one": "顯示 1 個項目", + "limitedList.showingItems_other": "顯示 {{count}} 個項目", "logs": { "file": "檔案", "location": "第 {{line}} 行,位於 {{name}}" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json index f1e3c8630982b..5f4925c2d8fe9 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json @@ -10,6 +10,7 @@ "hourly": "每小時", "legend": { "less": "較少", + "mixed": "混合", "more": "較多" }, "navigation": { @@ -19,7 +20,8 @@ "previousYear": "上一年" }, "noData": "沒有可用的資料", - "noRuns": "沒有執行", + "noFailedRuns": "無失敗的執行", + "noRuns": "無執行", "totalRuns": "總執行數", "week": "第 {{weekNumber}} 週", "weekdays": { @@ -62,7 +64,7 @@ "tooltip": "按下 {{hotkey}} 進入全螢幕" }, "info": "INFO", - "noTryNumber": "沒有嘗試次數", + "noTryNumber": "無嘗試次數", "settings": "日誌設定", "viewInExternal": "在 {{name}} 中檢視日誌(嘗試 {{attempt}})", "warning": "WARNING" diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dashboard.json index 10e6e5553ae54..0ecee8bd3da3c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dashboard.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dashboard.json @@ -14,7 +14,7 @@ "metaDatabase": "系統資料庫", "scheduler": "排程器", "status": "狀態", - "triggerer": "觸發器", + "triggerer": "觸發者", "unhealthy": "健康狀態異常" }, "history": "歷史記錄", diff --git a/airflow-core/src/airflow/ui/rules/rem.js b/airflow-core/src/airflow/ui/rules/rem.js new file mode 100644 index 0000000000000..404be4f3a6bf5 --- /dev/null +++ b/airflow-core/src/airflow/ui/rules/rem.js @@ -0,0 +1,150 @@ +/*! + * 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 { AST_NODE_TYPES } from "@typescript-eslint/utils"; + +export const remNamespace = "rem"; + +/** + * Check if a value contains rem units + * @param {string} value + * @returns {boolean} + */ +const containsRem = (value) => /\d+\.?\d*rem/u.test(value); + +/** + * Convert rem value to pixels (1rem = 16px) + * @param {string} value + * @returns {string} + */ +const convertRemToPixels = (value) => + value.replaceAll(/(?\d+\.?\d*)rem/gu, (_, number) => { + if (typeof number === "string") { + const pixels = parseFloat(number) * 16; + + return `${pixels}px`; + } + + return value; + }); + +export const remPlugin = { + rules: { + "no-rem-in-props": { + /** @param {import('@typescript-eslint/utils').TSESLint.RuleContext<'noRemInProps', []>} context */ + create(context) { + /** @param {import('@typescript-eslint/utils').TSESTree.JSXOpeningElement} node */ + const checkAttributes = (node) => { + // Check all attributes for rem values in size, width, height props (but not style) + node.attributes.forEach((attr) => { + if (attr.type !== AST_NODE_TYPES.JSXAttribute || !attr.value) { + return; + } + + const attrName = attr.name.name; + + // Skip style attributes - rem is allowed there + if (attrName === "style") { + return; + } + + // Only check size, width, height attributes + if (attrName !== "height" && attrName !== "size" && attrName !== "width") { + return; + } + + let attrValue = undefined; + + // Handle different attribute value types + if (attr.value.type === AST_NODE_TYPES.Literal) { + attrValue = attr.value.value; + } else if ( + attr.value.type === AST_NODE_TYPES.JSXExpressionContainer && + attr.value.expression.type === AST_NODE_TYPES.Literal + ) { + attrValue = attr.value.expression.value; + } + + // Check for rem values + if (typeof attrValue === "string" && containsRem(attrValue)) { + const fixedValue = convertRemToPixels(attrValue); + + context.report({ + data: { + attribute: attrName, + fixedValue, + value: attrValue, + }, + fix(fixer) { + // For string literals, replace the entire value + if (attr.value !== null && attr.value.type === AST_NODE_TYPES.Literal) { + return fixer.replaceText(attr.value, `{${fixedValue}}`); + } + // For JSX expressions with literal values, replace just the literal + if ( + attr.value !== null && + attr.value.type === AST_NODE_TYPES.JSXExpressionContainer && + attr.value.expression.type === AST_NODE_TYPES.Literal + ) { + return fixer.replaceText(attr.value.expression, fixedValue); + } + + // eslint-disable-next-line unicorn/no-null + return null; + }, + messageId: "noRemInProps", + node: attr, + }); + } + }); + }; + + return { + /** @param {import('@typescript-eslint/utils').TSESTree.JSXOpeningElement} node */ + JSXOpeningElement(node) { + checkAttributes(node); + }, + }; + }, + meta: { + docs: { + category: "Best Practices", + description: "Disallow rem units in size, width, and height attributes (but allow in style)", + recommended: "error", + }, + fixable: "code", + messages: { + noRemInProps: + "Avoid using rem units in {{attribute}} attribute. Use numeric pixel values instead of '{{value}}'. Auto-fix available: {{fixedValue}}", + }, + type: "problem", + }, + }, + }, +}; + +/** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig.Config} */ +export const remRules = { + files: ["**/*.tsx"], + plugins: { + [remNamespace]: remPlugin, + }, + rules: { + [`${remNamespace}/no-rem-in-props`]: "error", + }, +}; diff --git a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx index f9c6c42e7063e..498ada9f821c1 100644 --- a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx +++ b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx @@ -18,6 +18,7 @@ */ import { Box, Editable, Text, VStack } from "@chakra-ui/react"; import type { ChangeEvent } from "react"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { DAGRunResponse, TaskInstanceCollectionResponse } from "openapi/requests/types.gen"; @@ -39,6 +40,8 @@ const ActionAccordion = ({ affectedTasks, note, setNote }: Props) => { const showTaskSection = affectedTasks !== undefined; const { t: translate } = useTranslation(); + const columns = useMemo(() => getColumns(translate), [translate]); + return ( { diff --git a/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx b/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx new file mode 100644 index 0000000000000..aea00ca4afa1b --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/AnsiRenderer.tsx @@ -0,0 +1,235 @@ +/*! + * 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 { chakra } from "@chakra-ui/react"; +import Anser, { type AnserJsonEntry } from "anser"; +import * as React from "react"; + +const fixBackspace = (inputText: string): string => { + let tmp = inputText; + + do { + const previous = tmp; + + // eslint-disable-next-line no-control-regex + tmp = tmp.replaceAll(/[^\n]\u0008/gmu, ""); + if (tmp.length >= previous.length) { + break; + } + } while (tmp.length > 0); + + return tmp; +}; + +const ansiToJSON = (input: string, useClasses: boolean = false): Array => { + const processedInput = fixBackspace(input.replaceAll("\r", "")); + + return Anser.ansiToJson(processedInput, { + json: true, + remove_empty: true, + use_classes: useClasses, + }); +}; + +const createClass = (bundle: AnserJsonEntry): string | undefined => { + let classNames = ""; + + if (bundle.bg) { + classNames += `${bundle.bg}-bg `; + } + if (bundle.fg) { + classNames += `${bundle.fg}-fg `; + } + if (bundle.decoration) { + classNames += `ansi-${bundle.decoration} `; + } + + if (classNames === "") { + return undefined; + } + + return classNames.slice(0, classNames.length - 1); +}; + +// Map RGB values to Chakra UI semantic tokens +// These are the standard ANSI color RGB values that anser library outputs +const rgbToChakraColorMap: Record = { + "0, 0, 0": "gray.900", // Black (30m) + "0, 0, 187": "blue.fg", // Blue (34m) + "0, 187, 0": "green.fg", // Green (32m) + "0, 187, 187": "cyan.fg", // Cyan (36m) + "0, 255, 0": "green.400", // Bright Green (92m) + "85, 85, 85": "black", // Bright Black/Gray (90m) + "85, 85, 255": "blue.400", // Bright Blue (94m) + "85, 255, 255": "cyan.400", // Bright Cyan (96m) + "187, 0, 0": "red.fg", // Red (31m) + "187, 0, 187": "purple.fg", // Magenta (35m) + "187, 187, 0": "yellow.fg", // Yellow (33m) + "187, 187, 187": "gray.100", // White (37m) + "255, 85, 85": "red.400", // Bright Red (91m) + "255, 85, 255": "purple.400", // Bright Magenta (95m) + "255, 255, 85": "yellow.400", // Bright Yellow (93m) + "255, 255, 255": "white", // Bright White (97m) +}; + +const createChakraProps = (bundle: AnserJsonEntry) => { + const props: Record = {}; + + // Handle background colors + if (bundle.bg) { + const bgColor = rgbToChakraColorMap[bundle.bg]; + + props.bg = bgColor ?? `rgb(${bundle.bg})`; + } + + // Handle foreground colors + if (bundle.fg) { + const fgColor = rgbToChakraColorMap[bundle.fg]; + + props.color = fgColor ?? `rgb(${bundle.fg})`; + } + + // Handle text decorations + switch (bundle.decoration) { + case "blink": + props.textDecoration = "blink"; + break; + case "bold": + props.fontWeight = "bold"; + break; + case "dim": + props.opacity = "0.5"; + break; + case "hidden": + props.visibility = "hidden"; + break; + case "italic": + props.fontStyle = "italic"; + break; + case "reverse": + // Could implement reverse video if needed + break; + case "strikethrough": + props.textDecoration = "line-through"; + break; + case "underline": + props.textDecoration = "underline"; + break; + // eslint-disable-next-line unicorn/no-useless-switch-case + case null: + default: + break; + } + + return props; +}; + +const convertBundleIntoReact = (options: { + bundle: AnserJsonEntry; + key: number; + linkify: boolean; + useClasses: boolean; +}): JSX.Element => { + const { bundle, key, linkify, useClasses } = options; + const style = useClasses ? undefined : createChakraProps(bundle); + const className = useClasses ? createClass(bundle) : undefined; + + if (!linkify) { + if (useClasses) { + return ( + + {bundle.content} + + ); + } + + return ( + + {bundle.content} + + ); + } + + const content: Array = []; + const linkRegex = + /(?\s|^)(?https?:\/\/(?:www\.|(?!www))[^\s.]+\.\S{2,}|www\.\S+\.\S{2,})/gu; + + let index = 0; + let match: RegExpExecArray | null; + + while ((match = linkRegex.exec(bundle.content)) !== null) { + const { groups } = match; + const pre = groups?.whitespace ?? ""; + const url = groups?.url ?? ""; + const startIndex = match.index + pre.length; + + if (startIndex > index) { + content.push(bundle.content.slice(index, startIndex)); + } + + const href = url.startsWith("www.") ? `http://${url}` : url; + + content.push( + + {url} + , + ); + + index = linkRegex.lastIndex; + } + + if (index < bundle.content.length) { + content.push(bundle.content.slice(index)); + } + + if (useClasses) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +}; + +type AnsiRendererProps = { + readonly children?: string; + readonly className?: string; + readonly linkify?: boolean; + readonly useClasses?: boolean; +}; + +export const AnsiRenderer: React.FC = ({ + children = "", + className, + linkify = false, + useClasses = false, +}) => ( + + {ansiToJSON(children, useClasses).map((bundle, index) => + convertBundleIntoReact({ bundle, key: index, linkify, useClasses }), + )} + +); + +export default AnsiRenderer; diff --git a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx index 518517da17ca6..6289309ec59f4 100644 --- a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx +++ b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx @@ -34,7 +34,7 @@ export const AssetNode = ({ => ({ card: ({ row }) => , @@ -43,6 +44,7 @@ type AssetEventProps = { readonly isLoading?: boolean; readonly setOrderBy?: (order: string) => void; readonly setTableUrlState?: (state: TableState) => void; + readonly showFilters?: boolean; readonly tableUrlState?: TableState; readonly titleKey?: string; }; @@ -53,6 +55,7 @@ export const AssetEvents = ({ isLoading, setOrderBy, setTableUrlState, + showFilters = false, tableUrlState, titleKey, ...rest @@ -66,10 +69,10 @@ export const AssetEvents = ({ }); return ( - - + + - + {data?.total_entries ?? " "} @@ -101,6 +104,7 @@ export const AssetEvents = ({ )} + {showFilters ? : null} = [ + SearchParamsKeys.START_DATE, + SearchParamsKeys.END_DATE, + SearchParamsKeys.DAG_ID, + SearchParamsKeys.TASK_ID, +]; + +export const AssetEventsFilter = () => { + const { filterConfigs, handleFiltersChange, searchParams } = useFiltersHandler(FILTER_KEYS); + + const initialValues = useMemo(() => { + const values: Record = {}; + + FILTER_KEYS.forEach((key) => { + const value = searchParams.get(key); + + if (value !== null && value.trim() !== "") { + values[key] = value; + } + }); + + return values; + }, [searchParams]); + + return ( + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/Banner/BackfillBanner.tsx b/airflow-core/src/airflow/ui/src/components/Banner/BackfillBanner.tsx index 3b05ac63a0ddd..522a541fc40ff 100644 --- a/airflow-core/src/airflow/ui/src/components/Banner/BackfillBanner.tsx +++ b/airflow-core/src/airflow/ui/src/components/Banner/BackfillBanner.tsx @@ -29,6 +29,8 @@ import { useBackfillServicePauseBackfill, useBackfillServiceUnpauseBackfill, } from "openapi/queries"; +import type { BackfillResponse } from "openapi/requests/types.gen"; +import { useAutoRefresh } from "src/utils"; import Time from "../Time"; import { Button, ProgressBar } from "../ui"; @@ -38,9 +40,9 @@ type Props = { }; const buttonProps = { - _hover: { bg: "blue.contrast", color: "blue.muted" }, - borderColor: "blue.contrast", - color: "blue.contrast", + _hover: { bg: "info.contrast", color: "info.muted" }, + borderColor: "info.contrast", + color: "info.contrast", rounded: "full", size: "xs", variant: "outline", @@ -48,10 +50,21 @@ const buttonProps = { const BackfillBanner = ({ dagId }: Props) => { const { t: translate } = useTranslation("components"); - const { data, isLoading } = useBackfillServiceListBackfillsUi({ - dagId, - }); - const [backfill] = data?.backfills.filter((bf) => bf.completed_at === null) ?? []; + const refetchInterval = useAutoRefresh({ dagId }); + + const { data, isLoading } = useBackfillServiceListBackfillsUi( + { + dagId, + }, + undefined, + { + refetchInterval: (query) => + query.state.data?.backfills.some((bf: BackfillResponse) => bf.completed_at === null && !bf.is_paused) + ? refetchInterval + : false, + }, + ); + const [backfill] = data?.backfills.filter((bf: BackfillResponse) => bf.completed_at === null) ?? []; const queryClient = useQueryClient(); const onSuccess = async () => { @@ -64,7 +77,6 @@ const BackfillBanner = ({ dagId }: Props) => { const { isPending: isUnPausePending, mutate: unpauseMutate } = useBackfillServiceUnpauseBackfill({ onSuccess, }); - const { isPending: isStopPending, mutate: stopPending } = useBackfillServiceCancelBackfill({ onSuccess }); const togglePause = () => { @@ -90,7 +102,7 @@ const BackfillBanner = ({ dagId }: Props) => { } return ( - + {translate("banner.backfillInProgress")}: diff --git a/airflow-core/src/airflow/ui/src/components/BasicTooltip.tsx b/airflow-core/src/airflow/ui/src/components/BasicTooltip.tsx new file mode 100644 index 0000000000000..96400e5d5f053 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/BasicTooltip.tsx @@ -0,0 +1,127 @@ +/*! + * 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 { Box, Portal } from "@chakra-ui/react"; +import type { ReactElement, ReactNode } from "react"; +import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; + +type Props = { + readonly children: ReactElement; + readonly content: ReactNode; +}; + +const offset = 8; + +export const BasicTooltip = ({ children, content }: Props): ReactElement => { + const triggerRef = useRef(null); + const tooltipRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [showOnTop, setShowOnTop] = useState(false); + const timeoutRef = useRef(); + + const handleMouseEnter = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setIsOpen(true); + }, 500); + }, []); + + const handleMouseLeave = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = undefined; + } + setIsOpen(false); + }, []); + + // Calculate position based on actual tooltip height before paint + useLayoutEffect(() => { + if (isOpen && triggerRef.current && tooltipRef.current) { + const triggerRect = triggerRef.current.getBoundingClientRect(); + const tooltipHeight = tooltipRef.current.clientHeight; + const wouldOverflow = triggerRect.bottom + offset + tooltipHeight > globalThis.innerHeight; + + setShowOnTop(wouldOverflow); + } + }, [isOpen]); + + // Cleanup on unmount + useEffect( + () => () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, + [], + ); + + // Clone children and attach event handlers + ref + const trigger = cloneElement(children, { + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + ref: triggerRef, + }); + + if (!isOpen || !triggerRef.current) { + return trigger; + } + + const rect = triggerRef.current.getBoundingClientRect(); + const { scrollX, scrollY } = globalThis; + + return ( + <> + {trigger} + + + + {content} + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx index 3b3c9fe5f0eb3..156c11c7a50ab 100644 --- a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx +++ b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx @@ -22,13 +22,14 @@ import { useTranslation } from "react-i18next"; import { CgRedo } from "react-icons/cg"; import { useDagServiceGetDagDetails } from "openapi/queries"; -import type { DAGRunResponse } from "openapi/requests/types.gen"; +import type { DAGRunResponse, TaskInstanceResponse } from "openapi/requests/types.gen"; import { ActionAccordion } from "src/components/ActionAccordion"; import { Button, Dialog, Checkbox } from "src/components/ui"; import SegmentedControl from "src/components/ui/SegmentedControl"; import { useClearDagRunDryRun } from "src/queries/useClearDagRunDryRun"; import { useClearDagRun } from "src/queries/useClearRun"; import { usePatchDagRun } from "src/queries/usePatchDagRun"; +import { isStatePending, useAutoRefresh } from "src/utils"; type Props = { readonly dagRun: DAGRunResponse; @@ -51,9 +52,17 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) => { dagId, }); + const refetchInterval = useAutoRefresh({ dagId }); + const { data: affectedTasks = { task_instances: [], total_entries: 0 } } = useClearDagRunDryRun({ dagId, dagRunId, + options: { + refetchInterval: (query) => + query.state.data?.task_instances.some((ti: TaskInstanceResponse) => isStatePending(ti.state)) + ? refetchInterval + : false, + }, requestBody: { only_failed: onlyFailed, run_on_latest_version: runOnLatestVersion }, }); @@ -129,7 +138,7 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) => { ) : undefined} - diff --git a/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx b/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx index 667366c725ff8..2ca528c9e15a2 100644 --- a/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx @@ -20,6 +20,7 @@ import { Box, Heading, VStack } from "@chakra-ui/react"; import { type ReactElement, useState } from "react"; import { Button, Dialog } from "src/components/ui"; +import { ResizableWrapper, MARKDOWN_DIALOG_STORAGE_KEY } from "src/components/ui/ResizableWrapper"; import ReactMarkdown from "./ReactMarkdown"; @@ -48,14 +49,16 @@ const DisplayMarkdownButton = ({ open={isDocsOpen} size="md" > - - - {header} - - - - {mdContent} - + + + + {header} + + + + {mdContent} + + diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index b3a0b6ae1a4d0..186ebb5f6f19e 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Heading } from "@chakra-ui/react"; +import { Box, Heading, useToken } from "@chakra-ui/react"; import { Chart as ChartJS, CategoryScale, @@ -35,7 +35,9 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import type { TaskInstanceResponse, GridRunsResponse } from "openapi/requests/types.gen"; -import { system } from "src/theme"; +import { getComputedCSSVariableValue } from "src/theme"; +import { DEFAULT_DATETIME_FORMAT, renderDuration } from "src/utils/datetimeUtils"; +import { buildTaskInstanceUrl } from "src/utils/links"; ChartJS.register( CategoryScale, @@ -56,7 +58,16 @@ const average = (ctx: PartialEventContext, index: number) => { type RunResponse = GridRunsResponse | TaskInstanceResponse; -const getDuration = (start: string, end: string | null) => dayjs.duration(dayjs(end).diff(start)).asSeconds(); +const getDuration = (start: string, end: string | null) => { + const startDate = dayjs(start); + const endDate = end === null ? dayjs() : dayjs(end); + + if (!startDate.isValid() || !endDate.isValid()) { + return 0; + } + + return dayjs.duration(endDate.diff(startDate)).asSeconds(); +}; export const DurationChart = ({ entries, @@ -67,16 +78,33 @@ export const DurationChart = ({ }) => { const { t: translate } = useTranslation(["components", "common"]); const navigate = useNavigate(); + const [queuedColorToken] = useToken("colors", ["queued.solid"]); + + // Get states and create color tokens for them + const states = entries?.map((entry) => entry.state).filter(Boolean) ?? []; + const stateColorTokens = useToken( + "colors", + states.map((state) => `${state}.solid`), + ); if (!entries) { return undefined; } + // Create a mapping of state to color for easy lookup + const stateColorMap: Record = {}; + + states.forEach((state, index) => { + if (state) { + stateColorMap[state] = getComputedCSSVariableValue(stateColorTokens[index] ?? "oklch(0.5 0 0)"); + } + }); + const runAnnotation = { - borderColor: "green", + borderColor: "grey", borderWidth: 1, label: { - content: (ctx: PartialEventContext) => average(ctx, 1).toFixed(2), + content: (ctx: PartialEventContext) => renderDuration(average(ctx, 1), false) ?? "0", display: true, position: "end", }, @@ -88,7 +116,7 @@ export const DurationChart = ({ borderColor: "grey", borderWidth: 1, label: { - content: (ctx: PartialEventContext) => average(ctx, 0).toFixed(2), + content: (ctx: PartialEventContext) => renderDuration(average(ctx, 0), false) ?? "0", display: true, position: "end", }, @@ -107,7 +135,7 @@ export const DurationChart = ({ data={{ datasets: [ { - backgroundColor: system.tokens.categoryMap.get("colors")?.get("queued.600")?.value as string, + backgroundColor: getComputedCSSVariableValue(queuedColorToken ?? "oklch(0.5 0 0)"), data: entries.map((entry: RunResponse) => { switch (kind) { case "Dag Run": { @@ -135,7 +163,7 @@ export const DurationChart = ({ { backgroundColor: entries.map( (entry: RunResponse) => - system.tokens.categoryMap.get("colors")?.get(`${entry.state}.600`)?.value as string, + (entry.state ? stateColorMap[entry.state] : undefined) ?? "oklch(0.5 0 0)", ), data: entries.map((entry: RunResponse) => entry.start_date === null ? 0 : Number(getDuration(entry.start_date, entry.end_date)), @@ -143,7 +171,7 @@ export const DurationChart = ({ label: translate("durationChart.runDuration"), }, ], - labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format("YYYY-MM-DD, hh:mm:ss")), + labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format(DEFAULT_DATETIME_FORMAT)), }} datasetIdKey="id" options={{ @@ -159,14 +187,26 @@ export const DurationChart = ({ const entry = entries[element.index] as GridRunsResponse | undefined; const baseUrl = `/dags/${entry?.dag_id}/runs/${entry?.run_id}`; - navigate(baseUrl); + void Promise.resolve(navigate(baseUrl)); break; } case "Task Instance": { const entry = entries[element.index] as TaskInstanceResponse | undefined; - const baseUrl = `/dags/${entry?.dag_id}/runs/${entry?.dag_run_id}`; - navigate(`${baseUrl}/tasks/${entry?.task_id}`); + if (entry === undefined) { + break; + } + + const baseUrl = buildTaskInstanceUrl({ + currentPathname: location.pathname, + dagId: entry.dag_id, + isMapped: entry.map_index >= 0, + mapIndex: entry.map_index.toString(), + runId: entry.dag_run_id, + taskId: entry.task_id, + }); + + void Promise.resolve(navigate(baseUrl)); break; } default: @@ -182,6 +222,17 @@ export const DurationChart = ({ runAnnotation, }, }, + tooltip: { + callbacks: { + label: (context) => { + const datasetLabel = context.dataset.label ?? ""; + + const formatted = renderDuration(context.parsed.y, false) ?? "0"; + + return datasetLabel ? `${datasetLabel}: ${formatted}` : formatted; + }, + }, + }, }, responsive: true, scales: { @@ -193,6 +244,13 @@ export const DurationChart = ({ title: { align: "end", display: true, text: translate("common:dagRun.runAfter") }, }, y: { + ticks: { + callback: (value) => { + const num = typeof value === "number" ? value : Number(value); + + return renderDuration(num, false) ?? "0"; + }, + }, title: { align: "end", display: true, text: translate("common:duration") }, }, }, diff --git a/airflow-core/src/airflow/ui/src/components/EditableMarkdownArea.tsx b/airflow-core/src/airflow/ui/src/components/EditableMarkdownArea.tsx index a77bfcd617e93..3b467734a09eb 100644 --- a/airflow-core/src/airflow/ui/src/components/EditableMarkdownArea.tsx +++ b/airflow-core/src/airflow/ui/src/components/EditableMarkdownArea.tsx @@ -18,6 +18,7 @@ */ import { Box, VStack, Editable, Text } from "@chakra-ui/react"; import type { ChangeEvent } from "react"; +import { useState, useRef } from "react"; import ReactMarkdown from "./ReactMarkdown"; @@ -31,36 +32,54 @@ const EditableMarkdownArea = ({ readonly onBlur?: () => void; readonly placeholder?: string | null; readonly setMdContent: (value: string) => void; -}) => ( - - ) => setMdContent(event.target.value)} - value={mdContent ?? ""} - > - { + const [currentValue, setCurrentValue] = useState(mdContent ?? ""); + const prevMdContentRef = useRef(mdContent); + + // Sync local state with prop changes + if (mdContent !== prevMdContentRef.current) { + setCurrentValue(mdContent ?? ""); + prevMdContentRef.current = mdContent; + } + + return ( + + ) => { + const { value } = event.target; + + setCurrentValue(value); + setMdContent(value); + }} + value={currentValue} > - {Boolean(mdContent) ? ( - {mdContent} - ) : ( - {placeholder} - )} - - - - -); + + {Boolean(currentValue) ? ( + {currentValue} + ) : ( + {placeholder} + )} + + + + + ); +}; export default EditableMarkdownArea; diff --git a/airflow-core/src/airflow/ui/src/components/EditableMarkdownButton.tsx b/airflow-core/src/airflow/ui/src/components/EditableMarkdownButton.tsx index c4234895aaf24..3b07c7edc7b6c 100644 --- a/airflow-core/src/airflow/ui/src/components/EditableMarkdownButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/EditableMarkdownButton.tsx @@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next"; import { PiNoteBlankLight, PiNoteLight } from "react-icons/pi"; import { Button, Dialog } from "src/components/ui"; +import { ResizableWrapper, MARKDOWN_DIALOG_STORAGE_KEY } from "src/components/ui/ResizableWrapper"; import EditableMarkdownArea from "./EditableMarkdownArea"; import ActionButton from "./ui/ActionButton"; @@ -70,7 +71,7 @@ const EditableMarkdownButton = ({ /> {Boolean(mdContent?.trim()) && ( - - - {header} - - - - - - - - + + + + {header} + + + + + + + + + + + + + diff --git a/airflow-core/src/airflow/ui/src/components/ExpandCollapseButtons.tsx b/airflow-core/src/airflow/ui/src/components/ExpandCollapseButtons.tsx new file mode 100644 index 0000000000000..fac305a7624a0 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/ExpandCollapseButtons.tsx @@ -0,0 +1,60 @@ +/*! + * 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 { ButtonGroup, IconButton } from "@chakra-ui/react"; +import { MdCompress, MdExpand } from "react-icons/md"; + +type Props = { + readonly collapseLabel: string; + readonly expandLabel: string; + readonly isCollapseDisabled?: boolean; + readonly isExpandDisabled?: boolean; + readonly onCollapse: () => void; + readonly onExpand: () => void; +}; + +export const ExpandCollapseButtons = ({ + collapseLabel, + expandLabel, + isCollapseDisabled, + isExpandDisabled, + onCollapse, + onExpand, + ...rest +}: Props) => ( + + + + + + + + +); diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx new file mode 100644 index 0000000000000..849047dde072f --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx @@ -0,0 +1,175 @@ +/*! + * 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 { Button, HStack } from "@chakra-ui/react"; +import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { MdAdd, MdClear } from "react-icons/md"; +import { useDebouncedCallback } from "use-debounce"; + +import { Menu } from "src/components/ui"; + +import { getDefaultFilterIcon } from "./defaultIcons"; +import { DateFilter } from "./filters/DateFilter"; +import { NumberFilter } from "./filters/NumberFilter"; +import { SelectFilter } from "./filters/SelectFilter"; +import { TextSearchFilter } from "./filters/TextSearchFilter"; +import type { FilterBarProps, FilterConfig, FilterState, FilterValue } from "./types"; + +const defaultInitialValues: Record = {}; + +const getFilterIcon = (config: FilterConfig) => config.icon ?? getDefaultFilterIcon(config.type); + +export const FilterBar = ({ + configs, + initialValues = defaultInitialValues, + maxVisibleFilters = 10, + onFiltersChange, +}: FilterBarProps) => { + const { t: translate } = useTranslation(["admin", "common"]); + const [filters, setFilters] = useState>(() => + Object.entries(initialValues) + .filter(([, value]) => value !== null && value !== undefined && value !== "") + .map(([key, value]) => { + const config = configs.find((con) => con.key === key); + + if (!config) { + throw new Error(`Filter config not found for key: ${key}`); + } + + return { + config, + id: `${key}-${Date.now()}`, + value, + }; + }), + ); + + const debouncedOnFiltersChange = useDebouncedCallback((filtersRecord: Record) => { + onFiltersChange(filtersRecord); + }, 100); + + const updateFiltersRecord = useCallback( + (updatedFilters: Array) => { + const filtersRecord = updatedFilters.reduce>((accumulator, filter) => { + if (filter.value !== null && filter.value !== undefined && filter.value !== "") { + accumulator[filter.config.key] = filter.value; + } + + return accumulator; + }, {}); + + debouncedOnFiltersChange(filtersRecord); + }, + [debouncedOnFiltersChange], + ); + + const addFilter = (config: FilterConfig) => { + const newFilter: FilterState = { + config, + id: `${config.key}-${Date.now()}`, + value: config.defaultValue ?? "", + }; + + const updatedFilters = [...filters, newFilter]; + + setFilters(updatedFilters); + updateFiltersRecord(updatedFilters); + }; + + const updateFilter = (id: string, value: FilterValue) => { + const updatedFilters = filters.map((filter) => (filter.id === id ? { ...filter, value } : filter)); + + setFilters(updatedFilters); + updateFiltersRecord(updatedFilters); + }; + + const removeFilter = (id: string) => { + const updatedFilters = filters.filter((filter) => filter.id !== id); + + setFilters(updatedFilters); + updateFiltersRecord(updatedFilters); + }; + + const resetFilters = () => { + setFilters([]); + onFiltersChange({}); + }; + + const availableConfigs = configs.filter( + (config) => !filters.some((filter) => filter.config.key === config.key), + ); + + const renderFilter = (filter: FilterState) => { + const props = { + filter, + onChange: (value: FilterValue) => updateFilter(filter.id, value), + onRemove: () => removeFilter(filter.id), + }; + + switch (filter.config.type) { + case "date": + return ; + case "number": + return ; + case "select": + return ; + case "text": + return ; + default: + return undefined; + } + }; + + return ( + + {filters.slice(0, maxVisibleFilters).map(renderFilter)} + {availableConfigs.length > 0 && ( + + + + + + {availableConfigs.map((config) => ( + addFilter(config)} value={config.key}> + + {getFilterIcon(config)} + {config.label} + + + ))} + + + )} + {filters.length > 0 && ( + + )} + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx new file mode 100644 index 0000000000000..ef789617f203b --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx @@ -0,0 +1,161 @@ +/*! + * 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 { Box, HStack } from "@chakra-ui/react"; +import React from "react"; +import { useEffect, useRef, useState } from "react"; +import { MdClose } from "react-icons/md"; + +import { getDefaultFilterIcon } from "./defaultIcons"; +import type { FilterState, FilterValue } from "./types"; + +type FilterPillProps = { + readonly children: React.ReactNode; + readonly displayValue: React.ReactNode | string; + readonly filter: FilterState; + readonly hasValue: boolean; + readonly onChange: (value: FilterValue) => void; + readonly onRemove: () => void; +}; + +export const FilterPill = ({ + children, + displayValue, + filter, + hasValue, + onChange, + onRemove, +}: FilterPillProps) => { + const isEmpty = filter.value === null || filter.value === undefined || String(filter.value).trim() === ""; + const [isEditing, setIsEditing] = useState(isEmpty); + const inputRef = useRef(null); + const blurTimeoutRef = useRef(undefined); + + const handlePillClick = () => setIsEditing(true); + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" || event.key === "Escape") { + setIsEditing(false); + } + }; + + const handleBlur = () => { + blurTimeoutRef.current = setTimeout(() => setIsEditing(false), 150); + }; + + const handleFocus = () => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + blurTimeoutRef.current = undefined; + } + }; + + useEffect(() => { + if (isEditing && inputRef.current) { + const input = inputRef.current; + const focusInput = () => { + input.focus(); + try { + input.select(); + } catch { + // NumberInputField doesn't support select() + } + }; + + requestAnimationFrame(focusInput); + } + }, [isEditing]); + + useEffect( + () => () => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + } + }, + [], + ); + + const childrenWithProps = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + onBlur: handleBlur, + onChange, + onFocus: handleFocus, + onKeyDown: handleKeyDown, + ref: inputRef, + ...child.props, + } as Record); + } + + return child; + }); + + if (isEditing) { + return childrenWithProps; + } + + return ( + + + {filter.config.icon ?? getDefaultFilterIcon(filter.config.type)} + + {filter.config.label}: {displayValue} + + + { + event.stopPropagation(); + onRemove(); + }} + transition="all 0.2s" + w={6} + > + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx new file mode 100644 index 0000000000000..338b89422b789 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx @@ -0,0 +1,30 @@ +/*! + * 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 { MdCalendarToday, MdNumbers, MdTextFields, MdArrowDropDown } from "react-icons/md"; + +import type { FilterConfig } from "./types"; + +export const defaultFilterIcons = { + date: , + number: , + select: , + text: , +} as const; + +export const getDefaultFilterIcon = (type: FilterConfig["type"]) => defaultFilterIcons[type]; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx new file mode 100644 index 0000000000000..6bd7b02a8ddfd --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx @@ -0,0 +1,54 @@ +/*! + * 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 dayjs from "dayjs"; + +import { DateTimeInput } from "src/components/DateTimeInput"; + +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps } from "../types"; + +export const DateFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const hasValue = filter.value !== null && filter.value !== undefined && String(filter.value).trim() !== ""; + const displayValue = hasValue ? dayjs(String(filter.value)).format("MMM DD, YYYY, hh:mm A") : ""; + + const handleDateChange = (event: React.ChangeEvent) => { + const { value } = event.target; + + onChange(value || undefined); + }; + + return ( + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx new file mode 100644 index 0000000000000..d8a89895c0d3f --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx @@ -0,0 +1,112 @@ +/*! + * 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 { useState, useEffect, forwardRef } from "react"; + +import { NumberInputField, NumberInputRoot } from "src/components/ui/NumberInput"; + +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps } from "../types"; + +const NumberInputWithRef = forwardRef< + HTMLInputElement, + { + readonly max?: number; + readonly min?: number; + readonly onBlur?: () => void; + readonly onFocus?: () => void; + readonly onKeyDown?: (event: React.KeyboardEvent) => void; + readonly onValueChange: (details: { value: string }) => void; + readonly placeholder?: string; + readonly value: string; + } +>((props, ref) => { + const { max, min, onBlur, onFocus, onKeyDown, onValueChange, placeholder, value } = props; + + return ( + + + + ); +}); + +NumberInputWithRef.displayName = "NumberInputWithRef"; + +export const NumberFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const hasValue = filter.value !== null && filter.value !== undefined && filter.value !== ""; + + const [inputValue, setInputValue] = useState(filter.value?.toString() ?? ""); + + useEffect(() => { + setInputValue(filter.value?.toString() ?? ""); + }, [filter.value]); + + const handleValueChange = ({ value }: { value: string }) => { + setInputValue(value); + + if (value === "") { + onChange(undefined); + + return; + } + + // Allow user to input negative sign for negative number + if (value === "-") { + return; + } + + const parsedValue = Number(value); + + if (!isNaN(parsedValue)) { + onChange(parsedValue); + } + }; + + return ( + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx new file mode 100644 index 0000000000000..79374b27c23a4 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx @@ -0,0 +1,108 @@ +/*! + * 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 { createListCollection, Box, Text } from "@chakra-ui/react"; + +import { Select } from "src/components/ui"; + +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps, FilterConfig } from "../types"; + +type SelectOption = { + label: string; + value: string; +}; + +type SelectFilterConfig = { + options: Array; +}; + +export const SelectFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const config = filter.config as FilterConfig & SelectFilterConfig; + + const handleValueChange = ({ value }: { value: Array }) => { + const [newValue] = value; + + onChange(newValue); + + // Trigger blur to close the editing mode after selection + setTimeout(() => { + const activeElement = document.activeElement as HTMLElement; + + activeElement.blur(); + }, 0); + }; + + const hasValue = filter.value !== null && filter.value !== undefined && filter.value !== ""; + const displayValue = config.options.find((option) => option.value === String(filter.value))?.label; + + return ( + + + + {filter.config.label}: + + + + + + + {config.options.map((option) => ( + + {option.label} + + ))} + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx new file mode 100644 index 0000000000000..8cca8cf08532e --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx @@ -0,0 +1,63 @@ +/*! + * 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 { useRef } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { InputWithAddon } from "../../ui"; +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps } from "../types"; + +export const TextSearchFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const inputRef = useRef(null); + + const hasValue = filter.value !== null && filter.value !== undefined && String(filter.value).trim() !== ""; + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + + onChange(newValue || undefined); + }; + + useHotkeys( + "mod+k", + () => { + if (!filter.config.hotkeyDisabled) { + inputRef.current?.focus(); + } + }, + { enabled: !filter.config.hotkeyDisabled, preventDefault: true }, + ); + + return ( + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts b/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts new file mode 100644 index 0000000000000..fa3c5c7621da0 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts @@ -0,0 +1,25 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { FilterBar } from "./FilterBar"; +export { FilterPill } from "./FilterPill"; +export { defaultFilterIcons, getDefaultFilterIcon } from "./defaultIcons"; +export { DateFilter } from "./filters/DateFilter"; +export { NumberFilter } from "./filters/NumberFilter"; +export { TextSearchFilter } from "./filters/TextSearchFilter"; +export type * from "./types"; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts b/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts new file mode 100644 index 0000000000000..0f997c90671b6 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts @@ -0,0 +1,54 @@ +/*! + * 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 type React from "react"; + +export type FilterValue = Date | number | string | null | undefined; + +export type FilterConfig = { + readonly defaultValue?: FilterValue; + readonly hotkeyDisabled?: boolean; + readonly icon?: React.ReactNode; + readonly key: string; + readonly label: string; + readonly max?: number; + readonly min?: number; + readonly options?: Array<{ label: React.ReactNode | string; value: string }>; + readonly placeholder?: string; + readonly required?: boolean; + readonly type: "date" | "number" | "select" | "text"; +}; + +export type FilterState = { + readonly config: FilterConfig; + readonly id: string; + readonly value: FilterValue; +}; + +export type FilterBarProps = { + readonly configs: Array; + readonly initialValues?: Record; + readonly maxVisibleFilters?: number; + readonly onFiltersChange: (filters: Record) => void; +}; + +export type FilterPluginProps = { + readonly filter: FilterState; + readonly onChange: (value: FilterValue) => void; + readonly onRemove: () => void; +}; diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldBool.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldBool.tsx index f9a94253df1b6..6cced8977db11 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldBool.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldBool.tsx @@ -35,7 +35,7 @@ export const FieldBool = ({ name, namespace = "default" }: FlexibleFormElementPr return ( { return "array"; } + // Missing value, return 'null' as typeof(null) = 'dict' + if (param.value === null) { + return "null"; + } + return typeof param.value; }; @@ -61,7 +66,7 @@ const isFieldDate = (fieldType: string, fieldSchema: ParamSchema) => const isFieldDateTime = (fieldType: string, fieldSchema: ParamSchema) => fieldType === "string" && fieldSchema.format === "date-time"; -const enumTypes = ["string", "number", "integer"]; +const enumTypes = ["null", "string", "number", "integer"]; const isFieldDropdown = (fieldType: string, fieldSchema: ParamSchema) => enumTypes.includes(fieldType) && Array.isArray(fieldSchema.enum); @@ -81,16 +86,25 @@ const isFieldNumber = (fieldType: string) => { const isFieldObject = (fieldType: string) => fieldType === "object"; const isFieldStringArray = (fieldType: string, fieldSchema: ParamSchema) => - fieldType === "array" && - (!fieldSchema.items || fieldSchema.items.type === undefined || fieldSchema.items.type === "string"); + fieldType === "array" && (fieldSchema.items?.type === undefined || fieldSchema.items.type === "string"); const isFieldTime = (fieldType: string, fieldSchema: ParamSchema) => fieldType === "string" && fieldSchema.format === "time"; export const FieldSelector = ({ name, namespace = "default", onUpdate }: FlexibleFormElementProps) => { // FUTURE: Add support for other types as described in AIP-68 via Plugins - const { initialParamDict } = useParamStore(namespace); - const param = initialParamDict[name] ?? paramPlaceholder; + const { initialParamDict, paramsDict } = useParamStore(namespace); + + // Use current paramsDict (which has actual values) for type inference, fall back to initialParamDict for schema + const currentParam = paramsDict[name]; + const initialParam = initialParamDict[name] ?? paramPlaceholder; + + // Create a param object that combines the schema from initialParamDict with the value from paramsDict + const param: ParamSpec = { + ...initialParam, + value: currentParam?.value ?? initialParam.value, + }; + const fieldType = inferType(param); if (isFieldBool(fieldType)) { diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx index 07b7cbe629d64..ec140eb134bc6 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx @@ -108,7 +108,7 @@ export const FlexibleForm = ({ } }; - return Object.entries(params).some(([, param]) => typeof param.schema.section !== "string") ? ( + return Object.keys(params).length > 0 ? ( Object.entries(params).map(([, secParam]) => { const currentSection = secParam.schema.section ?? flexibleFormDefaultSection; diff --git a/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx b/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx index e1ff71d0add3b..2218e611984b7 100644 --- a/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx +++ b/airflow-core/src/airflow/ui/src/components/Graph/DownloadButton.tsx @@ -17,7 +17,7 @@ * under the License. */ import { IconButton } from "@chakra-ui/react"; -import { Panel, useReactFlow, getNodesBounds, getViewportForBounds } from "@xyflow/react"; +import { Panel, useReactFlow } from "@xyflow/react"; import { toPng } from "html-to-image"; import { useTranslation } from "react-i18next"; import { FiDownload } from "react-icons/fi"; @@ -26,24 +26,23 @@ import { toaster } from "src/components/ui"; export const DownloadButton = ({ name }: { readonly name: string }) => { const { t: translate } = useTranslation("components"); - const { getNodes, getZoom } = useReactFlow(); - const onClick = () => { - const nodesBounds = getNodesBounds(getNodes()); + const { fitView } = useReactFlow(); + + const onClick = async () => { + // Ensure the graph fits before taking screenshot + await fitView({ duration: 0, padding: 0.1 }); // Method obtained from https://reactflow.dev/examples/misc/download-image const container = document.querySelector(".react-flow__viewport"); if (container instanceof HTMLElement) { const dimensions = { height: container.clientHeight, width: container.clientWidth }; - const zoom = getZoom(); - const viewport = getViewportForBounds(nodesBounds, dimensions.width, dimensions.height, zoom, zoom, 2); toPng(container, { height: dimensions.height, style: { height: `${dimensions.height}px`, - transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, width: `${dimensions.width}px`, }, width: dimensions.width, @@ -69,7 +68,10 @@ export const DownloadButton = ({ name }: { readonly name: string }) => { { + void onClick(); + }} size="xs" title={translate("graph.downloadImage")} variant="ghost" diff --git a/airflow-core/src/airflow/ui/src/components/Graph/Edge.tsx b/airflow-core/src/airflow/ui/src/components/Graph/Edge.tsx index 83f4c84caa0da..a337572797da0 100644 --- a/airflow-core/src/airflow/ui/src/components/Graph/Edge.tsx +++ b/airflow-core/src/airflow/ui/src/components/Graph/Edge.tsx @@ -22,17 +22,12 @@ import { LinePath } from "@visx/shape"; import type { Edge as EdgeType } from "@xyflow/react"; import type { ElkPoint } from "elkjs"; -import { useColorMode } from "src/context/colorMode"; - import type { EdgeData } from "./reactflowUtils"; type Props = EdgeType; const CustomEdge = ({ data }: Props) => { - const { colorMode } = useColorMode(); - - // corresponds to the "border.inverted" semantic token - const [lightStroke, darkStroke] = useToken("colors", ["gray.800", "gray.200"]); + const [strokeColor, blueColor] = useToken("colors", ["border.inverted", "blue.500"]); if (data === undefined) { return undefined; @@ -65,9 +60,9 @@ const CustomEdge = ({ data }: Props) => { point.x} y={(point: ElkPoint) => point.y} /> diff --git a/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx b/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx index 5f84881418d90..2674d32a953a7 100644 --- a/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx +++ b/airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx @@ -75,7 +75,7 @@ export const TaskNode = ({ // Alternate background color for nested open groups bg={isOpen && depth !== undefined && depth % 2 === 0 ? "bg.muted" : "bg"} borderColor={ - taskInstance?.state ? `${taskInstance.state}.solid` : isSelected ? "border.inverted" : "border" + isSelected ? "blue.500" : taskInstance?.state ? `${taskInstance.state}.solid` : "border" } borderRadius={5} borderWidth={isSelected ? 4 : 2} @@ -116,7 +116,7 @@ export const TaskNode = ({ )} {isGroup ? ( + + + + + + {translate("limitedList.allItems", { count: items.length })} + + + + {interactive ? ( + + {items.map((item, index) => ( + + {item} + + ))} + + ) : ( + + {items.map((item, index) => ( + + {item} + + ))} + + )} + + + + {interactive + ? translate("limitedList.clickToInteract") + : translate("limitedList.copyPasteText")} + + + + ) ) : undefined} diff --git a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx index c91f05d4e95ba..370f10920de5a 100644 --- a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx +++ b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx @@ -62,7 +62,7 @@ const MarkRunAsDialog = ({ dagRun, onClose, open, state }: Props) => { )} diff --git a/airflow-core/src/airflow/ui/src/components/SearchDags/SearchDags.tsx b/airflow-core/src/airflow/ui/src/components/SearchDags/SearchDags.tsx index bb0e0f3765902..35f95f6415c54 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchDags/SearchDags.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchDags/SearchDags.tsx @@ -45,7 +45,7 @@ export const SearchDags = ({ const onSelect = (selected: SingleValue + {dagView === "grid" && ( { @@ -46,25 +46,14 @@ export const ToggleGroups = (props: ButtonGroupProps) => { const collapseLabel = translate("dag:taskGroups.collapseAll"); return ( - - - - - - - - + ); }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/AdminButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/AdminButton.tsx index 349a270e886ed..427d994c55aa7 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/AdminButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/AdminButton.tsx @@ -79,7 +79,7 @@ export const AdminButton = ({ return ( - } title={translate("nav.admin")} /> + {menuItems} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx index e24700e045172..986fa973bbc19 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx @@ -70,7 +70,7 @@ export const BrowseButton = ({ return ( - } title={translate("nav.browse")} /> + {menuItems} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/DocsButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/DocsButton.tsx index a33cc3724e9f3..3af4e7f93a636 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/DocsButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/DocsButton.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Link } from "@chakra-ui/react"; +import { Box, Icon, Link } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiBookOpen, FiExternalLink } from "react-icons/fi"; @@ -61,7 +61,7 @@ export const DocsButton = ({ return ( - } title={translate("nav.docs")} /> + {links @@ -73,17 +73,18 @@ export const DocsButton = ({ href={link.href} rel="noopener noreferrer" target="_blank" + textDecoration="none" > - {translate(`docs.${link.key}`)} - + {translate(`docs.${link.key}`)} + ))} {version === undefined ? undefined : ( - {version} - + {version} + )} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx index c309aeba0c03e..f270eb0f62d56 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx @@ -47,6 +47,16 @@ const LanguageSelector: React.FC = () => { + chakraStyles={{ + clearIndicator: (provided) => ({ + ...provided, + color: "fg.muted", + }), + control: (provided) => ({ + ...provided, + colorPalette: "input", + }), + }} onChange={handleLanguageChange} options={options} placeholder={translate("selectLanguage")} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/LogoutModal.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/LogoutModal.tsx index 10f98d73d41e8..6ad8226c22be1 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/LogoutModal.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/LogoutModal.tsx @@ -22,7 +22,6 @@ import { useTranslation } from "react-i18next"; import { ConfirmationModal } from "src/components/ConfirmationModal"; import { getRedirectPath } from "src/utils/links.ts"; -import { TOKEN_STORAGE_KEY } from "src/utils/tokenHandler"; type LogoutModalProps = { readonly isOpen: boolean; @@ -38,7 +37,6 @@ const LogoutModal: React.FC = ({ isOpen, onClose }) => { onConfirm={() => { const logoutPath = getRedirectPath("api/v2/auth/logout"); - localStorage.removeItem(TOKEN_STORAGE_KEY); globalThis.location.replace(logoutPath); }} onOpenChange={onClose} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx index b7d4edc2ca3aa..2203df73880c1 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx @@ -19,7 +19,7 @@ import { Box, Flex, VStack } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiDatabase, FiHome } from "react-icons/fi"; -import { NavLink } from "react-router-dom"; +import { Link } from "react-router-dom"; import { useAuthLinksServiceGetAuthMenus, @@ -136,31 +136,39 @@ export const Nav = () => { right: 0, }} alignItems="center" - bg="blue.muted" + bg="brand.muted" height="100%" justifyContent="space-between" position="fixed" - py={3} + py={1} top={0} - width={20} - zIndex={2} + width={16} + zIndex="docked" > - - - - - + + + + + - } title={translate("nav.home")} to="/" /> + } + icon={DagIcon} title={translate("nav.dags")} to="dags" /> } + icon={FiDatabase} title={translate("nav.assets")} to="assets" /> @@ -175,7 +183,7 @@ export const Nav = () => { - + > | IconType; readonly isExternal?: boolean; - readonly title?: string; + readonly pluginIcon?: ReactNode; + readonly title: string; readonly to?: string; } & ButtonProps; -export const NavButton = ({ icon, isExternal = false, title, to, ...rest }: NavButtonProps) => - to === undefined ? ( - - ) : isExternal ? ( - - - - ) : ( - - {({ isActive }: { readonly isActive: boolean }) => ( - - )} - + + ); + } + + return ( + ); +}; diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx index d28df1dff2342..5d36cf59e1053 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Link, Image, Menu } from "@chakra-ui/react"; +import { Link, Image, Menu, Icon, Box } from "@chakra-ui/react"; import { FiExternalLink } from "react-icons/fi"; import { LuPlug } from "react-icons/lu"; import { RiArchiveStackLine } from "react-icons/ri"; @@ -46,11 +46,11 @@ export const PluginMenuItem = ({ const displayIcon = colorMode === "dark" && typeof iconDarkMode === "string" ? iconDarkMode : icon; const pluginIcon = typeof displayIcon === "string" ? ( - + ) : urlRoute === "legacy-fab-views" ? ( - + ) : ( - + ); const isExternal = urlRoute === undefined || urlRoute === null; @@ -58,9 +58,10 @@ export const PluginMenuItem = ({ if (topLevel) { return ( @@ -80,13 +81,15 @@ export const PluginMenuItem = ({ width="100%" > {pluginIcon} - {name} - + {name} + ) : ( {pluginIcon} - {name} + + {name} + )} diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx index 2dfbf58ac3d4e..9ac31f6947fed 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box } from "@chakra-ui/react"; +import { Box, Icon } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiChevronRight } from "react-icons/fi"; import { LuPlug } from "react-icons/lu"; @@ -53,7 +53,7 @@ export const PluginMenus = ({ navItems }: { readonly navItems: Array= 2 ? ( - } title={translate("nav.plugins")} /> + {buttons.map((navItem) => ( @@ -63,7 +63,7 @@ export const PluginMenus = ({ navItems }: { readonly navItems: Array {key} - + {menuButtons.map((navItem) => ( diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/SecurityButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/SecurityButton.tsx index 38f67ddd9f631..2955abce5ee30 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/SecurityButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/SecurityButton.tsx @@ -36,7 +36,7 @@ export const SecurityButton = () => { return ( - } title={translate("nav.security")} /> + {authLinks.extra_menu_items.map(({ text }) => { diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneMenuItem.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneMenuItem.tsx index 360560ca199f9..42508f0316e05 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneMenuItem.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneMenuItem.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { Box, Icon } from "@chakra-ui/react"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; @@ -48,8 +49,10 @@ export const TimezoneMenuItem = ({ onOpen }: { readonly onOpen: () => void }) => return ( - - {translate("timezone")}: {dayjs(time).tz(selectedTimezone).format("HH:mm z (Z)")} + + + {translate("timezone")}: {dayjs(time).tz(selectedTimezone).format("HH:mm z (Z)")} + ); }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx index 7a458f668c3a2..1ab91744cb324 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx @@ -25,6 +25,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useTimezone } from "src/context/timezone"; +import { DEFAULT_DATETIME_FORMAT } from "src/utils/datetimeUtils"; import type { Option as TimezoneOption } from "src/utils/option"; dayjs.extend(utc); @@ -60,7 +61,7 @@ const TimezoneSelector: React.FC = () => { useEffect(() => { const updateTime = () => { - setCurrentTime(dayjs().tz(selectedTimezone).format("YYYY-MM-DD HH:mm:ss")); + setCurrentTime(dayjs().tz(selectedTimezone).format(DEFAULT_DATETIME_FORMAT)); }; updateTime(); diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx index a2b99016d9179..84d8c385fed87 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { useDisclosure } from "@chakra-ui/react"; +import { Box, Icon, useDisclosure } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiGrid, @@ -27,6 +27,7 @@ import { FiGlobe, FiEye, FiChevronRight, + FiChevronLeft, FiMonitor, } from "react-icons/fi"; import { MdOutlineAccountTree } from "react-icons/md"; @@ -52,82 +53,92 @@ const COLOR_MODES = { type ColorMode = (typeof COLOR_MODES)[keyof typeof COLOR_MODES]; export const UserSettingsButton = ({ externalViews }: { readonly externalViews: Array }) => { - const { t: translate } = useTranslation(); + const { i18n, t: translate } = useTranslation(); const { selectedTheme, setColorMode } = useColorMode(); + + const colorModeOptions = [ + { + icon: FiSun, + label: translate("appearance.lightMode"), + value: COLOR_MODES.LIGHT, + }, + { + icon: FiMoon, + label: translate("appearance.darkMode"), + value: COLOR_MODES.DARK, + }, + { + icon: FiMonitor, + label: translate("appearance.systemMode"), + value: COLOR_MODES.SYSTEM, + }, + ]; + const { onClose: onCloseTimezone, onOpen: onOpenTimezone, open: isOpenTimezone } = useDisclosure(); const { onClose: onCloseLogout, onOpen: onOpenLogout, open: isOpenLogout } = useDisclosure(); const { onClose: onCloseLanguage, onOpen: onOpenLanguage, open: isOpenLanguage } = useDisclosure(); + const [dagView, setDagView] = useLocalStorage<"graph" | "grid">("default_dag_view", "grid"); const theme = selectedTheme ?? COLOR_MODES.SYSTEM; + const isRTL = i18n.dir() === "rtl"; + return ( - - - } title={translate("user")} /> - - - - - {translate("selectLanguage")} - - - - - {translate("appearance.appearance")} - - - - setColorMode(element.value as ColorMode)} - value={theme} - > - - - {translate("appearance.lightMode")} - - - - - {translate("appearance.darkMode")} - - - - - {translate("appearance.systemMode")} - - - - - - (dagView === "grid" ? setDagView("graph") : setDagView("grid"))} - value={dagView} - > - {dagView === "grid" ? ( - <> - - {translate("defaultToGraphView")} - - ) : ( - <> - - {translate("defaultToGridView")} - - )} - - - {externalViews.map((view) => ( - - ))} - - - {translate("logout")} - - + <> + + + + + + + + {translate("selectLanguage")} + + + + + {translate("appearance.appearance")} + + + + setColorMode(element.value as ColorMode)} + value={theme} + > + {colorModeOptions.map(({ icon, label, value }) => ( + + + {label} + + + ))} + + + + (dagView === "grid" ? setDagView("graph") : setDagView("grid"))} + value={dagView} + > + + + {dagView === "grid" ? translate("defaultToGraphView") : translate("defaultToGridView")} + + + + {externalViews.map((view) => ( + + ))} + + + + {translate("logout")} + + + - + ); }; diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx index 41f5d1bd0f6b1..a98de89b4868e 100644 --- a/airflow-core/src/airflow/ui/src/main.tsx +++ b/airflow-core/src/airflow/ui/src/main.tsx @@ -25,6 +25,7 @@ import * as ReactDOM from "react-dom"; import { createRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; import { RouterProvider } from "react-router-dom"; +import * as ReactRouterDOM from "react-router-dom"; import * as ReactJSXRuntime from "react/jsx-runtime"; import type { HTTPExceptionResponse } from "openapi/requests/types.gen"; @@ -36,7 +37,6 @@ import { getRedirectPath } from "src/utils/links.ts"; import i18n from "./i18n/config"; import { client } from "./queryClient"; import { system } from "./theme"; -import { clearToken, tokenHandler } from "./utils/tokenHandler"; // Set React, ReactDOM, and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins. // Only one instance of React should be used. @@ -44,6 +44,7 @@ import { clearToken, tokenHandler } from "./utils/tokenHandler"; Reflect.set(globalThis, "React", React); Reflect.set(globalThis, "ReactDOM", ReactDOM); Reflect.set(globalThis, "ReactJSXRuntime", ReactJSXRuntime); +Reflect.set(globalThis, "ReactRouterDOM", ReactRouterDOM); // redirect to login page if the API responds with unauthorized or forbidden errors axios.interceptors.response.use( @@ -53,7 +54,6 @@ axios.interceptors.response.use( error.response?.status === 401 || (error.response?.status === 403 && error.response.data.detail === "Invalid JWT token") ) { - clearToken(); const params = new URLSearchParams(); params.set("next", globalThis.location.href); @@ -66,21 +66,10 @@ axios.interceptors.response.use( }, ); -axios.interceptors.request.use(tokenHandler); - -const html = document.documentElement; -const updateHtml = (lng: string) => { - html.setAttribute("dir", i18n.dir(lng)); - html.setAttribute("lang", lng); -}; - -updateHtml(i18n.language); -i18n.on("languageChanged", updateHtml); - createRoot(document.querySelector("#root") as HTMLDivElement).render( - + diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts index 35ba89a46ce7e..9b9a6a205f347 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts @@ -32,25 +32,19 @@ export const handlers: Array = [ fileloc: "/airflow/dags/tutorial_taskflow_api.py", has_import_errors: false, has_task_concurrency_limits: false, + is_favorite: true, is_paused: false, is_stale: false, last_parsed_time: "2025-01-13T07:34:01.593459Z", latest_dag_runs: [ { - conf: {}, dag_id: "tutorial_taskflow_api", - dag_run_id: "manual__2025-01-13T04:33:58.387988+00:00", - data_interval_end: "2025-01-13T04:33:58.396323Z", - data_interval_start: "2025-01-13T04:33:58.396323Z", end_date: "2025-01-13T04:34:12.143831Z", - external_trigger: true, - last_scheduling_decision: "2025-01-13T04:34:12.137382Z", + id: 1, logical_date: "2025-01-13T04:33:58.396323Z", - queued_at: "2025-01-13T04:33:58.404628Z", - run_type: "manual", + run_id: "manual__2025-01-13T04:33:58.387988+00:00", start_date: "2025-01-13T04:33:58.496197Z", state: "success", - triggered_by: "rest_api", }, ], max_active_runs: 16, @@ -69,25 +63,19 @@ export const handlers: Array = [ fileloc: "/airflow/dags/tutorial_taskflow_api_failed.py", has_import_errors: false, has_task_concurrency_limits: false, + is_favorite: false, is_paused: false, is_stale: false, last_parsed_time: "2025-01-13T07:34:01.593459Z", latest_dag_runs: [ { - conf: {}, dag_id: "tutorial_taskflow_api", - dag_run_id: "manual__2025-01-13T04:33:58.387988+00:00", - data_interval_end: "2025-01-13T04:33:58.396323Z", - data_interval_start: "2025-01-13T04:33:58.396323Z", end_date: "2025-01-13T04:34:12.143831Z", - external_trigger: true, - last_scheduling_decision: "2025-01-13T04:34:12.137382Z", + id: 2, logical_date: "2025-01-13T04:33:58.396323Z", - queued_at: "2025-01-13T04:33:58.404628Z", - run_type: "manual", + run_id: "manual__2025-01-13T04:33:58.387988+00:00", start_date: "2025-01-13T04:33:58.496197Z", state: "success", - triggered_by: "rest_api", }, ], max_active_runs: 16, @@ -128,6 +116,7 @@ export const handlers: Array = [ fileloc: "/airflow/dags/tutorial_taskflow_api_failed.py", has_import_errors: false, has_task_concurrency_limits: false, + is_favorite: false, is_paused: false, is_stale: false, last_expired: null, @@ -155,6 +144,7 @@ export const handlers: Array = [ fileloc: "/airflow/dags/tutorial_taskflow_api_success.py", has_import_errors: false, has_task_concurrency_limits: false, + is_favorite: true, is_paused: false, is_stale: false, last_expired: null, diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/log.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/log.ts index ec34c9dffd315..b9ed6b734480b 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/log.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/log.ts @@ -17,53 +17,55 @@ * under the License. */ -/* eslint-disable unicorn/no-null */ +/* eslint-disable unicorn/no-null,max-lines */ import { http, HttpResponse, type HttpHandler } from "msw"; +const ti = { + dag_id: "log_grouping", + dag_run_id: "", + dag_version: { + bundle_name: "dags-folder", + bundle_url: null, + bundle_version: null, + created_at: "2025-02-18T12:06:45.723238Z", + dag_id: "log_grouping", + id: "019518f4-1adb-7223-a917-45fe08b78947", + version_number: 1, + }, + duration: 0.203_977, + end_date: "2025-02-18T12:19:56.467235Z", + executor: null, + executor_config: "{}", + hostname: "laptop", + id: "01951900-16f6-7c1c-ae66-91bdfe9e0cfd", + logical_date: null, + map_index: -1, + max_tries: 0, + note: null, + operator: "_PythonDecoratedOperator", + pid: 20_703, + pool: "default_pool", + pool_slots: 1, + priority_weight: 1, + queue: "default", + queued_when: "2025-02-18T12:19:52.311873Z", + rendered_fields: { op_args: "()", op_kwargs: {}, templates_dict: null }, + rendered_map_index: null, + run_after: "2025-02-18T12:19:51.120210Z", + scheduled_when: "2025-02-18T12:19:52.289327Z", + start_date: "2025-02-18T12:19:56.263258Z", + state: "success", + task_display_name: "generate", + task_id: "generate", + trigger: null, + triggerer_job: null, + try_number: 1, + unixname: "testname", +}; + export const handlers: Array = [ http.get("/api/v2/dags/log_grouping/dagRuns/manual__2025-02-18T12:19/taskInstances/generate/-1", () => - HttpResponse.json({ - dag_id: "log_grouping", - dag_run_id: "manual__2025-02-18T12:19", - dag_version: { - bundle_name: "dags-folder", - bundle_url: null, - bundle_version: null, - created_at: "2025-02-18T12:06:45.723238Z", - dag_id: "log_grouping", - id: "019518f4-1adb-7223-a917-45fe08b78947", - version_number: 1, - }, - duration: 0.203_977, - end_date: "2025-02-18T12:19:56.467235Z", - executor: null, - executor_config: "{}", - hostname: "laptop", - id: "01951900-16f6-7c1c-ae66-91bdfe9e0cfd", - logical_date: null, - map_index: -1, - max_tries: 0, - note: null, - operator: "_PythonDecoratedOperator", - pid: 20_703, - pool: "default_pool", - pool_slots: 1, - priority_weight: 1, - queue: "default", - queued_when: "2025-02-18T12:19:52.311873Z", - rendered_fields: { op_args: "()", op_kwargs: {}, templates_dict: null }, - rendered_map_index: null, - run_after: "2025-02-18T12:19:51.120210Z", - scheduled_when: "2025-02-18T12:19:52.289327Z", - start_date: "2025-02-18T12:19:56.263258Z", - state: "success", - task_display_name: "generate", - task_id: "generate", - trigger: null, - triggerer_job: null, - try_number: 1, - unixname: "testname", - }), + HttpResponse.json({ ...ti, dag_run_id: "manual__2025-02-18T12:19" }), ), http.get("/api/v2/dags/log_grouping/dagRuns/manual__2025-02-18T12:19/taskInstances/generate/logs/1", () => HttpResponse.json({ @@ -191,4 +193,104 @@ export const handlers: Array = [ continuation_token: null, }), ), + http.get("/api/v2/dags/log_grouping/dagRuns/manual__2025-02-18T12:19/taskInstances/log_source/-1", () => + HttpResponse.json({ + ...ti, + dag_run_id: "manual__2025-02-18T12:19", + task_display_name: "log_source", + task_id: "log_source", + }), + ), + http.get("/api/v2/dags/log_grouping/dagRuns/manual__2025-02-18T12:19/taskInstances/log_source/logs/1", () => + HttpResponse.json({ + content: [ + { + event: "::group::Log message source details", + sources: [ + "/root/airflow/logs/dag_id=log_grouping/run_id=manual__2025-02-18T12:19/task_id=log_source/attempt=1.log", + ], + timestamp: null, + }, + { event: "::endgroup::", timestamp: null }, + { + event: "DAG bundles loaded: dags-folder, example_dags", + filename: "manager.py", + level: "info", + lineno: 179, + logger: "airflow.dag_processing.bundles.manager.DagBundlesManager", + timestamp: "2025-09-11T17:44:52.567095Z", + }, + { + event: + "Filling up the DagBag from /opt/airflow/airflow-core/src/airflow/example_dags/standard/example_python_decorator.py", + filename: "dagbag.py", + level: "info", + lineno: 593, + logger: "airflow.models.dagbag.DagBag", + timestamp: "2025-09-11T17:44:52.567407Z", + }, + { + event: "Task instance is in running state", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.597476Z", + }, + { + event: " Previous state of the Task instance: TaskInstanceState.QUEUED", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.597589Z", + }, + { + event: "Current task name:log_sql_query", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.597626Z", + }, + { + event: "Dag name:example_python_decorator", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.597657Z", + }, + { + event: + "Python task decorator query: CREATE TABLE Orders (\n order_id INT PRIMARY KEY,\n name TEXT,\n description TEXT\n)", + filename: "example_python_decorator.py", + level: "info", + lineno: 60, + logger: "unusual_prefix_7bb6f64024e819254594fca018f7123719f205f5_example_python_decorator", + timestamp: "2025-09-11T17:44:52.598196Z", + }, + { + event: "Done. Returned value was: None", + filename: "python.py", + level: "info", + lineno: 218, + logger: + "airflow.task.operators.airflow.providers.standard.decorators.python._PythonDecoratedOperator", + timestamp: "2025-09-11T17:44:52.598300Z", + }, + { + event: "Task instance in success state", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.607859Z", + }, + { + event: " Previous state of the Task instance: TaskInstanceState.RUNNING", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.607929Z", + }, + { + event: "Task operator:", + level: "info", + logger: "task.stdout", + timestamp: "2025-09-11T17:44:52.607985Z", + }, + ], + continuation_token: null, + }), + ), ]; diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx index 370a77a8f5408..f00c00b85023d 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx @@ -28,6 +28,7 @@ import type { CustomNodeProps } from "src/components/Graph/reactflowUtils"; import { useGraphLayout } from "src/components/Graph/useGraphLayout"; import { useColorMode } from "src/context/colorMode"; import { useDependencyGraph } from "src/queries/useDependencyGraph"; +import { getReactFlowThemeStyle } from "src/theme"; export const AssetGraph = ({ asset }: { readonly asset?: AssetResponse }) => { const { assetId } = useParams(); @@ -45,7 +46,7 @@ export const AssetGraph = ({ asset }: { readonly asset?: AssetResponse }) => { node.id === `asset:${assetId}` ? { ...node, data: { ...node.data, isSelected: true } } : node, ); - const [selectedDarkColor, selectedLightColor] = useToken("colors", ["gray.200", "gray.800"]); + const [selectedDarkColor, selectedLightColor] = useToken("colors", ["bg.muted", "bg.emphasized"]); const selectedColor = colorMode === "dark" ? selectedDarkColor : selectedLightColor; @@ -74,6 +75,7 @@ export const AssetGraph = ({ asset }: { readonly asset?: AssetResponse }) => { nodesDraggable={false} nodeTypes={nodeTypes} onlyRenderVisibleElements + style={getReactFlowThemeStyle(colorMode)} > diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx index 51a1442fcc90b..e499afda799bf 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx @@ -22,12 +22,14 @@ import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels"; import { useParams } from "react-router-dom"; +import { useSearchParams } from "react-router-dom"; import { useAssetServiceGetAsset, useAssetServiceGetAssetEvents } from "openapi/queries"; import { AssetEvents } from "src/components/Assets/AssetEvents"; import { BreadcrumbStats } from "src/components/BreadcrumbStats"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ProgressBar } from "src/components/ui"; +import { SearchParamsKeys } from "src/constants/searchParams"; import { AssetGraph } from "./AssetGraph"; import { CreateAssetEvent } from "./CreateAssetEvent"; @@ -59,12 +61,18 @@ export const AssetLayout = () => { }, ]; + const { DAG_ID, END_DATE, START_DATE, TASK_ID } = SearchParamsKeys; + const [searchParams] = useSearchParams(); const { data, isLoading: isLoadingEvents } = useAssetServiceGetAssetEvents( { assetId: asset?.id, limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize, orderBy, + sourceDagId: searchParams.get(DAG_ID) ?? undefined, + sourceTaskId: searchParams.get(TASK_ID) ?? undefined, + timestampGte: searchParams.get(START_DATE) ?? undefined, + timestampLte: searchParams.get(END_DATE) ?? undefined, }, undefined, { enabled: Boolean(asset?.id) }, @@ -127,6 +135,7 @@ export const AssetLayout = () => { isLoading={isLoadingEvents} setOrderBy={setOrderBy} setTableUrlState={setTableURLState} + showFilters={true} tableUrlState={tableURLState} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx index 10b00c9310acb..ecd5f5c67ba98 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx @@ -39,7 +39,7 @@ export const CreateAssetEvent = ({ asset, withText = true }: Props) => { } onClick={onOpen} diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx index 7f5f212469d86..500c82ded1c78 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx @@ -206,7 +206,7 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { ) : undefined} {eventType === "materialize" && dag?.is_paused ? ( - setUnpause(!unpause)}> + setUnpause(!unpause)}> {translate("createEvent.materialize.unpauseDag", { dagName: dag.dag_display_name })} ) : undefined} @@ -214,7 +214,7 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { diff --git a/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx b/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx index f194da7011375..ce83091f579fb 100644 --- a/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx @@ -68,7 +68,7 @@ export const Configs = () => { {translate("config.title")} {error === null ? ( - + ) : ( )} diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx index 2b437260e7248..5ae35a4101ffa 100644 --- a/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx @@ -48,7 +48,7 @@ const AddConnectionButton = () => { } onClick={onOpen} text={translate("connections.add")} diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx index d412fb12bc77f..dbecdf2ecf745 100644 --- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx @@ -59,7 +59,7 @@ const ConnectionForm = ({ const { conf: extra, setConf } = useParamStore(); const { control, - formState: { isValid }, + formState: { isDirty, isValid }, handleSubmit, reset, watch, @@ -96,6 +96,19 @@ const ConnectionForm = ({ mutateConnection(data); }; + // Check if extra fields have changed by comparing with initial connection + const isExtraFieldsDirty = (() => { + try { + const initialParsed = JSON.parse(initialConnection.extra) as Record; + const currentParsed = JSON.parse(extra) as Record; + + return JSON.stringify(initialParsed) !== JSON.stringify(currentParsed); + } catch { + // If parsing fails, fall back to string comparison + return extra !== initialConnection.extra; + } + })(); + const validateAndPrettifyJson = (value: string) => { try { if (value.trim() === "") { @@ -250,8 +263,10 @@ const ConnectionForm = ({ @@ -60,7 +66,7 @@ const DeleteConnectionsButton = ({ clearSelections, deleteKeys: connectionIds }: - + {translate("connections.delete.firstConfirmMessage", { count: connectionIds.length })}
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx index 62bfdaf4b3247..2e28f54949eb6 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx @@ -18,6 +18,7 @@ */ import { Box, Heading, Text } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; @@ -117,6 +118,8 @@ export const Backfills = () => { offset: pagination.pageIndex * pagination.pageSize, }); + const columns = useMemo(() => getColumns(translate), [translate]); + return ( @@ -124,11 +127,11 @@ export const Backfills = () => { {translate("backfill", { count: data ? data.total_entries : 0 })} diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx index 8bf4b8b590536..634ff609e35e7 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx @@ -183,14 +183,14 @@ export const Calendar = () => { + + + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx index 61f467630b444..ec21d60510905 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx @@ -16,44 +16,49 @@ * specific language governing permissions and limitations * under the License. */ -import { createListCollection, type SelectValueChangeDetails } from "@chakra-ui/react"; +import { Button, ButtonGroup } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; -import { Select } from "src/components/ui"; - type Props = { readonly defaultShowPaused: string; - readonly onPausedChange: (details: SelectValueChangeDetails) => void; + readonly onPausedChange: React.MouseEventHandler; readonly showPaused: string | null; }; export const PausedFilter = ({ defaultShowPaused, onPausedChange, showPaused }: Props) => { const { t: translate } = useTranslation("dags"); - const enabledOptions = createListCollection({ - items: [ - { label: translate("filters.paused.all"), value: "all" }, - { label: translate("filters.paused.active"), value: "false" }, - { label: translate("filters.paused.paused"), value: "true" }, - ], - }); + const currentValue = showPaused ?? defaultShowPaused; return ( - - - - - - {enabledOptions.items.map((option) => ( - - {option.label} - - ))} - - + + + + + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx new file mode 100644 index 0000000000000..debab07a25bb7 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx @@ -0,0 +1,47 @@ +/*! + * 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 { Button } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { LuUserRoundPen } from "react-icons/lu"; + +import { StateBadge } from "src/components/StateBadge"; + +type Props = { + readonly needsReview: boolean; + readonly onToggle: () => void; +}; + +export const RequiredActionFilter = ({ needsReview, onToggle }: Props) => { + const { t: translate } = useTranslation("hitl"); + + return ( + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx index 3bda2ba98feb2..0533de4dc3a45 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx @@ -16,11 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { HStack } from "@chakra-ui/react"; +import { Button, ButtonGroup } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { LuUserRoundPen } from "react-icons/lu"; -import { QuickFilterButton } from "src/components/QuickFilterButton"; import { StateBadge } from "src/components/StateBadge"; type Props = { @@ -45,57 +44,73 @@ export const StateFilters = ({ const { t: translate } = useTranslation(["dags", "common", "hitl"]); return ( - - + + + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx index c53429452f7cf..f40d5829e3237 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Field, HStack, Text } from "@chakra-ui/react"; +import { Box, Field, HStack, Text } from "@chakra-ui/react"; import { Select as ReactSelect, type MultiValue } from "chakra-react-select"; import { useTranslation } from "react-i18next"; @@ -46,7 +46,7 @@ export const TagFilter = ({ const { t: translate } = useTranslation("common"); return ( - <> + ({ ...provided, + maxWidth: 300, minWidth: 64, }), control: (provided) => ({ ...provided, - colorPalette: "blue", + colorPalette: "brand", }), menu: (provided) => ({ ...provided, @@ -105,6 +106,6 @@ export const TagFilter = ({
)} - +
); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx index 39608f4a7452a..1f840cc5d40af 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx @@ -26,12 +26,12 @@ describe("Dag Filters", () => { it("Filter by selected last run state", async () => { render(); - await waitFor(() => expect(screen.getByTestId("dags-success-filter")).toBeInTheDocument()); - await waitFor(() => screen.getByTestId("dags-success-filter").click()); + await waitFor(() => expect(screen.getByText("states.success")).toBeInTheDocument()); + await waitFor(() => screen.getByText("states.success").click()); await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); - await waitFor(() => expect(screen.getByTestId("dags-failed-filter")).toBeInTheDocument()); - await waitFor(() => screen.getByTestId("dags-failed-filter").click()); + await waitFor(() => expect(screen.getByText("states.failed")).toBeInTheDocument()); + await waitFor(() => screen.getByText("states.failed").click()); await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument()); }); }); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx index 7c3aca6d4ad7b..748cf5772bba7 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx @@ -38,7 +38,6 @@ import DeleteDagButton from "src/components/DagActions/DeleteDagButton"; import { FavoriteDagButton } from "src/components/DagActions/FavoriteDagButton"; import DagRunInfo from "src/components/DagRunInfo"; import { DataTable } from "src/components/DataTable"; -import { ToggleTableDisplay } from "src/components/DataTable/ToggleTableDisplay"; import type { CardDef } from "src/components/DataTable/types"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; @@ -119,7 +118,7 @@ const createColumns = ( cell: ({ row: { original } }) => original.latest_dag_runs[0] ? ( - + , + accessorKey: "favourite", + cell: ({ row: { original } }) => ( + + ), enableHiding: false, enableSorting: false, header: "", @@ -230,6 +231,7 @@ export const DagsList = () => { } else { searchParams.delete(NAME_PATTERN); } + searchParams.delete("offset"); setSearchParams(searchParams); }; @@ -285,6 +287,8 @@ export const DagsList = () => { [pagination, setTableURLState], ); + const totalEntries = data?.total_entries ?? 0; + return ( @@ -298,7 +302,7 @@ export const DagsList = () => { - {`${data?.total_entries ?? 0} ${translate("dag", { count: data?.total_entries ?? 0 })}`} + {`${totalEntries} ${translate("dag", { count: totalEntries })}`} @@ -307,8 +311,7 @@ export const DagsList = () => { ) : undefined} - - + { errorMessage={} initialState={tableURLState} isLoading={isLoading} - modelName={translate("dag_one")} + modelName="common:dag" + onDisplayToggleChange={setDisplay} onStateChange={setTableURLState} + showDisplayToggle + showRowCountHeading={false} skeletonCount={display === "card" ? 5 : undefined} - total={data?.total_entries ?? 0} + total={totalEntries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx index 3d98c30dccd1c..1bcd6eb663b49 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx @@ -23,9 +23,10 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen"; +import { StateIcon } from "src/components/StateIcon"; import Time from "src/components/Time"; import { Tooltip } from "src/components/ui"; -import { getDuration } from "src/utils"; +import { renderDuration } from "src/utils"; dayjs.extend(duration); @@ -42,19 +43,14 @@ export const RecentRuns = ({ return undefined; } - const runsWithDuration = latestRuns.map((run) => ({ - ...run, - duration: dayjs.duration(dayjs(run.end_date).diff(run.start_date)).asSeconds(), - })); - const max = Math.max.apply( undefined, - runsWithDuration.map((run) => run.duration), + latestRuns.map((run) => run.duration ?? 0), ); return ( - - {runsWithDuration.map((run) => ( + + {latestRuns.map((run) => ( @@ -75,11 +71,11 @@ export const RecentRuns = ({ )} - {translate("duration")}: {getDuration(run.start_date, run.end_date)} + {translate("duration")}: {renderDuration(run.duration)}
} - key={run.dag_run_id} + key={run.run_id} positioning={{ offset: { crossAxis: 5, @@ -88,16 +84,20 @@ export const RecentRuns = ({ placement: "bottom-start", }} > - - - - + + + + ))} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx index 969f1bd79c74a..eb4fe29debd84 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx @@ -71,7 +71,9 @@ export const Dashboard = () => {
) : undefined} - {typeof instanceName === "string" && instanceName !== "" ? instanceName : translate("welcome")} + {typeof instanceName === "string" && instanceName !== "" && instanceName !== "Airflow" + ? instanceName + : translate("welcome")} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDagCard.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDagCard.tsx index 2bbf4de24f316..b085dcdd4ccd4 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDagCard.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDagCard.tsx @@ -20,13 +20,13 @@ import { Box, Text, VStack } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import type { DAGRunResponse } from "openapi/requests/types.gen"; +import type { DAGRunLightResponse } from "openapi/requests/types.gen"; import { RecentRuns } from "src/pages/DagsList/RecentRuns"; type FavoriteDagProps = { readonly dagId: string; readonly dagName: string; - readonly latestRuns: Array; + readonly latestRuns: Array; }; export const FavoriteDagCard = ({ dagId, dagName, latestRuns }: FavoriteDagProps) => { @@ -38,18 +38,17 @@ export const FavoriteDagCard = ({ dagId, dagName, latestRuns }: FavoriteDagProps borderWidth="1px" display="flex" flexDirection="column" - height="100%" justifyContent="center" + maxWidth="200px" overflow="hidden" px={4} py={3} - width="100%" > - + {latestRuns.length > 0 ? ( ) : ( - + {translate("favorite.noDagRuns")} )} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDags.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDags.tsx index 2f54d45056680..4451cd88b9676 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDags.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/FavoriteDags/FavoriteDags.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex, Heading, SimpleGrid, Text } from "@chakra-ui/react"; +import { Box, Flex, Heading, Text } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiStar } from "react-icons/fi"; @@ -43,11 +43,11 @@ export const FavoriteDags = () => { {favorites.dags.length === 0 ? ( - + {translate("favorite.noFavoriteDags")} ) : ( - + {favorites.dags.map((dag) => ( { latestRuns={dag.latest_dag_runs} /> ))} - + )} ); diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx index cb8d87f482b99..054ceed5505f4 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Health/Health.tsx @@ -27,7 +27,7 @@ import { useAutoRefresh } from "src/utils"; import { HealthBadge } from "./HealthBadge"; export const Health = () => { - const refetchInterval = useAutoRefresh({}); + const refetchInterval = useAutoRefresh({ checkPendingRuns: true }); const { data, error, isLoading } = useMonitorServiceGetHealth(undefined, { refetchInterval, diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx index 6c65dffbacc15..8eb824b60f603 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx @@ -45,7 +45,7 @@ export const DagRunMetrics = ({ dagRunStates, endDate, startDate, total }: DagRu - + {total} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx index 2ccae75e91966..bd60f278edc43 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx @@ -41,7 +41,7 @@ export const HistoricalMetrics = () => { const [endDate, setEndDate] = useState(now.toISOString()); const [assetSortBy, setAssetSortBy] = useState("-timestamp"); - const refetchInterval = useAutoRefresh({}); + const refetchInterval = useAutoRefresh({ checkPendingRuns: true }); const { data, error, isLoading } = useDashboardServiceHistoricalMetrics( { @@ -85,8 +85,8 @@ export const HistoricalMetrics = () => { setStartDate={setStartDate} startDate={startDate} /> - - + + {isLoading ? : undefined} {!isLoading && data !== undefined && ( @@ -99,7 +99,7 @@ export const HistoricalMetrics = () => { )} - + - + {total} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx index 8446542e40270..9fadd5bab2bde 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx @@ -21,21 +21,35 @@ import { useTranslation } from "react-i18next"; import { BiTargetLock } from "react-icons/bi"; import { Link as RouterLink } from "react-router-dom"; -import { useAuthLinksServiceGetAuthMenus } from "openapi/queries"; +import { type PoolServiceGetPoolsDefaultResponse, useAuthLinksServiceGetAuthMenus } from "openapi/queries"; import { usePoolServiceGetPools } from "openapi/queries/queries"; +import type { ApiError } from "openapi/requests"; import { PoolBar } from "src/components/PoolBar"; import { useAutoRefresh } from "src/utils"; import { type Slots, slotKeys } from "src/utils/slots"; export const PoolSummary = () => { const { t: translate } = useTranslation("dashboard"); - const refetchInterval = useAutoRefresh({}); - const { data, isLoading } = usePoolServiceGetPools(undefined, undefined, { - refetchInterval, - }); + const refetchInterval = useAutoRefresh({ checkPendingRuns: true }); const { data: authLinks } = useAuthLinksServiceGetAuthMenus(); const hasPoolsAccess = authLinks?.authorized_menu_items.includes("Pools"); + const { data, error, isLoading } = usePoolServiceGetPools( + undefined, + undefined, + { + refetchInterval: (query) => { + const apiError = query.state.error; + + return apiError?.status === 403 ? false : refetchInterval; + }, + }, + ); + + if (error?.status === 403) { + return undefined; + } + const pools = data?.pools; const totalSlots = pools?.reduce((sum, pool) => sum + pool.slots, 0) ?? 0; const aggregatePool: Slots = { @@ -84,9 +98,7 @@ export const PoolSummary = () => { {isLoading ? ( ) : ( - - - + )} ); diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx index 079472cf353eb..9819987aa634e 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx @@ -29,11 +29,12 @@ import { DAGImportErrorsModal } from "./DAGImportErrorsModal"; export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: boolean }) => { const { onClose, onOpen, open } = useDisclosure(); - const { t: translate } = useTranslation("dashboard"); + const { i18n, t: translate } = useTranslation("dashboard"); - const { data, error, isLoading } = useImportErrorServiceGetImportErrors(); + const isRTL = i18n.dir() === "rtl"; + + const { data, error, isLoading } = useImportErrorServiceGetImportErrors({ limit: 1 }); const importErrorsCount = data?.total_entries ?? 0; - const importErrors = data?.import_errors ?? []; if (isLoading) { return ; @@ -54,7 +55,7 @@ export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: bool onClick={onOpen} title={translate("importErrors.dagImportError", { count: importErrorsCount })} > - + {importErrorsCount} ) : ( @@ -63,11 +64,12 @@ export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: bool count={importErrorsCount} icon={} isLoading={isLoading} + isRTL={isRTL} label={translate("importErrors.dagImportError", { count: importErrorsCount })} onClick={onOpen} /> )} - + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx index f52d51086e6c8..f27756003fb31 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx @@ -17,34 +17,39 @@ * under the License. */ import { Heading, Text, HStack } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { LuFileWarning } from "react-icons/lu"; import { PiFilePy } from "react-icons/pi"; -import type { ImportErrorResponse } from "openapi/requests/types.gen"; +import { useImportErrorServiceGetImportErrors } from "openapi/queries"; import { SearchBar } from "src/components/SearchBar"; import Time from "src/components/Time"; import { Accordion, Dialog } from "src/components/ui"; import { Pagination } from "src/components/ui/Pagination"; type ImportDAGErrorModalProps = { - importErrors: Array; onClose: () => void; open: boolean; }; const PAGE_LIMIT = 15; -export const DAGImportErrorsModal: React.FC = ({ importErrors, onClose, open }) => { +export const DAGImportErrorsModal: React.FC = ({ onClose, open }) => { const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(""); - const [filteredErrors, setFilteredErrors] = useState(importErrors); - const { t: translate } = useTranslation(["dashboard", "components"]); - const startRange = (page - 1) * PAGE_LIMIT; - const endRange = startRange + PAGE_LIMIT; - const visibleItems = filteredErrors.slice(startRange, endRange); + const { data } = useImportErrorServiceGetImportErrors( + { + filenamePattern: searchQuery || undefined, + limit: PAGE_LIMIT, + offset: PAGE_LIMIT * (page - 1), + }, + undefined, + { enabled: open }, + ); + + const { t: translate } = useTranslation(["dashboard", "components"]); const onOpenChange = () => { setSearchQuery(""); @@ -52,13 +57,10 @@ export const DAGImportErrorsModal: React.FC = ({ impor onClose(); }; - useEffect(() => { - const query = searchQuery.toLowerCase(); - const filtered = importErrors.filter((error) => error.filename.toLowerCase().includes(query)); - - setFilteredErrors(filtered); + const handleSearchChange = (value: string) => { + setSearchQuery(value); setPage(1); - }, [searchQuery, importErrors]); + }; return ( @@ -66,13 +68,13 @@ export const DAGImportErrorsModal: React.FC = ({ impor - {translate("importErrors.dagImportError", { count: importErrors.length })} + {translate("importErrors.dagImportError", { count: data?.total_entries ?? 0 })} @@ -81,7 +83,7 @@ export const DAGImportErrorsModal: React.FC = ({ impor - {visibleItems.map((importError) => ( + {data?.import_errors.map((importError) => ( @@ -108,7 +110,7 @@ export const DAGImportErrorsModal: React.FC = ({ impor setPage(event.page)} p={4} page={page} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx index cea92bed6d794..d92f9ab533e4c 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx @@ -16,22 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Text, Button, useDisclosure, Skeleton } from "@chakra-ui/react"; +import { Box, Button, useDisclosure, Skeleton } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; -import { FiChevronRight } from "react-icons/fi"; import { LuPlug } from "react-icons/lu"; import { usePluginServiceImportErrors } from "openapi/queries"; import { ErrorAlert, type ExpandedApiError } from "src/components/ErrorAlert"; import { StateBadge } from "src/components/StateBadge"; +import { StatsCard } from "src/components/StatsCard"; import { PluginImportErrorsModal } from "./PluginImportErrorsModal"; export const PluginImportErrors = ({ iconOnly = false }: { readonly iconOnly?: boolean }) => { const { onClose, onOpen, open } = useDisclosure(); - const { t: translate } = useTranslation("admin"); + const { i18n, t: translate } = useTranslation("admin"); const { data, error, isLoading } = usePluginServiceImportErrors(); + const isRTL = i18n.dir() === "rtl"; + const importErrorsCount = data?.total_entries ?? 0; const importErrors = data?.import_errors ?? []; @@ -48,7 +50,7 @@ export const PluginImportErrors = ({ iconOnly = false }: { readonly iconOnly?: b } return ( - + {iconOnly ? ( - + {importErrorsCount} ) : ( - + /> )} diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx index 2dc47403b382e..4192ad7b88652 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex, Heading, HStack } from "@chakra-ui/react"; +import { Box, Flex, Heading } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiClipboard, FiZap } from "react-icons/fi"; @@ -29,7 +29,7 @@ import { DAGImportErrors } from "./DAGImportErrors"; import { PluginImportErrors } from "./PluginImportErrors"; export const Stats = () => { - const refetchInterval = useAutoRefresh({}); + const refetchInterval = useAutoRefresh({ checkPendingRuns: true }); const { data: statsData, isLoading: isStatsLoading } = useDashboardServiceDagStats(undefined, { refetchInterval, }); @@ -38,7 +38,9 @@ export const Stats = () => { const queuedDagsCount = statsData?.queued_dag_count ?? 0; const runningDagsCount = statsData?.running_dag_count ?? 0; const activeDagsCount = statsData?.active_dag_count ?? 0; - const { t: translate } = useTranslation("dashboard"); + const { i18n, t: translate } = useTranslation("dashboard"); + + const isRTL = i18n.dir() === "rtl"; return ( @@ -49,13 +51,14 @@ export const Stats = () => { - + { colorScheme="queued" count={queuedDagsCount} isLoading={isStatsLoading} + isRTL={isRTL} label={translate("stats.queuedDags")} link="dags?last_dag_run_state=queued" state="queued" @@ -80,20 +84,22 @@ export const Stats = () => { colorScheme="running" count={runningDagsCount} isLoading={isStatsLoading} + isRTL={isRTL} label={translate("stats.runningDags")} link="dags?last_dag_run_state=running" state="running" /> } isLoading={isStatsLoading} + isRTL={isRTL} label={translate("stats.activeDags")} link="dags?paused=false" /> - + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DeleteRunButton.tsx b/airflow-core/src/airflow/ui/src/pages/DeleteRunButton.tsx index 1ecf39c991d6b..8ad71b2e33c14 100644 --- a/airflow-core/src/airflow/ui/src/pages/DeleteRunButton.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DeleteRunButton.tsx @@ -46,7 +46,7 @@ const DeleteRunButton = ({ dagRun, withText = true }: DeleteRunButtonProps) => { <> } onClick={onOpen} text={translate("dags:runAndTaskActions.delete.button", { type: translate("dagRun_one") })} diff --git a/airflow-core/src/airflow/ui/src/pages/Error.tsx b/airflow-core/src/airflow/ui/src/pages/Error.tsx index f486c9e730644..fb10bdb212a36 100644 --- a/airflow-core/src/airflow/ui/src/pages/Error.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Error.tsx @@ -30,7 +30,10 @@ export const ErrorPage = () => { let errorMessage = translate("error.defaultMessage"); let statusCode = ""; - if (isRouteErrorResponse(error)) { + if (error === null || error === undefined) { + statusCode = "404"; + errorMessage = translate("error.invalidUrl"); + } else if (isRouteErrorResponse(error)) { statusCode = String(error.status); errorMessage = ((error as unknown as Error).message || (error as { statusText?: string }).statusText) ?? @@ -61,10 +64,23 @@ export const ErrorPage = () => { - - diff --git a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx index 50161446b2893..3685cc90bf94f 100644 --- a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { ButtonGroup, Code, Flex, Heading, IconButton, useDisclosure, VStack } from "@chakra-ui/react"; +import { Code, Flex, Heading, useDisclosure, VStack } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; import dayjs from "dayjs"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { MdCompress, MdExpand } from "react-icons/md"; import { useParams, useSearchParams } from "react-router-dom"; import { useEventLogServiceGetEventLogs } from "openapi/queries"; @@ -28,6 +28,7 @@ import type { EventLogResponse } from "openapi/requests/types.gen"; import { DataTable } from "src/components/DataTable"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; +import { ExpandCollapseButtons } from "src/components/ExpandCollapseButtons"; import RenderedJsonField from "src/components/RenderedJsonField"; import Time from "src/components/Time"; import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; @@ -158,7 +159,7 @@ const { }: SearchParamsKeysType = SearchParamsKeys; export const Events = () => { - const { t: translate } = useTranslation("browse"); + const { t: translate } = useTranslation(["browse", "common"]); const { dagId, runId, taskId } = useParams(); const [searchParams] = useSearchParams(); const { setTableURLState, tableURLState } = useTableURLState(); @@ -207,46 +208,37 @@ export const Events = () => { undefined, ); + const columns = useMemo( + () => eventsColumn({ dagId, open, runId, taskId }, translate), + [dagId, open, runId, taskId, translate], + ); + return ( - + + {dagId === undefined && runId === undefined && taskId === undefined ? ( + {translate("auditLog.title")} + ) : undefined} - {dagId === undefined && runId === undefined && taskId === undefined ? ( - {translate("auditLog.title")} - ) : undefined} - - - - - - - - + + - - diff --git a/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx index 75af3d4bc3674..cffb91c4a57a7 100644 --- a/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx @@ -16,29 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, HStack, VStack, Text } from "@chakra-ui/react"; -import { useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { useSearchParams } from "react-router-dom"; +import { useMemo } from "react"; -import { useTableURLState } from "src/components/DataTable/useTableUrlState"; -import { DateTimeInput } from "src/components/DateTimeInput"; -import { SearchBar } from "src/components/SearchBar"; -import { ResetButton } from "src/components/ui"; +import { FilterBar, type FilterValue } from "src/components/FilterBar"; import { SearchParamsKeys } from "src/constants/searchParams"; -import { getFilterCount } from "src/utils/filterUtils"; - -const { - AFTER: AFTER_PARAM, - BEFORE: BEFORE_PARAM, - DAG_ID: DAG_ID_PARAM, - EVENT_TYPE: EVENT_TYPE_PARAM, - MAP_INDEX: MAP_INDEX_PARAM, - RUN_ID: RUN_ID_PARAM, - TASK_ID: TASK_ID_PARAM, - TRY_NUMBER: TRY_NUMBER_PARAM, - USER: USER_PARAM, -} = SearchParamsKeys; +import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils"; type EventsFiltersProps = { readonly urlDagId?: string; @@ -47,205 +29,57 @@ type EventsFiltersProps = { }; export const EventsFilters = ({ urlDagId, urlRunId, urlTaskId }: EventsFiltersProps) => { - const { t: translate } = useTranslation(["browse", "common", "components"]); - const [searchParams, setSearchParams] = useSearchParams(); - const { setTableURLState, tableURLState } = useTableURLState(); - - const { pagination, sorting } = tableURLState; - - const resetPagination = useCallback(() => { - setTableURLState({ - pagination: { ...pagination, pageIndex: 0 }, - sorting, - }); - }, [pagination, setTableURLState, sorting]); - - const afterFilter = searchParams.get(AFTER_PARAM); - const beforeFilter = searchParams.get(BEFORE_PARAM); - const dagIdFilter = searchParams.get(DAG_ID_PARAM); - const eventTypeFilter = searchParams.get(EVENT_TYPE_PARAM); - const mapIndexFilter = searchParams.get(MAP_INDEX_PARAM); - const runIdFilter = searchParams.get(RUN_ID_PARAM); - const taskIdFilter = searchParams.get(TASK_ID_PARAM); - const tryNumberFilter = searchParams.get(TRY_NUMBER_PARAM); - const userFilter = searchParams.get(USER_PARAM); - - const updateSearchParams = useCallback( - (paramName: string, value: string) => { - if (value) { - searchParams.set(paramName, value); - } else { - searchParams.delete(paramName); + const searchParamKeys = useMemo((): Array => { + const keys: Array = [ + SearchParamsKeys.AFTER, + SearchParamsKeys.BEFORE, + SearchParamsKeys.EVENT_TYPE, + SearchParamsKeys.USER, + SearchParamsKeys.MAP_INDEX, + SearchParamsKeys.TRY_NUMBER, + ]; + + // Only add DAG ID filter if not in URL context + if (urlDagId === undefined) { + keys.push(SearchParamsKeys.DAG_ID); + } + + // Only add Run ID filter if not in URL context + if (urlRunId === undefined) { + keys.push(SearchParamsKeys.RUN_ID); + } + + // Only add Task ID filter if not in URL context + if (urlTaskId === undefined) { + keys.push(SearchParamsKeys.TASK_ID); + } + + return keys; + }, [urlDagId, urlRunId, urlTaskId]); + + const { filterConfigs, handleFiltersChange, searchParams } = useFiltersHandler(searchParamKeys); + + const initialValues = useMemo(() => { + const values: Record = {}; + + filterConfigs.forEach((config) => { + const value = searchParams.get(config.key); + + if (value !== null && value !== "") { + if (config.type === "number") { + const parsedValue = Number(value); + + values[config.key] = isNaN(parsedValue) ? value : parsedValue; + } else { + values[config.key] = value; + } } - resetPagination(); - setSearchParams(searchParams); - }, - [resetPagination, searchParams, setSearchParams], - ); - - const onClearFilters = useCallback(() => { - searchParams.delete(AFTER_PARAM); - searchParams.delete(BEFORE_PARAM); - searchParams.delete(DAG_ID_PARAM); - searchParams.delete(EVENT_TYPE_PARAM); - searchParams.delete(MAP_INDEX_PARAM); - searchParams.delete(RUN_ID_PARAM); - searchParams.delete(TASK_ID_PARAM); - searchParams.delete(TRY_NUMBER_PARAM); - searchParams.delete(USER_PARAM); - resetPagination(); - setSearchParams(searchParams); - }, [resetPagination, searchParams, setSearchParams]); - - const handleSearchChange = useCallback( - (paramName: string) => (value: string) => { - updateSearchParams(paramName, value); - }, - [updateSearchParams], - ); - - const handleDateTimeChange = useCallback( - (paramName: string) => (event: React.ChangeEvent) => { - const { value } = event.target; - - updateSearchParams(paramName, value); - }, - [updateSearchParams], - ); + }); - const filterCount = getFilterCount({ - after: afterFilter, - before: beforeFilter, - dagId: dagIdFilter, - eventType: eventTypeFilter, - mapIndex: mapIndexFilter, - runId: runIdFilter, - taskId: taskIdFilter, - tryNumber: tryNumberFilter, - user: userFilter, - }); + return values; + }, [searchParams, filterConfigs]); return ( - - - {/* Timestamp Range Filters */} - - - {translate("components:backfill.dateRangeFrom")} - - - - - - {translate("components:backfill.dateRangeTo")} - - - - - {/* Event Type Filter */} - - - - - {/* User Filter */} - - - - - {/* DAG ID Filter - Hide if URL already has dagId */} - {urlDagId === undefined && ( - - - - )} - - {/* Task ID Filter - Hide if URL already has taskId */} - {urlTaskId === undefined && ( - - - - )} - - {/* Run ID Filter - Hide if URL already has runId */} - {urlRunId === undefined && ( - - - - )} - - {/* Map Index Filter */} - - - - - {/* Try Number Filter */} - - - - - - - - - + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx index feedee26b333f..a4efef7726514 100644 --- a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx +++ b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx @@ -23,7 +23,6 @@ import { useParams } from "react-router-dom"; import { DetailsLayout } from "src/layouts/Details/DetailsLayout"; import { useGridTiSummaries } from "src/queries/useGridTISummaries.ts"; -import { isStatePending, useAutoRefresh } from "src/utils"; import { Header } from "./Header"; @@ -33,19 +32,12 @@ export const GroupTaskInstance = () => { const { data: gridTISummaries } = useGridTiSummaries({ dagId, runId }); const taskInstance = gridTISummaries?.task_instances.find((ti) => ti.task_id === groupId); - const refetchInterval = useAutoRefresh({ dagId }); - const tabs = [{ icon: , label: translate("tabs.taskInstances"), value: "" }]; return ( - {taskInstance === undefined ? undefined : ( -
- )} + {taskInstance === undefined ? undefined :
} ); diff --git a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx index 9fc14c76f5d5a..16731d4de4c58 100644 --- a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx @@ -27,13 +27,7 @@ import { HeaderCard } from "src/components/HeaderCard"; import Time from "src/components/Time"; import { getDuration } from "src/utils"; -export const Header = ({ - isRefreshing, - taskInstance, -}: { - readonly isRefreshing?: boolean; - readonly taskInstance: LightGridTaskInstanceSummary; -}) => { +export const Header = ({ taskInstance }: { readonly taskInstance: LightGridTaskInstanceSummary }) => { const { t: translate } = useTranslation(); const entries: Array<{ label: string; value: number | ReactNode | string }> = []; @@ -62,7 +56,6 @@ export const Header = ({ } icon={} - isRefreshing={isRefreshing} state={taskInstance.state} stats={stats} subTitle={