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 aab7f16c389 Add resize function for Dag Documentation (#56344)
aab7f16c389 is described below
commit aab7f16c3897ea1447d7829cc8f4d2aaf1ba5142
Author: LI,JHE-CHEN <[email protected]>
AuthorDate: Thu Oct 16 09:13:24 2025 -0400
Add resize function for Dag Documentation (#56344)
* feat: Add dialog resize function
* feat: Memorize resize state
* refactor: replace inline style and use useLocalStorage
* fix: remove redundent style
* refactor: optimize state management
---
airflow-core/src/airflow/ui/package.json | 2 +
airflow-core/src/airflow/ui/pnpm-lock.yaml | 45 ++++++++++++
.../ui/src/components/DisplayMarkdownButton.tsx | 21 +++---
.../ui/src/components/ui/ResizableWrapper.tsx | 79 ++++++++++++++++++++++
.../ui/src/utils/usePersistentResizableState.ts | 41 +++++++++++
5 files changed, 180 insertions(+), 8 deletions(-)
diff --git a/airflow-core/src/airflow/ui/package.json
b/airflow-core/src/airflow/ui/package.json
index 2820cd7eb2b..88300c3f147 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -27,6 +27,7 @@
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.8",
"@types/debounce-promise": "^3.1.9",
+ "@types/react-resizable": "^3.0.8",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"@visx/group": "^3.12.0",
@@ -57,6 +58,7 @@
"react-innertext": "^1.1.5",
"react-json-view": "^1.21.3",
"react-markdown": "^9.1.0",
+ "react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^6.30.0",
"react-syntax-highlighter": "^15.6.1",
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml
b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index 065da9e6109..fb491d789f2 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -32,6 +32,9 @@ importers:
'@types/debounce-promise':
specifier: ^3.1.9
version: 3.1.9
+ '@types/react-resizable':
+ specifier: ^3.0.8
+ version: 3.0.8
'@uiw/codemirror-themes-all':
specifier: ^4.23.12
version:
4.23.12(@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])
@@ -122,6 +125,9 @@ importers:
react-markdown:
specifier: ^9.1.0
version: 9.1.0(@types/[email protected])([email protected])
+ react-resizable:
+ specifier: ^3.0.5
+ version: 3.0.5([email protected]([email protected]))([email protected])
react-resizable-panels:
specifier: ^2.1.7
version: 2.1.7([email protected]([email protected]))([email protected])
@@ -1243,6 +1249,9 @@ packages:
peerDependencies:
'@types/react': ^18.0.0
+ '@types/[email protected]':
+ resolution: {integrity:
sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==}
+
'@types/[email protected]':
resolution: {integrity:
sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
@@ -2097,6 +2106,10 @@ packages:
resolution: {integrity:
sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
+ [email protected]:
+ resolution: {integrity:
sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
[email protected]:
resolution: {integrity:
sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
@@ -3821,6 +3834,12 @@ packages:
peerDependencies:
react: ^19.1.1
+ [email protected]:
+ resolution: {integrity:
sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==}
+ peerDependencies:
+ react: '>= 16.3.0'
+ react-dom: '>= 16.3.0'
+
[email protected]:
resolution: {integrity:
sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==}
engines: {node: '>=18.0.0'}
@@ -3887,6 +3906,11 @@ packages:
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
+ [email protected]:
+ resolution: {integrity:
sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==}
+ peerDependencies:
+ react: '>= 16.3'
+
[email protected]:
resolution: {integrity:
sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==}
engines: {node: '>=14.0.0'}
@@ -5728,6 +5752,10 @@ snapshots:
dependencies:
'@types/react': 18.3.19
+ '@types/[email protected]':
+ dependencies:
+ '@types/react': 18.3.19
+
'@types/[email protected]':
dependencies:
'@types/react': 18.3.19
@@ -7241,6 +7269,8 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
+ [email protected]: {}
+
[email protected]: {}
[email protected](@lezer/[email protected]):
@@ -9359,6 +9389,13 @@ snapshots:
react: 19.1.1
scheduler: 0.26.0
+ [email protected]([email protected]([email protected]))([email protected]):
+ dependencies:
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 19.1.1
+ react-dom: 19.1.1([email protected])
+
[email protected]([email protected]):
dependencies:
react: 19.1.1
@@ -9428,6 +9465,14 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1([email protected])
+ [email protected]([email protected]([email protected]))([email protected]):
+ dependencies:
+ prop-types: 15.8.1
+ react: 19.1.1
+ react-draggable: 4.5.0([email protected]([email protected]))([email protected])
+ transitivePeerDependencies:
+ - react-dom
+
[email protected]([email protected]([email protected]))([email protected]):
dependencies:
'@remix-run/router': 1.23.0
diff --git
a/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx
b/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx
index 89bdd03f837..3eab57b6581 100644
--- a/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DisplayMarkdownButton.tsx
@@ -20,9 +20,12 @@ import { Box, Heading, VStack } from "@chakra-ui/react";
import { type ReactElement, useState } from "react";
import { Button, Dialog } from "src/components/ui";
+import { ResizableWrapper } from "src/components/ui/ResizableWrapper";
import ReactMarkdown from "./ReactMarkdown";
+const STORAGE_KEY = "airflow-markdown-dialog-size";
+
const DisplayMarkdownButton = ({
header,
icon,
@@ -48,14 +51,16 @@ const DisplayMarkdownButton = ({
open={isDocsOpen}
size="md"
>
- <Dialog.Content backdrop>
- <Dialog.Header bg="info.muted">
- <Heading size="xl">{header}</Heading>
- <Dialog.CloseTrigger closeButtonProps={{ size: "xl" }} />
- </Dialog.Header>
- <Dialog.Body alignItems="flex-start" as={VStack} gap="0">
- <ReactMarkdown>{mdContent}</ReactMarkdown>
- </Dialog.Body>
+ <Dialog.Content backdrop maxHeight="none" maxWidth="none" padding={0}
width="auto">
+ <ResizableWrapper storageKey={STORAGE_KEY}>
+ <Dialog.Header bg="brand.muted" flexShrink={0}>
+ <Heading size="xl">{header}</Heading>
+ <Dialog.CloseTrigger closeButtonProps={{ size: "xl" }} />
+ </Dialog.Header>
+ <Dialog.Body alignItems="flex-start" as={VStack} flex="1" gap="0"
overflow="auto">
+ <ReactMarkdown>{mdContent}</ReactMarkdown>
+ </Dialog.Body>
+ </ResizableWrapper>
</Dialog.Content>
</Dialog.Root>
</Box>
diff --git a/airflow-core/src/airflow/ui/src/components/ui/ResizableWrapper.tsx
b/airflow-core/src/airflow/ui/src/components/ui/ResizableWrapper.tsx
new file mode 100644
index 00000000000..26bdde642e0
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/ui/ResizableWrapper.tsx
@@ -0,0 +1,79 @@
+/*!
+ * 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 } from "@chakra-ui/react";
+import { forwardRef } from "react";
+import type { ReactNode } from "react";
+import { ResizableBox } from "react-resizable";
+import "react-resizable/css/styles.css";
+
+import { usePersistentResizableState } from
"src/utils/usePersistentResizableState";
+
+const ResizeHandle = forwardRef<HTMLDivElement>((props, ref) => (
+ <Box
+ background="linear-gradient(-45deg, transparent 6px, #ccc 6px, #ccc 8px,
transparent 8px, transparent 12px, #ccc 12px, #ccc 14px, transparent 14px)"
+ bottom={0}
+ cursor="se-resize"
+ height={5}
+ position="absolute"
+ ref={ref}
+ right={0}
+ width={5}
+ {...props}
+ />
+));
+
+type ResizableWrapperProps = {
+ readonly children: ReactNode;
+ readonly defaultSize?: { height: number; width: number };
+ readonly maxConstraints?: [width: number, height: number];
+ readonly storageKey: string;
+};
+
+const DEFAULT_SIZE = { height: 400, width: 500 };
+const MAX_SIZE: [number, number] = [1200, 800];
+
+export const ResizableWrapper = ({
+ children,
+ defaultSize = DEFAULT_SIZE,
+ maxConstraints = MAX_SIZE,
+ storageKey,
+}: ResizableWrapperProps) => {
+ const { handleResize, handleResizeStop, size } =
usePersistentResizableState(storageKey, defaultSize);
+
+ return (
+ <ResizableBox
+ handle={<ResizeHandle />}
+ height={size.height}
+ maxConstraints={maxConstraints}
+ minConstraints={[DEFAULT_SIZE.width, DEFAULT_SIZE.height]}
+ onResize={handleResize}
+ onResizeStop={handleResizeStop}
+ resizeHandles={["se"]}
+ style={{
+ backgroundColor: "inherit",
+ borderRadius: "inherit",
+ overflow: "hidden",
+ position: "relative",
+ }}
+ width={size.width}
+ >
+ {children}
+ </ResizableBox>
+ );
+};
diff --git
a/airflow-core/src/airflow/ui/src/utils/usePersistentResizableState.ts
b/airflow-core/src/airflow/ui/src/utils/usePersistentResizableState.ts
new file mode 100644
index 00000000000..6ce87970087
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/usePersistentResizableState.ts
@@ -0,0 +1,41 @@
+/*!
+ * 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 { useCallback, useState } from "react";
+import { useLocalStorage } from "usehooks-ts";
+
+type Size = { height: number; width: number };
+
+export const usePersistentResizableState = (storageKey: string, defaultSize:
Size) => {
+ const [storedSize, setStoredSize] = useLocalStorage(storageKey, defaultSize);
+ const [size, setSize] = useState(storedSize);
+
+ const handleResize = useCallback((_event: React.SyntheticEvent, { size:
newSize }: { size: Size }) => {
+ setSize(newSize);
+ }, []);
+
+ const handleResizeStop = useCallback(
+ (_event: React.SyntheticEvent, { size: finalSize }: { size: Size }) => {
+ setSize(finalSize);
+ setStoredSize(finalSize);
+ },
+ [setStoredSize],
+ );
+
+ return { handleResize, handleResizeStop, size };
+};