This is an automated email from the ASF dual-hosted git repository.

jscheffl 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 bede83ae390 Fix UI instance name title on non-Dag pages (#68288)
bede83ae390 is described below

commit bede83ae39067bae70222e11660951036a7f7d39
Author: Hemkumar Chheda <[email protected]>
AuthorDate: Mon Jun 15 01:20:40 2026 +0530

    Fix UI instance name title on non-Dag pages (#68288)
    
    * Fix UI instance name title on non-Dag pages
    
    * Address document title hook review comment
---
 .../src/airflow/ui/src/layouts/BaseLayout.test.tsx | 111 +++++++++++++++++++++
 .../src/airflow/ui/src/layouts/BaseLayout.tsx      |  39 ++++----
 .../utils/{index.ts => documentTitleContext.ts}    |  13 +--
 airflow-core/src/airflow/ui/src/utils/index.ts     |   1 +
 .../src/airflow/ui/src/utils/useDocumentTitle.ts   |  19 ++--
 ...cumentTitle.ts => useDocumentTitleProvider.tsx} |  24 ++---
 6 files changed, 158 insertions(+), 49 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.test.tsx 
b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.test.tsx
new file mode 100644
index 00000000000..e00f7577071
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.test.tsx
@@ -0,0 +1,111 @@
+/*!
+ * 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 { render, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { useDocumentTitle } from "src/utils";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { BaseLayout } from "./BaseLayout";
+
+const mockConfig: Record<string, unknown> = {
+  instance_name: "Market Data Dev (SpectroCloud)",
+};
+
+vi.mock("openapi/queries", () => ({
+  usePluginServiceGetPlugins: () => ({ data: { plugins: [] } }),
+}));
+
+vi.mock("src/queries/useConfig", () => ({
+  useConfig: (key: string) => mockConfig[key],
+}));
+
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    i18n: {
+      dir: () => "ltr",
+      language: "en",
+      off: vi.fn(),
+      on: vi.fn(),
+    },
+    // eslint-disable-next-line id-length
+    t: (key: string) => key,
+  }),
+}));
+
+vi.mock("./Nav", () => ({
+  Nav: () => <nav />,
+}));
+
+const DagPage = () => {
+  useDocumentTitle("example_dag");
+
+  return <div />;
+};
+
+describe("BaseLayout", () => {
+  beforeEach(() => {
+    document.title = "Airflow";
+    mockConfig.instance_name = "Market Data Dev (SpectroCloud)";
+  });
+
+  afterEach(() => {
+    document.title = "Airflow";
+  });
+
+  it("uses instance_name as the browser title for non-Dag pages", async () => {
+    render(<BaseLayout />, { wrapper: Wrapper });
+
+    await waitFor(() => expect(document.title).toBe("Market Data Dev 
(SpectroCloud)"));
+  });
+
+  it("lets Dag pages add their page title before instance_name", async () => {
+    render(
+      <BaseLayout>
+        <DagPage />
+      </BaseLayout>,
+      { wrapper: Wrapper },
+    );
+
+    await waitFor(() => expect(document.title).toBe("example_dag - Market Data 
Dev (SpectroCloud)"));
+  });
+
+  it("restores the instance_name title when leaving a titled page", async () 
=> {
+    const { rerender } = render(
+      <BaseLayout>
+        <DagPage />
+      </BaseLayout>,
+      { wrapper: Wrapper },
+    );
+
+    await waitFor(() => expect(document.title).toBe("example_dag - Market Data 
Dev (SpectroCloud)"));
+
+    rerender(<BaseLayout />);
+
+    await waitFor(() => expect(document.title).toBe("Market Data Dev 
(SpectroCloud)"));
+  });
+
+  it.each([[""], [undefined]])("falls back to Airflow when instance_name is 
%s", async (instanceName) => {
+    mockConfig.instance_name = instanceName;
+
+    render(<BaseLayout />, { wrapper: Wrapper });
+
+    await waitFor(() => expect(document.title).toBe("Airflow"));
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx
index f40cea8c54c..e06c92bc0e5 100644
--- a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx
@@ -25,6 +25,7 @@ import { usePluginServiceGetPlugins } from "openapi/queries";
 import type { ReactAppResponse } from "openapi/requests/types.gen";
 import { ReactPlugin } from "src/pages/ReactPlugin";
 import { useConfig } from "src/queries/useConfig";
+import { DocumentTitleProvider } from "src/utils";
 
 import { Nav } from "./Nav";
 
@@ -98,25 +99,27 @@ export const BaseLayout = ({ children }: PropsWithChildren) 
=> {
 
   return (
     <LocaleProvider locale={i18n.language || "en"}>
-      <Box display="flex" flexDirection="column" h="100vh">
-        <Nav />
-        <Box
-          _ltr={{ ml: 16 }}
-          _rtl={{ mr: 16 }}
-          data-testid="main-content"
-          display="flex"
-          flex={1}
-          flexDirection="column"
-          minH={0}
-          overflow="auto"
-          p={3}
-        >
-          {baseReactPlugins.map((plugin) => (
-            <ReactPlugin key={plugin.name} reactApp={plugin} />
-          ))}
-          {children ?? <Outlet />}
+      <DocumentTitleProvider>
+        <Box display="flex" flexDirection="column" h="100vh">
+          <Nav />
+          <Box
+            _ltr={{ ml: 16 }}
+            _rtl={{ mr: 16 }}
+            data-testid="main-content"
+            display="flex"
+            flex={1}
+            flexDirection="column"
+            minH={0}
+            overflow="auto"
+            p={3}
+          >
+            {baseReactPlugins.map((plugin) => (
+              <ReactPlugin key={plugin.name} reactApp={plugin} />
+            ))}
+            {children ?? <Outlet />}
+          </Box>
         </Box>
-      </Box>
+      </DocumentTitleProvider>
     </LocaleProvider>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/utils/documentTitleContext.ts
similarity index 61%
copy from airflow-core/src/airflow/ui/src/utils/index.ts
copy to airflow-core/src/airflow/ui/src/utils/documentTitleContext.ts
index c15a50f6dd7..02f1f385d84 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/utils/documentTitleContext.ts
@@ -16,13 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { createContext } from "react";
 
-export { capitalize } from "./capitalize";
-export { getDuration, renderDuration } from "./datetimeUtils";
-export { createErrorToaster, getErrorStatus } from "./errorHandling";
-export { getMetaKey } from "./getMetaKey";
-export { useContainerWidth } from "./useContainerWidth";
-export { useDocumentTitle } from "./useDocumentTitle";
-export { useFiltersHandler, type FilterableSearchParamsKeys } from 
"./useFiltersHandler";
-export * from "./query";
-export { STATE_PRIORITY, sortStateEntries } from "./stateUtils";
+export const DocumentTitleContext = createContext<((pageTitle: string | null) 
=> void) | undefined>(
+  undefined,
+);
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/utils/index.ts
index c15a50f6dd7..738b6a31dfe 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/utils/index.ts
@@ -23,6 +23,7 @@ export { createErrorToaster, getErrorStatus } from 
"./errorHandling";
 export { getMetaKey } from "./getMetaKey";
 export { useContainerWidth } from "./useContainerWidth";
 export { useDocumentTitle } from "./useDocumentTitle";
+export { DocumentTitleProvider } from "./useDocumentTitleProvider";
 export { useFiltersHandler, type FilterableSearchParamsKeys } from 
"./useFiltersHandler";
 export * from "./query";
 export { STATE_PRIORITY, sortStateEntries } from "./stateUtils";
diff --git a/airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts 
b/airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts
index e94f0d9c18c..5fd0075aa9c 100644
--- a/airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts
+++ b/airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts
@@ -16,23 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useEffect } from "react";
+import { useContext, useEffect } from "react";
 
-import { useConfig } from "src/queries/useConfig";
+import { DocumentTitleContext } from "./documentTitleContext";
 
 export const useDocumentTitle = (pageTitle?: string | null) => {
-  const instanceConfig = useConfig("instance_name");
-  const instanceName = typeof instanceConfig === "string" ? instanceConfig : 
"Airflow";
+  const setPageTitle = useContext(DocumentTitleContext);
 
   useEffect(() => {
-    const previousTitle = document.title;
-
-    if (typeof pageTitle === "string" && pageTitle.length > 0) {
-      document.title = `${pageTitle} - ${instanceName}`;
+    if (setPageTitle === undefined || typeof pageTitle !== "string" || 
pageTitle.length === 0) {
+      return undefined;
     }
 
+    setPageTitle(pageTitle);
+
     return () => {
-      document.title = previousTitle;
+      setPageTitle(null);
     };
-  }, [pageTitle, instanceName]);
+  }, [pageTitle, setPageTitle]);
 };
diff --git a/airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts 
b/airflow-core/src/airflow/ui/src/utils/useDocumentTitleProvider.tsx
similarity index 57%
copy from airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts
copy to airflow-core/src/airflow/ui/src/utils/useDocumentTitleProvider.tsx
index e94f0d9c18c..af356de3afd 100644
--- a/airflow-core/src/airflow/ui/src/utils/useDocumentTitle.ts
+++ b/airflow-core/src/airflow/ui/src/utils/useDocumentTitleProvider.tsx
@@ -16,23 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useEffect } from "react";
+import { useEffect, type PropsWithChildren, useState } from "react";
 
 import { useConfig } from "src/queries/useConfig";
 
-export const useDocumentTitle = (pageTitle?: string | null) => {
+import { DocumentTitleContext } from "./documentTitleContext";
+
+const getDefaultTitle = (instanceConfig: unknown) =>
+  typeof instanceConfig === "string" && instanceConfig.length > 0 ? 
instanceConfig : "Airflow";
+
+export const DocumentTitleProvider = ({ children }: PropsWithChildren) => {
   const instanceConfig = useConfig("instance_name");
-  const instanceName = typeof instanceConfig === "string" ? instanceConfig : 
"Airflow";
+  const instanceName = getDefaultTitle(instanceConfig);
+  const [pageTitle, setPageTitle] = useState<string | null>(null);
 
   useEffect(() => {
-    const previousTitle = document.title;
-
-    if (typeof pageTitle === "string" && pageTitle.length > 0) {
-      document.title = `${pageTitle} - ${instanceName}`;
-    }
+    document.title = pageTitle === null ? instanceName : `${pageTitle} - 
${instanceName}`;
+  }, [instanceName, pageTitle]);
 
-    return () => {
-      document.title = previousTitle;
-    };
-  }, [pageTitle, instanceName]);
+  return <DocumentTitleContext.Provider 
value={setPageTitle}>{children}</DocumentTitleContext.Provider>;
 };

Reply via email to