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

pierrejeambrun 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 2da70dc8d0e Upgrade react-resizable-panels to v4 (#70820)
2da70dc8d0e is described below

commit 2da70dc8d0e4bda8d9792ccb7d69df2e856bbcac
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Mon Sep 7 16:05:09 2026 +0200

    Upgrade react-resizable-panels to v4 (#70820)
    
    v4 renames PanelGroup/PanelResizeHandle to Group/Separator, replaces the
    autoSaveId persistence flow with the useDefaultLayout hook, removes the
    `order` prop (source order is used instead), and switches the imperative
    setLayout API from ordered array to panel-id map. onDragging is gone from
    Separator; the "refit graph zoom on drag end" behavior moves to the Group's
    onLayoutChanged callback and branches on meta.isUserInteraction so that
    PanelButtons' programmatic setLayout doesn't double-fit.
    
    v4's readLegacyLayout migrates the v3 localStorage entry format on first
    load, so users' remembered panel widths survive the upgrade.
    
    Deferred from #70777.
---
 airflow-core/src/airflow/ui/package.json           |  2 +-
 airflow-core/src/airflow/ui/pnpm-lock.yaml         | 14 +++----
 .../ui/src/layouts/Details/DetailsLayout.tsx       | 47 ++++++++++------------
 .../ui/src/layouts/Details/PanelButtons.tsx        |  9 +++--
 .../src/airflow/ui/src/pages/Asset/AssetLayout.tsx | 37 ++++++++---------
 5 files changed, 55 insertions(+), 54 deletions(-)

diff --git a/airflow-core/src/airflow/ui/package.json 
b/airflow-core/src/airflow/ui/package.json
index 7b0c5f1d732..823535b1ac4 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -63,7 +63,7 @@
     "react-icons": "^5.7.0",
     "react-innertext": "^1.1.5",
     "react-markdown": "^10.1.0",
-    "react-resizable-panels": "^3.0.6",
+    "react-resizable-panels": "^4.12.1",
     "react-router-dom": "^7.18.2",
     "react-syntax-highlighter": "^16.1.1",
     "rehype-katex": "^7.0.1",
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml 
b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index 8c049508a46..cd7c6a0f672 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -157,8 +157,8 @@ importers:
         specifier: ^10.1.0
         version: 10.1.0(@types/[email protected])([email protected])
       react-resizable-panels:
-        specifier: ^3.0.6
-        version: 3.0.6([email protected]([email protected]))([email protected])
+        specifier: ^4.12.1
+        version: 4.12.3([email protected]([email protected]))([email protected])
       react-router-dom:
         specifier: ^7.18.2
         version: 7.18.2([email protected]([email protected]))([email protected])
@@ -3839,11 +3839,11 @@ packages:
       '@types/react': '>=18'
       react: '>=18'
 
-  [email protected]:
-    resolution: {integrity: 
sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==}
+  [email protected]:
+    resolution: {integrity: 
sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA==}
     peerDependencies:
-      react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
-      react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
 
   [email protected]:
     resolution: {integrity: 
sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==}
@@ -8969,7 +8969,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  [email protected]([email protected]([email protected]))([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       react: 19.2.8
       react-dom: 19.2.8([email protected])
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
index 236670f134a..b0a5f0316f0 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -24,12 +24,7 @@ import { useReactFlow } from "@xyflow/react";
 import { useTranslation } from "react-i18next";
 import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
 import { LuFileWarning } from "react-icons/lu";
-import {
-  Panel,
-  PanelGroup,
-  PanelResizeHandle,
-  type ImperativePanelGroupHandle,
-} from "react-resizable-panels";
+import { Group, Panel, Separator, useDefaultLayout, useGroupRef } from 
"react-resizable-panels";
 import { Outlet, useParams, useSearchParams } from "react-router-dom";
 import { useLocalStorage } from "usehooks-ts";
 
@@ -97,7 +92,7 @@ export const DetailsLayout = ({ children, error, isLoading, 
outletContext, tabs
   const { dagId = "", runId } = useParams();
   const { data: dag } = useDagServiceGetDag({ dagId });
   const [dagView, setDagView] = useLocalStorage<DagView>(DEFAULT_DAG_VIEW_KEY, 
"grid");
-  const panelGroupRef = useRef<ImperativePanelGroupHandle | null>(null);
+  const panelGroupRef = useGroupRef();
   // Root for the delegated grid/gantt crosshair-hover handler (covers both the
   // grid and the gantt so their shared row highlight stays in sync, with no
   // React re-render on hover).
@@ -222,6 +217,7 @@ export const DetailsLayout = ({ children, error, isLoading, 
outletContext, tabs
   const panelViewKey = dagView === "gantt" ? "grid" : dagView;
   const minSize = dagView === "gantt" && Boolean(runId) ? 35 : 6;
   const defaultSize = Math.max(dagView === "graph" ? 70 : 20, minSize);
+  const { defaultLayout, onLayoutChanged } = useDefaultLayout({ id: 
`${panelViewKey}-${direction}` });
 
   return (
     <GroupsProvider dagId={dagId}>
@@ -271,14 +267,24 @@ export const DetailsLayout = ({ children, error, 
isLoading, outletContext, tabs
               {direction === "ltr" ? <FaChevronLeft /> : <FaChevronRight />}
             </IconButton>
           ) : undefined}
-          <PanelGroup
-            autoSaveId={`${panelViewKey}-${direction}`}
+          <Group
+            defaultLayout={defaultLayout}
             dir={direction}
-            direction="horizontal"
+            groupRef={panelGroupRef}
             key={`${panelViewKey}-${direction}`}
-            ref={panelGroupRef}
+            onLayoutChanged={(layout, meta) => {
+              onLayoutChanged(layout, meta);
+              // Programmatic setLayout() from PanelButtons handles its own 
fit-view; only
+              // fit here for user-driven resizes, otherwise the two callers 
double-fit.
+              if (meta.isUserInteraction) {
+                const zoom = getZoom();
+
+                void fitView({ maxZoom: zoom, minZoom: zoom });
+              }
+            }}
+            orientation="horizontal"
           >
-            <Panel defaultSize={defaultSize} id="main-panel" minSize={minSize} 
order={1}>
+            <Panel defaultSize={defaultSize} id="main-panel" minSize={minSize}>
               <Flex
                 bg={dagView === "graph" ? undefined : "bg.muted"}
                 borderColor="bg.muted"
@@ -374,16 +380,7 @@ export const DetailsLayout = ({ children, error, 
isLoading, outletContext, tabs
             </Panel>
             {!isRightPanelCollapsed && (
               <>
-                <PanelResizeHandle
-                  className="resize-handle"
-                  onDragging={(isDragging) => {
-                    if (!isDragging) {
-                      const zoom = getZoom();
-
-                      void fitView({ maxZoom: zoom, minZoom: zoom });
-                    }
-                  }}
-                >
+                <Separator className="resize-handle">
                   <Box
                     _hover={{ bg: "info.solid" }}
                     alignItems="center"
@@ -412,9 +409,9 @@ export const DetailsLayout = ({ children, error, isLoading, 
outletContext, tabs
                       {direction === "ltr" ? <FaChevronRight /> : 
<FaChevronLeft />}
                     </IconButton>
                   </Box>
-                </PanelResizeHandle>
+                </Separator>
 
-                <Panel defaultSize={dagView === "graph" ? 30 : 80} 
id="details-panel" minSize={20} order={2}>
+                <Panel defaultSize={dagView === "graph" ? 30 : 80} 
id="details-panel" minSize={20}>
                   <Box
                     display="flex"
                     flexDirection="column"
@@ -454,7 +451,7 @@ export const DetailsLayout = ({ children, error, isLoading, 
outletContext, tabs
                 </Panel>
               </>
             )}
-          </PanelGroup>
+          </Group>
         </Box>
       </Box>
     </GroupsProvider>
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
index a1516f69c02..cb9dc2f4891 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
@@ -33,7 +33,7 @@ import { useTranslation } from "react-i18next";
 import { FiGrid } from "react-icons/fi";
 import { LuChartGantt } from "react-icons/lu";
 import { MdOutlineAccountTree, MdSettings } from "react-icons/md";
-import type { ImperativePanelGroupHandle } from "react-resizable-panels";
+import type { GroupImperativeHandle } from "react-resizable-panels";
 import { useParams } from "react-router-dom";
 import { useLocalStorage } from "usehooks-ts";
 
@@ -66,7 +66,7 @@ import { VersionIndicatorSelect } from 
"./VersionIndicatorSelect";
 type Props = {
   readonly dagView: DagView;
   readonly limit: number;
-  readonly panelGroupRef: RefObject<ImperativePanelGroupHandle | null>;
+  readonly panelGroupRef: RefObject<GroupImperativeHandle | null>;
   readonly setDagView: (value: DagView) => void;
   readonly setLimit: (value: number) => void;
   readonly setShowVersionIndicatorMode: 
Dispatch<SetStateAction<VersionIndicatorOptions>>;
@@ -150,7 +150,10 @@ export const PanelButtons = ({
 
   const handleFocus = (view: string) => {
     if (panelGroupRef.current) {
-      const newLayout = view === "graph" ? [70, 30] : [30, 70];
+      const newLayout =
+        view === "graph"
+          ? { "details-panel": 30, "main-panel": 70 }
+          : { "details-panel": 70, "main-panel": 30 };
 
       panelGroupRef.current.setLayout(newLayout);
       // Used setTimeout to ensure DOM has been updated
diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx 
b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
index d538dcb00f5..ac007410c49 100644
--- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
@@ -22,7 +22,7 @@ import { Box, Code, HStack, Text } from "@chakra-ui/react";
 import { useReactFlow } from "@xyflow/react";
 import { useTranslation } from "react-i18next";
 import { MdOutlineStorage, MdTimeline } from "react-icons/md";
-import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
+import { Group, Panel, Separator, useDefaultLayout } from 
"react-resizable-panels";
 import { Outlet, useParams } from "react-router-dom";
 
 import { useAssetServiceGetAsset } from "openapi/queries";
@@ -60,6 +60,8 @@ export const AssetLayout = () => {
 
   const { fitView, getZoom } = useReactFlow();
 
+  const { defaultLayout, onLayoutChanged } = useDefaultLayout({ id: 
`asset-${direction}` });
+
   const externalTabs = usePluginTabs("asset");
 
   const tabs = [
@@ -88,13 +90,21 @@ export const AssetLayout = () => {
       </HStack>
       <ProgressBar size="xs" visibility={Boolean(isLoading) ? "visible" : 
"hidden"} />
       <Box flex={1} minH={0}>
-        <PanelGroup
-          autoSaveId={`asset-${direction}`}
+        <Group
+          defaultLayout={defaultLayout}
           dir={direction}
-          direction="horizontal"
           key={`asset-${direction}`}
+          onLayoutChanged={(layout, meta) => {
+            onLayoutChanged(layout, meta);
+            if (meta.isUserInteraction) {
+              const zoom = getZoom();
+
+              void fitView({ maxZoom: zoom, minZoom: zoom });
+            }
+          }}
+          orientation="horizontal"
         >
-          <Panel defaultSize={70} minSize={6}>
+          <Panel defaultSize={70} id="asset-graph" minSize={6}>
             <Box
               borderColor="bg.muted"
               borderRadius="md"
@@ -109,16 +119,7 @@ export const AssetLayout = () => {
               </GroupsProvider>
             </Box>
           </Panel>
-          <PanelResizeHandle
-            className="resize-handle"
-            onDragging={(isDragging) => {
-              if (!isDragging) {
-                const zoom = getZoom();
-
-                void fitView({ maxZoom: zoom, minZoom: zoom });
-              }
-            }}
-          >
+          <Separator className="resize-handle">
             <Box
               _hover={{ bg: "info.solid" }}
               borderRadius="full"
@@ -127,8 +128,8 @@ export const AssetLayout = () => {
               mb={3}
               w={1}
             />
-          </PanelResizeHandle>
-          <Panel defaultSize={30} minSize={20}>
+          </Separator>
+          <Panel defaultSize={30} id="asset-details" minSize={20}>
             <Box
               display="flex"
               flexDirection="column"
@@ -160,7 +161,7 @@ export const AssetLayout = () => {
               <Outlet />
             </Box>
           </Panel>
-        </PanelGroup>
+        </Group>
       </Box>
     </>
   );

Reply via email to