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>;
};