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}

Reply via email to