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 b9b471177c6 Extend user settings with additional configuration (#71699)
b9b471177c6 is described below

commit b9b471177c6a3663a0762a4eefdcf730de0d6adb
Author: Software Developer <[email protected]>
AuthorDate: Mon Aug 31 17:26:45 2026 +0200

    Extend user settings with additional configuration (#71699)
    
    * add a new configuration to the user settings:
    - make it possible to configure default landing page.
    - make it possible to configure default instance tab.
    
    * UI: Limit settings translations to the new default landing page and task 
tab keys
    
    The default-settings change bundled full settings-block translations into 
the
    non-English locales, including keys unrelated to this feature. Those belong 
to
    a separate translation PR, so keep only the general (landing page) and task
    instance (default tab) keys this change actually introduces.
    
    * revert translations for other languages then eng.
    
    * added Russian translation
    
    * remove Russian translation
    
    * addressing PR comments.
    
    * consolidate TaskInstanceTab with tab.ts. add more related tests.
---
 .../airflow/ui/public/i18n/locales/en/common.json  | 19 +++++
 .../HITLReview/HITLReviewDetailSummary.tsx         |  2 +-
 .../src/airflow/ui/src/constants/localStorage.ts   |  2 +
 airflow-core/src/airflow/ui/src/constants/tab.ts   | 33 ++++++++
 .../airflow/ui/src/hooks/useUserSettings.test.tsx  | 58 +++++++++++++
 .../src/airflow/ui/src/hooks/useUserSettings.ts    | 14 ++++
 .../src/airflow/ui/src/layouts/Nav/Nav.test.tsx    | 77 ++++++++++++++++++
 .../src/airflow/ui/src/layouts/Nav/Nav.tsx         |  9 ++-
 .../ui/src/pages/Dag/Overview/TaskLogPreview.tsx   |  2 +-
 .../pages/HITLTaskInstances/HITLTaskInstances.tsx  |  4 +-
 .../src/airflow/ui/src/pages/LandingPage.test.tsx  | 66 +++++++++++++++
 .../{constants/tab.ts => pages/LandingPage.tsx}    | 27 +++----
 .../ui/src/pages/Settings/Settings.test.tsx        | 42 +++++++++-
 .../src/airflow/ui/src/pages/Settings/Settings.tsx | 43 +++++++++-
 .../ui/src/pages/TaskInstance/DefaultTab.test.tsx  | 94 ++++++++++++++++++++++
 .../tab.ts => pages/TaskInstance/DefaultTab.tsx}   | 27 +++----
 .../ui/src/pages/TaskInstance/TaskInstance.tsx     |  9 ++-
 .../src/airflow/ui/src/queries/useLogs.tsx         |  2 +-
 airflow-core/src/airflow/ui/src/router.test.tsx    | 17 +++-
 airflow-core/src/airflow/ui/src/router.tsx         | 31 ++++---
 .../src/airflow/ui/src/utils/links.test.ts         | 15 ++++
 airflow-core/src/airflow/ui/src/utils/links.ts     | 21 ++++-
 22 files changed, 557 insertions(+), 57 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 dcb41805cb1..2bed02c1a22 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
@@ -180,6 +180,7 @@
     "assets": "Assets",
     "browse": "Browse",
     "dags": "Dags",
+    "dashboard": "Dashboard",
     "docs": "Docs",
     "home": "Home",
     "legacyFabViews": "Legacy Views",
@@ -273,6 +274,17 @@
       "title": "Clearing"
     },
     "description": "These preferences are saved in this browser only and apply 
across the whole app.",
+    "general": {
+      "landingPage": {
+        "helper": "Page shown when you open Airflow.",
+        "label": "Landing page",
+        "options": {
+          "dags": "Dags",
+          "dashboard": "Dashboard"
+        }
+      },
+      "title": "General"
+    },
     "graph": {
       "defaultDirection": {
         "helper": "Layout direction for Dag and asset graphs you haven't set 
individually.",
@@ -287,6 +299,13 @@
       },
       "title": "Marking"
     },
+    "taskInstance": {
+      "defaultTab": {
+        "helper": "Tab shown first when you open a task instance. Links to a 
specific tab still open that tab.",
+        "label": "Default task instance tab"
+      },
+      "title": "Task Instance"
+    },
     "title": "Settings"
   },
   "shortcuts": {
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
index f6d02117bc5..6d34d47bf57 100644
--- 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
@@ -53,7 +53,7 @@ export const HITLReviewDetailSummary = ({
         <HITLReviewRow
           label={translate("common:taskId")}
           value={
-            <RouterLink onClick={onOpenTask} 
to={`${getTaskInstanceLink(ti)}/required_actions`}>
+            <RouterLink onClick={onOpenTask} to={getTaskInstanceLink(ti, 
"required_actions")}>
               {ti.task_id}
             </RouterLink>
           }
diff --git a/airflow-core/src/airflow/ui/src/constants/localStorage.ts 
b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
index 29a60434f3d..5dde959e3fd 100644
--- a/airflow-core/src/airflow/ui/src/constants/localStorage.ts
+++ b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
@@ -34,6 +34,8 @@ export const CLEAR_RUN_DEFAULT_OPTIONS_KEY = 
"clear_run_default_options";
 export const CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY = 
"clear_task_instance_default_options";
 export const CLEAR_PREVENT_RUNNING_TASK_KEY = "clear_prevent_running_task";
 export const MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY = 
"mark_task_instance_default_options";
+export const DEFAULT_TASK_INSTANCE_TAB_KEY = "default_task_instance_tab";
+export const DEFAULT_LANDING_PAGE_KEY = "default_landing_page";
 
 // Dag-scoped keys
 export const dagRunsLimitKey = (dagId: string) => `dag_runs_limit-${dagId}`;
diff --git a/airflow-core/src/airflow/ui/src/constants/tab.ts 
b/airflow-core/src/airflow/ui/src/constants/tab.ts
index 051148442ac..43ff17cdb01 100644
--- a/airflow-core/src/airflow/ui/src/constants/tab.ts
+++ b/airflow-core/src/airflow/ui/src/constants/tab.ts
@@ -33,3 +33,36 @@ export enum TabName {
   Runs = "runs",
   Tasks = "tasks",
 }
+
+/** Route path segments for the task-instance detail tabs. Single source of 
truth powering the router. */
+export enum TaskInstanceTab {
+  AssetEvents = "asset_events",
+  Code = "code",
+  Details = "details",
+  Events = "events",
+  Logs = "logs",
+  RenderedTemplates = "rendered_templates",
+  RequiredActions = "required_actions",
+  TaskInstances = "task_instances",
+  TaskStateStore = "task-state-store",
+  XCom = "xcom",
+}
+
+/** Plain-string form of the tab paths, for APIs that accept a path segment. */
+export type TaskInstanceTabValue = `${TaskInstanceTab}`;
+
+/**
+ * Tabs offered as the user's default landing tab on the Settings page, each 
mapped to the
+ * route path it redirects to. Logs is the index route, so it maps to "" 
(renders in place).
+ */
+export const DEFAULT_TASK_INSTANCE_TAB_PATHS = {
+  asset_events: "asset_events",
+  code: "code",
+  details: "details",
+  events: "events",
+  logs: "",
+  rendered_templates: "rendered_templates",
+  xcom: "xcom",
+} as const satisfies Partial<Record<TaskInstanceTabValue, TaskInstanceTabValue 
| "">>;
+
+export type DefaultTaskInstanceTab = keyof typeof 
DEFAULT_TASK_INSTANCE_TAB_PATHS;
diff --git a/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx 
b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx
index 4117595b0de..ab9143f1bf4 100644
--- a/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx
+++ b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx
@@ -24,6 +24,8 @@ import {
   CLEAR_RUN_DEFAULT_OPTIONS_KEY,
   CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
   DEFAULT_GRAPH_DIRECTION_KEY,
+  DEFAULT_LANDING_PAGE_KEY,
+  DEFAULT_TASK_INSTANCE_TAB_KEY,
   MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
 } from "src/constants/localStorage";
 
@@ -32,6 +34,8 @@ import {
   useClearRunDefaultOptions,
   useClearTaskInstanceDefaultOptions,
   useDefaultGraphDirection,
+  useDefaultLandingPage,
+  useDefaultTaskInstanceTab,
   useMarkTaskInstanceDefaultOptions,
 } from "./useUserSettings";
 
@@ -127,6 +131,60 @@ describe("useClearPreventRunningTaskDefault", () => {
   });
 });
 
+describe("useDefaultTaskInstanceTab", () => {
+  it("defaults to logs when nothing is stored", () => {
+    const { result } = renderHook(() => useDefaultTaskInstanceTab());
+
+    expect(result.current[0]).toBe("logs");
+  });
+
+  it("reads an existing stored tab", () => {
+    localStorage.setItem(DEFAULT_TASK_INSTANCE_TAB_KEY, 
JSON.stringify("details"));
+
+    const { result } = renderHook(() => useDefaultTaskInstanceTab());
+
+    expect(result.current[0]).toBe("details");
+  });
+
+  it("persists a new tab to localStorage", () => {
+    const { result } = renderHook(() => useDefaultTaskInstanceTab());
+
+    act(() => {
+      result.current[1]("xcom");
+    });
+
+    expect(result.current[0]).toBe("xcom");
+    expect(JSON.parse(localStorage.getItem(DEFAULT_TASK_INSTANCE_TAB_KEY) ?? 
'""')).toBe("xcom");
+  });
+});
+
+describe("useDefaultLandingPage", () => {
+  it("defaults to dashboard when nothing is stored", () => {
+    const { result } = renderHook(() => useDefaultLandingPage());
+
+    expect(result.current[0]).toBe("dashboard");
+  });
+
+  it("reads an existing stored landing page", () => {
+    localStorage.setItem(DEFAULT_LANDING_PAGE_KEY, JSON.stringify("dags"));
+
+    const { result } = renderHook(() => useDefaultLandingPage());
+
+    expect(result.current[0]).toBe("dags");
+  });
+
+  it("persists a new landing page to localStorage", () => {
+    const { result } = renderHook(() => useDefaultLandingPage());
+
+    act(() => {
+      result.current[1]("dags");
+    });
+
+    expect(result.current[0]).toBe("dags");
+    expect(JSON.parse(localStorage.getItem(DEFAULT_LANDING_PAGE_KEY) ?? 
'""')).toBe("dags");
+  });
+});
+
 describe("useMarkTaskInstanceDefaultOptions", () => {
   it("defaults to an empty selection", () => {
     const { result } = renderHook(() => useMarkTaskInstanceDefaultOptions());
diff --git a/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts 
b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts
index bec8c853b5b..182f9af07da 100644
--- a/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts
@@ -24,8 +24,14 @@ import {
   CLEAR_RUN_DEFAULT_OPTIONS_KEY,
   CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
   DEFAULT_GRAPH_DIRECTION_KEY,
+  DEFAULT_LANDING_PAGE_KEY,
+  DEFAULT_TASK_INSTANCE_TAB_KEY,
   MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
 } from "src/constants/localStorage";
+import type { DefaultTaskInstanceTab } from "src/constants/tab";
+
+/** Page shown at the app root. */
+export type LandingPageOption = "dags" | "dashboard";
 
 /**
  * User-configurable defaults surfaced in the Settings page and consumed as
@@ -52,3 +58,11 @@ export const useClearPreventRunningTaskDefault = () =>
 /** Default selection for the "Mark as" task-instance dialog toggle (past / 
future / … ). */
 export const useMarkTaskInstanceDefaultOptions = () =>
   useLocalStorage<Array<string>>(MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY, []);
+
+/** Tab shown first when a task instance is opened without an explicit tab in 
the URL. */
+export const useDefaultTaskInstanceTab = () =>
+  useLocalStorage<DefaultTaskInstanceTab>(DEFAULT_TASK_INSTANCE_TAB_KEY, 
"logs");
+
+/** Page the app root ("/") lands on: the dashboard or the Dags list. */
+export const useDefaultLandingPage = () =>
+  useLocalStorage<LandingPageOption>(DEFAULT_LANDING_PAGE_KEY, "dashboard");
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.test.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.test.tsx
new file mode 100644
index 00000000000..96e1464333b
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.test.tsx
@@ -0,0 +1,77 @@
+/*!
+ * 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";
+import { render, screen } from "@testing-library/react";
+import type { PropsWithChildren } from "react";
+import { MemoryRouter } from "react-router-dom";
+import { describe, expect, it, vi } from "vitest";
+
+import { BaseWrapper } from "src/utils/Wrapper";
+
+import { Nav } from "./Nav";
+
+vi.mock("openapi/queries", () => ({
+  useAuthLinksServiceGetAuthMenus: () => ({ data: undefined }),
+  usePluginServiceGetPlugins: () => ({ data: undefined }),
+  useVersionServiceGetVersion: () => ({ data: undefined }),
+}));
+vi.mock("src/context/timezone", () => ({ useTimezone: () => ({ 
selectedTimezone: "UTC" }) }));
+vi.mock("src/utils/datetimeUtils", () => ({
+  getTimezoneOffsetString: () => "+00:00",
+  getTimezoneTooltipLabel: () => "UTC",
+}));
+vi.mock("src/queries/useConfig", () => ({ useConfig: () => undefined }));
+vi.mock("src/components/Logo", () => ({ Logo: () => <div /> }));
+vi.mock("./AdminButton", () => ({ AdminButton: () => <div /> }));
+vi.mock("./BrowseButton", () => ({ BrowseButton: () => <div /> }));
+vi.mock("./DocsButton", () => ({ DocsButton: () => <div /> }));
+vi.mock("./SecurityButton", () => ({ SecurityButton: () => <div /> }));
+vi.mock("./UserSettingsButton", () => ({ UserSettingsButton: () => <div /> }));
+vi.mock("./PluginMenus", () => ({ PluginMenus: () => <div /> }));
+vi.mock("./TimezoneModal", () => ({ default: () => <div /> }));
+
+const wrapperAt = (path: string) => {
+  const wrapper = ({ children }: PropsWithChildren) => (
+    <BaseWrapper>
+      <MemoryRouter initialEntries={[path]}>{children}</MemoryRouter>
+    </BaseWrapper>
+  );
+
+  return wrapper;
+};
+
+describe("Nav dashboard button", () => {
+  it("links to the dashboard at /home", () => {
+    render(<Nav />, { wrapper: wrapperAt("/") });
+
+    expect(screen.getByTestId("nav-dashboard-link")).toHaveAttribute("href", 
"/home");
+  });
+
+  it("is active on the dashboard route", () => {
+    render(<Nav />, { wrapper: wrapperAt("/home") });
+
+    
expect(screen.getByTestId("nav-dashboard-link")).toHaveAttribute("aria-current",
 "page");
+  });
+
+  it("is not active on the landing page", () => {
+    render(<Nav />, { wrapper: wrapperAt("/") });
+
+    
expect(screen.getByTestId("nav-dashboard-link")).not.toHaveAttribute("aria-current");
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
index 82bd724af00..a56a0c1335f 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx
@@ -18,7 +18,7 @@
  */
 import { Box, Flex, Text, VStack, useDisclosure } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
-import { FiDatabase, FiHome, FiClock } from "react-icons/fi";
+import { FiDatabase, FiGrid, FiClock } from "react-icons/fi";
 import { Link } from "react-router-dom";
 
 import {
@@ -171,7 +171,12 @@ export const Nav = () => {
             />
           </Link>
         </Box>
-        <NavButton data-testid="nav-home-link" icon={FiHome} 
title={translate("nav.home")} to="/" />
+        <NavButton
+          data-testid="nav-dashboard-link"
+          icon={FiGrid}
+          title={translate("nav.dashboard")}
+          to="/home"
+        />
         <NavButton
           data-testid="nav-dags-link"
           disabled={!authLinks?.authorized_menu_items.includes("Dags")}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
index 476d9a41dda..7becae19d1a 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
@@ -73,7 +73,7 @@ export const TaskLogPreview = ({
               : translate("overview.failedLogs.showLogs")}
           </Button>
           <ClearTaskInstanceButton taskInstance={taskInstance} />
-          <RouterLink fontSize="sm" to={getTaskInstanceLink(taskInstance)}>
+          <RouterLink fontSize="sm" to={getTaskInstanceLink(taskInstance, 
"logs")}>
             {translate("overview.failedLogs.viewFullLogs")}
           </RouterLink>
         </Flex>
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 20f0874b958..a5e750d27ae 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
@@ -122,7 +122,7 @@ const taskInstanceColumns = ({
       <RouterLink
         fontWeight="bold"
         onClick={(event) => event.stopPropagation()}
-        to={`${getTaskInstanceLink(original.task_instance)}/required_actions`}
+        to={getTaskInstanceLink(original.task_instance, "required_actions")}
       >
         <TruncatedText text={original.subject} />
       </RouterLink>
@@ -180,7 +180,7 @@ const taskInstanceColumns = ({
             <RouterLink
               fontWeight="bold"
               onClick={(event) => event.stopPropagation()}
-              
to={`${getTaskInstanceLink(original.task_instance)}/required_actions`}
+              to={getTaskInstanceLink(original.task_instance, 
"required_actions")}
             >
               <TruncatedText text={original.task_instance.task_display_name} />
             </RouterLink>
diff --git a/airflow-core/src/airflow/ui/src/pages/LandingPage.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/LandingPage.test.tsx
new file mode 100644
index 00000000000..babfcd183ad
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/LandingPage.test.tsx
@@ -0,0 +1,66 @@
+/*!
+ * 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";
+import { render, screen } from "@testing-library/react";
+import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+import { useDefaultLandingPage } from "src/hooks/useUserSettings";
+
+import { LandingPage } from "./LandingPage";
+
+vi.mock("src/hooks/useUserSettings", () => ({ useDefaultLandingPage: vi.fn() 
}));
+vi.mock("src/pages/Dashboard", () => ({ Dashboard: () => 
<div>dashboard-page</div> }));
+
+const setLandingPage = (value: unknown) =>
+  vi
+    .mocked(useDefaultLandingPage)
+    .mockReturnValue([value as never, vi.fn(), vi.fn()] as unknown as 
ReturnType<
+      typeof useDefaultLandingPage
+    >);
+
+const renderAt = () =>
+  render(
+    <MemoryRouter initialEntries={["/"]}>
+      <Routes>
+        <Route element={<LandingPage />} index />
+        <Route element={<div>dags-page</div>} path="dags" />
+      </Routes>
+    </MemoryRouter>,
+  );
+
+describe("LandingPage", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("renders the dashboard by default", () => {
+    setLandingPage("dashboard");
+    renderAt();
+
+    expect(screen.getByText("dashboard-page")).toBeInTheDocument();
+  });
+
+  it("redirects to the Dags list when configured", () => {
+    setLandingPage("dags");
+    renderAt();
+
+    expect(screen.getByText("dags-page")).toBeInTheDocument();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/constants/tab.ts 
b/airflow-core/src/airflow/ui/src/pages/LandingPage.tsx
similarity index 68%
copy from airflow-core/src/airflow/ui/src/constants/tab.ts
copy to airflow-core/src/airflow/ui/src/pages/LandingPage.tsx
index 051148442ac..5de94952b61 100644
--- a/airflow-core/src/airflow/ui/src/constants/tab.ts
+++ b/airflow-core/src/airflow/ui/src/pages/LandingPage.tsx
@@ -16,20 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { Navigate } from "react-router-dom";
 
-export enum TabEntity {
-  Dag = "dag",
-  Task = "task",
-  TaskInstance = "task-instance",
-}
+import { useDefaultLandingPage } from "src/hooks/useUserSettings";
+import { Dashboard } from "src/pages/Dashboard";
 
-export enum TabName {
-  Backfills = "backfills",
-  Calendar = "calendar",
-  Code = "code",
-  Details = "details",
-  Events = "events",
-  Overview = "",
-  Runs = "runs",
-  Tasks = "tasks",
-}
+export const LandingPage = () => {
+  const [defaultLandingPage] = useDefaultLandingPage();
+
+  if (defaultLandingPage === "dags") {
+    return <Navigate replace to="/dags" />;
+  }
+
+  return <Dashboard />;
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Settings/Settings.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Settings/Settings.test.tsx
index 51ef9cd8b8d..f1030f5e585 100644
--- a/airflow-core/src/airflow/ui/src/pages/Settings/Settings.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Settings/Settings.test.tsx
@@ -22,7 +22,12 @@ import i18n from "i18next";
 import { initReactI18next } from "react-i18next";
 import { afterEach, beforeAll, describe, expect, it } from "vitest";
 
-import { CLEAR_PREVENT_RUNNING_TASK_KEY, DEFAULT_GRAPH_DIRECTION_KEY } from 
"src/constants/localStorage";
+import {
+  CLEAR_PREVENT_RUNNING_TASK_KEY,
+  DEFAULT_GRAPH_DIRECTION_KEY,
+  DEFAULT_LANDING_PAGE_KEY,
+  DEFAULT_TASK_INSTANCE_TAB_KEY,
+} from "src/constants/localStorage";
 import { BaseWrapper } from "src/utils/Wrapper";
 
 import { Settings } from "./Settings";
@@ -45,6 +50,14 @@ beforeAll(async () => {
               title: "Clearing",
             },
             description: "browser only",
+            general: {
+              landingPage: {
+                helper: "helper",
+                label: "Landing page",
+                options: { dags: "Dags", dashboard: "DASHBOARD-OPT" },
+              },
+              title: "General",
+            },
             graph: {
               defaultDirection: { helper: "helper", label: "Default graph 
direction" },
               title: "Graph",
@@ -53,6 +66,10 @@ beforeAll(async () => {
               taskSelection: { helper: "helper", label: "Default mark 
selection" },
               title: "Marking",
             },
+            taskInstance: {
+              defaultTab: { helper: "helper", label: "Default task instance 
tab" },
+              title: "Task Instance",
+            },
             title: "Settings",
           },
         },
@@ -64,6 +81,17 @@ beforeAll(async () => {
             directionUp: "UP-LABEL",
           },
         },
+        dag: {
+          tabs: {
+            assetEvents: "Asset Events",
+            auditLog: "Audit Log",
+            code: "Code",
+            details: "DETAILS-TAB",
+            logs: "Logs",
+            renderedTemplates: "Rendered Templates",
+            xcom: "XCom",
+          },
+        },
         dags: {
           runAndTaskActions: {
             options: {
@@ -93,7 +121,12 @@ describe("Settings page", () => {
     expect(screen.getByText("Settings")).toBeInTheDocument();
 
     // Selects and the switch expose test ids.
-    for (const testId of ["default-graph-direction", 
"clear-prevent-running-task"]) {
+    for (const testId of [
+      "default-landing-page",
+      "default-graph-direction",
+      "default-task-instance-tab",
+      "clear-prevent-running-task",
+    ]) {
       expect(screen.getByTestId(testId)).toBeInTheDocument();
     }
 
@@ -110,9 +143,14 @@ describe("Settings page", () => {
     localStorage.setItem(DEFAULT_GRAPH_DIRECTION_KEY, JSON.stringify("DOWN"));
     localStorage.setItem(CLEAR_PREVENT_RUNNING_TASK_KEY, 
JSON.stringify(false));
 
+    localStorage.setItem(DEFAULT_TASK_INSTANCE_TAB_KEY, 
JSON.stringify("details"));
+    localStorage.setItem(DEFAULT_LANDING_PAGE_KEY, 
JSON.stringify("dashboard"));
+
     render(<Settings />, { wrapper: BaseWrapper });
 
+    
expect(screen.getByTestId("default-landing-page")).toHaveTextContent("DASHBOARD-OPT");
     
expect(screen.getByTestId("default-graph-direction")).toHaveTextContent("DOWN-LABEL");
+    
expect(screen.getByTestId("default-task-instance-tab")).toHaveTextContent("DETAILS-TAB");
     
expect(screen.getByTestId("clear-prevent-running-task")).toHaveAttribute("data-state",
 "unchecked");
   });
 });
diff --git a/airflow-core/src/airflow/ui/src/pages/Settings/Settings.tsx 
b/airflow-core/src/airflow/ui/src/pages/Settings/Settings.tsx
index 0400dde881b..310e699aabf 100644
--- a/airflow-core/src/airflow/ui/src/pages/Settings/Settings.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Settings/Settings.tsx
@@ -24,12 +24,16 @@ import type { Direction } from 
"src/components/Graph/DirectionDropdown";
 import { Select } from "src/components/ui";
 import SegmentedControl from "src/components/ui/SegmentedControl";
 import { Switch } from "src/components/ui/Switch";
+import type { DefaultTaskInstanceTab } from "src/constants/tab";
 import {
   useClearPreventRunningTaskDefault,
   useClearRunDefaultOptions,
   useClearTaskInstanceDefaultOptions,
   useDefaultGraphDirection,
+  useDefaultLandingPage,
+  useDefaultTaskInstanceTab,
   useMarkTaskInstanceDefaultOptions,
+  type LandingPageOption,
 } from "src/hooks/useUserSettings";
 import { useDocumentTitle } from "src/utils";
 import type { Option } from "src/utils/option";
@@ -160,7 +164,7 @@ const Section = ({ children, title }: { readonly children: 
ReactNode; readonly t
 );
 
 export const Settings = () => {
-  const { t: translate } = useTranslation(["common", "components", "dags"]);
+  const { t: translate } = useTranslation(["common", "components", "dags", 
"dag"]);
 
   useDocumentTitle(translate("settings.title"));
 
@@ -169,6 +173,23 @@ export const Settings = () => {
   const [clearTaskOptions, setClearTaskOptions] = 
useClearTaskInstanceDefaultOptions();
   const [preventRunningTask, setPreventRunningTask] = 
useClearPreventRunningTaskDefault();
   const [markTaskOptions, setMarkTaskOptions] = 
useMarkTaskInstanceDefaultOptions();
+  const [defaultTaskInstanceTab, setDefaultTaskInstanceTab] = 
useDefaultTaskInstanceTab();
+  const [defaultLandingPage, setDefaultLandingPage] = useDefaultLandingPage();
+
+  const taskInstanceTabOptions: Array<SelectOption<DefaultTaskInstanceTab>> = [
+    { label: translate("dag:tabs.logs"), value: "logs" },
+    { label: translate("dag:tabs.details"), value: "details" },
+    { label: translate("dag:tabs.renderedTemplates"), value: 
"rendered_templates" },
+    { label: translate("dag:tabs.code"), value: "code" },
+    { label: translate("dag:tabs.auditLog"), value: "events" },
+    { label: translate("dag:tabs.assetEvents"), value: "asset_events" },
+    { label: translate("dag:tabs.xcom"), value: "xcom" },
+  ];
+
+  const landingPageOptions: Array<SelectOption<LandingPageOption>> = [
+    { label: translate("settings.general.landingPage.options.dashboard"), 
value: "dashboard" },
+    { label: translate("settings.general.landingPage.options.dags"), value: 
"dags" },
+  ];
 
   const directionOptions: Array<SelectOption<Direction>> = [
     { label: translate("components:graph.directionRight"), value: "RIGHT" },
@@ -199,6 +220,16 @@ export const Settings = () => {
         {translate("settings.description")}
       </Text>
       <Stack gap={8}>
+        <Section title={translate("settings.general.title")}>
+          <SelectSetting
+            helper={translate("settings.general.landingPage.helper")}
+            label={translate("settings.general.landingPage.label")}
+            onChange={setDefaultLandingPage}
+            options={landingPageOptions}
+            testId="default-landing-page"
+            value={defaultLandingPage}
+          />
+        </Section>
         <Section title={translate("settings.graph.title")}>
           <SelectSetting
             helper={translate("settings.graph.defaultDirection.helper")}
@@ -250,6 +281,16 @@ export const Settings = () => {
             options={directionalToggleOptions}
           />
         </Section>
+        <Section title={translate("settings.taskInstance.title")}>
+          <SelectSetting
+            helper={translate("settings.taskInstance.defaultTab.helper")}
+            label={translate("settings.taskInstance.defaultTab.label")}
+            onChange={setDefaultTaskInstanceTab}
+            options={taskInstanceTabOptions}
+            testId="default-task-instance-tab"
+            value={defaultTaskInstanceTab}
+          />
+        </Section>
       </Stack>
     </Box>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/DefaultTab.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/DefaultTab.test.tsx
new file mode 100644
index 00000000000..d0a5660a300
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/DefaultTab.test.tsx
@@ -0,0 +1,94 @@
+/*!
+ * 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";
+import { render, screen } from "@testing-library/react";
+import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+import { useDefaultTaskInstanceTab } from "src/hooks/useUserSettings";
+
+import { DefaultTab } from "./DefaultTab";
+
+vi.mock("src/hooks/useUserSettings", () => ({ useDefaultTaskInstanceTab: 
vi.fn() }));
+vi.mock("./Logs", () => ({ Logs: () => <div>logs-page</div> }));
+
+const setDefaultTab = (value: unknown) =>
+  vi
+    .mocked(useDefaultTaskInstanceTab)
+    .mockReturnValue([value as never, vi.fn(), vi.fn()] as unknown as 
ReturnType<
+      typeof useDefaultTaskInstanceTab
+    >);
+
+const LocationEcho = ({ label }: { readonly label: string }) => {
+  const { search } = useLocation();
+
+  return (
+    <div>
+      {label}
+      {search}
+    </div>
+  );
+};
+
+const renderAt = (url: string) =>
+  render(
+    <MemoryRouter initialEntries={[url]}>
+      <Routes>
+        <Route path="dags/:dagId/runs/:runId/tasks/:taskId">
+          <Route element={<DefaultTab />} index />
+          <Route element={<div>logs-page</div>} path="logs" />
+          <Route element={<LocationEcho label="details-page" />} 
path="details" />
+        </Route>
+      </Routes>
+    </MemoryRouter>,
+  );
+
+describe("DefaultTab", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it("renders logs on the index route by default", () => {
+    setDefaultTab("logs");
+    renderAt("/dags/d1/runs/r1/tasks/t1");
+
+    expect(screen.getByText("logs-page")).toBeInTheDocument();
+  });
+
+  it("redirects to the configured tab", () => {
+    setDefaultTab("details");
+    renderAt("/dags/d1/runs/r1/tasks/t1");
+
+    expect(screen.getByText("details-page")).toBeInTheDocument();
+  });
+
+  it("preserves the query string when redirecting", () => {
+    setDefaultTab("details");
+    renderAt("/dags/d1/runs/r1/tasks/t1?foo=bar");
+
+    expect(screen.getByText("details-page?foo=bar")).toBeInTheDocument();
+  });
+
+  it("falls back to logs for an unknown tab value", () => {
+    setDefaultTab("not-a-tab");
+    renderAt("/dags/d1/runs/r1/tasks/t1");
+
+    expect(screen.getByText("logs-page")).toBeInTheDocument();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/constants/tab.ts 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/DefaultTab.tsx
similarity index 60%
copy from airflow-core/src/airflow/ui/src/constants/tab.ts
copy to airflow-core/src/airflow/ui/src/pages/TaskInstance/DefaultTab.tsx
index 051148442ac..ea5eb980431 100644
--- a/airflow-core/src/airflow/ui/src/constants/tab.ts
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/DefaultTab.tsx
@@ -16,20 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { Navigate, useLocation } from "react-router-dom";
 
-export enum TabEntity {
-  Dag = "dag",
-  Task = "task",
-  TaskInstance = "task-instance",
-}
+import { useDefaultTaskInstanceTab } from "src/hooks/useUserSettings";
+import { getDefaultTaskInstanceTabPath } from "src/utils/links";
 
-export enum TabName {
-  Backfills = "backfills",
-  Calendar = "calendar",
-  Code = "code",
-  Details = "details",
-  Events = "events",
-  Overview = "",
-  Runs = "runs",
-  Tasks = "tasks",
-}
+import { Logs } from "./Logs";
+
+export const DefaultTab = () => {
+  const [defaultTab] = useDefaultTaskInstanceTab();
+  const tabPath = getDefaultTaskInstanceTabPath(defaultTab);
+  const { hash, search } = useLocation();
+
+  return tabPath === "" ? <Logs /> : <Navigate replace to={{ hash, pathname: 
tabPath, search }} />;
+};
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 75371ce6270..bc4fb8721f4 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
@@ -28,9 +28,11 @@ import { useTaskInstanceServiceGetMappedTaskInstance } from 
"openapi/queries";
 import { useHITLReviewTabs } from "src/hooks/useHITLReviewTabs";
 import { usePluginTabs } from "src/hooks/usePluginTabs";
 import { useRequiredActionTabs } from "src/hooks/useRequiredActionTabs";
+import { useDefaultTaskInstanceTab } from "src/hooks/useUserSettings";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
 import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
 import { isStatePending, useAutoRefresh, useDocumentTitle } from "src/utils";
+import { getDefaultTaskInstanceTabPath } from "src/utils/links";
 
 import { Header } from "./Header";
 
@@ -43,8 +45,13 @@ export const TaskInstance = () => {
   // Get external views with task_instance destination
   const externalTabs = usePluginTabs("task_instance");
 
+  // When another tab is the default, the index route redirects to it, so the 
Logs
+  // tab must point at the explicit /logs path to stay reachable.
+  const [defaultTab] = useDefaultTaskInstanceTab();
+  const logsTabValue = getDefaultTaskInstanceTabPath(defaultTab) === "" ? "" : 
"logs";
+
   const tabs = [
-    { icon: <MdReorder />, label: translate("tabs.logs"), value: "" },
+    { icon: <MdReorder />, label: translate("tabs.logs"), matchPaths: 
["logs"], value: logsTabValue },
     { icon: <FiUser />, label: translate("tabs.requiredActions"), value: 
"required_actions" },
     {
       icon: <PiBracketsCurlyBold />,
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx 
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index d9bf323cf84..ab6f0df1e6b 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -120,7 +120,7 @@ const parseLogs = ({
   let parsedLines;
   const sources: Array<string> = [];
 
-  const logLink = taskInstance ? 
`${getTaskInstanceLink(taskInstance)}?try_number=${tryNumber}` : "";
+  const logLink = taskInstance ? `${getTaskInstanceLink(taskInstance, 
"logs")}?try_number=${tryNumber}` : "";
 
   try {
     let lineNumber = 0;
diff --git a/airflow-core/src/airflow/ui/src/router.test.tsx 
b/airflow-core/src/airflow/ui/src/router.test.tsx
index e34b5fa31a5..2598dff5d14 100644
--- a/airflow-core/src/airflow/ui/src/router.test.tsx
+++ b/airflow-core/src/airflow/ui/src/router.test.tsx
@@ -19,10 +19,10 @@
 import { matchRoutes } from "react-router-dom";
 import { describe, expect, it } from "vitest";
 
-import { TabEntity } from "src/constants/tab";
+import { TabEntity, TaskInstanceTab } from "src/constants/tab";
 import { getTabPath } from "src/utils/tab";
 
-import { routerConfig } from "./router";
+import { routerConfig, taskInstanceRoutes } from "./router";
 
 const getAdditionalPath = (pathname: string) => {
   const matches = matchRoutes(routerConfig, pathname) ?? [];
@@ -60,3 +60,16 @@ describe("Dag route handles", () => {
     expect(getAdditionalPath(pathname)).toBe("");
   });
 });
+
+type RouteLike = { readonly children?: ReadonlyArray<RouteLike>; readonly 
path?: string };
+
+describe("taskInstanceRoutes stay in sync with the TaskInstanceTab enum", () 
=> {
+  it("uses exactly the enum values as its named tab paths", () => {
+    const namedPaths = (taskInstanceRoutes as ReadonlyArray<RouteLike>)
+      .flatMap((route) => route.children ?? [route])
+      .map((route) => route.path)
+      .filter((path): path is string => path !== undefined && 
!path.startsWith("plugin/"));
+
+    
expect(namedPaths.sort()).toStrictEqual(Object.values(TaskInstanceTab).sort());
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/router.tsx 
b/airflow-core/src/airflow/ui/src/router.tsx
index d342fbf5878..f94e2ecb08d 100644
--- a/airflow-core/src/airflow/ui/src/router.tsx
+++ b/airflow-core/src/airflow/ui/src/router.tsx
@@ -21,7 +21,7 @@ import { createBrowserRouter } from "react-router-dom";
 
 import { UseConfigServiceGetConfigsKeyFn } from "openapi/queries";
 import { ConfigService } from "openapi/requests/services.gen";
-import { TabEntity, TabName } from "src/constants/tab";
+import { TabEntity, TabName, TaskInstanceTab } from "src/constants/tab";
 import { BaseLayout } from "src/layouts/BaseLayout";
 import { DagsLayout } from "src/layouts/DagsLayout";
 import { Asset } from "src/pages/Asset";
@@ -47,6 +47,7 @@ import { ExternalView } from "src/pages/ExternalView";
 import { GroupTaskInstance } from "src/pages/GroupTaskInstance";
 import { HITLTaskInstances } from "src/pages/HITLTaskInstances";
 import { Jobs } from "src/pages/Jobs";
+import { LandingPage } from "src/pages/LandingPage";
 import { MappedTaskInstance } from "src/pages/MappedTaskInstance";
 import { Details as MappedTaskInstanceDetails } from 
"src/pages/MappedTaskInstance/Details";
 import { Plugins } from "src/pages/Plugins";
@@ -61,6 +62,7 @@ import { Task } from "src/pages/Task";
 import { Overview as TaskOverview } from "src/pages/Task/Overview";
 import { TaskInstance, Logs } from "src/pages/TaskInstance";
 import { AssetEvents as TaskInstanceAssetEvents } from 
"src/pages/TaskInstance/AssetEvents";
+import { DefaultTab as TaskInstanceDefaultTab } from 
"src/pages/TaskInstance/DefaultTab";
 import { Details as TaskInstanceDetails } from 
"src/pages/TaskInstance/Details";
 import { HITLResponse } from "src/pages/TaskInstance/HITLResponse";
 import { RenderedTemplates } from "src/pages/TaskInstance/RenderedTemplates";
@@ -78,21 +80,22 @@ const pluginRoute = {
 };
 
 export const taskInstanceRoutes = [
-  { element: <Logs />, index: true, path: undefined },
-  { element: <Events />, path: "events" },
+  { element: <TaskInstanceDefaultTab />, index: true, path: undefined },
+  { element: <Logs />, path: TaskInstanceTab.Logs },
+  { element: <Events />, path: TaskInstanceTab.Events },
   {
     children: [
-      { element: <TaskStateStore />, path: "task-state-store" },
-      { element: <XCom />, path: "xcom" },
+      { element: <TaskStateStore />, path: TaskInstanceTab.TaskStateStore },
+      { element: <XCom />, path: TaskInstanceTab.XCom },
     ],
     element: <StorageLayout />,
   },
-  { element: <Code />, path: "code" },
-  { element: <TaskInstanceDetails />, path: "details" },
-  { element: <RenderedTemplates />, path: "rendered_templates" },
-  { element: <TaskInstances />, path: "task_instances" },
-  { element: <TaskInstanceAssetEvents />, path: "asset_events" },
-  { element: <HITLResponse />, path: "required_actions" },
+  { element: <Code />, path: TaskInstanceTab.Code },
+  { element: <TaskInstanceDetails />, path: TaskInstanceTab.Details },
+  { element: <RenderedTemplates />, path: TaskInstanceTab.RenderedTemplates },
+  { element: <TaskInstances />, path: TaskInstanceTab.TaskInstances },
+  { element: <TaskInstanceAssetEvents />, path: TaskInstanceTab.AssetEvents },
+  { element: <HITLResponse />, path: TaskInstanceTab.RequiredActions },
   pluginRoute,
 ];
 
@@ -100,9 +103,13 @@ export const routerConfig = [
   {
     children: [
       {
-        element: <Dashboard />,
+        element: <LandingPage />,
         index: true,
       },
+      {
+        element: <Dashboard />,
+        path: "home",
+      },
       {
         element: <HITLTaskInstances enableHITLReviewDrawer />,
         path: "required_actions",
diff --git a/airflow-core/src/airflow/ui/src/utils/links.test.ts 
b/airflow-core/src/airflow/ui/src/utils/links.test.ts
index 5afe9f0d4cc..e22e0dc9581 100644
--- a/airflow-core/src/airflow/ui/src/utils/links.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/links.test.ts
@@ -22,6 +22,7 @@ import type { TaskInstanceResponse } from 
"openapi/requests/types.gen";
 
 import {
   buildTaskInstanceUrl,
+  getDefaultTaskInstanceTabPath,
   getNextHref,
   getSafeExternalUrl,
   getTaskInstanceAdditionalPath,
@@ -118,6 +119,7 @@ describe("getTaskInstanceAdditionalPath", () => {
 
   it("should handle all known task instance routes", () => {
     const knownRoutes = [
+      "logs",
       "events",
       "xcom",
       "code",
@@ -156,6 +158,19 @@ describe("getTaskInstanceAdditionalPath", () => {
   });
 });
 
+describe("getDefaultTaskInstanceTabPath", () => {
+  it.each(["details", "rendered_templates", "code", "events", "asset_events", 
"xcom"])(
+    "returns the tab path for %s",
+    (tab) => {
+      expect(getDefaultTaskInstanceTabPath(tab)).toBe(tab);
+    },
+  );
+
+  it.each(["logs", "", "unknown_tab", undefined, 42])("falls back to the index 
route for %s", (value) => {
+    expect(getDefaultTaskInstanceTabPath(value)).toBe("");
+  });
+});
+
 describe("buildTaskInstanceUrl", () => {
   it("should build basic URL types", () => {
     // Basic task instance URL
diff --git a/airflow-core/src/airflow/ui/src/utils/links.ts 
b/airflow-core/src/airflow/ui/src/utils/links.ts
index 132a9e3f471..dffd32c3580 100644
--- a/airflow-core/src/airflow/ui/src/utils/links.ts
+++ b/airflow-core/src/airflow/ui/src/utils/links.ts
@@ -17,6 +17,11 @@
  * under the License.
  */
 import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import {
+  DEFAULT_TASK_INSTANCE_TAB_PATHS,
+  type DefaultTaskInstanceTab,
+  type TaskInstanceTabValue,
+} from "src/constants/tab";
 import { taskInstanceRoutes } from "src/router";
 
 export const getTaskInstanceLink = (
@@ -28,16 +33,21 @@ export const getTaskInstanceLink = (
         mapIndex?: number;
         taskId: string;
       },
+  tab?: TaskInstanceTabValue,
 ): string => {
+  const tabPath = tab === undefined ? "" : `/${tab}`;
+
   if ("dag_id" in tiOrParams) {
     return 
`/dags/${tiOrParams.dag_id}/runs/${tiOrParams.dag_run_id}/tasks/${tiOrParams.task_id}${
       tiOrParams.map_index >= 0 ? `/mapped/${tiOrParams.map_index}` : ""
-    }`;
+    }${tabPath}`;
   }
 
   const { dagId, dagRunId, mapIndex = -1, taskId } = tiOrParams;
 
-  return `/dags/${dagId}/runs/${dagRunId}/tasks/${taskId}${mapIndex >= 0 ? 
`/mapped/${mapIndex}` : ""}`;
+  return `/dags/${dagId}/runs/${dagRunId}/tasks/${taskId}${
+    mapIndex >= 0 ? `/mapped/${mapIndex}` : ""
+  }${tabPath}`;
 };
 
 export const getRedirectPath = (targetPath: string): string => {
@@ -93,6 +103,13 @@ export const getTaskInstanceAdditionalPath = (pathname: 
string): string => {
   return "";
 };
 
+// Resolve a stored default-tab preference to a route path. Logs maps to "" 
(the index
+// route), and any unknown value falls back to "" so the index keeps rendering 
in place.
+export const getDefaultTaskInstanceTabPath = (value: unknown): string =>
+  typeof value === "string" && value in DEFAULT_TASK_INSTANCE_TAB_PATHS
+    ? DEFAULT_TASK_INSTANCE_TAB_PATHS[value as DefaultTaskInstanceTab]
+    : "";
+
 const SAFE_EXTERNAL_URL_SCHEMES = new Set(["http:", "https:", "mailto:";]);
 
 /**

Reply via email to