diff --git a/.github/actions/breeze/action.yml b/.github/actions/breeze/action.yml index b6fbacda16e92..1465d45077ea4 100644 --- a/.github/actions/breeze/action.yml +++ b/.github/actions/breeze/action.yml @@ -24,7 +24,7 @@ inputs: default: "3.10" uv-version: description: 'uv version to use' - default: "0.9.14" # Keep this comment to allow automatic replacement of uv version + default: "0.9.16" # Keep this comment to allow automatic replacement of uv version outputs: host-python-version: description: Python version used in host diff --git a/.github/actions/install-prek/action.yml b/.github/actions/install-prek/action.yml index 81b91b18a4b82..e72cf95adcf6f 100644 --- a/.github/actions/install-prek/action.yml +++ b/.github/actions/install-prek/action.yml @@ -24,7 +24,7 @@ inputs: default: "3.10" uv-version: description: 'uv version to use' - default: "0.9.14" # Keep this comment to allow automatic replacement of uv version + default: "0.9.16" # Keep this comment to allow automatic replacement of uv version prek-version: description: 'prek version to use' default: "0.2.19" # Keep this comment to allow automatic replacement of prek version diff --git a/.github/workflows/airflow-distributions-tests.yml b/.github/workflows/airflow-distributions-tests.yml index 69d31d60dfc2d..03236b9d40dbf 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.16.1 + uv tool install hatch==1.16.2 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/basic-tests.yml b/.github/workflows/basic-tests.yml index abc5ef8f9c076..664945af7b04c 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.9.14" # Keep this comment to allow automatic replacement of uv version + default: "0.9.16" # Keep this comment to allow automatic replacement of uv version type: string platform: description: 'Platform for the build - linux/amd64 or linux/arm64' diff --git a/.github/workflows/release_dockerhub_image.yml b/.github/workflows/release_dockerhub_image.yml index 99e2098bf91b9..6118057a214ef 100644 --- a/.github/workflows/release_dockerhub_image.yml +++ b/.github/workflows/release_dockerhub_image.yml @@ -58,7 +58,7 @@ jobs: AIRFLOW_VERSION: ${{ github.event.inputs.airflowVersion }} AMD_ONLY: ${{ github.event.inputs.amdOnly }} LIMIT_PYTHON_VERSIONS: ${{ github.event.inputs.limitPythonVersions }} - UV_VERSION: "0.9.14" # Keep this comment to allow automatic replacement of uv version + UV_VERSION: "0.9.16" # Keep this comment to allow automatic replacement of uv version if: contains(fromJSON('[ "ashb", "bugraoz93", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 421ca4c359f74..9c9d80d45e946 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: - "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: f5cfd5fdaf0211dfd1027d9d8442b764a232c7ad hooks: - id: insert-license name: Add license for all SQL files @@ -350,7 +350,7 @@ repos: types_or: [python, pyi] args: [--fix] require_serial: true - additional_dependencies: ['ruff==0.14.7'] + additional_dependencies: ['ruff==0.14.8'] exclude: ^airflow-core/tests/unit/dags/test_imports\.py$|^performance/tests/test_.*\.py$ - id: ruff-format name: Run 'ruff format' diff --git a/Dockerfile b/Dockerfile index 9b1f3afa64b6e..2ad4757c6d8fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,7 @@ ARG AIRFLOW_PYTHON_VERSION="3.12.12" # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` ARG AIRFLOW_PIP_VERSION=25.3 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.9.14 +ARG AIRFLOW_UV_VERSION=0.9.16 ARG AIRFLOW_USE_UV="false" ARG UV_HTTP_TIMEOUT="300" ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow" diff --git a/Dockerfile.ci b/Dockerfile.ci index c4921b7df90c9..9364093c53c51 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1324,8 +1324,12 @@ 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() { @@ -1695,7 +1699,7 @@ COPY --from=scripts common.sh install_packaging_tools.sh install_additional_depe # Also use `force pip` label on your PR to swap all places we use `uv` to `pip` ARG AIRFLOW_PIP_VERSION=25.3 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main" -ARG AIRFLOW_UV_VERSION=0.9.14 +ARG AIRFLOW_UV_VERSION=0.9.16 ARG AIRFLOW_PREK_VERSION="0.2.19" # UV_LINK_MODE=copy is needed since we are using cache mounted from the host diff --git a/airflow-core/docs/best-practices.rst b/airflow-core/docs/best-practices.rst index 1985c2731c66c..114375c29b740 100644 --- a/airflow-core/docs/best-practices.rst +++ b/airflow-core/docs/best-practices.rst @@ -310,7 +310,7 @@ Installing and Using ruff .. code-block:: bash - pip install "ruff>=0.14.7" + pip install "ruff>=0.14.8" 2. **Running ruff**: Execute ``ruff`` to check your Dags for potential issues: diff --git a/airflow-core/docs/core-concepts/executor/local.rst b/airflow-core/docs/core-concepts/executor/local.rst index a533ae3a5b830..dc7a70fd87361 100644 --- a/airflow-core/docs/core-concepts/executor/local.rst +++ b/airflow-core/docs/core-concepts/executor/local.rst @@ -29,8 +29,27 @@ This parameter must be greater than ``0``. 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`. +LocalExecutor receives the call to shutdown the executor a poison token is sent to the workers to terminate them. + +The worker spawning behavior differs based on the multiprocessing start method: + +- **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:: + + 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:: diff --git a/airflow-core/docs/howto/index.rst b/airflow-core/docs/howto/index.rst index 9abc9caa76e3f..0d4f79e74e547 100644 --- a/airflow-core/docs/howto/index.rst +++ b/airflow-core/docs/howto/index.rst @@ -56,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/src/airflow/api_fastapi/app.py b/airflow-core/src/airflow/api_fastapi/app.py index 7c05295807e62..58cfb157083ce 100644 --- a/airflow-core/src/airflow/api_fastapi/app.py +++ b/airflow-core/src/airflow/api_fastapi/app.py @@ -29,7 +29,6 @@ init_config, init_error_handlers, init_flask_plugins, - init_middlewares, init_ui_plugins, init_views, ) @@ -100,7 +99,6 @@ def create_app(apps: str = "all") -> FastAPI: init_ui_plugins(app) init_views(app) # Core views need to be the last routes added - it has a catch all route init_error_handlers(app) - init_middlewares(app) init_config(app) 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 b8656cd068f10..d57dd3cdc39f1 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,15 +135,12 @@ def get_url_logout(self) -> str | None: """ return None - def refresh_user(self, *, user: T) -> T | None: + def get_url_refresh(self) -> str | None: """ - Refresh the user if needed. + Return the URL to refresh the authentication 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. + 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. """ return None diff --git a/airflow-core/src/airflow/api_fastapi/auth/middlewares/__init__.py b/airflow-core/src/airflow/api_fastapi/auth/middlewares/__init__.py deleted file mode 100644 index 217e5db960782..0000000000000 --- a/airflow-core/src/airflow/api_fastapi/auth/middlewares/__init__.py +++ /dev/null @@ -1,17 +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. 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 deleted file mode 100644 index f304eb9517f65..0000000000000 --- a/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py +++ /dev/null @@ -1,68 +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 - -from fastapi import Request -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.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_user = None - current_token = request.cookies.get(COOKIE_NAME_JWT_TOKEN) - if current_token: - new_user = await self._refresh_user(current_token) - if new_user: - request.state.user = new_user - - response = await call_next(request) - - 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) - secure = bool(conf.get("api", "ssl_cert", fallback="")) - response.set_cookie( - COOKIE_NAME_JWT_TOKEN, - new_token, - httponly=True, - secure=secure, - samesite="lax", - ) - - return response - - @staticmethod - async def _refresh_user(current_token: str) -> BaseUser | None: - user = await resolve_user_from_token(current_token) - return get_auth_manager().refresh_user(user=user) diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index 668e6721f8736..123b6d9841f95 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 @@ -248,11 +248,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: 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 8db1fa66680bc..cd34b28f3cbc5 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py @@ -181,12 +181,6 @@ def init_error_handlers(app: FastAPI) -> None: app.add_exception_handler(handler.exception_cls, handler.exception_handler) -def init_middlewares(app: FastAPI) -> None: - from airflow.api_fastapi.auth.middlewares.refresh_token import JWTRefreshMiddleware - - app.add_middleware(JWTRefreshMiddleware) - - def init_ui_plugins(app: FastAPI) -> None: """Initialize UI plugins.""" from airflow import plugins_manager 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 ceaf90b60f805..bef548133c6b9 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 @@ -8478,6 +8478,40 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPExceptionResponse' + /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: 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 a97b7fd9972dc..d1e770b927786 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 @@ -65,3 +65,23 @@ def logout(request: Request) -> RedirectResponse: ) return response + + +@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) 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 82e7fad3493bb..68764b5456aa6 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 @@ -729,54 +729,32 @@ def post_clear_task_instances( if future: body.end_date = None - if (task_markers_to_clear := body.task_ids) is not None: - mapped_tasks_tuples = {t for t in task_markers_to_clear if isinstance(t, tuple)} + task_ids = body.task_ids + if task_ids is not None: + 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) - normal_task_ids = {t for t in task_markers_to_clear if not isinstance(t, tuple)} - - def _collect_relatives(run_id: str, direction: Literal["upstream", "downstream"]) -> None: - from airflow.models.taskinstance import find_relevant_relatives - - relevant_relatives = find_relevant_relatives( - normal_task_ids, - mapped_tasks_tuples, - dag=dag, - run_id=run_id, - direction=direction, - session=session, + 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, ) - normal_task_ids.update(t for t in relevant_relatives if not isinstance(t, tuple)) - mapped_tasks_tuples.update(t for t in relevant_relatives if isinstance(t, tuple)) - - # We can't easily calculate upstream/downstream map indexes when not - # working for a specific dag run. It's possible by looking at the runs - # one by one, but that is both resource-consuming and logically complex. - # So instead we'll just clear all the tis based on task ID and hope - # that's good enough for most cases. - if dag_run_id is None: - if upstream or downstream: - partial_dag = dag.partial_subset( - task_ids=normal_task_ids.union(tid for tid, _ in mapped_tasks_tuples), - include_downstream=downstream, - include_upstream=upstream, - exclude_original=True, - ) - normal_task_ids.update(partial_dag.task_dict) - else: - if upstream: - _collect_relatives(dag_run_id, "upstream") - if downstream: - _collect_relatives(dag_run_id, "downstream") - - task_markers_to_clear = [ - *normal_task_ids, - *((t, m) for t, m in mapped_tasks_tuples if t not in normal_task_ids), + unmapped_task_ids = unmapped_task_ids | set(relatives.task_dict.keys()) + + 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 = { "dry_run": True, - "task_ids": task_markers_to_clear, + "task_ids": task_ids, "session": session, "run_on_latest_version": body.run_on_latest_version, "only_failed": body.only_failed, 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 7bc7b155b68eb..e17f92776da12 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/security.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py @@ -22,7 +22,7 @@ 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 @@ -66,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 diff --git a/airflow-core/src/airflow/dag_processing/manager.py b/airflow-core/src/airflow/dag_processing/manager.py index c774aa29fc65d..2a37a1bbf6fa5 100644 --- a/airflow-core/src/airflow/dag_processing/manager.py +++ b/airflow-core/src/airflow/dag_processing/manager.py @@ -624,7 +624,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: diff --git a/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py b/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py index c7b3a02301daa..750c3da1ec17b 100644 --- a/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py +++ b/airflow-core/src/airflow/example_dags/example_dynamic_task_mapping.py @@ -22,9 +22,9 @@ # [START example_dynamic_task_mapping] from datetime import datetime -from airflow.sdk import DAG, task, task_group +from airflow.sdk import DAG, task -with DAG(dag_id="example_dynamic_task_mapping", schedule=None, start_date=datetime(2022, 3, 4)): +with DAG(dag_id="example_dynamic_task_mapping", schedule=None, start_date=datetime(2022, 3, 4)) as dag: @task def add_one(x: int): @@ -39,11 +39,8 @@ def sum_it(values): sum_it(added_values) with DAG( - dag_id="example_task_mapping_second_order", - schedule=None, - catchup=False, - start_date=datetime(2022, 3, 4), -): + dag_id="example_task_mapping_second_order", schedule=None, catchup=False, start_date=datetime(2022, 3, 4) +) as dag2: @task def get_nums(): @@ -61,25 +58,4 @@ def add_10(num): _times_2 = times_2.expand(num=_get_nums) add_10.expand(num=_times_2) -with DAG( - dag_id="example_task_group_mapping", - schedule=None, - catchup=False, - start_date=datetime(2022, 3, 4), -): - - @task_group - def op(num): - @task - def add_1(num): - return num + 1 - - @task - def mul_2(num): - return num * 2 - - return mul_2(add_1(num)) - - op.expand(num=[1, 2, 3]) - # [END example_dynamic_task_mapping] 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/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 06597a24fc93d..29f2d98eefa2c 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -28,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 @@ -121,7 +121,7 @@ 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, SerializedDAG + from airflow.serialization.serialized_objects import SerializedBaseOperator from airflow.utils.context import Context Operator: TypeAlias = MappedOperator | SerializedBaseOperator @@ -294,6 +294,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() @@ -2037,16 +2038,87 @@ def get_relevant_upstream_map_indexes( *, session: Session, ) -> int | range | None: + """ + Infer the map indexes of an upstream "relevant" to this ti. + + The bulk of the logic mainly exists to solve the problem described by + the following example, where 'val' must resolve to different values, + depending on where the reference is being used:: + + @task + def this_task(v): # This is self.task. + return v * 2 + + + @task_group + def tg1(inp): + val = upstream(inp) # This is the upstream task. + this_task(val) # When inp is 1, val here should resolve to 2. + return val + + + # This val is the same object returned by tg1. + val = tg1.expand(inp=[1, 2, 3]) + + + @task_group + def tg2(inp): + another_task(inp, val) # val here should resolve to [2, 4, 6]. + + + tg2.expand(inp=["a", "b"]) + + The surrounding mapped task groups of ``upstream`` and ``self.task`` are + inspected to find a common "ancestor". If such an ancestor is found, + we need to return specific map indexes to pull a partial value from + upstream XCom. + + :param upstream: The referenced upstream task. + :param ti_count: The total count of task instance this task was expanded + by the scheduler, i.e. ``expanded_ti_count`` in the template context. + :return: Specific map index or map indexes to pull, or ``None`` if we + want to "whole" return value (i.e. no mapped task groups involved). + """ + from airflow.models.mappedoperator import get_mapped_ti_count + if TYPE_CHECKING: - assert self.task - return _get_relevant_map_indexes( - run_id=self.run_id, - map_index=self.map_index, - ti_count=ti_count, - task=self.task, - relative=upstream, - session=session, - ) + assert self.task is not None + + # This value should never be None since we already know the current task + # is in a mapped task group, and should have been expanded, despite that, + # we need to check that it is not None to satisfy Mypy. + # But this value can be 0 when we expand an empty list, for that it is + # necessary to check that ti_count is not 0 to avoid dividing by 0. + if not ti_count: + return None + + # Find the innermost common mapped task group between the current task + # If the current task and the referenced task does not have a common + # mapped task group, the two are in different task mapping contexts + # (like another_task above), and we should use the "whole" value. + common_ancestor = _find_common_ancestor_mapped_group(self.task, upstream) + if common_ancestor is None: + return None + + # At this point we know the two tasks share a mapped task group, and we + # should use a "partial" value. Let's break down the mapped ti count + # between the ancestor and further expansion happened inside it. + + ancestor_ti_count = get_mapped_ti_count(common_ancestor, self.run_id, session=session) + ancestor_map_index = self.map_index * ancestor_ti_count // ti_count + + # If the task is NOT further expanded inside the common ancestor, we + # only want to reference one single ti. We must walk the actual DAG, + # and "ti_count == ancestor_ti_count" does not work, since the further + # expansion may be of length 1. + if not _is_further_mapped_inside(upstream, common_ancestor): + return ancestor_map_index + + # Otherwise we need a partial aggregation for values from selected task + # instances in the ancestor's expansion context. + further_count = ti_count // ancestor_ti_count + map_index_start = ancestor_map_index * further_count + return range(map_index_start, map_index_start + further_count) def clear_db_references(self, session: Session): """ @@ -2136,159 +2208,6 @@ def _is_further_mapped_inside(operator: Operator, container: SerializedTaskGroup return False -def _get_relevant_map_indexes( - *, - task: Operator, - run_id: str, - map_index: int, - relative: Operator, - ti_count: int | None, - session: Session, -) -> int | range | None: - """ - Infer the map indexes of a relative that's "relevant" to this ti. - - The bulk of the logic mainly exists to solve the problem described by - the following example, where 'val' must resolve to different values, - depending on where the reference is being used:: - - @task - def this_task(v): # This is self.task. - return v * 2 - - - @task_group - def tg1(inp): - val = upstream(inp) # This is the upstream task. - this_task(val) # When inp is 1, val here should resolve to 2. - return val - - - # This val is the same object returned by tg1. - val = tg1.expand(inp=[1, 2, 3]) - - - @task_group - def tg2(inp): - another_task(inp, val) # val here should resolve to [2, 4, 6]. - - - tg2.expand(inp=["a", "b"]) - - The surrounding mapped task groups of ``upstream`` and ``task`` are - inspected to find a common "ancestor". If such an ancestor is found, - we need to return specific map indexes to pull a partial value from - upstream XCom. - - The same logic apply for finding downstream tasks. - - :param task: Current task being inspected. - :param run_id: Current run ID. - :param map_index: Map index of the current task instance. - :param relative: The relative task to find relevant map indexes for. - :param ti_count: The total count of task instance this task was expanded - by the scheduler, i.e. ``expanded_ti_count`` in the template context. - :return: Specific map index or map indexes to pull, or ``None`` if we - want to "whole" return value (i.e. no mapped task groups involved). - """ - from airflow.models.mappedoperator import get_mapped_ti_count - - # This value should never be None since we already know the current task - # is in a mapped task group, and should have been expanded, despite that, - # we need to check that it is not None to satisfy Mypy. - # But this value can be 0 when we expand an empty list, for that it is - # necessary to check that ti_count is not 0 to avoid dividing by 0. - if not ti_count: - return None - - # Find the innermost common mapped task group between the current task - # If the current task and the referenced task does not have a common - # mapped task group, the two are in different task mapping contexts - # (like another_task above), and we should use the "whole" value. - if (common_ancestor := _find_common_ancestor_mapped_group(task, relative)) is None: - return None - - # At this point we know the two tasks share a mapped task group, and we - # should use a "partial" value. Let's break down the mapped ti count - # between the ancestor and further expansion happened inside it. - - ancestor_ti_count = get_mapped_ti_count(common_ancestor, run_id, session=session) - ancestor_map_index = map_index * ancestor_ti_count // ti_count - - # If the task is NOT further expanded inside the common ancestor, we - # only want to reference one single ti. We must walk the actual DAG, - # and "ti_count == ancestor_ti_count" does not work, since the further - # expansion may be of length 1. - if not _is_further_mapped_inside(relative, common_ancestor): - return ancestor_map_index - - # Otherwise we need a partial aggregation for values from selected task - # instances in the ancestor's expansion context. - further_count = ti_count // ancestor_ti_count - map_index_start = ancestor_map_index * further_count - return range(map_index_start, map_index_start + further_count) - - -def find_relevant_relatives( - normal_tasks: Iterable[str], - mapped_tasks: Iterable[tuple[str, int]], - *, - direction: Literal["upstream", "downstream"], - dag: SerializedDAG, - run_id: str, - session: Session, -) -> Collection[str | tuple[str, int]]: - from airflow.models.mappedoperator import get_mapped_ti_count - - visited: set[str | tuple[str, int]] = set() - - def _visit_relevant_relatives_for_normal(task_ids: Iterable[str]) -> None: - partial_dag = dag.partial_subset( - task_ids=task_ids, - include_downstream=direction == "downstream", - include_upstream=direction == "upstream", - exclude_original=True, - ) - visited.update(partial_dag.task_dict) - - def _visit_relevant_relatives_for_mapped(mapped_tasks: Iterable[tuple[str, int]]) -> None: - for task_id, map_index in mapped_tasks: - task = dag.get_task(task_id) - ti_count = get_mapped_ti_count(task, run_id, session=session) - # TODO (GH-52141): This should return scheduler operator types, but - # currently get_flat_relatives is inherited from SDK DAGNode. - relatives = cast("Iterable[Operator]", task.get_flat_relatives(upstream=direction == "upstream")) - for relative in relatives: - if relative.task_id in visited: - continue - relative_map_indexes = _get_relevant_map_indexes( - task=task, - relative=relative, # type: ignore[arg-type] - run_id=run_id, - map_index=map_index, - ti_count=ti_count, - session=session, - ) - visiting_mapped: set[tuple[str, int]] = set() - visiting_normal: set[str] = set() - match relative_map_indexes: - case int(): - if (item := (relative.task_id, relative_map_indexes)) not in visited: - visiting_mapped.add(item) - case range(): - visiting_mapped.update((relative.task_id, i) for i in relative_map_indexes) - case None: - if (task_id := relative.task_id) not in visited: - visiting_normal.add(task_id) - _visit_relevant_relatives_for_normal(visiting_normal) - _visit_relevant_relatives_for_mapped(visiting_mapped) - visited.update(visiting_mapped, visiting_normal) - - _visit_relevant_relatives_for_normal(normal_tasks) - _visit_relevant_relatives_for_mapped(mapped_tasks) - return visited - - class TaskInstanceNote(Base): """For storage of arbitrary notes concerning the task instance.""" diff --git a/airflow-core/src/airflow/serialization/helpers.py b/airflow-core/src/airflow/serialization/helpers.py index 949b3cb9c9f09..5f564df6188ed 100644 --- a/airflow-core/src/airflow/serialization/helpers.py +++ b/airflow-core/src/airflow/serialization/helpers.py @@ -51,6 +51,16 @@ def translate_tuples_to_lists(obj: Any): return {key: translate_tuples_to_lists(value) for key, value in obj.items()} return obj + def sort_dict_recursively(obj: Any) -> 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): @@ -70,6 +80,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/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index 994fe2d91c14d..de2bbc9647e27 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -753,6 +753,12 @@ export type LoginServiceLogoutDefaultResponse = Awaited = UseQueryResult; export const useLoginServiceLogoutKey = "LoginServiceLogout"; export const UseLoginServiceLogoutKeyFn = (queryKey?: Array) => [useLoginServiceLogoutKey, ...(queryKey ?? [])]; +export type LoginServiceRefreshDefaultResponse = Awaited>; +export type LoginServiceRefreshQueryResult = UseQueryResult; +export const useLoginServiceRefreshKey = "LoginServiceRefresh"; +export const UseLoginServiceRefreshKeyFn = ({ next }: { + next?: string; +} = {}, queryKey?: Array) => [useLoginServiceRefreshKey, ...(queryKey ?? [{ next }])]; export type AuthLinksServiceGetAuthMenusDefaultResponse = Awaited>; export type AuthLinksServiceGetAuthMenusQueryResult = UseQueryResult; export const useAuthLinksServiceGetAuthMenusKey = "AuthLinksServiceGetAuthMenus"; 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 41fe0005dcd1b..c38c395850017 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1431,6 +1431,17 @@ export const ensureUseLoginServiceLoginData = (queryClient: QueryClient, { next */ export const ensureUseLoginServiceLogoutData = (queryClient: QueryClient) => queryClient.ensureQueryData({ queryKey: Common.UseLoginServiceLogoutKeyFn(), queryFn: () => LoginService.logout() }); /** +* 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 }) }); +/** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response * @throws ApiError 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 fa6162ec588bf..ec8e3471a4cc4 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1431,6 +1431,17 @@ export const prefetchUseLoginServiceLogin = (queryClient: QueryClient, { next }: */ export const prefetchUseLoginServiceLogout = (queryClient: QueryClient) => queryClient.prefetchQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn(), queryFn: () => LoginService.logout() }); /** +* 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 }) }); +/** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response * @throws ApiError 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 955e5049e6049..5211c77b349f2 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1431,6 +1431,17 @@ export const useLoginServiceLogin = = unknown[]>(queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn(queryKey), queryFn: () => LoginService.logout() 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 }); +/** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response * @throws ApiError diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts index aafe12ed9bcac..9b980d1cbece5 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -1431,6 +1431,17 @@ export const useLoginServiceLoginSuspense = = unknown[]>(queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseLoginServiceLogoutKeyFn(queryKey), queryFn: () => LoginService.logout() 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 }); +/** * Get Auth Menus * @returns MenuItemCollectionResponse Successful Response * @throws ApiError diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 91ded2463e7f3..c9cb3594caecc 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -3,7 +3,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesData, GetGridTiSummariesResponse, GetCalendarData, GetCalendarResponse } from './types.gen'; +import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, RefreshData, RefreshResponse, GetAuthMenusResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesData, GetGridTiSummariesResponse, GetCalendarData, GetCalendarResponse } from './types.gen'; export class AssetService { /** @@ -3714,6 +3714,28 @@ export class LoginService { }); } + /** + * 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' + } + }); + } + } export class AuthLinksService { 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 006f33286fdd3..18efcbc82bac0 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 @@ -3208,6 +3208,12 @@ export type LoginResponse = unknown; export type LogoutResponse = unknown; +export type RefreshData = { + next?: string | null; +}; + +export type RefreshResponse = unknown; + export type GetAuthMenusResponse = MenuItemCollectionResponse; export type GetDependenciesData = { @@ -6278,6 +6284,25 @@ export type $OpenApiTs = { }; }; }; + '/api/v2/auth/refresh': { + get: { + req: RefreshData; + res: { + /** + * Successful Response + */ + 200: unknown; + /** + * Temporary Redirect + */ + 307: HTTPExceptionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; '/ui/auth/menus': { get: { res: { 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 bad98315e82f8..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 @@ -109,8 +109,8 @@ "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/hu/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/hu/components.json index 481812441f24a..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 @@ -82,7 +82,7 @@ "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" @@ -110,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 b06f88d010938..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 @@ -121,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/nl/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/nl/components.json index 9997e06717296..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 @@ -105,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/pt/assets.json b/airflow-core/src/airflow/ui/public/i18n/locales/pt/assets.json index f40bb8e70c986..ca1a65fc735de 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/assets.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/assets.json @@ -1,5 +1,5 @@ { - "consumingDags": "Consumindo DAGs", + "consumingDags": "Consumindo Dags", "createEvent": { "button": "Criar Evento", "manual": { @@ -8,15 +8,15 @@ "label": "Manual" }, "materialize": { - "description": "Ativar o DAG acima deste Asset", - "descriptionWithDag": "Ativar o DAG acima deste Asset: {{dagName}}", + "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.", + "materializeDescription": "Dag acima {{dagId}} foi ativado com sucesso.", "materializeTitle": "Materializando Asset" }, "title": "Criar Evento de Asset para {{name}}" @@ -25,6 +25,6 @@ "lastAssetEvent": "Último Evento de Asset", "name": "Nome", "producingTasks": "Produzindo Tarefas", - "scheduledDags": "DAGs Programados", + "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 index d22701bfabc8c..1ae262481befe 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/browse.json @@ -17,7 +17,7 @@ }, "xcom": { "columns": { - "dag": "DAG", + "dag": "Dag", "key": "Chave", "value": "Valor" }, 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 index 8ab097b993ce4..f47b866123944 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json @@ -36,22 +36,22 @@ "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", + "dag_many": "Dags", + "dag_one": "Dag", + "dag_other": "Dags", + "dag_zero": "Nenhum Dag", "dagDetails": { "catchup": "Catchup", - "dagRunTimeout": "Tempo Limite da Execução do DAG", + "dagRunTimeout": "Tempo Limite da Execução do Dag", "defaultArgs": "Argumentos Padrão", "description": "Descrição", - "documentation": "Documentação do DAG", + "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", + "latestDagVersion": "Última Versão do Dag", "latestRun": "Última Execução", "maxActiveRuns": "Máximo de Execuções Ativas", "maxActiveTasks": "Máximo de Tarefas Ativas", @@ -62,10 +62,10 @@ "schedule": "Agendamento", "tags": "Etiquetas" }, - "dagId": "ID do DAG", + "dagId": "ID do Dag", "dagRun": { "conf": "Conf", - "dagVersions": "Versão(s) do DAG", + "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", @@ -76,12 +76,12 @@ "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", + "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", @@ -134,7 +134,7 @@ "admin": "Administração", "assets": "Assets", "browse": "Navegar", - "dags": "DAGs", + "dags": "Dags", "docs": "Documentação", "home": "Início", "legacyFabViews": "Visualizações Legacy FAB", @@ -144,7 +144,7 @@ "noItemsFound": "Nenhum {{modelName}} encontrado", "note": { "add": "Adicionar uma nota", - "dagRun": "Nota da Execução do DAG", + "dagRun": "Nota da Execução do Dag", "label": "Nota", "placeholder": "Adicionar uma nota...", "taskInstance": "Nota da Instância de Tarefa" @@ -215,7 +215,7 @@ "table": { "completedAt": "Concluído em", "createdAt": "Criado em", - "filterByTag": "Filtrar DAGs por tag", + "filterByTag": "Filtrar Dags por tag", "filterColumns": "Filtrar colunas da tabela", "filterReset_many": "Resetar filtros", "filterReset_one": "Resetar filtro", @@ -244,7 +244,7 @@ "taskGroup": "Grupo de Tarefas", "taskId": "ID da Tarefa", "taskInstance": { - "dagVersion": "Versão do DAG", + "dagVersion": "Versão do Dag", "executor": "Executor", "executorConfig": "Configuração do Executor", "hostname": "Hostname", 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 index 5b30826d8b716..6649bee37ebc5 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/components.json @@ -14,7 +14,7 @@ "missingRuns": "Execuções Faltando", "reprocessBehavior": "Comportamento de Reprocessamento", "run": "Executar Backfill", - "selectDescription": "Executar este DAG para um intervalo de datas", + "selectDescription": "Executar este Dag para um intervalo de datas", "selectLabel": "Backfill", "title": "Executar Backfill", "toaster": { @@ -58,10 +58,10 @@ }, "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", + "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", @@ -94,7 +94,7 @@ "downloadImage": "Baixar imagem do gráfico", "downloadImageError": "Falha ao baixar a imagem do gráfico.", "downloadImageErrorTitle": "Download Falhou", - "otherDagRuns": "+Outras Execuções do DAG", + "otherDagRuns": "+Outras Execuções do Dag", "taskCount_many": "{{count}} Tarefas", "taskCount_one": "{{count}} Tarefa", "taskCount_other": "{{count}} Tarefas", @@ -110,7 +110,7 @@ "file": "Arquivo", "location": "linha {{line}} em {{name}}" }, - "reparseDag": "Reparse DAG", + "reparseDag": "Reparse Dag", "sortedAscending": "Ordenado em ordem crescente", "sortedDescending": "Ordenado em ordem decrescente", "sortedUnsorted": "Não ordenado", @@ -119,16 +119,16 @@ "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.", + "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", + "selectDescription": "Acionar uma única execução deste Dag", "selectLabel": "Execução Única", - "title": "Acionar DAG", + "title": "Acionar Dag", "toaster": { "success": { - "description": "A execução do DAG foi acionada com sucesso.", - "title": "Execução do DAG acionada" + "description": "A execução do Dag foi acionada com sucesso.", + "title": "Execução do Dag acionada" } }, "unpause": "Despausar {{dagDisplayName}} ao acionar" @@ -146,7 +146,7 @@ "versionId": "ID da Versão" }, "versionSelect": { - "dagVersion": "Versão do DAG", + "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 index c4a4a58b92a7a..2c9cb5cdbb4ef 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dag.json @@ -48,7 +48,7 @@ "header": { "buttons": { "advanced": "Avançado", - "dagDocs": "Documentação do DAG" + "dagDocs": "Documentação do Dag" } }, "logs": { @@ -107,12 +107,12 @@ "showGridShortcut": "Mostrar Grade (Pressione g)" }, "dagRuns": { - "label": "Número de Execuções do DAG" + "label": "Número de Execuções do Dag" }, "dependencies": { "label": "Dependências", "options": { - "allDagDependencies": "Todas as Dependências do DAG", + "allDagDependencies": "Todas as Dependências do Dag", "externalConditions": "Condições Externas", "onlyTasks": "Somente Tarefas" }, @@ -126,11 +126,11 @@ "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" + "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.", + "description": "O Dag deve ser reprocessado em breve.", "title": "Solicitação de Reprocessamento Enviada com Sucesso" } } 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 index 6ac6422c19ed1..138a7b9293139 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dags.json @@ -2,11 +2,11 @@ "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." + "button": "Excluir Dag", + "warning": "Isso removerá todas as metadados relacionados ao Dag, incluindo Execuções e Tarefas." } }, - "favoriteDag": "DAG Favorito", + "favoriteDag": "Dag Favorito", "filters": { "allRunTypes": "Todos os Tipos de Execução", "allStates": "Todos os Estados", @@ -20,7 +20,7 @@ "all": "Todos", "paused": "Pausado" }, - "runIdPatternFilter": "Pesquisar Execuções de DAG" + "runIdPatternFilter": "Pesquisar Execuções de Dag" }, "ownerLink": "Link do Proprietário para {{owner}}", "runAndTaskActions": { @@ -69,7 +69,7 @@ "search": { "advanced": "Pesquisa Avançada", "clear": "Limpar pesquisa", - "dags": "Pesquisar DAGs", + "dags": "Pesquisar Dags", "hotkey": "+K", "tasks": "Pesquisar Tarefas" }, @@ -87,10 +87,10 @@ "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)" + "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" + "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 index 725875494a1bc..d5245e4b6236e 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/dashboard.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/dashboard.json @@ -1,15 +1,15 @@ { "favorite": { - "favoriteDags_many": "Primeiros {{count}} DAGs favoritos", - "favoriteDags_one": "Primeiro {{count}} DAG favorito", - "favoriteDags_other": "Primeiros {{count}} DAGs favoritos", - "favoriteDags_zero": "Nenhum DAG favorito", + "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." + "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", + "dagProcessor": "Processador de Dag", "health": "Saúde", "healthy": "Saúde", "lastHeartbeat": "Último Heartbeat", @@ -21,10 +21,10 @@ }, "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", + "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" }, @@ -37,11 +37,11 @@ }, "source": "Fonte", "stats": { - "activeDags": "DAGs Ativos", - "failedDags": "DAGs Falhados", - "queuedDags": "DAGs em Fila", + "activeDags": "Dags Ativos", + "failedDags": "Dags Falhados", + "queuedDags": "Dags em Fila", "requiredActions": "Ações Necessárias", - "runningDags": "DAGs em Execução", + "runningDags": "Dags em Execução", "stats": "Estatísticas" }, "uri": "URI", 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 index f55f65988278d..c7a609906f7b9 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json @@ -32,7 +32,7 @@ "dag_other": "Dags", "dagDetails": { "catchup": "Catchup", - "dagRunTimeout": "ระยะหมดเวลาของ DAG Run", + "dagRunTimeout": "ระยะหมดเวลาของ Dag Run", "defaultArgs": "ค่า Arguments เริ่มต้น", "description": "คำอธิบาย", "documentation": "เอกสารประกอบ Dag", 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 index 5c59ecd6908a1..d632937f24dcb 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/th/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/dag.json @@ -121,10 +121,10 @@ "toaster": { "error": { "description": "การร้องขอประมวลผล Dag ล้มเหลว อาจมีคำขอการประมวลผลที่รอดำเนินการอยู่", - "title": "การประมวลผล DAG ล้มเหลว" + "title": "การประมวลผล Dag ล้มเหลว" }, "success": { - "description": "DAG จะถูกประมวลผลใหม่ในเร็ว ๆ นี้", + "description": "Dag จะถูกประมวลผลใหม่ในเร็ว ๆ นี้", "title": "ส่งคำขอประมวลผลใหม่สำเร็จแล้ว" } } 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..0cb5c8b276a0c 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": { 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 04be0181046d4..673d0f00db68b 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 @@ -89,6 +89,7 @@ "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" }, 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 index 652011da2bea8..b52f071f60775 100644 --- 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 @@ -87,7 +87,7 @@ }, "limitedList": "+ 其他 {{count}} 项", "limitedList.allItems": "所有 {{count}} 项:", - "limitedList.clickToInteract": "点击标签以筛选 DAGs", + "limitedList.clickToInteract": "点击标签以筛选 Dags", "limitedList.clickToOpenFull": "点击 \"+{{count}} 更多\" 打开完整视图", "limitedList.copyPasteText": "你可以复制并粘贴上方文本", "logs": { 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/DagRunInfo.tsx b/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx index 0fa1cab76585a..fe6370fb8ddf4 100644 --- a/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx +++ b/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx @@ -42,7 +42,7 @@ const DagRunInfo = ({ endDate, logicalDate, runAfter, startDate, state }: Props) {state === undefined ? undefined : ( - {translate("state")}: {state} + {translate("state")}: {translate(`common:states.${state}`)} )} {Boolean(logicalDate) ? ( diff --git a/airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx b/airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx deleted file mode 100644 index 46466858caec6..0000000000000 --- a/airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Portal } from "@chakra-ui/react"; -import { useState, useRef, useCallback, cloneElement } from "react"; -import type { ReactElement, ReactNode, RefObject } from "react"; - -type Props = { - readonly children: ReactElement; - readonly delayMs?: number; - readonly tooltip: (triggerRef: RefObject) => ReactNode; -}; - -export const HoverTooltip = ({ children, delayMs = 200, tooltip }: Props) => { - const triggerRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const timeoutRef = useRef(); - - const handleMouseEnter = useCallback(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - setIsOpen(true); - }, delayMs); - }, [delayMs]); - - const handleMouseLeave = useCallback(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = undefined; - } - setIsOpen(false); - }, []); - - const trigger = cloneElement(children, { - onMouseEnter: handleMouseEnter, - onMouseLeave: handleMouseLeave, - ref: triggerRef, - }); - - return ( - <> - {trigger} - {Boolean(isOpen) && {tooltip(triggerRef)}} - - ); -}; diff --git a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx index 025463dc955f6..a3494ee4a0b01 100644 --- a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx +++ b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx @@ -43,7 +43,10 @@ const TaskInstanceTooltip = ({ children, positioning, taskInstance, ...rest }: P content={ - {translate("state")}: {taskInstance.state} + {translate("state")}:{" "} + {taskInstance.state + ? translate(`common:states.${taskInstance.state}`) + : translate("common:states.no_status")} {"dag_run_id" in taskInstance ? ( diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridButton.tsx index f3dd0b3dc077f..c452cf43a7953 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridButton.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridButton.tsx @@ -17,9 +17,11 @@ * under the License. */ import { Flex, type FlexProps } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import type { DagRunState, TaskInstanceState } from "openapi/requests/types.gen"; +import { BasicTooltip } from "src/components/BasicTooltip"; type Props = { readonly dagId: string; @@ -41,39 +43,53 @@ export const GridButton = ({ state, taskId, ...rest -}: Props) => - isGroup ? ( - - {children} - - ) : ( - +}: Props) => { + const { t: translate } = useTranslation(); + + const tooltipContent = ( + <> + {label} +
+ {translate("state")}:{" "} + {state ? translate(`common:states.${state}`) : translate("common:states.no_status")} + + ); + + return isGroup ? ( + {children} - + + ) : ( + + + + {children} + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx index ddf294353cb70..d4d775da97771 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx @@ -23,9 +23,9 @@ import { useTranslation } from "react-i18next"; import { Link, useLocation, useParams, useSearchParams } from "react-router-dom"; import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; +import { BasicTooltip } from "src/components/BasicTooltip"; import { StateIcon } from "src/components/StateIcon"; import Time from "src/components/Time"; -import { Tooltip } from "src/components/ui"; import { type HoverContextType, useHover } from "src/context/hover"; import { buildTaskInstanceUrl } from "src/utils/links"; @@ -106,35 +106,38 @@ const Instance = ({ dagId, instance, isGroup, isMapped, onClick, runId, taskId } py={0} transition="background-color 0.2s" > - + {translate("taskId")}: {taskId} +
+ {translate("state")}:{" "} + {instance.state + ? translate(`common:states.${instance.state}`) + : translate("common:states.no_status")} + {instance.min_start_date !== null && ( + <> +
+ {translate("startDate")}: