This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 28e20e1684f Show the current page name in the browser tab title
(#69622) (#69656)
28e20e1684f is described below
commit 28e20e1684fa9e0428df3cb8e28d82b3398676e1
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Thu Jul 9 16:36:57 2026 +0200
Show the current page name in the browser tab title (#69622) (#69656)
Only Dag pages set a page-specific browser tab title today, so every other
page shows just the instance name and multiple open Airflow tabs cannot be
told apart. Set a localized, page-aware title on the remaining pages so each
tab identifies the page it is showing.
(cherry picked from commit 493312b9f9ce3fc70b946c725182e2a9ff4f460c)
---
.../src/airflow/ui/src/pages/Asset/AssetLayout.tsx | 3 +
.../airflow/ui/src/pages/AssetsList/AssetsList.tsx | 4 ++
.../src/airflow/ui/src/pages/Configs/Configs.tsx | 4 ++
.../ui/src/pages/Connections/Connections.tsx | 4 ++
.../src/airflow/ui/src/pages/DagRuns/DagRuns.tsx | 6 +-
.../src/airflow/ui/src/pages/DagsList/DagsList.tsx | 4 ++
.../airflow/ui/src/pages/Dashboard/Dashboard.tsx | 4 ++
.../src/airflow/ui/src/pages/Deadlines/index.tsx | 5 +-
.../src/airflow/ui/src/pages/Events/Events.tsx | 5 ++
.../pages/GroupTaskInstance/GroupTaskInstance.tsx | 4 ++
.../pages/HITLTaskInstances/HITLTaskInstances.tsx | 6 +-
airflow-core/src/airflow/ui/src/pages/Jobs.tsx | 5 +-
.../MappedTaskInstance/MappedTaskInstance.tsx | 4 ++
airflow-core/src/airflow/ui/src/pages/Plugins.tsx | 4 ++
.../src/airflow/ui/src/pages/Pools/Pools.tsx | 3 +
.../src/airflow/ui/src/pages/Providers.tsx | 4 ++
airflow-core/src/airflow/ui/src/pages/Run/Run.tsx | 4 +-
airflow-core/src/airflow/ui/src/pages/Security.tsx | 5 ++
.../src/airflow/ui/src/pages/Task/Task.tsx | 3 +
.../ui/src/pages/TaskInstance/TaskInstance.tsx | 5 +-
.../ui/src/pages/TaskInstances/TaskInstances.tsx | 6 +-
.../airflow/ui/src/pages/Variables/Variables.tsx | 4 ++
.../src/airflow/ui/src/pages/XCom/XCom.tsx | 4 ++
.../airflow/ui/src/pages/documentTitle.test.tsx | 73 ++++++++++++++++++++++
24 files changed, 166 insertions(+), 7 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
index d53e2029703..ade558468cd 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
@@ -29,6 +29,7 @@ import { BreadcrumbStats } from
"src/components/BreadcrumbStats";
import { ProgressBar } from "src/components/ui";
import { GroupsProvider } from "src/context/groups";
import { NavTabs } from "src/layouts/Details/NavTabs";
+import { useDocumentTitle } from "src/utils";
import { AssetGraph } from "./AssetGraph";
import { AssetPanelButtons } from "./AssetPanelButtons";
@@ -49,6 +50,8 @@ export const AssetLayout = () => {
},
);
+ useDocumentTitle(asset?.name);
+
const links = [
{
label: asset?.name,
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 9ddbe3d77ee..150b57ba968 100644
--- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
@@ -34,6 +34,7 @@ import { RouterLink } from "src/components/ui";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
import { CreateAssetEvent } from "src/pages/Asset/CreateAssetEvent";
+import { useDocumentTitle } from "src/utils";
import { DependencyPopover } from "./DependencyPopover";
@@ -117,6 +118,9 @@ const { NAME_PATTERN, OFFSET }: SearchParamsKeysType =
SearchParamsKeys;
export const AssetsList = () => {
const { t: translate } = useTranslation(["assets", "common"]);
+
+ useDocumentTitle(translate("common:nav.assets"));
+
const [searchParams, setSearchParams] = useSearchParams();
const namePattern = searchParams.get(NAME_PATTERN) ?? "";
diff --git a/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx
b/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx
index 9c12e10a49e..4e7cc724bf7 100644
--- a/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx
@@ -25,6 +25,7 @@ import { useConfigServiceGetConfig } from "openapi/queries";
import type { ConfigOption } from "openapi/requests/types.gen";
import { DataTable } from "src/components/DataTable";
import { ErrorAlert } from "src/components/ErrorAlert";
+import { useDocumentTitle } from "src/utils";
type ConfigColums = {
section: string;
@@ -50,6 +51,9 @@ const createColumns = (translate: TFunction):
Array<ColumnDef<ConfigColums>> =>
export const Configs = () => {
const { t: translate } = useTranslation(["admin", "common"]);
+
+ useDocumentTitle(translate("common:admin.Config"));
+
const { data, error } = useConfigServiceGetConfig();
const columns = createColumns(translate);
diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
index 42a56a8fac1..44a86347f70 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
@@ -42,6 +42,7 @@ import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searc
import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
import { useConfig } from "src/queries/useConfig.tsx";
import { useConnectionTypeMeta } from "src/queries/useConnectionTypeMeta";
+import { useDocumentTitle } from "src/utils";
import AddConnectionButton from "./AddConnectionButton";
import DeleteConnectionButton from "./DeleteConnectionButton";
@@ -132,6 +133,9 @@ const getColumns = ({
export const Connections = () => {
const { t: translate } = useTranslation(["admin", "common"]);
+
+ useDocumentTitle(translate("common:admin.Connections"));
+
const { setTableURLState, tableURLState } = useTableURLState();
const [searchParams, setSearchParams] = useSearchParams();
const { NAME_PATTERN, OFFSET }: SearchParamsKeysType = SearchParamsKeys;
diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
index 06374d10b27..ac4d1b9a886 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
@@ -47,7 +47,7 @@ import { RouterLink } from "src/components/ui";
import { ActionBar } from "src/components/ui/ActionBar";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
-import { renderDuration, useAutoRefresh, isStatePending } from "src/utils";
+import { renderDuration, useAutoRefresh, isStatePending, useDocumentTitle }
from "src/utils";
import BulkClearDagRunsButton from "./BulkClearDagRunsButton";
import BulkDeleteDagRunsButton from "./BulkDeleteDagRunsButton";
@@ -221,6 +221,10 @@ const runColumns = ({ dagId, translate }: ColumnProps):
Array<ColumnDef<DAGRunRe
export const DagRuns = () => {
const { t: translate } = useTranslation();
const { dagId } = useParams();
+
+ // Only the standalone list page owns the tab title; the Dag-scoped tab
inherits the Dag page's title.
+ useDocumentTitle(dagId === undefined ? translate("common:dagRun_other") :
undefined);
+
const [searchParams] = useSearchParams();
const { setTableURLState, tableURLState } = useTableURLState({
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index eaedd94406a..f9eb2884c0f 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -41,6 +41,7 @@ import { useAdvancedSearch } from
"src/hooks/useAdvancedSearch";
import { DagsLayout } from "src/layouts/DagsLayout";
import { useConfig } from "src/queries/useConfig";
import { useDags } from "src/queries/useDags";
+import { useDocumentTitle } from "src/utils";
import { DagImportErrors } from "../Dashboard/Stats/DagImportErrors";
import { DagCard } from "./DagCard";
@@ -188,6 +189,9 @@ const cardDef: CardDef<DAGWithLatestDagRunsResponse> = {
export const DagsList = () => {
const { t: translate } = useTranslation();
+
+ useDocumentTitle(translate("common:nav.dags"));
+
const [searchParams, setSearchParams] = useSearchParams();
const [display, setDisplay] = useLocalStorage<"card" |
"table">(DAGS_LIST_DISPLAY_KEY, "card");
const dagRunsLimit = display === "card" ? 14 : 1;
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
index a5867947d15..8af92bb6f4d 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
@@ -29,6 +29,7 @@ import TimeRangeSelector from
"src/components/TimeRangeSelector";
import { IconButton } from "src/components/ui";
import { COLLAPSED_UI_ALERTS_KEY } from "src/constants/localStorage";
import { useConfig } from "src/queries/useConfig";
+import { useDocumentTitle } from "src/utils";
import { ReactPlugin } from "../ReactPlugin";
import { AlertContent } from "./AlertContent";
@@ -44,6 +45,9 @@ const defaultHour = "24";
export const Dashboard = () => {
const alerts = useConfig("dashboard_alert") as Array<UIAlert>;
const { t: translate } = useTranslation("dashboard");
+
+ useDocumentTitle(translate("common:nav.home"));
+
const instanceName = useConfig("instance_name");
const now = dayjs();
diff --git a/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
b/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
index 720648c4247..54a01f5cdb0 100644
--- a/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
@@ -31,7 +31,7 @@ import { FilterBar } from "src/components/FilterBar";
import Time from "src/components/Time";
import { TruncatedText } from "src/components/TruncatedText";
import { SearchParamsKeys } from "src/constants/searchParams";
-import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
+import { useDocumentTitle, useFiltersHandler, type FilterableSearchParamsKeys
} from "src/utils";
type DeadlineRow = { row: { original: DeadlineResponse } };
@@ -100,6 +100,9 @@ const deadlinesFilterKeys:
Array<FilterableSearchParamsKeys> = [
export const Deadlines = () => {
const { t: translate } = useTranslation(["browse", "common"]);
+
+ useDocumentTitle(translate("common:browse.deadlines"));
+
const { setTableURLState, tableURLState } = useTableURLState();
const [searchParams] = useSearchParams();
diff --git a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
index dcc8adf784e..1b79cc771bb 100644
--- a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx
@@ -32,6 +32,7 @@ import RenderedJsonField from
"src/components/RenderedJsonField";
import Time from "src/components/Time";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
+import { useDocumentTitle } from "src/utils";
import { EventsFilters } from "./EventsFilters";
@@ -161,6 +162,10 @@ const {
export const Events = () => {
const { t: translate } = useTranslation(["browse", "common"]);
const { dagId, runId, taskId } = useParams();
+
+ // Only the standalone audit-log page owns the tab title; nested tabs
inherit their parent page's title.
+ useDocumentTitle(dagId === undefined ? translate("common:browse.auditLog") :
undefined);
+
const [searchParams] = useSearchParams();
const { setTableURLState, tableURLState } = useTableURLState();
const { pagination, sorting } = tableURLState;
diff --git
a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx
b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx
index abb741e9498..3f819b64c89 100644
---
a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/GroupTaskInstance.tsx
@@ -23,12 +23,16 @@ import { useParams } from "react-router-dom";
import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
+import { useDocumentTitle } from "src/utils";
import { Header } from "./Header";
export const GroupTaskInstance = () => {
const { dagId = "", groupId = "", runId = "" } = useParams();
const { t: translate } = useTranslation("dag");
+
+ useDocumentTitle(groupId);
+
const { summariesByRunId } = useGridTiSummariesStream({ dagId, runIds: runId
? [runId] : [] });
const gridTISummaries = summariesByRunId.get(runId);
const taskInstance = gridTISummaries?.task_instances.find((ti) => ti.task_id
=== groupId);
diff --git
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
index 71a0faaaf7b..46667420dbd 100644
---
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
@@ -37,7 +37,7 @@ import { TruncatedText } from "src/components/TruncatedText";
import { IconButton, RouterLink } from "src/components/ui";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
-import { useAutoRefresh } from "src/utils";
+import { useAutoRefresh, useDocumentTitle } from "src/utils";
import { getHITLState, isHITLPending } from "src/utils/hitl";
import { getTaskInstanceLink } from "src/utils/links";
@@ -222,6 +222,10 @@ export const HITLTaskInstances = ({
}) => {
const { t: translate } = useTranslation("hitl");
const { dagId, runId, taskId } = useParams();
+
+ // Only the standalone required-actions page owns the tab title; nested tabs
inherit their parent's.
+ useDocumentTitle(enableHITLReviewDrawer ?
translate("common:browse.requiredActions") : undefined);
+
const { closeHITLReviewDrawer, isHITLReviewDrawerOpen, openHITLReviewDrawer,
selectedDetail } =
useHITLReviewDrawer();
const [searchParams, setSearchParams] = useSearchParams();
diff --git a/airflow-core/src/airflow/ui/src/pages/Jobs.tsx
b/airflow-core/src/airflow/ui/src/pages/Jobs.tsx
index 568c023d818..992328c4cd0 100644
--- a/airflow-core/src/airflow/ui/src/pages/Jobs.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Jobs.tsx
@@ -31,7 +31,7 @@ import { FilterBar } from "src/components/FilterBar";
import { StateBadge } from "src/components/StateBadge";
import Time from "src/components/Time";
import { SearchParamsKeys } from "src/constants/searchParams";
-import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
+import { useDocumentTitle, useFiltersHandler, type FilterableSearchParamsKeys
} from "src/utils";
const createColumns = (translate: TFunction): Array<ColumnDef<JobResponse>> =>
[
{
@@ -95,6 +95,9 @@ const jobsFilterKeys: Array<FilterableSearchParamsKeys> = [
export const Jobs = () => {
const { t: translate } = useTranslation(["admin", "common"]);
+
+ useDocumentTitle(translate("common:browse.jobs"));
+
const { setTableURLState, tableURLState } = useTableURLState();
const [searchParams] = useSearchParams();
diff --git
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
index 2bc1daa3ed6..a7cee933b47 100644
---
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
@@ -24,12 +24,16 @@ import { useParams } from "react-router-dom";
import { useDagRunServiceGetDagRun } from "openapi/queries";
import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
+import { useDocumentTitle } from "src/utils";
import { Header } from "./Header";
export const MappedTaskInstance = () => {
const { dagId = "", runId = "", taskId = "" } = useParams();
const { t: translate } = useTranslation("dag");
+
+ useDocumentTitle(taskId);
+
// Pass the run state so the summaries stream keeps auto-refreshing while
the run is running;
// without it the Header and Details tab would freeze on the first fetch.
const { data: dagRun } = useDagRunServiceGetDagRun({ dagId, dagRunId: runId
}, undefined, {
diff --git a/airflow-core/src/airflow/ui/src/pages/Plugins.tsx
b/airflow-core/src/airflow/ui/src/pages/Plugins.tsx
index 623a7176c85..cde75c253d1 100644
--- a/airflow-core/src/airflow/ui/src/pages/Plugins.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Plugins.tsx
@@ -23,11 +23,15 @@ import { usePluginServiceGetPlugins } from
"openapi/queries";
import { DataTable } from "src/components/DataTable";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
+import { useDocumentTitle } from "src/utils";
import { PluginImportErrors } from "./Dashboard/Stats/PluginImportErrors";
export const Plugins = () => {
const { t: translate } = useTranslation(["admin", "common"]);
+
+ useDocumentTitle(translate("common:admin.Plugins"));
+
const { setTableURLState, tableURLState } = useTableURLState();
const { pagination } = tableURLState;
const { data, error } = usePluginServiceGetPlugins({
diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx
b/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx
index 89c08c22d12..1fab831ac49 100644
--- a/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx
@@ -33,6 +33,7 @@ import { Select } from "src/components/ui";
import type { SearchParamsKeysType } from "src/constants/searchParams";
import { SearchParamsKeys } from "src/constants/searchParams";
import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
+import { useDocumentTitle } from "src/utils";
import AddPoolButton from "./AddPoolButton";
import PoolBarCard from "./PoolBarCard";
@@ -47,6 +48,8 @@ const cardDef = (): CardDef<PoolResponse> => ({
export const Pools = () => {
const { t: translate } = useTranslation(["admin", "common"]);
+ useDocumentTitle(translate("common:admin.Pools"));
+
const poolSortOptions = createListCollection({
items: [
{ label: translate("pools.sort.asc"), value: "name" },
diff --git a/airflow-core/src/airflow/ui/src/pages/Providers.tsx
b/airflow-core/src/airflow/ui/src/pages/Providers.tsx
index e8e762fcb9b..96834c948b1 100644
--- a/airflow-core/src/airflow/ui/src/pages/Providers.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Providers.tsx
@@ -27,6 +27,7 @@ import { DataTable } from "src/components/DataTable";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
import { urlRegex } from "src/constants/urlRegex";
+import { useDocumentTitle } from "src/utils";
const createColumns = (translate: TFunction):
Array<ColumnDef<ProviderResponse>> => [
{
@@ -77,6 +78,9 @@ const createColumns = (translate: TFunction):
Array<ColumnDef<ProviderResponse>>
export const Providers = () => {
const { t: translate } = useTranslation(["admin", "common"]);
+
+ useDocumentTitle(translate("common:admin.Providers"));
+
const { setTableURLState, tableURLState } = useTableURLState();
const columns = createColumns(translate);
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
b/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
index ea49ef73b7c..dcb6e4e1397 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
@@ -25,7 +25,7 @@ import { useParams } from "react-router-dom";
import { useDagRunServiceGetDagRun } from "openapi/queries";
import { usePluginTabs } from "src/hooks/usePluginTabs";
import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
-import { isStatePending, useAutoRefresh } from "src/utils";
+import { isStatePending, useAutoRefresh, useDocumentTitle } from "src/utils";
import { Header } from "./Header";
@@ -33,6 +33,8 @@ export const Run = () => {
const { t: translate } = useTranslation(["dag", "hitl"]);
const { dagId = "", runId = "" } = useParams();
+ useDocumentTitle(runId);
+
// Get external views with dag_run destination
const externalTabs = usePluginTabs("dag_run");
diff --git a/airflow-core/src/airflow/ui/src/pages/Security.tsx
b/airflow-core/src/airflow/ui/src/pages/Security.tsx
index 9a56996dcbc..f6ccf567f6d 100644
--- a/airflow-core/src/airflow/ui/src/pages/Security.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Security.tsx
@@ -18,10 +18,12 @@
*/
import { Box } from "@chakra-ui/react";
import { useRef } from "react";
+import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { useAuthLinksServiceGetAuthMenus } from "openapi/queries";
import { ProgressBar } from "src/components/ui";
+import { useDocumentTitle } from "src/utils";
import { ErrorPage } from "./Error";
@@ -33,6 +35,9 @@ const SANDBOX = "allow-scripts allow-same-origin allow-forms";
export const Security = () => {
const { page } = useParams();
+ const { t: translate } = useTranslation("common");
+
+ useDocumentTitle(translate("nav.security"));
const { data: authLinks, isLoading } = useAuthLinksServiceGetAuthMenus();
diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx
b/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx
index 3d84c55c114..c53d18c2ee3 100644
--- a/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx
@@ -28,6 +28,7 @@ import { usePluginTabs } from "src/hooks/usePluginTabs";
import { useRequiredActionTabs } from "src/hooks/useRequiredActionTabs";
import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
import { useGridStructure } from "src/queries/useGridStructure.ts";
+import { useDocumentTitle } from "src/utils";
import { getGroupTask } from "src/utils/groupTask";
import { GroupTaskHeader } from "./GroupTaskHeader";
@@ -37,6 +38,8 @@ export const Task = () => {
const { t: translate } = useTranslation(["dag", "hitl"]);
const { dagId = "", groupId, runId, taskId } = useParams();
+ useDocumentTitle(groupId ?? taskId);
+
// Get external views with task destination
const externalTabs = usePluginTabs("task");
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
index 1d390f14596..c6a53a5ece1 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
@@ -30,13 +30,16 @@ import { usePluginTabs } from "src/hooks/usePluginTabs";
import { useRequiredActionTabs } from "src/hooks/useRequiredActionTabs";
import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
-import { isStatePending, useAutoRefresh } from "src/utils";
+import { isStatePending, useAutoRefresh, useDocumentTitle } from "src/utils";
import { Header } from "./Header";
export const TaskInstance = () => {
const { t: translate } = useTranslation(["dag", "common", "hitl"]);
const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams();
+
+ useDocumentTitle(taskId);
+
// Get external views with task_instance destination
const externalTabs = usePluginTabs("task_instance");
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
index 3249b876621..f09d7f07a22 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -44,7 +44,7 @@ import { RouterLink } from "src/components/ui";
import { ActionBar } from "src/components/ui/ActionBar";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
-import { useAutoRefresh, isStatePending, renderDuration } from "src/utils";
+import { useAutoRefresh, isStatePending, renderDuration, useDocumentTitle }
from "src/utils";
import { getTaskInstanceLink } from "src/utils/links";
import BulkClearTaskInstancesButton from "./BulkClearTaskInstancesButton";
@@ -253,6 +253,10 @@ const taskInstanceColumns = ({
export const TaskInstances = () => {
const { t: translate } = useTranslation();
const { dagId, groupId, runId, taskId } = useParams();
+
+ // Only the standalone list page owns the tab title; nested tabs inherit
their parent page's title.
+ useDocumentTitle(dagId === undefined ?
translate("common:taskInstance_other") : undefined);
+
const [searchParams] = useSearchParams();
const { setTableURLState, tableURLState } = useTableURLState({
diff --git a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
index bff5770bb81..fbb44813cf4 100644
--- a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
@@ -42,6 +42,7 @@ import { ActionBar } from "src/components/ui/ActionBar";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
import { useConfig } from "src/queries/useConfig.tsx";
+import { useDocumentTitle } from "src/utils";
import { TrimText } from "src/utils/TrimText";
import DeleteVariablesButton from "./DeleteVariablesButton";
@@ -136,6 +137,9 @@ const getColumns = ({
export const Variables = () => {
const { t: translate } = useTranslation("admin");
+
+ useDocumentTitle(translate("common:admin.Variables"));
+
const { setTableURLState, tableURLState } = useTableURLState({
pagination: { pageIndex: 0, pageSize: 30 },
sorting: [{ desc: false, id: "key" }],
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx
b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx
index da6582398cf..851095c182d 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx
@@ -32,6 +32,7 @@ import { TruncatedText } from "src/components/TruncatedText";
import { RouterLink } from "src/components/ui";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
+import { useDocumentTitle } from "src/utils";
import { getTaskInstanceLink } from "src/utils/links";
import AddXComButton from "./AddXComButton";
@@ -150,6 +151,9 @@ const getColumns = ({
export const XCom = () => {
const { dagId = "~", mapIndex = "-1", runId = "~", taskId = "~" } =
useParams();
const { t: translate } = useTranslation(["browse", "common"]);
+
+ // Only the standalone list page owns the tab title; the task-instance tab
inherits that page's title.
+ useDocumentTitle(dagId === "~" ? translate("common:browse.xcoms") :
undefined);
const { setTableURLState, tableURLState } = useTableURLState();
const { pagination, sorting } = tableURLState;
const [sort] = sorting;
diff --git a/airflow-core/src/airflow/ui/src/pages/documentTitle.test.tsx
b/airflow-core/src/airflow/ui/src/pages/documentTitle.test.tsx
new file mode 100644
index 00000000000..5312ea23e24
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/documentTitle.test.tsx
@@ -0,0 +1,73 @@
+/*!
+ * 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 { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { AppWrapper } from "src/utils/AppWrapper";
+
+// The detail pages open a task-instance summaries stream that would otherwise
stay active when the
+// tiny title-only assertion resolves and the tree unmounts. Stub it so no
stream is left open.
+vi.mock("src/queries/useGridTISummaries.ts", () => ({
+ useGridTiSummariesStream: () => ({ isLoading: false, summariesByRunId: new
Map() }),
+}));
+
+// instance_name in the mocked config is "Airflow"
(src/mocks/handlers/config.ts). The test i18n
+// backend is not loaded, so translations resolve to their keys — asserting on
the key still proves
+// each page wires the intended translation key into the browser tab title.
+const INSTANCE = "Airflow";
+
+describe("document title", () => {
+ beforeEach(() => {
+ document.title = "";
+ });
+
+ it.each([
+ ["/dags", "nav.dags"],
+ ["/assets", "nav.assets"],
+ ["/dag_runs", "dagRun_other"],
+ ["/task_instances", "taskInstance_other"],
+ ["/connections", "admin.Connections"],
+ ["/variables", "admin.Variables"],
+ ["/pools", "admin.Pools"],
+ ["/providers", "admin.Providers"],
+ ["/plugins", "admin.Plugins"],
+ ["/configs", "admin.Config"],
+ ["/deadlines", "browse.deadlines"],
+ ["/jobs", "browse.jobs"],
+ ["/xcoms", "browse.xcoms"],
+ ["/events", "browse.auditLog"],
+ ["/required_actions", "browse.requiredActions"],
+ ["/security/users", "nav.security"],
+ ])("shows the page name before the instance name on %s", async (route,
pageTitle) => {
+ render(<AppWrapper initialEntries={[route]} />);
+
+ await waitFor(() => expect(document.title).toBe(`${pageTitle} -
${INSTANCE}`));
+ });
+
+ it.each([
+ ["/dags/log_grouping/runs/manual__2025-02-18T12:19",
"manual__2025-02-18T12:19"],
+ ["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source",
"log_source"],
+ // Nested list/audit tabs are gated, so the detail page keeps ownership of
the title.
+
["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source/events",
"log_source"],
+ ])("uses the entity identifier for detail page %s", async (route, pageTitle)
=> {
+ render(<AppWrapper initialEntries={[route]} />);
+
+ await waitFor(() => expect(document.title).toBe(`${pageTitle} -
${INSTANCE}`));
+ });
+});