From 034c75f591d6ad0feba65f5472abda5d69c5af79 Mon Sep 17 00:00:00 2001 From: Arturo Reuschenbach Puncernau Date: Fri, 6 Mar 2026 11:15:43 +0100 Subject: [PATCH 1/2] refactor(greenhouse): migrate YamlViewer from @uiw/react-codemirror to native CodeMirror packages --- apps/greenhouse/package.json | 7 +- .../admin/common/YamlViewer.test.tsx | 24 ++-- .../components/admin/common/YamlViewer.tsx | 114 +++++++++++++----- pnpm-lock.yaml | 107 ++-------------- 4 files changed, 114 insertions(+), 138 deletions(-) diff --git a/apps/greenhouse/package.json b/apps/greenhouse/package.json index 62f24dfb6e..e61b08d2a1 100644 --- a/apps/greenhouse/package.json +++ b/apps/greenhouse/package.json @@ -56,10 +56,13 @@ "@cloudoperators/juno-ui-components": "workspace:*", "@cloudoperators/juno-url-state-provider": "workspace:*", "@cloudoperators/greenhouse-auth-provider": "workspace:*", - "@codemirror/lang-yaml": "6.1.2", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.2", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.2", + "@codemirror/view": "^6.39.15", "@tanstack/react-query": "5.90.21", "@tanstack/react-router": "1.161.3", - "@uiw/react-codemirror": "4.25.4", "js-yaml": "4.1.1", "lodash": "4.17.23" } diff --git a/apps/greenhouse/src/components/admin/common/YamlViewer.test.tsx b/apps/greenhouse/src/components/admin/common/YamlViewer.test.tsx index 5cb2274d7c..eb81a882b6 100644 --- a/apps/greenhouse/src/components/admin/common/YamlViewer.test.tsx +++ b/apps/greenhouse/src/components/admin/common/YamlViewer.test.tsx @@ -4,7 +4,7 @@ */ import React from "react" -import { render, screen, waitFor } from "@testing-library/react" +import { render, screen, waitFor, within } from "@testing-library/react" import { describe, it, expect } from "vitest" import YamlViewer from "./YamlViewer" @@ -21,13 +21,13 @@ describe("YamlViewer", () => { }, } - render() + render() await waitFor(() => { - const editor = screen.getByTestId("codemirror") - expect(editor).toBeInTheDocument() - expect(editor).toHaveAttribute("aria-label", "YAML data viewer (read-only)") - expect(editor).toHaveAttribute("aria-readonly", "true") + const editorWrapper = screen.getByTestId("yaml-viewer") + expect(editorWrapper).toBeInTheDocument() + const editorContent = within(editorWrapper).getByLabelText("YAML data viewer (read-only)") + expect(editorContent).toHaveAttribute("aria-readonly", "true") }) }) @@ -40,11 +40,11 @@ describe("YamlViewer", () => { }, } - render() + render() await waitFor(() => { - const editor = screen.getByTestId("codemirror") - const editorText = editor.textContent || "" + const editorWrapper = screen.getByTestId("yaml-viewer") + const editorText = editorWrapper.textContent || "" expect(editorText).toContain("apiVersion") expect(editorText).toContain("v1") @@ -65,12 +65,14 @@ describe("YamlViewer", () => { invalidFunction: () => {}, } - render() + render() await waitFor(() => { // Check if ErrorMessage is rendered (outside editor) expect(screen.getByText(/Failed to serialize object to YAML/i)).toBeInTheDocument() - expect(screen.queryByTestId("codemirror")).not.toBeInTheDocument() + // expect(screen.queryByTestId("yaml-viewer")).not.toBeInTheDocument() + const editorWrapper = screen.getByTestId("yaml-viewer") + expect(within(editorWrapper).queryByLabelText("YAML data viewer (read-only)")).not.toBeInTheDocument() }) }) }) diff --git a/apps/greenhouse/src/components/admin/common/YamlViewer.tsx b/apps/greenhouse/src/components/admin/common/YamlViewer.tsx index f31cb2b2a2..eae14d1cea 100644 --- a/apps/greenhouse/src/components/admin/common/YamlViewer.tsx +++ b/apps/greenhouse/src/components/admin/common/YamlViewer.tsx @@ -3,18 +3,50 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useRef } from "react" -import CodeMirror, { EditorView, highlightWhitespace } from "@uiw/react-codemirror" +import React, { useMemo, useRef, useEffect } from "react" +import { EditorView, highlightWhitespace, lineNumbers } from "@codemirror/view" +import { EditorState } from "@codemirror/state" import { yaml } from "@codemirror/lang-yaml" +import { oneDark } from "@codemirror/theme-one-dark" import yamlParser from "js-yaml" import { ErrorMessage } from "../common/ErrorBoundary/ErrorMessage" -interface YamlViewerProps extends Omit, "value"> { +interface YamlViewerProps extends Omit, "value"> { value: object + className?: string } -export default function YamlViewer({ value, ...props }: YamlViewerProps) { +function createEditorExtensions() { + return [ + yaml(), + oneDark, + highlightWhitespace(), + lineNumbers(), + EditorView.editable.of(false), + EditorView.lineWrapping, + EditorView.theme({ + ".cm-highlightSpace": { + backgroundImage: + "url(\"data:image/svg+xml,\")", + backgroundRepeat: "no-repeat", + backgroundPosition: "center", + backgroundSize: "contain", + opacity: 0.1, + }, + ".cm-scroller": { + fontFamily: "monospace", + }, + }), + EditorView.contentAttributes.of({ + "aria-label": "YAML data viewer (read-only)", + "aria-readonly": "true", + }), + ] +} + +export default function YamlViewer({ value, className = "", ...props }: YamlViewerProps) { const containerRef = useRef(null) + const editorViewRef = useRef(null) const { yamlContent, error } = useMemo(() => { try { @@ -33,34 +65,54 @@ export default function YamlViewer({ value, ...props }: YamlViewerProps) { } }, [value]) + // Store initial content in a ref to avoid triggering effect re-runs + const initialContentRef = useRef(yamlContent) + + // Create the CodeMirror editor instance once + useEffect(() => { + if (!containerRef.current) return + + const state = EditorState.create({ + doc: initialContentRef.current, + extensions: createEditorExtensions(), + }) + + const view = new EditorView({ + state, + parent: containerRef.current, + }) + + editorViewRef.current = view + + return () => { + view.destroy() + editorViewRef.current = null + } + }, []) + + // Update editor content when yamlContent changes + useEffect(() => { + if (!editorViewRef.current) return + + const currentDoc = editorViewRef.current.state.doc.toString() + if (currentDoc !== yamlContent) { + const scrollPos = editorViewRef.current.scrollDOM.scrollTop + + editorViewRef.current.dispatch({ + changes: { + from: 0, + to: editorViewRef.current.state.doc.length, + insert: yamlContent, + }, + }) + + editorViewRef.current.scrollDOM.scrollTop = scrollPos + } + }, [yamlContent]) + return ( -
- {error ? ( - - ) : ( - \")", - backgroundRepeat: "no-repeat", - backgroundPosition: "center", - backgroundSize: "contain", - opacity: 0.1, - }, - }), - ]} - editable={false} - aria-label="YAML data viewer (read-only)" - aria-readonly="true" - {...props} - /> - )} +
+ {error ? :
}
) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f361389d..dd35b08c6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,17 +332,26 @@ importers: specifier: workspace:* version: link:../../packages/url-state-provider '@codemirror/lang-yaml': - specifier: 6.1.2 + specifier: ^6.1.2 version: 6.1.2 + '@codemirror/language': + specifier: ^6.12.2 + version: 6.12.2 + '@codemirror/state': + specifier: ^6.5.4 + version: 6.5.4 + '@codemirror/theme-one-dark': + specifier: ^6.1.2 + version: 6.1.3 + '@codemirror/view': + specifier: ^6.39.15 + version: 6.39.15 '@tanstack/react-query': specifier: 5.90.21 version: 5.90.21(react@19.2.4) '@tanstack/react-router': specifier: 1.161.3 version: 1.161.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@uiw/react-codemirror': - specifier: 4.25.4 - version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) js-yaml: specifier: 4.1.1 version: 4.1.1 @@ -1739,21 +1748,12 @@ packages: '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} - '@codemirror/commands@6.10.2': - resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==} - '@codemirror/lang-yaml@6.1.2': resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} '@codemirror/language@6.12.2': resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==} - '@codemirror/lint@6.9.4': - resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==} - - '@codemirror/search@6.6.0': - resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} - '@codemirror/state@6.5.4': resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} @@ -3651,28 +3651,6 @@ packages: resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@uiw/codemirror-extensions-basic-setup@4.25.4': - resolution: {integrity: sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==} - peerDependencies: - '@codemirror/autocomplete': '>=6.0.0' - '@codemirror/commands': '>=6.0.0' - '@codemirror/language': '>=6.0.0' - '@codemirror/lint': '>=6.0.0' - '@codemirror/search': '>=6.0.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/view': '>=6.0.0' - - '@uiw/react-codemirror@4.25.4': - resolution: {integrity: sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==} - peerDependencies: - '@babel/runtime': '>=7.11.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/theme-one-dark': '>=6.0.0' - '@codemirror/view': '>=6.0.0' - codemirror: '>=6.0.0' - react: '>=17.0.0' - react-dom: '>=17.0.0' - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -4160,9 +4138,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -8254,13 +8229,6 @@ snapshots: '@codemirror/view': 6.39.15 '@lezer/common': 1.5.1 - '@codemirror/commands@6.10.2': - dependencies: - '@codemirror/language': 6.12.2 - '@codemirror/state': 6.5.4 - '@codemirror/view': 6.39.15 - '@lezer/common': 1.5.1 - '@codemirror/lang-yaml@6.1.2': dependencies: '@codemirror/autocomplete': 6.20.0 @@ -8280,18 +8248,6 @@ snapshots: '@lezer/lr': 1.4.8 style-mod: 4.1.3 - '@codemirror/lint@6.9.4': - dependencies: - '@codemirror/state': 6.5.4 - '@codemirror/view': 6.39.15 - crelt: 1.0.6 - - '@codemirror/search@6.6.0': - dependencies: - '@codemirror/state': 6.5.4 - '@codemirror/view': 6.39.15 - crelt: 1.0.6 - '@codemirror/state@6.5.4': dependencies: '@marijn/find-cluster-break': 1.0.2 @@ -10370,33 +10326,6 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 - '@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)': - dependencies: - '@codemirror/autocomplete': 6.20.0 - '@codemirror/commands': 6.10.2 - '@codemirror/language': 6.12.2 - '@codemirror/lint': 6.9.4 - '@codemirror/search': 6.6.0 - '@codemirror/state': 6.5.4 - '@codemirror/view': 6.39.15 - - '@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@babel/runtime': 7.28.6 - '@codemirror/commands': 6.10.2 - '@codemirror/state': 6.5.4 - '@codemirror/theme-one-dark': 6.1.3 - '@codemirror/view': 6.39.15 - '@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15) - codemirror: 6.0.2 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@codemirror/autocomplete' - - '@codemirror/language' - - '@codemirror/lint' - - '@codemirror/search' - '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.19)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': @@ -10991,16 +10920,6 @@ snapshots: clsx@2.1.1: {} - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.20.0 - '@codemirror/commands': 6.10.2 - '@codemirror/language': 6.12.2 - '@codemirror/lint': 6.9.4 - '@codemirror/search': 6.6.0 - '@codemirror/state': 6.5.4 - '@codemirror/view': 6.39.15 - color-convert@2.0.1: dependencies: color-name: 1.1.4 From 7ef50abadd895d0df3e5f35e1d265ef5ef2e6ef2 Mon Sep 17 00:00:00 2001 From: Arturo Reuschenbach Puncernau Date: Fri, 6 Mar 2026 11:25:26 +0100 Subject: [PATCH 2/2] chore(greenhouse): adds changeset --- .changeset/many-meteors-add.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/many-meteors-add.md diff --git a/.changeset/many-meteors-add.md b/.changeset/many-meteors-add.md new file mode 100644 index 0000000000..b311f5dbb0 --- /dev/null +++ b/.changeset/many-meteors-add.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-app-greenhouse": patch +--- + +Migrate YamlViewer from @uiw/react-codemirror to native CodeMirror packages