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 83d8f400274 UI: Fix Expand/Collapse All on XComs and Audit Log JSON 
cells (#67316)
83d8f400274 is described below

commit 83d8f4002749365b597717c2e463259a452f6530
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Sat May 23 00:42:49 2026 +0200

    UI: Fix Expand/Collapse All on XComs and Audit Log JSON cells (#67316)
    
    * UI: Fix Expand/Collapse All on XComs and Audit Log JSON cells
    
    The Expand/Collapse All toggle on the XComs and Audit Log pages was
    no-op: clicking the buttons updated the toggle state but the JSON
    content in each row stayed in its initial fold state.
    
    `RenderedJsonField` runs Monaco's `editor.foldAll` action in
    `handleMount` based on the initial `collapsed` prop, but never reacts
    to subsequent changes to that prop. Save a ref to the editor instance
    and add a `useEffect` that runs `editor.foldAll`/`editor.unfoldAll`
    whenever `collapsed` changes after mount.
    
    * UI: Register Monaco folding contribution so foldAll/unfoldAll actions 
exist
    
    The previous useEffect in RenderedJsonField calls `editor.foldAll` /
    `editor.unfoldAll` to react to `collapsed` prop changes, but #66647
    switched Monaco from the CDN bundle to a local `editor.api` import that
    does not register any editor contributions. As a result those actions
    do not exist, `editor.getAction(...)` returns null, and the toggle does
    nothing.
    
    Side-effect-import the folding contribution alongside `editor.api` to
    register the FoldingController and its `editor.foldAll` /
    `editor.unfoldAll` actions — the minimum needed for the expand/collapse
    toggle to work. Other contributions are intentionally left out to keep
    this change focused on the bug.
    
    * UI: Unblock RenderedJsonField visibility from the foldAll promise
    
    `editor.foldAll` awaits the FoldingModel, which in turn waits for fold
    ranges from the JSON syntax provider. When the JSON worker is unavailable
    (common in proxied dev setups where Vite's worker URLs do not resolve)
    that promise can stay pending forever, leaving `isReady` false and the
    parent Flex wrapper collapsed to `height: 0px; overflow: hidden` — which
    makes the editor completely invisible.
    
    Fire the foldAll action and set `isReady` immediately rather than
    gating it on the promise. The folded layout still settles via
    `onDidContentSizeChange` once Monaco computes it; this just removes the
    hard dependency on a promise that may never resolve.
    
    * UI: Set Vite server.origin so dev-mode worker URLs are absolute
    
    In dev mode the SPA shell is served by the airflow api-server on a
    different origin than Vite. Without `server.origin`, Vite emits asset
    URLs as absolute paths (e.g. `/node_modules/.../json.worker.js?worker_file`)
    which the browser resolves against the page origin — the api-server,
    which serves the SPA fallback HTML for unknown paths. The Monaco workers
    therefore fail to load (text/html MIME), and the JSON fold-range
    provider's promise stays pending, which is what made the editor stay
    unfolded on collapse in dev mode.
    
    Pinning `server.origin` to localhost:5173 (matching the `--strictPort`
    on the dev script) makes Vite emit fully-qualified URLs so workers load
    from Vite directly, regardless of the page origin.
    
    * Revert "UI: Unblock RenderedJsonField visibility from the foldAll promise"
    
    This reverts commit 8c2439fa1fb29f4902f3968ef1e3a13009c2864a.
    
    * UI: Load codicon styles so the fold gutter glyph renders
    
    The Monaco folding contribution renders the per-line expand/collapse
    indicator (the `>` arrow next to a foldable region) by placing a
    `codicon codicon-folding-collapsed` / `codicon-folding-expanded` span in
    the gutter. Without the codicon CSS (which defines the @font-face and
    maps the class names to glyph characters) the span renders as an empty
    square.
    
    The CDN bundle pulled in `codiconStyles` transitively via `editor.all`;
    the local ESM `editor.api` entry point does not. Import it explicitly
    alongside the folding contribution.
    
    * UI: Inject codicon font face with a Vite-resolved URL
    
    Importing `codiconStyles` (which transitively imports `codicon.css`)
    broke the production build through `vite-plugin-css-injected-by-js`:
    
    - the inlined `@font-face { src: url(./codicon.ttf) }` rule resolved
      the relative URL against the page origin (the airflow api-server)
      instead of `/static/assets/`, so the font fetch fell back to the SPA
      HTML and Chrome reported 'OTS parsing error';
    - the plugin removes the emitted CSS chunk but the JS bundle still
      contained a phantom `modulepreload` for it, which 404'd and rejected
      the `configureMonaco` promise — `isReady` never flipped and the
      editor never mounted.
    
    Inject the @font-face + base `.codicon` class ourselves, using Vite's
    `?url` import to resolve the font asset path. Monaco still registers
    the per-icon `::before` content rules at runtime via its theme service,
    so that is the only piece of the static codicon CSS we need.
    
    * Revert "UI: Inject codicon font face with a Vite-resolved URL"
    
    This reverts commit 9b6fc3f6caf56c91f6773e292bde8da8eed21121.
    
    * UI: Keep Monaco codicon CSS as an emitted file
    
    Rather than hand-injecting an @font-face with a manually resolved URL,
    exclude the codicon CSS from vite-plugin-css-injected-by-js. The plugin
    inlining the stylesheet was what broke the relative font URL in the
    first place (the CSS ended up inside a <style> tag, where
    url(./codicon.ttf) gets resolved against the page origin instead of the
    asset directory).
    
    With cssAssetsFilterFunction skipping codicon CSS, Monaco's stylesheet
    stays as a normal file at /static/assets/, the font URL resolves
    naturally next to it, and the standard codiconStyles import is enough
    to get the gutter glyph rendered.
---
 .../src/components/MonacoEditor/configureMonaco.ts | 10 +++++++++-
 .../ui/src/components/RenderedJsonField.tsx        | 22 +++++++++++++++++++++-
 airflow-core/src/airflow/ui/src/vite-env.d.ts      |  5 +++++
 airflow-core/src/airflow/ui/vite.config.ts         | 16 +++++++++++++++-
 4 files changed, 50 insertions(+), 3 deletions(-)

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 629b0cc46b6..de8b8b0ebc6 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<void> | 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 ff629eaa1fb..76b0b2587d0 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<OnMount>[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<EditorInstance | null>(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 (
     <Flex
       flex={1}
diff --git a/airflow-core/src/airflow/ui/src/vite-env.d.ts 
b/airflow-core/src/airflow/ui/src/vite-env.d.ts
index 6aefea40ce7..c7f6825d014 100644
--- a/airflow-core/src/airflow/ui/src/vite-env.d.ts
+++ b/airflow-core/src/airflow/ui/src/vite-env.d.ts
@@ -24,3 +24,8 @@
 interface ImportMeta {
   readonly env: ImportMetaEnv;
 }
+
+// monaco-editor ships .d.ts only for `editor.api`; contribution side-effect 
imports have
+// no typings of their own.
+declare module "monaco-editor/esm/vs/editor/contrib/folding/browser/folding";
+declare module "monaco-editor/esm/vs/base/browser/ui/codicons/codiconStyles";
diff --git a/airflow-core/src/airflow/ui/vite.config.ts 
b/airflow-core/src/airflow/ui/vite.config.ts
index c4b822256c5..4d1c8dc2932 100644
--- a/airflow-core/src/airflow/ui/vite.config.ts
+++ b/airflow-core/src/airflow/ui/vite.config.ts
@@ -39,11 +39,25 @@ export default defineConfig({
       transformIndexHtml: (html) =>
         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 `<style>` tag the URL resolves against the page 
origin
+    // (the api-server) instead of the asset directory and the font fails to 
load.
+    // Keeping the CSS as an emitted file lets the browser resolve the URL 
relative
+    // to the stylesheet's own location (`/static/assets/`). Vite still chunks 
it so
+    // it only loads on the routes that pull Monaco in.
+    cssInjectedByJsPlugin({
+      cssAssetsFilterFunction: (asset: { fileName: string }) => 
!asset.fileName.includes("codicon"),
+    }),
   ],
   resolve: { alias: { openapi: "/openapi-gen", src: "/src" } },
   server: {
     cors: true, // Only used by the dev server.
+    // The dev SPA shell is served by the airflow api-server (a different 
origin), so
+    // Vite must emit fully-qualified URLs — otherwise asset paths (notably 
worker
+    // module URLs) resolve against the api-server origin and 404. The `dev` 
script
+    // pins this port via --strictPort.
+    origin: "http://localhost:5173";,
     proxy: {
       "/hitl-review": {
         changeOrigin: true,

Reply via email to