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>
</>
);