This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 4cb967fd5c2 Add /ui/assets endpoint with last-event sort and use it in
the Assets list (#69940)
4cb967fd5c2 is described below
commit 4cb967fd5c2304e65d05721100d6d59d2b27b266
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu Jul 30 04:32:23 2026 -0400
Add /ui/assets endpoint with last-event sort and use it in the Assets list
(#69940)
The Assets list needs recency sorting and richer filtering (by group and
last
event time) that the public /assets endpoint intentionally does not expose.
Add a dedicated UI-only endpoint for those needs and point the Assets list
at
it, leaving the public API contract untouched.
---
.../src/airflow/api_fastapi/common/db/assets.py | 54 +++
.../src/airflow/api_fastapi/common/parameters.py | 6 +
.../api_fastapi/core_api/datamodels/assets.py | 29 ++
.../api_fastapi/core_api/openapi/_private_ui.yaml | 415 +++++++++++++++++++++
.../api_fastapi/core_api/routes/ui/assets.py | 97 ++++-
.../src/airflow/ui/openapi-gen/queries/common.ts | 21 ++
.../ui/openapi-gen/queries/ensureQueryData.ts | 41 ++
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 41 ++
.../src/airflow/ui/openapi-gen/queries/queries.ts | 41 ++
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 41 ++
.../ui/openapi-gen/requests/services.gen.ts | 53 ++-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 60 +++
.../airflow/ui/public/i18n/locales/en/assets.json | 5 +-
.../src/airflow/ui/src/constants/filterConfigs.tsx | 17 +-
.../src/airflow/ui/src/constants/searchParams.ts | 4 +
.../airflow/ui/src/pages/AssetsList/AssetsList.tsx | 84 ++---
.../src/airflow/ui/src/utils/useFiltersHandler.ts | 2 +
.../api_fastapi/core_api/routes/ui/test_assets.py | 286 +++++++++++++-
18 files changed, 1245 insertions(+), 52 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/common/db/assets.py
b/airflow-core/src/airflow/api_fastapi/common/db/assets.py
new file mode 100644
index 00000000000..e393ea19060
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/db/assets.py
@@ -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.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from sqlalchemy import func, select
+from sqlalchemy.orm import subqueryload
+
+from airflow.models.asset import AssetEvent, AssetModel, AssetWatcherModel
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql import Select
+
+
+def generate_assets_with_last_event_query() -> Select:
+ """Fetch Assets outer-joined to their latest AssetEvent id/timestamp."""
+ max_asset_event_id_query = (
+ select(AssetEvent.asset_id,
func.max(AssetEvent.id).label("max_asset_event_id"))
+ .group_by(AssetEvent.asset_id)
+ .subquery()
+ )
+
+ return (
+ select(
+ AssetModel,
+ AssetEvent.id.label("last_asset_event_id"),
+ AssetEvent.timestamp.label("last_asset_event_timestamp"),
+ )
+ .outerjoin(max_asset_event_id_query, AssetModel.id ==
max_asset_event_id_query.c.asset_id)
+ .outerjoin(AssetEvent, AssetEvent.id ==
max_asset_event_id_query.c.max_asset_event_id)
+ .options(
+ subqueryload(AssetModel.scheduled_dags),
+ subqueryload(AssetModel.producing_tasks),
+ subqueryload(AssetModel.consuming_tasks),
+ subqueryload(AssetModel.aliases),
+
subqueryload(AssetModel.watchers).joinedload(AssetWatcherModel.trigger),
+ )
+ )
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 1284fdacb66..76fe643fd6b 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -1812,6 +1812,12 @@ QueryUriExactMatch = Annotated[
)
),
]
+QueryAssetGroupPatternSearch = Annotated[
+ _SearchParam, Depends(search_param_factory(AssetModel.group,
"group_pattern"))
+]
+QueryAssetGroupPrefixPatternSearch = Annotated[
+ _PrefixSearchParam, Depends(prefix_search_param_factory(AssetModel.group,
"group_prefix_pattern"))
+]
QueryAssetAliasNamePatternSearch = Annotated[
_SearchParam, Depends(search_param_factory(AssetAliasModel.name,
"name_pattern"))
]
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 3be0707bee1..8966891cf8b 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
@@ -40,6 +40,7 @@ from airflow.models.base import ID_LEN
from airflow.utils.types import DagRunType
if TYPE_CHECKING:
+ from airflow.models.asset import AssetModel
from airflow.serialization.definitions.dag import SerializedDAG
@@ -106,6 +107,34 @@ class AssetResponse(BaseModel):
def redact_extra(cls, v: dict):
return redact(v)
+ @classmethod
+ def from_asset_row(
+ cls,
+ asset: AssetModel,
+ last_asset_event_id: int | None,
+ last_asset_event_timestamp: datetime | None,
+ ) -> AssetResponse:
+ """Build a response from an ``AssetModel`` row joined with its last
AssetEvent id/timestamp."""
+ watchers_data = [
+ {
+ "name": watcher.name,
+ "trigger_id": watcher.trigger_id,
+ "created_date": watcher.trigger.created_date,
+ }
+ for watcher in asset.watchers
+ ]
+ return cls.model_validate(
+ {
+ **asset.__dict__,
+ "aliases": asset.aliases,
+ "watchers": watchers_data,
+ "last_asset_event": {
+ "id": last_asset_event_id,
+ "timestamp": last_asset_event_timestamp,
+ },
+ }
+ )
+
class AssetCollectionResponse(BaseModel):
"""Asset collection response."""
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 2237e622d75..a6d31b9956b 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
@@ -81,6 +81,206 @@ paths:
security:
- OAuth2PasswordBearer: []
- HTTPBearer: []
+ /ui/assets:
+ get:
+ tags:
+ - Asset
+ summary: Get Assets
+ description: Get assets. Like the public endpoint, but also supports
sorting
+ by group and last asset event timestamp.
+ operationId: get_assets_ui
+ security:
+ - OAuth2PasswordBearer: []
+ - HTTPBearer: []
+ parameters:
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: integer
+ minimum: 0
+ default: 50
+ title: Limit
+ - name: offset
+ in: query
+ required: false
+ schema:
+ type: integer
+ minimum: 0
+ default: 0
+ title: Offset
+ - name: name_pattern
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: "Case-insensitive substring match (SQL `ILIKE`). Slower
than\
+ \ `name_prefix_pattern` on large tables \u2014 see \"Filtering
with pattern\
+ \ parameters\"."
+ title: Name Pattern
+ description: "Case-insensitive substring match (SQL `ILIKE`). Slower
than\
+ \ `name_prefix_pattern` on large tables \u2014 see \"Filtering with
pattern\
+ \ parameters\"."
+ - name: name_prefix_pattern
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Case-sensitive, index-friendly prefix match. See
"Filtering
+ with pattern parameters".
+ title: Name Prefix Pattern
+ description: Case-sensitive, index-friendly prefix match. See
"Filtering with
+ pattern parameters".
+ - name: uri
+ in: query
+ required: false
+ schema:
+ type: array
+ items:
+ type: string
+ description: Exact-match filter on the full asset URI. Compiles to
an indexed
+ equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``)
+ to match multiple assets.
+ title: Uri
+ description: Exact-match filter on the full asset URI. Compiles to an
indexed
+ equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``)
+ to match multiple assets.
+ - name: uri_pattern
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: "Case-insensitive substring match (SQL `ILIKE`). Slower
than\
+ \ `uri_prefix_pattern` on large tables \u2014 see \"Filtering with
pattern\
+ \ parameters\"."
+ title: Uri Pattern
+ description: "Case-insensitive substring match (SQL `ILIKE`). Slower
than\
+ \ `uri_prefix_pattern` on large tables \u2014 see \"Filtering with
pattern\
+ \ parameters\"."
+ - name: uri_prefix_pattern
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Case-sensitive, index-friendly prefix match. See
"Filtering
+ with pattern parameters".
+ title: Uri Prefix Pattern
+ description: Case-sensitive, index-friendly prefix match. See
"Filtering with
+ pattern parameters".
+ - name: group_pattern
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: "Case-insensitive substring match (SQL `ILIKE`). Slower
than\
+ \ `group_prefix_pattern` on large tables \u2014 see \"Filtering
with pattern\
+ \ parameters\"."
+ title: Group Pattern
+ description: "Case-insensitive substring match (SQL `ILIKE`). Slower
than\
+ \ `group_prefix_pattern` on large tables \u2014 see \"Filtering with
pattern\
+ \ parameters\"."
+ - name: group_prefix_pattern
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Case-sensitive, index-friendly prefix match. See
"Filtering
+ with pattern parameters".
+ title: Group Prefix Pattern
+ description: Case-sensitive, index-friendly prefix match. See
"Filtering with
+ pattern parameters".
+ - name: dag_ids
+ in: query
+ required: false
+ schema:
+ type: array
+ items:
+ type: string
+ title: Dag Ids
+ - name: only_active
+ in: query
+ required: false
+ schema:
+ type: boolean
+ default: true
+ title: Only Active
+ - name: last_asset_event_timestamp_gte
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Last Asset Event Timestamp Gte
+ - name: last_asset_event_timestamp_gt
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Last Asset Event Timestamp Gt
+ - name: last_asset_event_timestamp_lte
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Last Asset Event Timestamp Lte
+ - name: last_asset_event_timestamp_lt
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Last Asset Event Timestamp Lt
+ - name: order_by
+ in: query
+ required: false
+ schema:
+ 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, group, created_at, updated_at, last_asset_event_timestamp`'
+ default:
+ - -last_asset_event_timestamp
+ title: Order By
+ description: 'Attributes to order by, multi criteria sort is
supported. Prefix
+ with `-` for descending order. Supported attributes: `id, name, uri,
group,
+ created_at, updated_at, last_asset_event_timestamp`'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AssetCollectionResponse'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
/ui/next_run_assets/{dag_id}:
get:
tags:
@@ -1918,6 +2118,40 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
+ AssetAliasResponse:
+ properties:
+ id:
+ type: integer
+ title: Id
+ name:
+ type: string
+ title: Name
+ group:
+ type: string
+ title: Group
+ type: object
+ required:
+ - id
+ - name
+ - group
+ title: AssetAliasResponse
+ description: Asset alias serializer for responses.
+ AssetCollectionResponse:
+ properties:
+ assets:
+ items:
+ $ref: '#/components/schemas/AssetResponse'
+ type: array
+ title: Assets
+ total_entries:
+ type: integer
+ title: Total Entries
+ type: object
+ required:
+ - assets
+ - total_entries
+ title: AssetCollectionResponse
+ description: Asset collection response.
AssetExpressionAlias:
properties:
alias:
@@ -2035,6 +2269,98 @@ components:
title: AssetExpressionRef
description: 'An unresolved asset reference leaf: ``{"asset_ref":
{"name": ...}}``
or ``{"asset_ref": {"uri": ...}}``.'
+ AssetResponse:
+ properties:
+ id:
+ type: integer
+ title: Id
+ name:
+ type: string
+ title: Name
+ uri:
+ type: string
+ title: Uri
+ group:
+ type: string
+ title: Group
+ extra:
+ anyOf:
+ - additionalProperties:
+ $ref: '#/components/schemas/JsonValue'
+ type: object
+ - type: 'null'
+ title: Extra
+ created_at:
+ type: string
+ format: date-time
+ title: Created At
+ updated_at:
+ type: string
+ format: date-time
+ title: Updated At
+ scheduled_dags:
+ items:
+ $ref: '#/components/schemas/DagScheduleAssetReference'
+ type: array
+ title: Scheduled Dags
+ producing_tasks:
+ items:
+ $ref: '#/components/schemas/TaskOutletAssetReference'
+ type: array
+ title: Producing Tasks
+ consuming_tasks:
+ items:
+ $ref: '#/components/schemas/TaskInletAssetReference'
+ type: array
+ title: Consuming Tasks
+ aliases:
+ items:
+ $ref: '#/components/schemas/AssetAliasResponse'
+ type: array
+ title: Aliases
+ watchers:
+ items:
+ $ref: '#/components/schemas/AssetWatcherResponse'
+ type: array
+ title: Watchers
+ last_asset_event:
+ anyOf:
+ - $ref: '#/components/schemas/LastAssetEventResponse'
+ - type: 'null'
+ type: object
+ required:
+ - id
+ - name
+ - uri
+ - group
+ - created_at
+ - updated_at
+ - scheduled_dags
+ - producing_tasks
+ - consuming_tasks
+ - aliases
+ - watchers
+ title: AssetResponse
+ description: Asset serializer for responses.
+ AssetWatcherResponse:
+ properties:
+ name:
+ type: string
+ title: Name
+ trigger_id:
+ type: integer
+ title: Trigger Id
+ created_date:
+ type: string
+ format: date-time
+ title: Created Date
+ type: object
+ required:
+ - name
+ - trigger_id
+ - created_date
+ title: AssetWatcherResponse
+ description: Asset watcher serializer for responses.
AuthenticatedMeResponse:
properties:
id:
@@ -2786,6 +3112,27 @@ components:
- asset_materialization
title: DagRunType
description: Class with DagRun types.
+ DagScheduleAssetReference:
+ properties:
+ dag_id:
+ type: string
+ title: Dag Id
+ created_at:
+ type: string
+ format: date-time
+ title: Created At
+ updated_at:
+ type: string
+ format: date-time
+ title: Updated At
+ additionalProperties: false
+ type: object
+ required:
+ - dag_id
+ - created_at
+ - updated_at
+ title: DagScheduleAssetReference
+ description: Dag schedule reference serializer for assets.
DagTagResponse:
properties:
name:
@@ -3498,6 +3845,24 @@ components:
- unixname
title: JobResponse
description: Job serializer for responses.
+ JsonValue: {}
+ LastAssetEventResponse:
+ properties:
+ id:
+ anyOf:
+ - type: integer
+ minimum: 0.0
+ - type: 'null'
+ title: Id
+ timestamp:
+ anyOf:
+ - type: string
+ format: date-time
+ - type: 'null'
+ title: Timestamp
+ type: object
+ title: LastAssetEventResponse
+ description: Last asset event response serializer.
LightGridTaskInstanceSummary:
properties:
task_id:
@@ -4000,6 +4365,31 @@ components:
- nodes
title: StructureDataResponse
description: Structure Data serializer for responses.
+ TaskInletAssetReference:
+ properties:
+ dag_id:
+ type: string
+ title: Dag Id
+ task_id:
+ type: string
+ title: Task Id
+ created_at:
+ type: string
+ format: date-time
+ title: Created At
+ updated_at:
+ type: string
+ format: date-time
+ title: Updated At
+ additionalProperties: false
+ type: object
+ required:
+ - dag_id
+ - task_id
+ - created_at
+ - updated_at
+ title: TaskInletAssetReference
+ description: Task inlet reference serializer for assets.
TaskInstanceResponse:
properties:
id:
@@ -4273,6 +4663,31 @@ components:
- awaiting_input
title: TaskInstanceStateCount
description: TaskInstance serializer for responses.
+ TaskOutletAssetReference:
+ properties:
+ dag_id:
+ type: string
+ title: Dag Id
+ task_id:
+ type: string
+ title: Task Id
+ created_at:
+ type: string
+ format: date-time
+ title: Created At
+ updated_at:
+ type: string
+ format: date-time
+ title: Updated At
+ additionalProperties: false
+ type: object
+ required:
+ - dag_id
+ - task_id
+ - created_at
+ - updated_at
+ title: TaskOutletAssetReference
+ description: Task outlet reference serializer for assets.
TeamCollectionResponse:
properties:
teams:
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 d3da95178a1..61384896625 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
@@ -16,20 +16,42 @@
# under the License.
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Annotated, Any, cast
import structlog
from fastapi import Depends, HTTPException, status
from sqlalchemy import ColumnElement, and_, case, exists, func, select, true
-from airflow.api_fastapi.common.db.common import SessionDep
+from airflow.api_fastapi.common.db.assets import
generate_assets_with_last_event_query
+from airflow.api_fastapi.common.db.common import SessionDep, paginated_select
+from airflow.api_fastapi.common.parameters import (
+ QueryAssetDagIdPatternSearch,
+ QueryAssetGroupPatternSearch,
+ QueryAssetGroupPrefixPatternSearch,
+ QueryAssetNamePatternSearch,
+ QueryAssetNamePrefixPatternSearch,
+ QueryLimit,
+ QueryOffset,
+ QueryUriExactMatch,
+ QueryUriPatternSearch,
+ QueryUriPrefixPatternSearch,
+ RangeFilter,
+ SortParam,
+ datetime_range_filter_factory,
+)
from airflow.api_fastapi.common.partition_helpers import
load_partitioned_timetable
from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.datamodels.assets import
AssetCollectionResponse, AssetResponse
from airflow.api_fastapi.core_api.datamodels.ui.assets import (
NextRunAssetEventResponse,
NextRunAssetsResponse,
)
-from airflow.api_fastapi.core_api.security import requires_access_asset,
requires_access_dag
+from airflow.api_fastapi.core_api.routes.public.assets import OnlyActiveFilter
+from airflow.api_fastapi.core_api.security import (
+ requires_access_asset,
+ requires_access_asset_alias,
+ requires_access_dag,
+)
from airflow.models import DagModel
from airflow.models.asset import (
AssetActive,
@@ -49,6 +71,75 @@ log = structlog.get_logger(logger_name=__name__)
assets_router = AirflowRouter(tags=["Asset"])
+@assets_router.get(
+ "/assets",
+ dependencies=[
+ Depends(requires_access_asset(method="GET")),
+ Depends(requires_access_asset_alias(method="GET")),
+ ],
+ operation_id="get_assets_ui",
+)
+def get_assets(
+ limit: QueryLimit,
+ offset: QueryOffset,
+ name_pattern: QueryAssetNamePatternSearch,
+ name_prefix_pattern: QueryAssetNamePrefixPatternSearch,
+ uri: QueryUriExactMatch,
+ uri_pattern: QueryUriPatternSearch,
+ uri_prefix_pattern: QueryUriPrefixPatternSearch,
+ group_pattern: QueryAssetGroupPatternSearch,
+ group_prefix_pattern: QueryAssetGroupPrefixPatternSearch,
+ dag_ids: QueryAssetDagIdPatternSearch,
+ only_active: Annotated[OnlyActiveFilter,
Depends(OnlyActiveFilter.depends)],
+ last_asset_event_timestamp_range: Annotated[
+ RangeFilter,
+ Depends(
+ datetime_range_filter_factory(
+ "last_asset_event_timestamp", AssetEvent,
attribute_name="timestamp"
+ )
+ ),
+ ],
+ order_by: Annotated[
+ SortParam,
+ Depends(
+ SortParam(
+ ["id", "name", "uri", "group", "created_at", "updated_at"],
+ AssetModel,
+ {"last_asset_event_timestamp": AssetEvent.timestamp},
+ ).dynamic_depends(default="-last_asset_event_timestamp")
+ ),
+ ],
+ session: SessionDep,
+) -> AssetCollectionResponse:
+ """Get assets. Like the public endpoint, but also supports sorting by
group and last asset event timestamp."""
+ assets_select, total_entries = paginated_select(
+ statement=generate_assets_with_last_event_query(),
+ filters=[
+ only_active,
+ name_pattern,
+ name_prefix_pattern,
+ uri,
+ uri_pattern,
+ uri_prefix_pattern,
+ group_pattern,
+ group_prefix_pattern,
+ dag_ids,
+ last_asset_event_timestamp_range,
+ ],
+ order_by=order_by,
+ offset=offset,
+ limit=limit,
+ session=session,
+ )
+
+ assets = [
+ AssetResponse.from_asset_row(asset, last_asset_event_id,
last_asset_event_timestamp)
+ for asset, last_asset_event_id, last_asset_event_timestamp in
session.execute(assets_select)
+ ]
+
+ return AssetCollectionResponse(assets=assets, total_entries=total_entries)
+
+
@assets_router.get(
"/next_run_assets/{dag_id}",
dependencies=[Depends(requires_access_asset(method="GET")),
Depends(requires_access_dag(method="GET"))],
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 64e87bd2a1d..0b1fda69bba 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -84,6 +84,27 @@ export const UseAssetServiceGetDagAssetQueuedEventKeyFn = ({
assetId, before, da
before?: string;
dagId: string;
}, queryKey?: Array<unknown>) => [useAssetServiceGetDagAssetQueuedEventKey,
...(queryKey ?? [{ assetId, before, dagId }])];
+export type AssetServiceGetAssetsUiDefaultResponse = Awaited<ReturnType<typeof
AssetService.getAssetsUi>>;
+export type AssetServiceGetAssetsUiQueryResult<TData =
AssetServiceGetAssetsUiDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
+export const useAssetServiceGetAssetsUiKey = "AssetServiceGetAssetsUi";
+export const UseAssetServiceGetAssetsUiKeyFn = ({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
+ dagIds?: string[];
+ groupPattern?: string;
+ groupPrefixPattern?: string;
+ lastAssetEventTimestampGt?: string;
+ lastAssetEventTimestampGte?: string;
+ lastAssetEventTimestampLt?: string;
+ lastAssetEventTimestampLte?: string;
+ limit?: number;
+ namePattern?: string;
+ namePrefixPattern?: string;
+ offset?: number;
+ onlyActive?: boolean;
+ orderBy?: string[];
+ uri?: string[];
+ uriPattern?: string;
+ uriPrefixPattern?: string;
+} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetsUiKey,
...(queryKey ?? [{ dagIds, groupPattern, groupPrefixPattern,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }])];
export type AssetServiceNextRunAssetsDefaultResponse =
Awaited<ReturnType<typeof AssetService.nextRunAssets>>;
export type AssetServiceNextRunAssetsQueryResult<TData =
AssetServiceNextRunAssetsDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useAssetServiceNextRunAssetsKey = "AssetServiceNextRunAssets";
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 c3c73d7d427..091573243df 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -159,6 +159,47 @@ export const
ensureUseAssetServiceGetDagAssetQueuedEventData = (queryClient: Que
dagId: string;
}) => queryClient.ensureQueryData({ queryKey:
Common.UseAssetServiceGetDagAssetQueuedEventKeyFn({ assetId, before, dagId }),
queryFn: () => AssetService.getDagAssetQueuedEvent({ assetId, before, dagId })
});
/**
+* Get Assets
+* Get assets. Like the public endpoint, but also supports sorting by group and
last asset event timestamp.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.namePattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `name_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.namePrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.uri Exact-match filter on the full asset URI. Compiles to an
indexed equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``) to match multiple assets.
+* @param data.uriPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `uri_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.uriPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
+* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.dagIds
+* @param data.onlyActive
+* @param data.lastAssetEventTimestampGte
+* @param data.lastAssetEventTimestampGt
+* @param data.lastAssetEventTimestampLte
+* @param data.lastAssetEventTimestampLt
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
name, uri, group, created_at, updated_at, last_asset_event_timestamp`
+* @returns AssetCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseAssetServiceGetAssetsUiData = (queryClient: QueryClient,
{ dagIds, groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: {
+ dagIds?: string[];
+ groupPattern?: string;
+ groupPrefixPattern?: string;
+ lastAssetEventTimestampGt?: string;
+ lastAssetEventTimestampGte?: string;
+ lastAssetEventTimestampLt?: string;
+ lastAssetEventTimestampLte?: string;
+ limit?: number;
+ namePattern?: string;
+ namePrefixPattern?: string;
+ offset?: number;
+ onlyActive?: boolean;
+ orderBy?: string[];
+ uri?: string[];
+ uriPattern?: string;
+ uriPrefixPattern?: string;
+} = {}) => queryClient.ensureQueryData({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }), queryFn: () => AssetService.getAssetsUi({ dagIds,
groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, las [...]
+/**
* Next Run Assets
* @param data The data for the request.
* @param data.dagId
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 53e93745026..2550f816011 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -159,6 +159,47 @@ export const prefetchUseAssetServiceGetDagAssetQueuedEvent
= (queryClient: Query
dagId: string;
}) => queryClient.prefetchQuery({ queryKey:
Common.UseAssetServiceGetDagAssetQueuedEventKeyFn({ assetId, before, dagId }),
queryFn: () => AssetService.getDagAssetQueuedEvent({ assetId, before, dagId })
});
/**
+* Get Assets
+* Get assets. Like the public endpoint, but also supports sorting by group and
last asset event timestamp.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.namePattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `name_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.namePrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.uri Exact-match filter on the full asset URI. Compiles to an
indexed equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``) to match multiple assets.
+* @param data.uriPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `uri_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.uriPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
+* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.dagIds
+* @param data.onlyActive
+* @param data.lastAssetEventTimestampGte
+* @param data.lastAssetEventTimestampGt
+* @param data.lastAssetEventTimestampLte
+* @param data.lastAssetEventTimestampLt
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
name, uri, group, created_at, updated_at, last_asset_event_timestamp`
+* @returns AssetCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseAssetServiceGetAssetsUi = (queryClient: QueryClient, {
dagIds, groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: {
+ dagIds?: string[];
+ groupPattern?: string;
+ groupPrefixPattern?: string;
+ lastAssetEventTimestampGt?: string;
+ lastAssetEventTimestampGte?: string;
+ lastAssetEventTimestampLt?: string;
+ lastAssetEventTimestampLte?: string;
+ limit?: number;
+ namePattern?: string;
+ namePrefixPattern?: string;
+ offset?: number;
+ onlyActive?: boolean;
+ orderBy?: string[];
+ uri?: string[];
+ uriPattern?: string;
+ uriPrefixPattern?: string;
+} = {}) => queryClient.prefetchQuery({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }), queryFn: () => AssetService.getAssetsUi({ dagIds,
groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastA [...]
+/**
* Next Run Assets
* @param data The data for the request.
* @param data.dagId
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 49f1fa6d734..9da0d1e5ab1 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -159,6 +159,47 @@ export const useAssetServiceGetDagAssetQueuedEvent =
<TData = Common.AssetServic
dagId: string;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetDagAssetQueuedEventKeyFn({ assetId, before, dagId },
queryKey), queryFn: () => AssetService.getDagAssetQueuedEvent({ assetId,
before, dagId }) as TData, ...options });
/**
+* Get Assets
+* Get assets. Like the public endpoint, but also supports sorting by group and
last asset event timestamp.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.namePattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `name_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.namePrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.uri Exact-match filter on the full asset URI. Compiles to an
indexed equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``) to match multiple assets.
+* @param data.uriPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `uri_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.uriPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
+* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.dagIds
+* @param data.onlyActive
+* @param data.lastAssetEventTimestampGte
+* @param data.lastAssetEventTimestampGt
+* @param data.lastAssetEventTimestampLte
+* @param data.lastAssetEventTimestampLt
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
name, uri, group, created_at, updated_at, last_asset_event_timestamp`
+* @returns AssetCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useAssetServiceGetAssetsUi = <TData =
Common.AssetServiceGetAssetsUiDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagIds, groupPattern, groupPrefixPattern,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
+ dagIds?: string[];
+ groupPattern?: string;
+ groupPrefixPattern?: string;
+ lastAssetEventTimestampGt?: string;
+ lastAssetEventTimestampGte?: string;
+ lastAssetEventTimestampLt?: string;
+ lastAssetEventTimestampLte?: string;
+ limit?: number;
+ namePattern?: string;
+ namePrefixPattern?: string;
+ offset?: number;
+ onlyActive?: boolean;
+ orderBy?: string[];
+ uri?: string[];
+ uriPattern?: string;
+ uriPrefixPattern?: string;
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssetsUi({ [...]
+/**
* Next Run Assets
* @param data The data for the request.
* @param data.dagId
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 a67ccaa19ee..2b694fe3b5a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -159,6 +159,47 @@ export const useAssetServiceGetDagAssetQueuedEventSuspense
= <TData = Common.Ass
dagId: string;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetDagAssetQueuedEventKeyFn({ assetId, before, dagId },
queryKey), queryFn: () => AssetService.getDagAssetQueuedEvent({ assetId,
before, dagId }) as TData, ...options });
/**
+* Get Assets
+* Get assets. Like the public endpoint, but also supports sorting by group and
last asset event timestamp.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.namePattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `name_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.namePrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.uri Exact-match filter on the full asset URI. Compiles to an
indexed equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``) to match multiple assets.
+* @param data.uriPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `uri_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+* @param data.uriPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
+* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
+* @param data.dagIds
+* @param data.onlyActive
+* @param data.lastAssetEventTimestampGte
+* @param data.lastAssetEventTimestampGt
+* @param data.lastAssetEventTimestampLte
+* @param data.lastAssetEventTimestampLt
+* @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
name, uri, group, created_at, updated_at, last_asset_event_timestamp`
+* @returns AssetCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useAssetServiceGetAssetsUiSuspense = <TData =
Common.AssetServiceGetAssetsUiDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagIds, groupPattern, groupPrefixPattern,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
+ dagIds?: string[];
+ groupPattern?: string;
+ groupPrefixPattern?: string;
+ lastAssetEventTimestampGt?: string;
+ lastAssetEventTimestampGte?: string;
+ lastAssetEventTimestampLt?: string;
+ lastAssetEventTimestampLte?: string;
+ limit?: number;
+ namePattern?: string;
+ namePrefixPattern?: string;
+ offset?: number;
+ onlyActive?: boolean;
+ orderBy?: string[];
+ uri?: string[];
+ uriPattern?: string;
+ uriPrefixPattern?: string;
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAss [...]
+/**
* Next Run Assets
* @param data The data for the request.
* @param data.dagId
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 d34910b8276..c31f14f957b 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,
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
export class AssetService {
/**
@@ -407,6 +407,57 @@ export class AssetService {
});
}
+ /**
+ * Get Assets
+ * Get assets. Like the public endpoint, but also supports sorting by
group and last asset event timestamp.
+ * @param data The data for the request.
+ * @param data.limit
+ * @param data.offset
+ * @param data.namePattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `name_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+ * @param data.namePrefixPattern Case-sensitive, index-friendly prefix
match. See "Filtering with pattern parameters".
+ * @param data.uri Exact-match filter on the full asset URI. Compiles to
an indexed equality comparison (``uri = ...``). Repeat the parameter
(``?uri=a&uri=b``) to match multiple assets.
+ * @param data.uriPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `uri_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+ * @param data.uriPrefixPattern Case-sensitive, index-friendly prefix
match. See "Filtering with pattern parameters".
+ * @param data.groupPattern Case-insensitive substring match (SQL
`ILIKE`). Slower than `group_prefix_pattern` on large tables — see "Filtering
with pattern parameters".
+ * @param data.groupPrefixPattern Case-sensitive, index-friendly prefix
match. See "Filtering with pattern parameters".
+ * @param data.dagIds
+ * @param data.onlyActive
+ * @param data.lastAssetEventTimestampGte
+ * @param data.lastAssetEventTimestampGt
+ * @param data.lastAssetEventTimestampLte
+ * @param data.lastAssetEventTimestampLt
+ * @param data.orderBy Attributes to order by, multi criteria sort is
supported. Prefix with `-` for descending order. Supported attributes: `id,
name, uri, group, created_at, updated_at, last_asset_event_timestamp`
+ * @returns AssetCollectionResponse Successful Response
+ * @throws ApiError
+ */
+ public static getAssetsUi(data: GetAssetsUiData = {}):
CancelablePromise<GetAssetsUiResponse> {
+ return __request(OpenAPI, {
+ method: 'GET',
+ url: '/ui/assets',
+ query: {
+ limit: data.limit,
+ offset: data.offset,
+ name_pattern: data.namePattern,
+ name_prefix_pattern: data.namePrefixPattern,
+ uri: data.uri,
+ uri_pattern: data.uriPattern,
+ uri_prefix_pattern: data.uriPrefixPattern,
+ group_pattern: data.groupPattern,
+ group_prefix_pattern: data.groupPrefixPattern,
+ dag_ids: data.dagIds,
+ only_active: data.onlyActive,
+ last_asset_event_timestamp_gte:
data.lastAssetEventTimestampGte,
+ last_asset_event_timestamp_gt: data.lastAssetEventTimestampGt,
+ last_asset_event_timestamp_lte:
data.lastAssetEventTimestampLte,
+ last_asset_event_timestamp_lt: data.lastAssetEventTimestampLt,
+ order_by: data.orderBy
+ },
+ errors: {
+ 422: 'Validation Error'
+ }
+ });
+ }
+
/**
* Next Run Assets
* @param data The data for the request.
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 932326c9d6f..2c6a870e0e3 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
@@ -2988,6 +2988,51 @@ export type DeleteDagAssetQueuedEventData = {
export type DeleteDagAssetQueuedEventResponse = void;
+export type GetAssetsUiData = {
+ dagIds?: Array<(string)>;
+ /**
+ * Case-insensitive substring match (SQL `ILIKE`). Slower than
`group_prefix_pattern` on large tables — see "Filtering with pattern
parameters".
+ */
+ groupPattern?: string | null;
+ /**
+ * Case-sensitive, index-friendly prefix match. See "Filtering with
pattern parameters".
+ */
+ groupPrefixPattern?: string | null;
+ lastAssetEventTimestampGt?: string | null;
+ lastAssetEventTimestampGte?: string | null;
+ lastAssetEventTimestampLt?: string | null;
+ lastAssetEventTimestampLte?: string | null;
+ limit?: number;
+ /**
+ * Case-insensitive substring match (SQL `ILIKE`). Slower than
`name_prefix_pattern` on large tables — see "Filtering with pattern parameters".
+ */
+ namePattern?: string | null;
+ /**
+ * Case-sensitive, index-friendly prefix match. See "Filtering with
pattern parameters".
+ */
+ namePrefixPattern?: 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, group,
created_at, updated_at, last_asset_event_timestamp`
+ */
+ orderBy?: Array<(string)>;
+ /**
+ * Exact-match filter on the full asset URI. Compiles to an indexed
equality comparison (``uri = ...``). Repeat the parameter (``?uri=a&uri=b``) to
match multiple assets.
+ */
+ uri?: Array<(string)>;
+ /**
+ * Case-insensitive substring match (SQL `ILIKE`). Slower than
`uri_prefix_pattern` on large tables — see "Filtering with pattern parameters".
+ */
+ uriPattern?: string | null;
+ /**
+ * Case-sensitive, index-friendly prefix match. See "Filtering with
pattern parameters".
+ */
+ uriPrefixPattern?: string | null;
+};
+
+export type GetAssetsUiResponse = AssetCollectionResponse;
+
export type NextRunAssetsData = {
dagId: string;
};
@@ -5117,6 +5162,21 @@ export type $OpenApiTs = {
};
};
};
+ '/ui/assets': {
+ get: {
+ req: GetAssetsUiData;
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: AssetCollectionResponse;
+ /**
+ * Validation Error
+ */
+ 422: HTTPValidationError;
+ };
+ };
+ };
'/ui/next_run_assets/{dag_id}': {
get: {
req: NextRunAssetsData;
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 a5d044e6264..0b963c05f2a 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
@@ -42,7 +42,10 @@
"title": "Create Asset Event for {{name}}"
},
"events": "Events",
- "extra": "Extra",
+ "filters": {
+ "groupPlaceholder": "Search group",
+ "lastEventDateRange": "Last Event Date"
+ },
"group": "Group",
"lastAssetEvent": "Last Asset Event",
"name": "Name",
diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
index 7c29e276d7b..d29df264d34 100644
--- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
+++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
@@ -61,7 +61,7 @@ export enum FilterTypes {
}
export const useFilterConfigs = () => {
- const { t: translate } = useTranslation(["browse", "common", "components",
"admin", "hitl"]);
+ const { t: translate } = useTranslation(["assets", "browse", "common",
"components", "admin", "hitl"]);
const multiTeamEnabled = Boolean(useConfig("multi_team"));
const { data: teamsData } = useTeamsServiceListTeams({ orderBy: ["name"] },
undefined, {
enabled: multiTeamEnabled,
@@ -179,6 +179,14 @@ export const useFilterConfigs = () => {
label: translate("admin:jobs.columns.executorClass"),
type: FilterTypes.TEXT,
},
+ [SearchParamsKeys.GROUP_PATTERN]: {
+ hotkeyDisabled: true,
+ icon: <FiDatabase />,
+ label: translate("assets:group"),
+ placeholder: translate("assets:filters.groupPlaceholder"),
+ supportsAdvancedSearch: true,
+ type: FilterTypes.TEXT,
+ },
[SearchParamsKeys.HOSTNAME]: {
hotkeyDisabled: true,
icon: <MdComputer />,
@@ -209,6 +217,13 @@ export const useFilterConfigs = () => {
supportsAdvancedSearch: true,
type: FilterTypes.TEXT,
},
+ [SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_RANGE]: {
+ endKey: SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_LTE,
+ icon: <MdDateRange />,
+ label: translate("assets:filters.lastEventDateRange"),
+ startKey: SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_GTE,
+ type: FilterTypes.DATERANGE,
+ },
[SearchParamsKeys.LOGICAL_DATE_RANGE]: {
endKey: SearchParamsKeys.LOGICAL_DATE_LTE,
icon: <MdDateRange />,
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index 71f2b763e8a..b2ac5532911 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -54,11 +54,15 @@ export enum SearchParamsKeys {
GRAPH_OPERATOR = "graph-operator",
GRAPH_TASK_GROUP = "graph-task_group",
GRAPH_TASK_STATE = "graph-task_state",
+ GROUP_PATTERN = "group_pattern",
HOSTNAME = "hostname",
INCLUDED_EVENTS = "included_events",
JOB_STATE = "job_state",
JOB_TYPE = "job_type",
KEY_PATTERN = "key_pattern",
+ LAST_ASSET_EVENT_TIMESTAMP_GTE = "last_asset_event_timestamp_gte",
+ LAST_ASSET_EVENT_TIMESTAMP_LTE = "last_asset_event_timestamp_lte",
+ LAST_ASSET_EVENT_TIMESTAMP_RANGE = "last_asset_event_timestamp_range",
LAST_DAG_RUN_STATE = "last_dag_run_state",
LIMIT = "limit",
LOG_LEVEL = "log_level",
diff --git a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
index 150b57ba968..33518105231 100644
--- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
@@ -16,34 +16,35 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Flex, Heading, useDisclosure, VStack } from "@chakra-ui/react";
+import { Heading, VStack } from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
-import { useAssetServiceGetAssets } from "openapi/queries";
+import { useAssetServiceGetAssetsUi } from "openapi/queries";
import type { AssetResponse } 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 { FilterBar } from "src/components/FilterBar";
import { SearchBar } from "src/components/SearchBar";
import Time from "src/components/Time";
import { RouterLink } from "src/components/ui";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
-import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
+import { useAdvancedSearch, useAdvancedSearchArg } from
"src/hooks/useAdvancedSearch";
import { CreateAssetEvent } from "src/pages/Asset/CreateAssetEvent";
-import { useDocumentTitle } from "src/utils";
+import { useDocumentTitle, useFiltersHandler, type FilterableSearchParamsKeys
} from "src/utils";
import { DependencyPopover } from "./DependencyPopover";
+const assetsFilterKeys: Array<FilterableSearchParamsKeys> = [
+ SearchParamsKeys.GROUP_PATTERN,
+ SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_RANGE,
+];
+
type AssetRow = { row: { original: AssetResponse } };
-const createColumns = (
- translate: (key: string) => string,
- open?: boolean,
-): Array<ColumnDef<AssetResponse>> => [
+const createColumns = (translate: (key: string) => string):
Array<ColumnDef<AssetResponse>> => [
{
accessorKey: "name",
cell: ({ row: { original } }: AssetRow) => (
@@ -54,7 +55,7 @@ const createColumns = (
header: () => translate("name"),
},
{
- accessorKey: "last_asset_event",
+ accessorKey: "last_asset_event_timestamp",
cell: ({ row: { original } }: AssetRow) => {
const assetEvent = original.last_asset_event;
const timestamp = assetEvent?.timestamp;
@@ -65,12 +66,10 @@ const createColumns = (
return <Time datetime={timestamp} />;
},
- enableSorting: false,
header: () => translate("lastAssetEvent"),
},
{
accessorKey: "group",
- enableSorting: false,
header: () => translate("group"),
},
{
@@ -97,21 +96,6 @@ const createColumns = (
enableSorting: false,
header: "",
},
- {
- accessorKey: "extra",
- cell: ({ row: { original } }) => {
- if (original.extra !== null) {
- return <RenderedJsonField collapsed={!open} content={original.extra ??
{}} />;
- }
-
- return undefined;
- },
- enableSorting: false,
- header: translate("extra"),
- meta: {
- skeletonWidth: 200,
- },
- },
];
const { NAME_PATTERN, OFFSET }: SearchParamsKeysType = SearchParamsKeys;
@@ -129,18 +113,31 @@ export const AssetsList = () => {
const { setTableURLState, tableURLState } = useTableURLState();
const { pagination, sorting } = tableURLState;
const [sort] = sorting;
- const orderBy = sort ? [`${sort.desc ? "-" : ""}${sort.id}`] : undefined;
+ const orderBy = sort ? [`${sort.desc ? "-" : ""}${sort.id}`] :
["-last_asset_event_timestamp"];
- const { onClose, onOpen, open } = useDisclosure();
+ const { filterConfigs, handleFiltersChange, initialValues } =
useFiltersHandler(assetsFilterKeys);
- const { data, error, isLoading } = useAssetServiceGetAssets({
+ const lastAssetEventTimestampGte =
searchParams.get(SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_GTE);
+ const lastAssetEventTimestampLte =
searchParams.get(SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_LTE);
+ const groupArg = useAdvancedSearchArg({
+ patternApiKey: "groupPattern",
+ prefixApiKey: "groupPrefixPattern",
+ storageKey: SearchParamsKeys.GROUP_PATTERN,
+ value: searchParams.get(SearchParamsKeys.GROUP_PATTERN),
+ });
+
+ const { data, error, isLoading } = useAssetServiceGetAssetsUi({
+ ...groupArg,
+ lastAssetEventTimestampGte: lastAssetEventTimestampGte ?? undefined,
+ lastAssetEventTimestampLte: lastAssetEventTimestampLte ?? undefined,
limit: pagination.pageSize,
...(advancedSearch.enabled ? { namePattern } : { namePrefixPattern:
namePattern }),
offset: pagination.pageIndex * pagination.pageSize,
orderBy,
});
- const columns = createColumns(translate, open);
+ const columns = createColumns(translate);
+ const totalEntries = data?.total_entries ?? 0;
const handleSearchChange = (value: string) => {
setTableURLState({
@@ -166,18 +163,15 @@ export const AssetsList = () => {
placeholder={translate("searchPlaceholder")}
/>
- <Flex alignItems="center" justifyContent="space-between">
- <Heading py={3} size="md">
- {data?.total_entries} {translate("common:asset", { count:
data?.total_entries })}
- </Heading>
- <ExpandCollapseButtons
- collapseLabel={translate("common:collapseAllExtra")}
- expandLabel={translate("common:expandAllExtra")}
- isExpanded={open}
- onCollapse={onClose}
- onExpand={onOpen}
- />
- </Flex>
+ <FilterBar
+ configs={filterConfigs}
+ initialValues={initialValues}
+ onFiltersChange={handleFiltersChange}
+ />
+
+ <Heading py={3} size="md">
+ {totalEntries} {translate("common:asset", { count: totalEntries })}
+ </Heading>
</VStack>
<DataTable
columns={columns}
@@ -188,7 +182,7 @@ export const AssetsList = () => {
modelName="common:asset"
onStateChange={setTableURLState}
showRowCountHeading={false}
- total={data?.total_entries}
+ total={totalEntries}
/>
</>
);
diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
index 54f71369370..9349249c078 100644
--- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
+++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
@@ -73,10 +73,12 @@ export type FilterableSearchParamsKeys =
| SearchParamsKeys.EVENT_DATE_RANGE
| SearchParamsKeys.EVENT_TYPE
| SearchParamsKeys.EXECUTOR_CLASS
+ | SearchParamsKeys.GROUP_PATTERN
| SearchParamsKeys.HOSTNAME
| SearchParamsKeys.JOB_STATE
| SearchParamsKeys.JOB_TYPE
| SearchParamsKeys.KEY_PATTERN
+ | SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_RANGE
| SearchParamsKeys.LOGICAL_DATE_RANGE
| SearchParamsKeys.MAP_INDEX
| SearchParamsKeys.MISSED
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
index 810b809a131..05735154c5f 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
@@ -23,12 +23,15 @@ import pendulum
import pytest
from sqlalchemy import select
+from airflow.models import DagModel
from airflow.models.asset import (
AssetActive,
+ AssetAliasModel,
AssetDagRunQueue,
AssetEvent,
AssetModel,
AssetPartitionDagRun,
+ DagScheduleAssetReference,
PartitionedAssetKeyLog,
)
from airflow.partition_mappers.base import RollupMapper
@@ -39,7 +42,13 @@ from airflow.sdk.definitions.asset import Asset
from airflow.sdk.definitions.timetables.assets import PartitionedAssetTimetable
from tests_common.test_utils.asserts import assert_queries_count
-from tests_common.test_utils.db import clear_db_apdr, clear_db_dags,
clear_db_pakl, clear_db_serialized_dags
+from tests_common.test_utils.db import (
+ clear_db_apdr,
+ clear_db_assets,
+ clear_db_dags,
+ clear_db_pakl,
+ clear_db_serialized_dags,
+)
pytestmark = pytest.mark.db_test
@@ -471,3 +480,278 @@ class TestNextRunAssets:
body = response.json()
assert len(body["events"]) == 1
assert body["events"][0]["asset_inactive"] is True
+
+
+class TestGetAssetsUi:
+ @pytest.fixture(autouse=True)
+ def cleanup_assets(self):
+ clear_db_assets()
+
+ yield
+
+ clear_db_assets()
+
+ def test_should_respond_401(self, unauthenticated_test_client):
+ response = unauthenticated_test_client.get("/assets")
+ assert response.status_code == 401
+
+ def test_should_respond_403(self, unauthorized_test_client):
+ response = unauthorized_test_client.get("/assets")
+ assert response.status_code == 403
+
+ def test_should_respond_200(self, test_client, session):
+ asset = AssetModel(name="ui_asset", uri="s3://bucket/ui_asset",
group="asset")
+ session.add(asset)
+ session.add(AssetActive.for_asset(asset))
+ session.commit()
+
+ response = test_client.get("/assets")
+ assert response.status_code == 200
+ body = response.json()
+ assert body["total_entries"] == 1
+ assert body["assets"][0]["name"] == "ui_asset"
+
+ def test_sort_by_last_asset_event_timestamp(self, test_client, session):
+ older = AssetModel(name="older", uri="s3://bucket/older",
group="asset")
+ newer = AssetModel(name="newer", uri="s3://bucket/newer",
group="asset")
+ session.add_all([older, newer])
+ session.add(AssetActive.for_asset(older))
+ session.add(AssetActive.for_asset(newer))
+ session.flush()
+
+ base = pendulum.datetime(2024, 1, 1)
+ session.add(AssetEvent(asset_id=older.id, timestamp=base))
+ session.add(AssetEvent(asset_id=newer.id, timestamp=base.add(days=1)))
+ session.commit()
+
+ response =
test_client.get("/assets?order_by=last_asset_event_timestamp")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["older",
"newer"]
+
+ response =
test_client.get("/assets?order_by=-last_asset_event_timestamp")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["newer",
"older"]
+
+ def test_default_sort_is_last_asset_event_timestamp_desc(self,
test_client, session):
+ older = AssetModel(name="older", uri="s3://bucket/older_default",
group="asset")
+ newer = AssetModel(name="newer", uri="s3://bucket/newer_default",
group="asset")
+ session.add_all([older, newer])
+ session.add(AssetActive.for_asset(older))
+ session.add(AssetActive.for_asset(newer))
+ session.flush()
+
+ base = pendulum.datetime(2024, 1, 1)
+ session.add(AssetEvent(asset_id=older.id, timestamp=base))
+ session.add(AssetEvent(asset_id=newer.id, timestamp=base.add(days=1)))
+ session.commit()
+
+ response = test_client.get("/assets")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["newer",
"older"]
+
+ def test_sort_by_group(self, test_client, session):
+ billing = AssetModel(name="billing_asset",
uri="s3://bucket/billing_sort", group="billing")
+ marketing = AssetModel(name="marketing_asset",
uri="s3://bucket/marketing_sort", group="marketing")
+ session.add_all([billing, marketing])
+ session.add(AssetActive.for_asset(billing))
+ session.add(AssetActive.for_asset(marketing))
+ session.commit()
+
+ response = test_client.get("/assets?order_by=group")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] ==
["billing_asset", "marketing_asset"]
+
+ response = test_client.get("/assets?order_by=-group")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] ==
["marketing_asset", "billing_asset"]
+
+ def test_filter_by_group_pattern(self, test_client, session):
+ billing = AssetModel(name="billing_asset", uri="s3://bucket/billing",
group="billing")
+ marketing = AssetModel(name="marketing_asset",
uri="s3://bucket/marketing", group="marketing")
+ session.add_all([billing, marketing])
+ session.add(AssetActive.for_asset(billing))
+ session.add(AssetActive.for_asset(marketing))
+ session.commit()
+
+ response = test_client.get("/assets?group_pattern=bill")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] ==
["billing_asset"]
+
+ def test_filter_by_group_prefix_pattern(self, test_client, session):
+ billing = AssetModel(name="billing_asset",
uri="s3://bucket/billing_prefix", group="billing")
+ rebilling = AssetModel(name="rebilling_asset",
uri="s3://bucket/rebilling", group="rebilling")
+ session.add_all([billing, rebilling])
+ session.add(AssetActive.for_asset(billing))
+ session.add(AssetActive.for_asset(rebilling))
+ session.commit()
+
+ # Prefix match anchors at the start, so "bill" excludes "rebilling"
(substring would not).
+ response = test_client.get("/assets?group_prefix_pattern=bill")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] ==
["billing_asset"]
+
+ def test_filter_by_last_asset_event_timestamp_range(self, test_client,
session):
+ older = AssetModel(name="older", uri="s3://bucket/older_range",
group="asset")
+ newer = AssetModel(name="newer", uri="s3://bucket/newer_range",
group="asset")
+ session.add_all([older, newer])
+ session.add(AssetActive.for_asset(older))
+ session.add(AssetActive.for_asset(newer))
+ session.flush()
+
+ base = pendulum.datetime(2024, 1, 1)
+ session.add(AssetEvent(asset_id=older.id, timestamp=base))
+ session.add(AssetEvent(asset_id=newer.id, timestamp=base.add(days=10)))
+ session.commit()
+
+ response = test_client.get(
+ "/assets", params={"last_asset_event_timestamp_gte":
base.add(days=5).isoformat()}
+ )
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["newer"]
+
+ def test_aliases_present_for_asset_via_alias(self, test_client, session):
+ """
+ Regression test for https://github.com/apache/airflow/issues/58058:
+ aliases must be visible via the Assets endpoints, not just fetchable
+ through the CLI/DB.
+ """
+ asset = AssetModel(name="alias_target",
uri="s3://bucket/alias_target", group="asset")
+ alias = AssetAliasModel(name="my-alias", group="")
+ session.add_all([asset, alias])
+ session.flush()
+ session.add(AssetActive.for_asset(asset))
+ asset.aliases.append(alias)
+ session.commit()
+
+ response = test_client.get("/assets")
+ assert response.status_code == 200
+ body = response.json()
+ assert len(body["assets"]) == 1
+ assert body["assets"][0]["aliases"] == [{"id": alias.id, "name":
"my-alias", "group": ""}]
+
+ def test_total_entries_counts_assets_not_events(self, test_client,
session):
+ """The outer join to AssetEvent must not inflate total_entries for
assets with many events."""
+ multi = AssetModel(name="multi", uri="s3://bucket/multi",
group="reporting")
+ solo = AssetModel(name="solo", uri="s3://bucket/solo",
group="reporting")
+ other = AssetModel(name="other", uri="s3://bucket/other", group="ops")
+ session.add_all([multi, solo, other])
+ for asset in (multi, solo, other):
+ session.add(AssetActive.for_asset(asset))
+ session.flush()
+
+ base = pendulum.datetime(2024, 1, 1)
+ for offset in range(3):
+ session.add(AssetEvent(asset_id=multi.id,
timestamp=base.add(hours=offset)))
+ session.add(AssetEvent(asset_id=solo.id, timestamp=base))
+ session.commit()
+
+ response = test_client.get("/assets")
+ assert response.status_code == 200
+ body = response.json()
+ assert body["total_entries"] == 3
+ assert sorted(a["name"] for a in body["assets"]) == ["multi", "other",
"solo"]
+
+ response = test_client.get("/assets?group_pattern=report")
+ assert response.status_code == 200
+ body = response.json()
+ assert body["total_entries"] == 2
+ assert sorted(a["name"] for a in body["assets"]) == ["multi", "solo"]
+
+ def test_pagination_covers_all_assets(self, test_client, session):
+ """Paging returns every asset exactly once with a stable total, mixing
evented and never-evented."""
+ evented_new = AssetModel(name="evented_new", uri="s3://bucket/en",
group="asset")
+ evented_old = AssetModel(name="evented_old", uri="s3://bucket/eo",
group="asset")
+ never_a = AssetModel(name="never_a", uri="s3://bucket/na",
group="asset")
+ never_b = AssetModel(name="never_b", uri="s3://bucket/nb",
group="asset")
+ session.add_all([evented_new, evented_old, never_a, never_b])
+ for asset in (evented_new, evented_old, never_a, never_b):
+ session.add(AssetActive.for_asset(asset))
+ session.flush()
+
+ base = pendulum.datetime(2024, 1, 1)
+ session.add(AssetEvent(asset_id=evented_old.id, timestamp=base))
+ session.add(AssetEvent(asset_id=evented_new.id,
timestamp=base.add(days=1)))
+ session.commit()
+
+ page_one = test_client.get("/assets?limit=2&offset=0")
+ page_two = test_client.get("/assets?limit=2&offset=2")
+ assert page_one.status_code == 200
+ assert page_two.status_code == 200
+ assert page_one.json()["total_entries"] == 4
+ assert page_two.json()["total_entries"] == 4
+
+ names = [a["name"] for a in page_one.json()["assets"] +
page_two.json()["assets"]]
+ assert sorted(names) == ["evented_new", "evented_old", "never_a",
"never_b"]
+
+ def test_timestamp_range_filter_excludes_never_evented_assets(self,
test_client, session):
+ """A timestamp range filter excludes assets with no event (NULL fails
the range predicate)."""
+ evented = AssetModel(name="evented", uri="s3://bucket/re",
group="asset")
+ never = AssetModel(name="never", uri="s3://bucket/rn", group="asset")
+ session.add_all([evented, never])
+ session.add(AssetActive.for_asset(evented))
+ session.add(AssetActive.for_asset(never))
+ session.flush()
+
+ base = pendulum.datetime(2024, 1, 1)
+ session.add(AssetEvent(asset_id=evented.id, timestamp=base))
+ session.commit()
+
+ response = test_client.get(
+ "/assets", params={"last_asset_event_timestamp_gte":
base.subtract(days=1).isoformat()}
+ )
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["evented"]
+
+ def test_filter_by_only_active(self, test_client, session):
+ active = AssetModel(name="active", uri="s3://bucket/active",
group="asset")
+ inactive = AssetModel(name="inactive", uri="s3://bucket/inactive",
group="asset")
+ session.add_all([active, inactive])
+ session.add(AssetActive.for_asset(active))
+ session.commit()
+
+ response = test_client.get("/assets")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["active"]
+
+ response = test_client.get("/assets?only_active=false")
+ assert response.status_code == 200
+ assert sorted(a["name"] for a in response.json()["assets"]) ==
["active", "inactive"]
+
+ def test_filter_by_uri_pattern(self, test_client, session):
+ s3 = AssetModel(name="s3_asset", uri="s3://bucket/key", group="asset")
+ gcs = AssetModel(name="gcs_asset", uri="gcs://bucket/key",
group="asset")
+ session.add_all([s3, gcs])
+ session.add(AssetActive.for_asset(s3))
+ session.add(AssetActive.for_asset(gcs))
+ session.commit()
+
+ response = test_client.get("/assets?uri_pattern=s3")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["s3_asset"]
+
+ @pytest.mark.usefixtures("testing_dag_bundle")
+ def test_filter_by_dag_ids(self, test_client, session):
+ referenced = AssetModel(name="referenced",
uri="s3://bucket/referenced", group="asset")
+ unreferenced = AssetModel(name="unreferenced",
uri="s3://bucket/unreferenced", group="asset")
+ session.add_all([referenced, unreferenced])
+ session.add(AssetActive.for_asset(referenced))
+ session.add(AssetActive.for_asset(unreferenced))
+ session.add(DagModel(dag_id="consumer_dag", bundle_name="testing"))
+ session.add(DagScheduleAssetReference(dag_id="consumer_dag",
asset=referenced))
+ session.commit()
+
+ response = test_client.get("/assets?dag_ids=consumer_dag")
+ assert response.status_code == 200
+ assert [a["name"] for a in response.json()["assets"]] == ["referenced"]
+
+ def test_query_count(self, test_client, session):
+ """The asset relationships are eager-loaded, so the query count stays
fixed regardless of
+ how many assets are returned (a lazy-loading regression would issue
queries per asset)."""
+ for i in range(5):
+ asset = AssetModel(name=f"asset{i}", uri=f"s3://bucket/asset{i}",
group="asset")
+ session.add(asset)
+ session.add(AssetActive.for_asset(asset))
+ session.commit()
+
+ with assert_queries_count(7):
+ assert test_client.get("/assets").status_code == 200