This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 4ca5754ade4 UI: Show a label instead of a broken link for hidden
assets (#75178)
4ca5754ade4 is described below
commit 4ca5754ade46209d214e04e24e8373d3ebee7a4a
Author: Henry Chen <[email protected]>
AuthorDate: Wed Oct 21 04:09:36 2026 +0800
UI: Show a label instead of a broken link for hidden assets (#75178)
`next_run_assets` blanks the name and id of an asset leaf the caller may
not read and marks it `hidden`, keeping the shape of the schedule without
revealing which asset it waits on. The asset schedule popover did not know
about that flag, so a redacted leaf rendered as a link with no text
pointing at `/assets/null`.
Only deployments whose auth manager authorizes assets individually reach
this: FAB and the simple auth manager grant every asset id, so nothing is
redacted there.
---
.../airflow/ui/public/i18n/locales/en/common.json | 1 +
.../components/AssetExpression/AssetNode.test.tsx | 52 ++++++++++++++++++++++
.../src/components/AssetExpression/AssetNode.tsx | 13 ++++--
3 files changed, 63 insertions(+), 3 deletions(-)
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index 6d1cdb99622..7df9e8bc34c 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -139,6 +139,7 @@
"all": "All",
"and": "AND",
"any": "Any",
+ "hiddenAsset": "Hidden asset",
"or": "OR"
},
"filter": "Filter",
diff --git
a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.test.tsx
b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.test.tsx
new file mode 100644
index 00000000000..efc9ec33f19
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.test.tsx
@@ -0,0 +1,52 @@
+/*!
+ * 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/vitest";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import type { AssetExpressionAsset } from "openapi/requests/types.gen";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { AssetNode } from "./AssetNode";
+
+const readableAsset: AssetExpressionAsset = {
+ asset: { group: "asset", id: 1, name: "sales_daily", uri:
"s3://bucket/sales" },
+};
+
+// The shape `next_run_assets` serves for an asset the caller may not read.
+const hiddenAsset: AssetExpressionAsset = {
+ asset: { group: "asset", hidden: true, id: null, name: null, uri: null },
+};
+
+describe("AssetNode", () => {
+ it("labels a hidden asset instead of linking to it, leaving readable assets
linked", () => {
+ render(
+ <>
+ <AssetNode asset={readableAsset} />
+ <AssetNode asset={hiddenAsset} />
+ </>,
+ { wrapper: Wrapper },
+ );
+
+ expect(screen.getByText("expression.hiddenAsset")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "sales_daily"
})).toHaveAttribute("href", "/assets/1");
+ expect(screen.getAllByRole("link")).toHaveLength(1);
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
index b5398ef10bc..469b14b4698 100644
--- a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
+++ b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
@@ -18,7 +18,7 @@
*/
import { Box, HStack, Text } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
-import { FiDatabase } from "react-icons/fi";
+import { FiDatabase, FiLock } from "react-icons/fi";
import { PiRectangleDashed } from "react-icons/pi";
import type {
@@ -42,7 +42,10 @@ export const AssetNode = ({
readonly asset: AssetExpressionAlias | AssetExpressionAsset;
readonly event?: NextRunAssetEventResponse;
}) => {
- const { i18n } = useTranslation();
+ const { i18n, t: translate } = useTranslation();
+ // An asset leaf the caller may not read arrives from `next_run_assets` with
`hidden` set and its
+ // name and id blanked, so there is nothing to label the node with and
nowhere to link it to.
+ const isHidden = "asset" in asset && Boolean(asset.asset.hidden);
const isFullyReceived = Boolean(event?.last_update);
const isPartial =
!isFullyReceived &&
@@ -72,9 +75,13 @@ export const AssetNode = ({
position="relative"
>
<HStack gap={2}>
- {"asset" in asset ? <FiDatabase /> : <PiRectangleDashed />}
+ {"alias" in asset ? <PiRectangleDashed /> : isHidden ? <FiLock /> :
<FiDatabase />}
{"alias" in asset ? (
<Text fontSize="sm">{asset.alias.name}</Text>
+ ) : isHidden ? (
+ <Text color="fg.muted" fontSize="sm">
+ {translate("expression.hiddenAsset")}
+ </Text>
) : (
<RouterLink display="block" py={2} to={`/assets/${asset.asset.id}`}>
{asset.asset.name}