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 326c119e44d Add a user settings page for cross-page UI defaults 
(#70687)
326c119e44d is described below

commit 326c119e44d888df2c54de6d3bacb726432f8ec3
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Tue Aug 11 18:42:59 2026 +0200

    Add a user settings page for cross-page UI defaults (#70687)
    
    Several cross-page UI behaviors have no single place to set a personal 
default. Graph layout direction and dependency scope are remembered per-Dag, 
and the clear / mark-as dialog selections reset to a fixed choice every time, 
so users re-apply the same options over and over. Centralizing these as 
browser-local preferences lets a user set their default once and have the 
individual features fall back to it.
---
 .../airflow/ui/public/i18n/locales/en/common.json  |  35 ++-
 .../ui/src/components/Clear/Run/ClearRunDialog.tsx |   6 +-
 .../TaskInstance/ClearGroupTaskInstanceDialog.tsx  |   6 +-
 .../Clear/TaskInstance/ClearTaskInstanceDialog.tsx |  12 +-
 .../components/Graph/DirectionDropdown.test.tsx    |  74 ++++++
 .../ui/src/components/Graph/DirectionDropdown.tsx  |   4 +-
 .../TaskInstance/MarkTaskInstanceAsDialog.tsx      |   5 +-
 .../src/airflow/ui/src/constants/localStorage.ts   |   5 +
 .../airflow/ui/src/hooks/useUserSettings.test.tsx  | 149 ++++++++++++
 .../src/airflow/ui/src/hooks/useUserSettings.ts    |  54 +++++
 .../airflow/ui/src/layouts/Details/Graph/Graph.tsx |   4 +-
 .../ui/src/layouts/Nav/UserSettingsButton.test.tsx |  37 +++
 .../ui/src/layouts/Nav/UserSettingsButton.tsx      |  23 +-
 .../src/airflow/ui/src/pages/Asset/AssetGraph.tsx  |   4 +-
 .../ui/src/pages/Settings/Settings.test.tsx        | 118 ++++++++++
 .../src/airflow/ui/src/pages/Settings/Settings.tsx | 256 +++++++++++++++++++++
 .../src/airflow/ui/src/pages/Settings/index.tsx    |  19 ++
 airflow-core/src/airflow/ui/src/router.tsx         |   5 +
 18 files changed, 788 insertions(+), 28 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 7db7ed5306c..1d37e59d795 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
@@ -88,8 +88,6 @@
   "dagRunId": "Dag Run ID",
   "dagRunState": "Dag Run State",
   "dagWarnings": "Dag warnings/errors",
-  "defaultToGraphView": "Default to graph view",
-  "defaultToGridView": "Default to grid view",
   "delete": "Delete",
   "diff": "Diff",
   "diffCompareWith": "Compare with",
@@ -246,6 +244,39 @@
   },
   "selectLanguage": "Select Language",
   "selected": "Selected",
+  "settings": {
+    "clearing": {
+      "preventRunningTask": {
+        "helper": "Skip tasks that are currently running when clearing task 
instances.",
+        "label": "Prevent clearing running tasks"
+      },
+      "runSelection": {
+        "helper": "Options selected by default when clearing a Dag run.",
+        "label": "Default run clear selection"
+      },
+      "taskSelection": {
+        "helper": "Options selected by default when clearing task instances.",
+        "label": "Default task clear selection"
+      },
+      "title": "Clearing"
+    },
+    "description": "These preferences are saved in this browser only and apply 
across the whole app.",
+    "graph": {
+      "defaultDirection": {
+        "helper": "Layout direction for Dag and asset graphs you haven't set 
individually.",
+        "label": "Default graph direction"
+      },
+      "title": "Graph"
+    },
+    "marking": {
+      "taskSelection": {
+        "helper": "Options selected by default when marking a task instance's 
state.",
+        "label": "Default mark selection"
+      },
+      "title": "Marking"
+    },
+    "title": "Settings"
+  },
   "shortcuts": {
     "categories": {
       "code": "Code",
diff --git 
a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx 
b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
index c864f0b047c..c4124cc8878 100644
--- a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
@@ -28,6 +28,7 @@ import { getRunOnLatestVersionState } from 
"src/components/Clear/TaskInstance/ru
 import { useRerunWithLatestVersion } from 
"src/components/Clear/useRerunWithLatestVersion";
 import { Checkbox, Dialog } from "src/components/ui";
 import SegmentedControl from "src/components/ui/SegmentedControl";
+import { useClearRunDefaultOptions } from "src/hooks/useUserSettings";
 import { useClearDagRunDryRun } from "src/queries/useClearDagRunDryRun";
 import { useClearDagRun } from "src/queries/useClearRun";
 import { isStatePending, useAutoRefresh } from "src/utils";
@@ -55,7 +56,8 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) => {
     setNote(dagRun.note);
     onClose();
   };
-  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(["existingTasks"]);
+  const [clearRunDefaultOptions] = useClearRunDefaultOptions();
+  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(clearRunDefaultOptions);
   const onlyFailed = selectedOptions.includes("onlyFailed");
   const onlyNew = selectedOptions.includes("newTasks");
 
@@ -132,7 +134,7 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) 
=> {
         <Dialog.Body width="full">
           <Flex justifyContent="center">
             <SegmentedControl
-              defaultValues={["existingTasks"]}
+              defaultValues={clearRunDefaultOptions}
               onChange={setSelectedOptions}
               options={[
                 {
diff --git 
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx
 
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx
index ab61c46e7ae..6a7e33354ae 100644
--- 
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearGroupTaskInstanceDialog.tsx
@@ -28,6 +28,7 @@ import { ActionAccordion } from 
"src/components/ActionAccordion";
 import { useRerunWithLatestVersion } from 
"src/components/Clear/useRerunWithLatestVersion";
 import { Checkbox, Dialog } from "src/components/ui";
 import SegmentedControl from "src/components/ui/SegmentedControl";
+import { useClearTaskInstanceDefaultOptions } from "src/hooks/useUserSettings";
 import { useClearTaskInstances } from "src/queries/useClearTaskInstances";
 import { useClearTaskInstancesDryRun } from 
"src/queries/useClearTaskInstancesDryRun";
 import { isStatePending, useAutoRefresh } from "src/utils";
@@ -51,7 +52,8 @@ export const ClearGroupTaskInstanceDialog = ({ onClose, open, 
taskInstance }: Pr
     onSuccessConfirm: onClose,
   });
 
-  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(["downstream"]);
+  const [clearTaskInstanceDefaultOptions] = 
useClearTaskInstanceDefaultOptions();
+  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(clearTaskInstanceDefaultOptions);
 
   const onlyFailed = selectedOptions.includes("onlyFailed");
   const past = selectedOptions.includes("past");
@@ -144,7 +146,7 @@ export const ClearGroupTaskInstanceDialog = ({ onClose, 
open, taskInstance }: Pr
         <Dialog.Body width="full">
           <Flex justifyContent="center">
             <SegmentedControl
-              defaultValues={["downstream"]}
+              defaultValues={clearTaskInstanceDefaultOptions}
               multiple
               onChange={setSelectedOptions}
               options={[
diff --git 
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
 
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
index 0026da07395..53525e320b6 100644
--- 
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
@@ -29,6 +29,10 @@ import { useRerunWithLatestVersion } from 
"src/components/Clear/useRerunWithLate
 import Time from "src/components/Time";
 import { Checkbox, Dialog } from "src/components/ui";
 import SegmentedControl from "src/components/ui/SegmentedControl";
+import {
+  useClearPreventRunningTaskDefault,
+  useClearTaskInstanceDefaultOptions,
+} from "src/hooks/useUserSettings";
 import { useClearTaskInstances } from "src/queries/useClearTaskInstances";
 import { useClearTaskInstancesDryRun } from 
"src/queries/useClearTaskInstancesDryRun";
 import { isStatePending, useAutoRefresh } from "src/utils";
@@ -77,14 +81,16 @@ const ClearTaskInstanceDialog = (props: Props) => {
   const { t: translate } = useTranslation();
   const { onClose, onOpen, open } = useDisclosure();
 
-  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(["downstream"]);
+  const [clearTaskInstanceDefaultOptions] = 
useClearTaskInstanceDefaultOptions();
+  const [preventRunningTaskDefault] = useClearPreventRunningTaskDefault();
+  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(clearTaskInstanceDefaultOptions);
 
   const onlyFailed = selectedOptions.includes("onlyFailed");
   const past = selectedOptions.includes("past");
   const future = selectedOptions.includes("future");
   const upstream = selectedOptions.includes("upstream");
   const downstream = selectedOptions.includes("downstream");
-  const [preventRunningTask, setPreventRunningTask] = useState(true);
+  const [preventRunningTask, setPreventRunningTask] = 
useState(preventRunningTaskDefault);
 
   const [note, setNote] = useState<string | null>(taskInstance?.note ?? null);
 
@@ -215,7 +221,7 @@ const ClearTaskInstanceDialog = (props: Props) => {
           <Dialog.Body width="full">
             <Flex justifyContent="center">
               <SegmentedControl
-                defaultValues={["downstream"]}
+                defaultValues={clearTaskInstanceDefaultOptions}
                 multiple
                 onChange={setSelectedOptions}
                 options={[
diff --git 
a/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.test.tsx 
b/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.test.tsx
new file mode 100644
index 00000000000..a78b1506811
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.test.tsx
@@ -0,0 +1,74 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import "@testing-library/jest-dom/vitest";
+import { render, screen } from "@testing-library/react";
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import { afterEach, beforeAll, describe, expect, it } from "vitest";
+
+import { DEFAULT_GRAPH_DIRECTION_KEY, directionKey } from 
"src/constants/localStorage";
+import { ChakraWrapper } from "src/utils/ChakraWrapper";
+
+import { DirectionDropdown } from "./DirectionDropdown";
+
+beforeAll(async () => {
+  await i18n.use(initReactI18next).init({
+    defaultNS: "components",
+    fallbackLng: "en",
+    interpolation: { escapeValue: false },
+    lng: "en",
+    ns: ["components", "dag"],
+    resources: {
+      en: {
+        components: {
+          graph: {
+            directionDown: "DOWN-LABEL",
+            directionLeft: "LEFT-LABEL",
+            directionRight: "RIGHT-LABEL",
+            directionUp: "UP-LABEL",
+          },
+        },
+        dag: { panel: { graphDirection: { label: "Direction" } } },
+      },
+    },
+  });
+});
+
+afterEach(() => {
+  localStorage.clear();
+});
+
+describe("DirectionDropdown", () => {
+  it("initializes from the global default when no per-graph direction is 
stored", () => {
+    localStorage.setItem(DEFAULT_GRAPH_DIRECTION_KEY, JSON.stringify("DOWN"));
+
+    render(<DirectionDropdown graphId="test-dag" />, { wrapper: ChakraWrapper 
});
+
+    expect(screen.getByRole("combobox")).toHaveTextContent("DOWN-LABEL");
+  });
+
+  it("uses the stored per-graph direction over the global default", () => {
+    localStorage.setItem(DEFAULT_GRAPH_DIRECTION_KEY, JSON.stringify("DOWN"));
+    localStorage.setItem(directionKey("test-dag"), JSON.stringify("LEFT"));
+
+    render(<DirectionDropdown graphId="test-dag" />, { wrapper: ChakraWrapper 
});
+
+    expect(screen.getByRole("combobox")).toHaveTextContent("LEFT-LABEL");
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx 
b/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx
index 41ad483ac90..82228d5b408 100644
--- a/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx
@@ -21,13 +21,15 @@ import { useTranslation } from "react-i18next";
 import { useLocalStorage } from "usehooks-ts";
 
 import { directionKey } from "src/constants/localStorage";
+import { useDefaultGraphDirection } from "src/hooks/useUserSettings";
 
 export type Direction = "DOWN" | "LEFT" | "RIGHT" | "UP";
 
 export const DirectionDropdown = ({ graphId }: { readonly graphId: string }) 
=> {
   const { t: translate } = useTranslation(["components", "dag"]);
 
-  const [direction, setDirection] = 
useLocalStorage<Direction>(directionKey(graphId), "RIGHT");
+  const [defaultDirection] = useDefaultGraphDirection();
+  const [direction, setDirection] = 
useLocalStorage<Direction>(directionKey(graphId), defaultDirection);
 
   const directionOptions = () =>
     createListCollection({
diff --git 
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
 
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
index a89e6189f5f..dc1919a54a5 100644
--- 
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
@@ -26,6 +26,7 @@ import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { Dialog } from "src/components/ui";
 import SegmentedControl from "src/components/ui/SegmentedControl";
+import { useMarkTaskInstanceDefaultOptions } from "src/hooks/useUserSettings";
 import { usePatchTaskInstance } from "src/queries/usePatchTaskInstance";
 import { usePatchTaskInstanceDryRun } from 
"src/queries/usePatchTaskInstanceDryRun";
 
@@ -43,7 +44,8 @@ const MarkTaskInstanceAsDialog = ({ onClose, open, state, 
taskInstance }: Props)
   const mapIndex = taskInstance.map_index;
   const { t: translate } = useTranslation();
 
-  const [selectedOptions, setSelectedOptions] = useState<Array<string>>([]);
+  const [markTaskInstanceDefaultOptions] = useMarkTaskInstanceDefaultOptions();
+  const [selectedOptions, setSelectedOptions] = 
useState<Array<string>>(markTaskInstanceDefaultOptions);
 
   const past = selectedOptions.includes("past");
   const future = selectedOptions.includes("future");
@@ -126,6 +128,7 @@ const MarkTaskInstanceAsDialog = ({ onClose, open, state, 
taskInstance }: Props)
         <Dialog.Body width="full">
           <Flex justifyContent="center">
             <SegmentedControl
+              defaultValues={markTaskInstanceDefaultOptions}
               multiple
               onChange={setSelectedOptions}
               options={[
diff --git a/airflow-core/src/airflow/ui/src/constants/localStorage.ts 
b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
index 9e366796fd6..85e1d101350 100644
--- a/airflow-core/src/airflow/ui/src/constants/localStorage.ts
+++ b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
@@ -29,6 +29,11 @@ export const LOG_SHOW_SOURCE_KEY = "log_show_source";
 export const VERSION_INDICATOR_DISPLAY_MODE_KEY = 
"version_indicator_display_mode";
 export const COLLAPSED_UI_ALERTS_KEY = "collapsed_ui_alerts";
 export const SHOW_ALL_DEPENDENCIES_KEY = "show_all_dependencies";
+export const DEFAULT_GRAPH_DIRECTION_KEY = "default_graph_direction";
+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";
 
 // Dag-scoped keys
 export const dagRunsLimitKey = (dagId: string) => `dag_runs_limit-${dagId}`;
diff --git a/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx 
b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx
new file mode 100644
index 00000000000..4117595b0de
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.test.tsx
@@ -0,0 +1,149 @@
+/*!
+ * 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 { act, renderHook } from "@testing-library/react";
+import { afterEach, describe, expect, it } from "vitest";
+
+import {
+  CLEAR_PREVENT_RUNNING_TASK_KEY,
+  CLEAR_RUN_DEFAULT_OPTIONS_KEY,
+  CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
+  DEFAULT_GRAPH_DIRECTION_KEY,
+  MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
+} from "src/constants/localStorage";
+
+import {
+  useClearPreventRunningTaskDefault,
+  useClearRunDefaultOptions,
+  useClearTaskInstanceDefaultOptions,
+  useDefaultGraphDirection,
+  useMarkTaskInstanceDefaultOptions,
+} from "./useUserSettings";
+
+afterEach(() => {
+  localStorage.clear();
+});
+
+describe("useDefaultGraphDirection", () => {
+  it("defaults to RIGHT when nothing is stored", () => {
+    const { result } = renderHook(() => useDefaultGraphDirection());
+
+    expect(result.current[0]).toBe("RIGHT");
+  });
+
+  it("reads an existing stored direction", () => {
+    localStorage.setItem(DEFAULT_GRAPH_DIRECTION_KEY, JSON.stringify("LEFT"));
+
+    const { result } = renderHook(() => useDefaultGraphDirection());
+
+    expect(result.current[0]).toBe("LEFT");
+  });
+
+  it("persists a new direction to localStorage", () => {
+    const { result } = renderHook(() => useDefaultGraphDirection());
+
+    act(() => {
+      result.current[1]("DOWN");
+    });
+
+    expect(result.current[0]).toBe("DOWN");
+    expect(JSON.parse(localStorage.getItem(DEFAULT_GRAPH_DIRECTION_KEY) ?? 
'""')).toBe("DOWN");
+  });
+});
+
+describe("useClearRunDefaultOptions", () => {
+  it("defaults to ['existingTasks']", () => {
+    const { result } = renderHook(() => useClearRunDefaultOptions());
+
+    expect(result.current[0]).toEqual(["existingTasks"]);
+  });
+
+  it("persists a new selection", () => {
+    const { result } = renderHook(() => useClearRunDefaultOptions());
+
+    act(() => {
+      result.current[1](["onlyFailed"]);
+    });
+
+    expect(result.current[0]).toEqual(["onlyFailed"]);
+    expect(JSON.parse(localStorage.getItem(CLEAR_RUN_DEFAULT_OPTIONS_KEY) ?? 
"[]")).toEqual(["onlyFailed"]);
+  });
+});
+
+describe("useClearTaskInstanceDefaultOptions", () => {
+  it("defaults to ['downstream']", () => {
+    const { result } = renderHook(() => useClearTaskInstanceDefaultOptions());
+
+    expect(result.current[0]).toEqual(["downstream"]);
+  });
+
+  it("persists a new selection", () => {
+    const { result } = renderHook(() => useClearTaskInstanceDefaultOptions());
+
+    act(() => {
+      result.current[1](["past", "future", "downstream"]);
+    });
+
+    expect(result.current[0]).toEqual(["past", "future", "downstream"]);
+    
expect(JSON.parse(localStorage.getItem(CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY) 
?? "[]")).toEqual([
+      "past",
+      "future",
+      "downstream",
+    ]);
+  });
+});
+
+describe("useClearPreventRunningTaskDefault", () => {
+  it("defaults to true", () => {
+    const { result } = renderHook(() => useClearPreventRunningTaskDefault());
+
+    expect(result.current[0]).toBe(true);
+  });
+
+  it("persists a new value", () => {
+    const { result } = renderHook(() => useClearPreventRunningTaskDefault());
+
+    act(() => {
+      result.current[1](false);
+    });
+
+    expect(result.current[0]).toBe(false);
+    expect(JSON.parse(localStorage.getItem(CLEAR_PREVENT_RUNNING_TASK_KEY) ?? 
"true")).toBe(false);
+  });
+});
+
+describe("useMarkTaskInstanceDefaultOptions", () => {
+  it("defaults to an empty selection", () => {
+    const { result } = renderHook(() => useMarkTaskInstanceDefaultOptions());
+
+    expect(result.current[0]).toEqual([]);
+  });
+
+  it("persists a new selection", () => {
+    const { result } = renderHook(() => useMarkTaskInstanceDefaultOptions());
+
+    act(() => {
+      result.current[1](["downstream"]);
+    });
+
+    expect(result.current[0]).toEqual(["downstream"]);
+    
expect(JSON.parse(localStorage.getItem(MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY) 
?? "[]")).toEqual([
+      "downstream",
+    ]);
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts 
b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts
new file mode 100644
index 00000000000..bec8c853b5b
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/hooks/useUserSettings.ts
@@ -0,0 +1,54 @@
+/*!
+ * 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 { useLocalStorage } from "usehooks-ts";
+
+import type { Direction } from "src/components/Graph/DirectionDropdown";
+import {
+  CLEAR_PREVENT_RUNNING_TASK_KEY,
+  CLEAR_RUN_DEFAULT_OPTIONS_KEY,
+  CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
+  DEFAULT_GRAPH_DIRECTION_KEY,
+  MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
+} from "src/constants/localStorage";
+
+/**
+ * User-configurable defaults surfaced in the Settings page and consumed as
+ * fallbacks by the individual features. Everything is persisted in this
+ * browser's localStorage only — there is no server-side user profile.
+ */
+
+/** Fallback graph layout direction used when a graph has no per-graph choice. 
*/
+export const useDefaultGraphDirection = () =>
+  useLocalStorage<Direction>(DEFAULT_GRAPH_DIRECTION_KEY, "RIGHT");
+
+/** Default selection for the Dag-run Clear dialog toggle (existing / 
only-failed / queue-new). */
+export const useClearRunDefaultOptions = () =>
+  useLocalStorage<Array<string>>(CLEAR_RUN_DEFAULT_OPTIONS_KEY, 
["existingTasks"]);
+
+/** Default selection for the task-instance Clear dialog toggle (past / future 
/ … / only-failed). */
+export const useClearTaskInstanceDefaultOptions = () =>
+  useLocalStorage<Array<string>>(CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY, 
["downstream"]);
+
+/** Default state of the "prevent running tasks" checkbox when clearing task 
instances. */
+export const useClearPreventRunningTaskDefault = () =>
+  useLocalStorage<boolean>(CLEAR_PREVENT_RUNNING_TASK_KEY, true);
+
+/** Default selection for the "Mark as" task-instance dialog toggle (past / 
future / … ). */
+export const useMarkTaskInstanceDefaultOptions = () =>
+  useLocalStorage<Array<string>>(MARK_TASK_INSTANCE_DEFAULT_OPTIONS_KEY, []);
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
index 73d92d89bfc..3803a4079d5 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
@@ -33,6 +33,7 @@ import { SHOW_ALL_DEPENDENCIES_KEY, directionKey } from 
"src/constants/localStor
 import { useColorMode } from "src/context/colorMode";
 import { useGroups } from "src/context/groups";
 import useSelectedVersion from "src/hooks/useSelectedVersion";
+import { useDefaultGraphDirection } from "src/hooks/useUserSettings";
 import { flattenGraphNodes } from "src/layouts/Details/Grid/utils.ts";
 import { useDependencyGraph } from "src/queries/useDependencyGraph";
 import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
@@ -71,7 +72,8 @@ export const Graph = () => {
   const { allGroupIds, openGroupIds, setAllGroupIds } = useGroups();
 
   const [showAllDependencies] = 
useLocalStorage<boolean>(SHOW_ALL_DEPENDENCIES_KEY, false);
-  const [direction] = useLocalStorage<Direction>(directionKey(dagId), "RIGHT");
+  const [defaultDirection] = useDefaultGraphDirection();
+  const [direction] = useLocalStorage<Direction>(directionKey(dagId), 
defaultDirection);
 
   const selectedColor = colorMode === "dark" ? selectedDarkColor : 
selectedLightColor;
   const { data: graphData = { edges: [], nodes: [] } } = 
useStructureServiceStructureData(
diff --git 
a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.test.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.test.tsx
new file mode 100644
index 00000000000..c6688151b16
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.test.tsx
@@ -0,0 +1,37 @@
+/*!
+ * 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 { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { UserSettingsButton } from "./UserSettingsButton";
+
+describe("UserSettingsButton", () => {
+  it("links to the settings page from the user menu", async () => {
+    render(<UserSettingsButton externalViews={[]} />, { wrapper: Wrapper });
+
+    fireEvent.click(screen.getByRole("button", { name: /user/iu }));
+
+    const settingsLink = await screen.findByRole("menuitem", { name: 
/settings.title/iu });
+
+    expect(settingsLink).toHaveAttribute("href", "/settings");
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
index 115737892ee..df41e3912fd 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
@@ -19,10 +19,10 @@
 import { Box, Icon, useDisclosure } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 import {
-  FiGrid,
   FiKey,
   FiLogOut,
   FiMoon,
+  FiSettings,
   FiSun,
   FiUser,
   FiGlobe,
@@ -31,12 +31,10 @@ import {
   FiChevronLeft,
   FiMonitor,
 } from "react-icons/fi";
-import { MdOutlineAccountTree } from "react-icons/md";
-import { useLocalStorage } from "usehooks-ts";
 
 import { useAuthLinksServiceGetCurrentUserInfo } from "openapi/queries";
 import { Menu } from "src/components/ui";
-import { DEFAULT_DAG_VIEW_KEY } from "src/constants/localStorage";
+import { RouterLink } from "src/components/ui/RouterLink";
 import { useColorMode } from "src/context/colorMode/useColorMode";
 import type { NavItemResponse } from "src/utils/types";
 
@@ -79,8 +77,6 @@ export const UserSettingsButton = ({ externalViews }: { 
readonly externalViews:
   const { onClose: onCloseLanguage, onOpen: onOpenLanguage, open: 
isOpenLanguage } = useDisclosure();
   const { onClose: onCloseToken, onOpen: onOpenToken, open: isOpenToken } = 
useDisclosure();
 
-  const [dagView, setDagView] = useLocalStorage<"graph" | 
"grid">(DEFAULT_DAG_VIEW_KEY, "grid");
-
   const theme = selectedTheme ?? COLOR_MODES.SYSTEM;
 
   const isRTL = i18n.dir() === "rtl";
@@ -105,6 +101,12 @@ export const UserSettingsButton = ({ externalViews }: { 
readonly externalViews:
               <Menu.Separator />
             </>
           ) : undefined}
+          <Menu.Item asChild value="settings">
+            <RouterLink color="inherit" to="/settings">
+              <Icon as={FiSettings} boxSize={4} />
+              <Box flex="1">{translate("settings.title")}</Box>
+            </RouterLink>
+          </Menu.Item>
           <Menu.Item onClick={onOpenLanguage} value="language">
             <Icon as={FiGlobe} boxSize={4} />
             <Box flex="1">{translate("selectLanguage")}</Box>
@@ -127,15 +129,6 @@ export const UserSettingsButton = ({ externalViews }: { 
readonly externalViews:
               </Menu.RadioItemGroup>
             </Menu.Content>
           </Menu.Root>
-          <Menu.Item
-            onClick={() => (dagView === "grid" ? setDagView("graph") : 
setDagView("grid"))}
-            value={dagView}
-          >
-            <Icon as={dagView === "grid" ? MdOutlineAccountTree : FiGrid} 
boxSize={4} />
-            <Box flex="1">
-              {dagView === "grid" ? translate("defaultToGraphView") : 
translate("defaultToGridView")}
-            </Box>
-          </Menu.Item>
           <Menu.Item onClick={onOpenToken} value="generateToken">
             <Icon as={FiKey} boxSize={4} />
             <Box flex="1">{translate("generateToken")}</Box>
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx 
b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
index e49c4a5cea2..ed0e2eab41c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx
@@ -30,6 +30,7 @@ import { getGatePathEdgeIdsForSelection, type CustomNodeProps 
} from "src/compon
 import { useGraphLayout } from "src/components/Graph/useGraphLayout";
 import { directionKey } from "src/constants/localStorage";
 import { useColorMode } from "src/context/colorMode";
+import { useDefaultGraphDirection } from "src/hooks/useUserSettings";
 import { useDependencyGraph } from "src/queries/useDependencyGraph";
 import { getReactFlowThemeStyle } from "src/theme";
 
@@ -48,7 +49,8 @@ export const AssetGraph = ({
     dependencyType,
   });
 
-  const [direction] = useLocalStorage<Direction>(directionKey(assetId ?? ""), 
"RIGHT");
+  const [defaultDirection] = useDefaultGraphDirection();
+  const [direction] = useLocalStorage<Direction>(directionKey(assetId ?? ""), 
defaultDirection);
 
   const { data: layoutData } = useGraphLayout({
     ...graphData,
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
new file mode 100644
index 00000000000..51ef9cd8b8d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Settings/Settings.test.tsx
@@ -0,0 +1,118 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import "@testing-library/jest-dom/vitest";
+import { render, screen } from "@testing-library/react";
+import 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 { BaseWrapper } from "src/utils/Wrapper";
+
+import { Settings } from "./Settings";
+
+beforeAll(async () => {
+  await i18n.use(initReactI18next).init({
+    defaultNS: "common",
+    fallbackLng: "en",
+    interpolation: { escapeValue: false },
+    lng: "en",
+    ns: ["common", "components", "dag", "dags"],
+    resources: {
+      en: {
+        common: {
+          settings: {
+            clearing: {
+              preventRunningTask: { helper: "helper", label: "Prevent clearing 
running tasks" },
+              runSelection: { helper: "helper", label: "Default run clear 
selection" },
+              taskSelection: { helper: "helper", label: "Default task clear 
selection" },
+              title: "Clearing",
+            },
+            description: "browser only",
+            graph: {
+              defaultDirection: { helper: "helper", label: "Default graph 
direction" },
+              title: "Graph",
+            },
+            marking: {
+              taskSelection: { helper: "helper", label: "Default mark 
selection" },
+              title: "Marking",
+            },
+            title: "Settings",
+          },
+        },
+        components: {
+          graph: {
+            directionDown: "DOWN-LABEL",
+            directionLeft: "LEFT-LABEL",
+            directionRight: "RIGHT-LABEL",
+            directionUp: "UP-LABEL",
+          },
+        },
+        dags: {
+          runAndTaskActions: {
+            options: {
+              downstream: "DOWNSTREAM-OPT",
+              existingTasks: "EXISTING-OPT",
+              future: "FUTURE-OPT",
+              onlyFailed: "ONLY-FAILED-OPT",
+              past: "PAST-OPT",
+              queueNew: "QUEUE-NEW-OPT",
+              upstream: "UPSTREAM-OPT",
+            },
+          },
+        },
+      },
+    },
+  });
+});
+
+afterEach(() => {
+  localStorage.clear();
+});
+
+describe("Settings page", () => {
+  it("renders the graph, clearing and marking settings", () => {
+    render(<Settings />, { wrapper: BaseWrapper });
+
+    expect(screen.getByText("Settings")).toBeInTheDocument();
+
+    // Selects and the switch expose test ids.
+    for (const testId of ["default-graph-direction", 
"clear-prevent-running-task"]) {
+      expect(screen.getByTestId(testId)).toBeInTheDocument();
+    }
+
+    // Toggle settings are identified by their labels.
+    expect(screen.getByText("Default run clear 
selection")).toBeInTheDocument();
+    expect(screen.getByText("Default task clear 
selection")).toBeInTheDocument();
+    expect(screen.getByText("Default mark selection")).toBeInTheDocument();
+
+    // The prevent-running switch defaults to on.
+    
expect(screen.getByTestId("clear-prevent-running-task")).toHaveAttribute("data-state",
 "checked");
+  });
+
+  it("reflects stored values in the controls", () => {
+    localStorage.setItem(DEFAULT_GRAPH_DIRECTION_KEY, JSON.stringify("DOWN"));
+    localStorage.setItem(CLEAR_PREVENT_RUNNING_TASK_KEY, 
JSON.stringify(false));
+
+    render(<Settings />, { wrapper: BaseWrapper });
+
+    
expect(screen.getByTestId("default-graph-direction")).toHaveTextContent("DOWN-LABEL");
+    
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
new file mode 100644
index 00000000000..0400dde881b
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Settings/Settings.tsx
@@ -0,0 +1,256 @@
+/*!
+ * 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 { Box, createListCollection, Flex, Heading, Stack, Text } from 
"@chakra-ui/react";
+import type { ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+
+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 {
+  useClearPreventRunningTaskDefault,
+  useClearRunDefaultOptions,
+  useClearTaskInstanceDefaultOptions,
+  useDefaultGraphDirection,
+  useMarkTaskInstanceDefaultOptions,
+} from "src/hooks/useUserSettings";
+import { useDocumentTitle } from "src/utils";
+import type { Option } from "src/utils/option";
+
+type SelectOption<T extends string> = { label: string; value: T };
+
+const SettingRow = ({
+  control,
+  helper,
+  label,
+}: {
+  readonly control: ReactNode;
+  readonly helper?: string;
+  readonly label: string;
+}) => (
+  <Flex align="center" gap={6} justifyContent="space-between" py={3}>
+    <Box>
+      <Text fontWeight="medium">{label}</Text>
+      {helper === undefined ? undefined : (
+        <Text color="fg.muted" fontSize="sm">
+          {helper}
+        </Text>
+      )}
+    </Box>
+    {control}
+  </Flex>
+);
+
+const SelectSetting = <T extends string>({
+  helper,
+  label,
+  onChange,
+  options,
+  testId,
+  value,
+}: {
+  readonly helper?: string;
+  readonly label: string;
+  readonly onChange: (value: T) => void;
+  readonly options: Array<SelectOption<T>>;
+  readonly testId: string;
+  readonly value: T;
+}) => {
+  const collection = createListCollection({ items: options });
+
+  return (
+    <SettingRow
+      control={
+        <Select.Root
+          collection={collection}
+          onValueChange={(event) => {
+            const [next] = event.value;
+
+            if (next !== undefined) {
+              onChange(next as T);
+            }
+          }}
+          value={[value]}
+          width="220px"
+        >
+          <Select.Trigger dataTestId={testId}>
+            <Select.ValueText />
+          </Select.Trigger>
+          <Select.Content>
+            {options.map((option) => (
+              <Select.Item item={option} key={option.value}>
+                {option.label}
+              </Select.Item>
+            ))}
+          </Select.Content>
+        </Select.Root>
+      }
+      helper={helper}
+      label={label}
+    />
+  );
+};
+
+const ToggleSetting = ({
+  defaultValues,
+  helper,
+  label,
+  multiple = false,
+  onChange,
+  options,
+}: {
+  readonly defaultValues: Array<string>;
+  readonly helper?: string;
+  readonly label: string;
+  readonly multiple?: boolean;
+  readonly onChange: (values: Array<string>) => void;
+  readonly options: Array<Option>;
+}) => (
+  <Box py={3}>
+    <Text fontWeight="medium">{label}</Text>
+    {helper === undefined ? undefined : (
+      <Text color="fg.muted" fontSize="sm" mb={2}>
+        {helper}
+      </Text>
+    )}
+    <SegmentedControl
+      defaultValues={defaultValues}
+      multiple={multiple}
+      onChange={onChange}
+      options={options}
+    />
+  </Box>
+);
+
+const Section = ({ children, title }: { readonly children: ReactNode; readonly 
title: string }) => (
+  <Box>
+    <Heading
+      borderBottomWidth="1px"
+      color="fg.muted"
+      fontSize="sm"
+      fontWeight="bold"
+      letterSpacing="wider"
+      mb={1}
+      pb={2}
+      textTransform="uppercase"
+    >
+      {title}
+    </Heading>
+    <Stack divideY="1px" gap={0}>
+      {children}
+    </Stack>
+  </Box>
+);
+
+export const Settings = () => {
+  const { t: translate } = useTranslation(["common", "components", "dags"]);
+
+  useDocumentTitle(translate("settings.title"));
+
+  const [graphDirection, setGraphDirection] = useDefaultGraphDirection();
+  const [clearRunOptions, setClearRunOptions] = useClearRunDefaultOptions();
+  const [clearTaskOptions, setClearTaskOptions] = 
useClearTaskInstanceDefaultOptions();
+  const [preventRunningTask, setPreventRunningTask] = 
useClearPreventRunningTaskDefault();
+  const [markTaskOptions, setMarkTaskOptions] = 
useMarkTaskInstanceDefaultOptions();
+
+  const directionOptions: Array<SelectOption<Direction>> = [
+    { label: translate("components:graph.directionRight"), value: "RIGHT" },
+    { label: translate("components:graph.directionLeft"), value: "LEFT" },
+    { label: translate("components:graph.directionUp"), value: "UP" },
+    { label: translate("components:graph.directionDown"), value: "DOWN" },
+  ];
+
+  const clearRunToggleOptions: Array<Option> = [
+    { label: translate("dags:runAndTaskActions.options.existingTasks"), value: 
"existingTasks" },
+    { label: translate("dags:runAndTaskActions.options.onlyFailed"), value: 
"onlyFailed" },
+    { label: translate("dags:runAndTaskActions.options.queueNew"), value: 
"newTasks" },
+  ];
+
+  const directionalToggleOptions: Array<Option> = [
+    { label: translate("dags:runAndTaskActions.options.past"), value: "past" },
+    { label: translate("dags:runAndTaskActions.options.future"), value: 
"future" },
+    { label: translate("dags:runAndTaskActions.options.upstream"), value: 
"upstream" },
+    { label: translate("dags:runAndTaskActions.options.downstream"), value: 
"downstream" },
+  ];
+
+  return (
+    <Box maxW="720px">
+      <Heading mb={1} size="lg">
+        {translate("settings.title")}
+      </Heading>
+      <Text color="fg.muted" mb={6}>
+        {translate("settings.description")}
+      </Text>
+      <Stack gap={8}>
+        <Section title={translate("settings.graph.title")}>
+          <SelectSetting
+            helper={translate("settings.graph.defaultDirection.helper")}
+            label={translate("settings.graph.defaultDirection.label")}
+            onChange={setGraphDirection}
+            options={directionOptions}
+            testId="default-graph-direction"
+            value={graphDirection}
+          />
+        </Section>
+        <Section title={translate("settings.clearing.title")}>
+          <ToggleSetting
+            defaultValues={clearRunOptions}
+            helper={translate("settings.clearing.runSelection.helper")}
+            label={translate("settings.clearing.runSelection.label")}
+            onChange={setClearRunOptions}
+            options={clearRunToggleOptions}
+          />
+          <ToggleSetting
+            defaultValues={clearTaskOptions}
+            helper={translate("settings.clearing.taskSelection.helper")}
+            label={translate("settings.clearing.taskSelection.label")}
+            multiple
+            onChange={setClearTaskOptions}
+            options={[
+              ...directionalToggleOptions,
+              { label: translate("dags:runAndTaskActions.options.onlyFailed"), 
value: "onlyFailed" },
+            ]}
+          />
+          <SettingRow
+            control={
+              <Switch
+                checked={preventRunningTask}
+                data-testid="clear-prevent-running-task"
+                onCheckedChange={(event) => 
setPreventRunningTask(event.checked)}
+              />
+            }
+            helper={translate("settings.clearing.preventRunningTask.helper")}
+            label={translate("settings.clearing.preventRunningTask.label")}
+          />
+        </Section>
+        <Section title={translate("settings.marking.title")}>
+          <ToggleSetting
+            defaultValues={markTaskOptions}
+            helper={translate("settings.marking.taskSelection.helper")}
+            label={translate("settings.marking.taskSelection.label")}
+            multiple
+            onChange={setMarkTaskOptions}
+            options={directionalToggleOptions}
+          />
+        </Section>
+      </Stack>
+    </Box>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Settings/index.tsx 
b/airflow-core/src/airflow/ui/src/pages/Settings/index.tsx
new file mode 100644
index 00000000000..96d4f418c2a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Settings/index.tsx
@@ -0,0 +1,19 @@
+/*!
+ * 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.
+ */
+export { Settings } from "./Settings";
diff --git a/airflow-core/src/airflow/ui/src/router.tsx 
b/airflow-core/src/airflow/ui/src/router.tsx
index 38a160e935a..fd3ea5ef30d 100644
--- a/airflow-core/src/airflow/ui/src/router.tsx
+++ b/airflow-core/src/airflow/ui/src/router.tsx
@@ -55,6 +55,7 @@ import { Run } from "src/pages/Run";
 import { AssetEvents as DagRunAssetEvents } from "src/pages/Run/AssetEvents";
 import { Details as DagRunDetails } from "src/pages/Run/Details";
 import { Security } from "src/pages/Security";
+import { Settings } from "src/pages/Settings";
 import { Task } from "src/pages/Task";
 import { Overview as TaskOverview } from "src/pages/Task/Overview";
 import { TaskInstance, Logs } from "src/pages/TaskInstance";
@@ -133,6 +134,10 @@ export const routerConfig = [
         element: <Configs />,
         path: "configs",
       },
+      {
+        element: <Settings />,
+        path: "settings",
+      },
       {
         children: [
           { element: <AssetEvents />, index: true },

Reply via email to