diff --git a/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts b/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts index 629b0cc46b69c..de8b8b0ebc66c 100644 --- a/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts +++ b/airflow-core/src/airflow/ui/src/components/MonacoEditor/configureMonaco.ts @@ -25,7 +25,15 @@ type MonacoEnvironment = { let configurationPromise: Promise | undefined; const loadMonacoModules = async () => { - const monacoApi = import("monaco-editor/esm/vs/editor/editor.api"); + // `editor.api` is API-only — also load the folding contribution so `editor.foldAll` / + // `editor.unfoldAll` actions and the fold-gutter UI are actually registered, and the + // codicon styles so the gutter glyph (the `>` arrow) renders instead of an empty box. + // The CDN bundle used to pull these in transitively; the local ESM `editor.api` does not. + const monacoApi = Promise.all([ + import("monaco-editor/esm/vs/editor/editor.api"), + import("monaco-editor/esm/vs/editor/contrib/folding/browser/folding"), + import("monaco-editor/esm/vs/base/browser/ui/codicons/codiconStyles"), + ]).then(([api]) => api); const workerConstructors = Promise.all([ import("monaco-editor/esm/vs/editor/editor.worker?worker").then((module) => module.default), diff --git a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx index ff629eaa1fb5c..76b0b2587d00c 100644 --- a/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx +++ b/airflow-core/src/airflow/ui/src/components/RenderedJsonField.tsx @@ -17,7 +17,7 @@ * under the License. */ import { Flex, type FlexProps } from "@chakra-ui/react"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import Editor, { type OnMount } from "src/components/MonacoEditor"; import { ClipboardRoot, ClipboardIconButton } from "src/components/ui"; @@ -26,6 +26,8 @@ import { useMonacoTheme } from "src/context/colorMode"; const MAX_HEIGHT = 300; const MIN_HEIGHT = 40; +type EditorInstance = Parameters[0]; + type Props = { readonly collapsed?: boolean; readonly content: object; @@ -39,9 +41,12 @@ const RenderedJsonField = ({ collapsed = false, content, enableClipboard = true, const expandedHeight = Math.min(Math.max(lineCount * 19 + 10, MIN_HEIGHT), MAX_HEIGHT); const [editorHeight, setEditorHeight] = useState(collapsed ? MIN_HEIGHT : expandedHeight); const [isReady, setIsReady] = useState(!collapsed); + const editorRef = useRef(null); const handleMount: OnMount = useCallback( (editorInstance) => { + editorRef.current = editorInstance; + editorInstance.onDidContentSizeChange(() => { const contentHeight = editorInstance.getContentHeight(); @@ -63,6 +68,21 @@ const RenderedJsonField = ({ collapsed = false, content, enableClipboard = true, [collapsed], ); + // Sync fold state when the `collapsed` prop changes after mount (e.g. via Expand/Collapse All). + // The initial fold is handled in `handleMount` to avoid the unfolded->folded flicker. + useEffect(() => { + const editor = editorRef.current; + + if (editor === null || !isReady) { + return; + } + const action = editor.getAction(collapsed ? "editor.foldAll" : "editor.unfoldAll"); + + if (action) { + void action.run(); + } + }, [collapsed, isReady]); + return ( html.replace(`src="./assets/`, `src="./static/assets/`).replace(`href="/`, `href="./`), }, - cssInjectedByJsPlugin(), + // Keep Monaco's codicon CSS as a real CSS file (rather than inlined into JS). + // The codicon stylesheet references `codicon.ttf` with a CSS-relative URL — when + // it gets inlined into a `