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

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 3db206056b5 [v3-4-test] UI: Fix task groups not expanding when 
localStorage is full (#76302) (#76452)
3db206056b5 is described below

commit 3db206056b570bc92fa80bb94f66a78f4c47767e
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Nov 4 22:41:37 2026 +0530

    [v3-4-test] UI: Fix task groups not expanding when localStorage is full 
(#76302) (#76452)
    
    * UI: Fix task groups not expanding when localStorage is full
    
    * Remove saved task group state when it matches the default
    (cherry picked from commit 37d4d9395f774d8d264dd18de8e6fba6a243d247)
    
    Co-authored-by: Lazizbek Ergashev <[email protected]>
---
 .../ui/src/context/groups/GroupsProvider.test.tsx  | 34 +++++++++++++++-
 .../ui/src/context/groups/GroupsProvider.tsx       | 45 ++++++++++++++++++----
 2 files changed, 71 insertions(+), 8 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.test.tsx 
b/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.test.tsx
index 0b231864832..5a0301bc356 100644
--- a/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.test.tsx
+++ b/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.test.tsx
@@ -23,7 +23,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } 
from "vitest";
 
 import type { NodeResponse } from "openapi/requests/types.gen";
 
-import { DEFAULT_TASK_GROUPS_EXPANDED_KEY, openGroupsKey } from 
"src/constants/localStorage";
+import { allGroupsKey, DEFAULT_TASK_GROUPS_EXPANDED_KEY, openGroupsKey } from 
"src/constants/localStorage";
 
 import { GroupsProvider } from "./GroupsProvider";
 import { useGroups } from "./useGroups";
@@ -52,6 +52,7 @@ beforeEach(() => {
   structure.nodes = nodes;
 });
 afterEach(() => {
+  vi.restoreAllMocks();
   localStorage.clear();
 });
 
@@ -109,4 +110,35 @@ describe("task group defaults", () => {
 
     expect(next.result.current.openGroupIds).toEqual([]);
   });
+  it.each([
+    { expanded: "false", selection: [] },
+    { expanded: "true", selection: ["group.nested", "group"] },
+  ])(
+    "removes the saved selection when it matches the default (expanded: 
$expanded)",
+    ({ expanded, selection }) => {
+      localStorage.setItem(DEFAULT_TASK_GROUPS_EXPANDED_KEY, expanded);
+      localStorage.setItem(openGroupsKey("example"), 
JSON.stringify(["group.nested"]));
+      const { result } = renderHook(useGroups, { wrapper });
+
+      act(() => result.current.setOpenGroupIds(selection));
+      expect(result.current.openGroupIds).toEqual(selection);
+      expect(localStorage.getItem(openGroupsKey("example"))).toBeNull();
+    },
+  );
+  it("removes the saved group list when the Dag has no groups", () => {
+    localStorage.setItem(allGroupsKey("example"), JSON.stringify(["group"]));
+    structure.nodes = [];
+    renderHook(useGroups, { wrapper });
+
+    expect(localStorage.getItem(allGroupsKey("example"))).toBeNull();
+  });
+  it("toggles a group when localStorage is full", async () => {
+    vi.spyOn(localStorage, "setItem").mockImplementation(() => {
+      throw new DOMException("Quota exceeded", "QuotaExceededError");
+    });
+    const { result } = renderHook(useGroups, { wrapper });
+
+    act(() => result.current.toggleGroupId("group"));
+    await waitFor(() => 
expect(result.current.openGroupIds).toEqual(["group"]));
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.tsx 
b/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.tsx
index b74926b3afa..eb19e7c4a13 100644
--- a/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.tsx
+++ b/airflow-core/src/airflow/ui/src/context/groups/GroupsProvider.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useEffect, useRef, type PropsWithChildren } from "react";
+import { useCallback, useEffect, useRef, useState, type PropsWithChildren } 
from "react";
 
 import { useDebouncedCallback } from "use-debounce";
 import { useLocalStorage } from "usehooks-ts";
@@ -36,12 +36,17 @@ type Props = {
 } & PropsWithChildren;
 
 export const GroupsProvider = ({ children, dagId }: Props) => {
-  const [storedOpenGroupIds, setOpenGroupIds] = useLocalStorage<Array<string> 
| null>(
-    openGroupsKey(dagId),
-    null,
-  );
+  const [storedOpenGroupIds, setStoredOpenGroupIds, removeStoredOpenGroupIds] =
+    useLocalStorage<Array<string> | null>(openGroupsKey(dagId), null);
+  const [sessionOpenGroupIds, setSessionOpenGroupIds] = useState<{
+    dagId: string;
+    groupIds: Array<string>;
+  }>();
   const [defaultTaskGroupsExpanded] = useDefaultTaskGroupsExpanded();
-  const [allGroupIds, setAllGroupIds] = 
useLocalStorage<Array<string>>(allGroupsKey(dagId), []);
+  const [allGroupIds, setStoredAllGroupIds, removeStoredAllGroupIds] = 
useLocalStorage<Array<string>>(
+    allGroupsKey(dagId),
+    [],
+  );
 
   const allGroupIdsRef = useRef(allGroupIds);
 
@@ -61,7 +66,33 @@ export const GroupsProvider = ({ children, dagId }: Props) 
=> {
   );
 
   const { allGroupIds: observedGroupIds, allOperators } = 
flattenGraphNodes(structure.nodes);
-  const openGroupIds = storedOpenGroupIds ?? (defaultTaskGroupsExpanded ? 
observedGroupIds : []);
+  const savedOpenGroupIds =
+    sessionOpenGroupIds?.dagId === dagId ? sessionOpenGroupIds.groupIds : 
storedOpenGroupIds;
+  const defaultOpenGroupIds = defaultTaskGroupsExpanded ? observedGroupIds : 
[];
+  const openGroupIds = savedOpenGroupIds ?? defaultOpenGroupIds;
+
+  const setOpenGroupIds = (groupIds: Array<string>) => {
+    setSessionOpenGroupIds({ dagId, groupIds });
+    if (
+      groupIds.length === defaultOpenGroupIds.length &&
+      groupIds.every((id) => defaultOpenGroupIds.includes(id))
+    ) {
+      removeStoredOpenGroupIds();
+    } else {
+      setStoredOpenGroupIds(groupIds);
+    }
+  };
+
+  const setAllGroupIds = useCallback(
+    (groupIds: Array<string>) => {
+      if (groupIds.length === 0) {
+        removeStoredAllGroupIds();
+      } else {
+        setStoredAllGroupIds(groupIds);
+      }
+    },
+    [removeStoredAllGroupIds, setStoredAllGroupIds],
+  );
 
   useEffect(() => {
     if (JSON.stringify(observedGroupIds) !== 
JSON.stringify(allGroupIdsRef.current)) {

Reply via email to