Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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();

Expand All @@ -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}
Expand Down
5 changes: 5 additions & 0 deletions airflow-core/src/airflow/ui/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
16 changes: 15 additions & 1 deletion airflow-core/src/airflow/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading