You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ra...@apache.org on 2026/09/09 06:24:42 UTC
(airflow) branch backport-d1b359bec7-v3-3-test created (now 6b96e36b19b)
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a change to branch backport-d1b359bec7-v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
at 6b96e36b19b Add Consuming Tasks, Aliases and Watchers to assets pages (#71468)
This branch includes the following new commits:
new 6b96e36b19b Add Consuming Tasks, Aliases and Watchers to assets pages (#71468)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(airflow) 01/01: Add Consuming Tasks, Aliases and Watchers to assets pages (#71468)
Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch backport-d1b359bec7-v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 6b96e36b19bbc7b445e8d08600e247a8a2f7b956
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Wed Aug 12 16:31:42 2026 -0400
Add Consuming Tasks, Aliases and Watchers to assets pages (#71468)
* Add Alises and Watchers to assets pages
* Fix tests
(cherry picked from commit d1b359bec74ea373b770d340218496c352f65a78)
---
.../airflow/ui/public/i18n/locales/en/assets.json | 8 ++-
.../ui/src/components/Assets/ListPopover.tsx | 80 ++++++++++++++++++++++
.../src/airflow/ui/src/mocks/handlers/assets.ts | 50 ++++++++++++++
.../src/airflow/ui/src/mocks/handlers/index.ts | 2 +
.../src/airflow/ui/src/pages/Asset/Header.test.tsx | 44 ++++++++++++
.../src/airflow/ui/src/pages/Asset/Header.tsx | 9 +++
.../ui/src/pages/AssetsList/AssetsList.test.tsx | 43 ++++++++++++
.../airflow/ui/src/pages/AssetsList/AssetsList.tsx | 31 +++++++--
8 files changed, 262 insertions(+), 5 deletions(-)
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..00082cebebd 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json
@@ -1,5 +1,8 @@
{
"additional_data": "Additional Data",
+ "alias_one": "Alias",
+ "alias_other": "Aliases",
+ "aliases": "Aliases",
"asset_many": "Assets",
"asset_one": "Asset",
"assetStateStore": {
@@ -50,5 +53,8 @@
"scheduledDags": "Scheduled Dags",
"scheduling": "Scheduling",
"searchPlaceholder": "Search Assets",
- "taskDependencies": "Task Dependencies"
+ "taskDependencies": "Task Dependencies",
+ "watcher_one": "Watcher",
+ "watcher_other": "Watchers",
+ "watchers": "Watchers"
}
diff --git a/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx b/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx
new file mode 100644
index 00000000000..4b5917be6dd
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx
@@ -0,0 +1,80 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Button, Text } from "@chakra-ui/react";
+import type { ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+
+import type { AssetAliasResponse, AssetWatcherResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+import { Popover } from "src/components/ui";
+
+type ListPopoverProps = {
+ readonly items: Array<{ key: string; label: ReactNode }>;
+ readonly noun: string;
+};
+
+const ListPopover = ({ items, noun }: ListPopoverProps) => (
+ // eslint-disable-next-line jsx-a11y/no-autofocus
+ <Popover.Root autoFocus={false} lazyMount unmountOnExit>
+ <Popover.Trigger asChild disabled={items.length === 0}>
+ <Button variant="outline">
+ {items.length} {noun}
+ </Button>
+ </Popover.Trigger>
+ <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} width="fit-content">
+ <Popover.Arrow />
+ <Popover.Body>
+ {items.map(({ key, label }) => (
+ <Text key={key} py={2}>
+ {label}
+ </Text>
+ ))}
+ </Popover.Body>
+ </Popover.Content>
+ </Popover.Root>
+);
+
+export const AliasesPopover = ({ aliases }: { readonly aliases: Array<AssetAliasResponse> }) => {
+ const { t: translate } = useTranslation("assets");
+
+ return (
+ <ListPopover
+ items={aliases.map((alias) => ({ key: String(alias.id), label: alias.name }))}
+ noun={translate("alias", { count: aliases.length })}
+ />
+ );
+};
+
+export const WatchersPopover = ({ watchers }: { readonly watchers: Array<AssetWatcherResponse> }) => {
+ const { t: translate } = useTranslation("assets");
+
+ return (
+ <ListPopover
+ items={watchers.map((watcher) => ({
+ key: String(watcher.trigger_id),
+ label: (
+ <>
+ {watcher.name} <Time datetime={watcher.created_date} />
+ </>
+ ),
+ }))}
+ noun={translate("watcher", { count: watchers.length })}
+ />
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/assets.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/assets.ts
new file mode 100644
index 00000000000..e643259343e
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/assets.ts
@@ -0,0 +1,50 @@
+/*!
+ * 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 { http, HttpResponse, type HttpHandler } from "msw";
+
+const assetWithDependencies = {
+ aliases: [{ group: "alias_group", id: 1, name: "my_alias" }],
+ consuming_tasks: [{ created_at: "2025-01-01T00:00:00Z", dag_id: "consumer_dag", task_id: "consumer_task" }],
+ created_at: "2025-01-01T00:00:00Z",
+ extra: { owner: "data_team" },
+ group: "asset_group",
+ id: 1,
+ last_asset_event: { id: 10, timestamp: "2025-01-15T00:00:00Z" },
+ name: "asset_with_dependencies",
+ producing_tasks: [],
+ scheduled_dags: [],
+ updated_at: "2025-01-02T00:00:00Z",
+ uri: "s3://bucket/asset",
+ watchers: [{ created_date: "2025-01-01T00:00:00Z", name: "my_watcher", trigger_id: 5 }],
+};
+
+const plainAsset = {
+ ...assetWithDependencies,
+ aliases: [],
+ id: 2,
+ name: "plain_asset",
+ watchers: [],
+};
+
+export const handlers: Array<HttpHandler> = [
+ http.get("/api/v2/assets", () => HttpResponse.json({ assets: [assetWithDependencies], total_entries: 1 })),
+ http.get("/api/v2/assets/:assetId", ({ params }) =>
+ HttpResponse.json(params.assetId === "2" ? plainAsset : assetWithDependencies),
+ ),
+];
diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts
index 9f60605f2b6..3d9e36b8602 100644
--- a/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { handlers as assetsHandlers } from "./assets";
import { handlers as configHandlers } from "./config";
import { handlers as dagHandlers } from "./dag";
import { handlers as dagRunsHandlers } from "./dag_runs";
@@ -23,6 +24,7 @@ import { handlers as dagsHandlers } from "./dags";
import { handlers as logHandlers } from "./log";
export const handlers = [
+ ...assetsHandlers,
...configHandlers,
...dagHandlers,
...dagRunsHandlers,
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/Header.test.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/Header.test.tsx
new file mode 100644
index 00000000000..1db8677dfff
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/Header.test.tsx
@@ -0,0 +1,44 @@
+/*!
+ * 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 "@testing-library/jest-dom";
+import { render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { AppWrapper } from "src/utils/AppWrapper";
+
+// The assets mock handler (see src/mocks/handlers/assets.ts) serves asset 1 with one
+// alias and one watcher, and asset 2 with neither.
+describe("Asset header", () => {
+ it("lists the aliases and watchers of an asset", async () => {
+ render(<AppWrapper initialEntries={["/assets/1"]} />);
+
+ await waitFor(() => expect(screen.getByRole("button", { name: "1 alias" })).toBeInTheDocument());
+
+ expect(screen.getByRole("button", { name: "1 watcher" })).toBeInTheDocument();
+ });
+
+ it("omits the aliases and watchers stats for an asset that has none", async () => {
+ render(<AppWrapper initialEntries={["/assets/2"]} />);
+
+ await waitFor(() => expect(screen.getAllByText("plain_asset").length).toBeGreaterThan(0));
+
+ expect(screen.queryByRole("button", { name: /alias/iu })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /watcher/iu })).not.toBeInTheDocument();
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx
index 6eb0929b580..c9dd67119b0 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx
@@ -20,6 +20,7 @@ import { useTranslation } from "react-i18next";
import { FiDatabase } from "react-icons/fi";
import type { AssetResponse } from "openapi/requests/types.gen";
+import { AliasesPopover, WatchersPopover } from "src/components/Assets/ListPopover";
import { HeaderCard } from "src/components/HeaderCard";
import { DependencyPopover } from "../AssetsList/DependencyPopover";
@@ -27,6 +28,9 @@ import { DependencyPopover } from "../AssetsList/DependencyPopover";
export const Header = ({ asset }: { readonly asset?: AssetResponse }) => {
const { t: translate } = useTranslation("assets");
+ const aliases = asset?.aliases ?? [];
+ const watchers = asset?.watchers ?? [];
+
const stats = [
{ label: translate("group"), value: asset?.group },
{
@@ -41,6 +45,11 @@ export const Header = ({ asset }: { readonly asset?: AssetResponse }) => {
label: translate("scheduledDags"),
value: <DependencyPopover dependencies={asset?.scheduled_dags ?? []} type="Dag" />,
},
+ // Only assets that actually have them — unlike the dependency stats above, most assets have neither.
+ ...(aliases.length ? [{ label: translate("aliases"), value: <AliasesPopover aliases={aliases} /> }] : []),
+ ...(watchers.length
+ ? [{ label: translate("watchers"), value: <WatchersPopover watchers={watchers} /> }]
+ : []),
];
return <HeaderCard icon={<FiDatabase />} stats={stats} title={asset?.name} />;
diff --git a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx
new file mode 100644
index 00000000000..7763a9e92e3
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx
@@ -0,0 +1,43 @@
+/*!
+ * 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 "@testing-library/jest-dom";
+import { render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { AppWrapper } from "src/utils/AppWrapper";
+
+// The assets mock handler (see src/mocks/handlers/assets.ts) returns a single asset
+// with one consuming task, one alias and one watcher.
+describe("AssetsList columns", () => {
+ it("shows the consuming tasks of an asset", async () => {
+ render(<AppWrapper initialEntries={["/assets"]} />);
+
+ await waitFor(() => expect(screen.getByText("asset_with_dependencies")).toBeInTheDocument());
+
+ expect(screen.getByRole("button", { name: "1 task" })).toBeInTheDocument();
+ });
+
+ it("lists the aliases and watchers of an asset", async () => {
+ render(<AppWrapper initialEntries={["/assets"]} />);
+
+ await waitFor(() => expect(screen.getByRole("button", { name: "1 alias" })).toBeInTheDocument());
+
+ expect(screen.getByRole("button", { name: "1 watcher" })).toBeInTheDocument();
+ });
+});
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..f9748d44014 100644
--- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
@@ -18,11 +18,13 @@
*/
import { Flex, Heading, useDisclosure, VStack } from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
+import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { useAssetServiceGetAssets } from "openapi/queries";
import type { AssetResponse } from "openapi/requests/types.gen";
+import { AliasesPopover, WatchersPopover } from "src/components/Assets/ListPopover";
import { DataTable } from "src/components/DataTable";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
@@ -40,10 +42,7 @@ import { DependencyPopover } from "./DependencyPopover";
type AssetRow = { row: { original: AssetResponse } };
-const createColumns = (
- translate: (key: string) => string,
- open?: boolean,
-): Array<ColumnDef<AssetResponse>> => [
+const createColumns = (translate: TFunction, open?: boolean): Array<ColumnDef<AssetResponse>> => [
{
accessorKey: "name",
cell: ({ row: { original } }: AssetRow) => (
@@ -91,9 +90,33 @@ const createColumns = (
enableSorting: false,
header: () => translate("producingTasks"),
},
+ {
+ accessorKey: "consuming_tasks",
+ cell: ({ row: { original } }: AssetRow) =>
+ original.consuming_tasks.length ? (
+ <DependencyPopover dependencies={original.consuming_tasks} type="Task" />
+ ) : undefined,
+ enableSorting: false,
+ header: () => translate("consumingTasks"),
+ },
+ {
+ accessorKey: "aliases",
+ cell: ({ row: { original } }: AssetRow) =>
+ original.aliases.length ? <AliasesPopover aliases={original.aliases} /> : undefined,
+ enableSorting: false,
+ header: () => translate("aliases"),
+ },
+ {
+ accessorKey: "watchers",
+ cell: ({ row: { original } }: AssetRow) =>
+ original.watchers.length ? <WatchersPopover watchers={original.watchers} /> : undefined,
+ enableSorting: false,
+ header: () => translate("watchers"),
+ },
{
accessorKey: "trigger",
cell: ({ row }) => <CreateAssetEvent asset={row.original} />,
+ enableHiding: false,
enableSorting: false,
header: "",
},