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,