Skip to content

Commit 5e608e9

Browse files
authored
Merge branch 'main' into artie-fix-preview-landingpage
2 parents e35a04e + 0b12903 commit 5e608e9

21 files changed

Lines changed: 774 additions & 211 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cloudoperators/juno-app-greenhouse": patch
3+
"@cloudoperators/juno-app-heureka": patch
4+
---
5+
6+
Improve ErrorMessage type safety.

.changeset/many-meteors-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudoperators/juno-app-greenhouse": patch
3+
---
4+
5+
Migrate YamlViewer from @uiw/react-codemirror to native CodeMirror packages

.changeset/stale-onions-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
3+
---
4+
5+
feat(docs): add first version of sign-in ux docs page

apps/greenhouse/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@
5656
"@cloudoperators/juno-ui-components": "workspace:*",
5757
"@cloudoperators/juno-url-state-provider": "workspace:*",
5858
"@cloudoperators/greenhouse-auth-provider": "workspace:*",
59-
"@codemirror/lang-yaml": "6.1.2",
59+
"@codemirror/lang-yaml": "^6.1.2",
60+
"@codemirror/language": "^6.12.2",
61+
"@codemirror/state": "^6.5.4",
62+
"@codemirror/theme-one-dark": "^6.1.2",
63+
"@codemirror/view": "^6.39.15",
6064
"@tanstack/react-query": "5.90.21",
6165
"@tanstack/react-router": "1.161.3",
62-
"@uiw/react-codemirror": "4.25.4",
6366
"js-yaml": "4.1.1",
6467
"lodash": "4.17.23"
6568
}

apps/greenhouse/src/Shell.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import styles from "./styles.css?inline"
1515
import StoreProvider, { useGlobalsApiEndpoint } from "./components/StoreProvider"
1616
import { AuthProvider, useAuth } from "./components/AuthProvider"
1717
import { routeTree } from "./routeTree.gen"
18+
import { getRouterBasePath } from "./utils/organizationResolver"
1819

1920
// Create a new query client instance
2021
const queryClient = new QueryClient()
@@ -47,18 +48,7 @@ export type AppProps = {
4748
demoUserToken?: string
4849
currentHost?: string
4950
enableHashedRouting?: boolean
50-
}
51-
52-
const getBasePath = (auth: any) => {
53-
// Determine if org is part of the domain
54-
const currentUrl = new URL(window.location.href)
55-
const organizationIsPartOfDomain = currentUrl.host.match(/^(.+)\.dashboard\..+/)
56-
if (organizationIsPartOfDomain) {
57-
return "/"
58-
}
59-
// If the organization is not part of the domain, extract it from the auth token
60-
const orgString = auth?.data?.raw.groups?.find((g: any) => g.indexOf("organization:") === 0)
61-
return orgString ? orgString.split(":")[1] : undefined
51+
basePath?: string
6252
}
6353

6454
const getUser = (auth: unknown) => ({
@@ -85,7 +75,8 @@ function App(props: AppProps) {
8575
* want the app to use browser history.
8676
*/
8777
router.update({
88-
basepath: getBasePath(auth),
78+
// @ts-expect-error - auth?.data type needs to be properly defined
79+
basepath: getRouterBasePath(auth?.data?.raw?.groups, props.basePath),
8980
context: { appProps: props, apiClient, user },
9081
stringifySearch: encodeV2,
9182
parseSearch: decodeV2,

apps/greenhouse/src/components/AuthProvider.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import React, { createContext, useContext, useState, useMemo, useRef, useEffect } from "react"
77
import { oidcSession, mockedSession, tokenSession } from "@cloudoperators/juno-oauth"
88
import { createAuthStore, AuthStore } from "@cloudoperators/greenhouse-auth-provider"
9+
import { extractOrganizationName } from "../utils/organizationResolver"
910

1011
const setOrganizationToUrl = (groups: any, enableHashedRouting: boolean) => {
1112
const orgName = groups?.find((g: any) => g.startsWith("organization:"))?.split(":")[1]
@@ -72,18 +73,6 @@ function resolveMockAuth(value: any) {
7273
return result
7374
}
7475

75-
const extractOrganizationName = (enableHashedRouting: boolean) => {
76-
const currentUrl = new URL(window.location.href)
77-
78-
// Try to extract from subdomain
79-
let match = currentUrl.host.match(/^(.+)\.dashboard\..+/)
80-
if (match) return match[1]
81-
// If enableHashedRouting is true, take path from the hashed part of the URL otherwise take it from the pathname
82-
const path = enableHashedRouting ? currentUrl.hash.replace("#/", "") : currentUrl.pathname
83-
const pathParts = path.split("/").filter(Boolean)
84-
return pathParts.length > 0 ? pathParts[0] : undefined
85-
}
86-
8776
const initializeDemoAuth = (
8877
orgName: any,
8978
demoUserToken: any,
@@ -200,9 +189,10 @@ export const AuthProvider = ({ options, children }: any) => {
200189
demoOrg = "demo",
201190
demoUserToken,
202191
enableHashedRouting,
192+
basePath,
203193
} = options || {}
204194

205-
const orgName = extractOrganizationName(enableHashedRouting)
195+
const orgName = extractOrganizationName(enableHashedRouting, basePath)
206196

207197
// extract mock params
208198
const { isMock, parsedAuth } = resolveMockAuth(mockAuth)

apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,18 @@ describe("ErrorMessage", () => {
3131
const errorText = screen.getByText("TestError: Something went wrong")
3232
expect(errorText).toBeInTheDocument()
3333
})
34+
35+
it("renders error from string", () => {
36+
const error = "Failed to load data"
37+
render(<ErrorMessage error={error} />)
38+
const errorText = screen.getByText("Error: Failed to load data")
39+
expect(errorText).toBeInTheDocument()
40+
})
41+
42+
it("renders default message for unknown error types", () => {
43+
const error = { someOtherProperty: "value" }
44+
render(<ErrorMessage error={error} />)
45+
const errorText = screen.getByText("Error: Something went wrong")
46+
expect(errorText).toBeInTheDocument()
47+
})
3448
})

apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,37 @@
44
*/
55

66
import React from "react"
7-
import { FallbackProps } from "react-error-boundary"
87
import { Icon, Stack } from "@cloudoperators/juno-ui-components"
98

10-
type ErrorMessageProps =
11-
| {
12-
error: Error
9+
function getErrorInfo(error: unknown): { name: string; message: string } {
10+
const defaultError = { name: "Error", message: "Something went wrong" }
11+
12+
if (error instanceof Error) {
13+
return {
14+
name: error.name || defaultError.name,
15+
message: error.message || defaultError.message,
1316
}
14-
| FallbackProps
17+
}
18+
19+
if (typeof error === "string") {
20+
return { name: defaultError.name, message: error }
21+
}
22+
23+
return defaultError
24+
}
25+
26+
export interface ErrorMessageProps {
27+
error: unknown
28+
}
1529

1630
export const ErrorMessage = ({ error }: ErrorMessageProps) => {
17-
// Handle both direct Error prop and FallbackProps from react-error-boundary
18-
const errorObj = error as Error
19-
const errorName = errorObj.name ? `${errorObj.name}: ` : "Error: "
20-
const errorMessage = errorObj.message || "Something went wrong"
31+
const { name, message } = getErrorInfo(error)
2132

2233
return (
2334
<Stack gap="2" alignment="center">
2435
<Icon icon="danger" className="text-theme-danger" />
2536
<span>
26-
{errorName}
27-
{errorMessage}
37+
{name}: {message}
2838
</span>
2939
</Stack>
3040
)

apps/greenhouse/src/components/admin/common/YamlViewer.test.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import React from "react"
7-
import { render, screen, waitFor } from "@testing-library/react"
7+
import { render, screen, waitFor, within } from "@testing-library/react"
88
import { describe, it, expect } from "vitest"
99
import YamlViewer from "./YamlViewer"
1010

@@ -21,13 +21,13 @@ describe("YamlViewer", () => {
2121
},
2222
}
2323

24-
render(<YamlViewer value={mockData} data-testid="codemirror" />)
24+
render(<YamlViewer value={mockData} data-testid="yaml-viewer" />)
2525

2626
await waitFor(() => {
27-
const editor = screen.getByTestId("codemirror")
28-
expect(editor).toBeInTheDocument()
29-
expect(editor).toHaveAttribute("aria-label", "YAML data viewer (read-only)")
30-
expect(editor).toHaveAttribute("aria-readonly", "true")
27+
const editorWrapper = screen.getByTestId("yaml-viewer")
28+
expect(editorWrapper).toBeInTheDocument()
29+
const editorContent = within(editorWrapper).getByLabelText("YAML data viewer (read-only)")
30+
expect(editorContent).toHaveAttribute("aria-readonly", "true")
3131
})
3232
})
3333

@@ -40,11 +40,11 @@ describe("YamlViewer", () => {
4040
},
4141
}
4242

43-
render(<YamlViewer value={mockData} data-testid="codemirror" />)
43+
render(<YamlViewer value={mockData} data-testid="yaml-viewer" />)
4444

4545
await waitFor(() => {
46-
const editor = screen.getByTestId("codemirror")
47-
const editorText = editor.textContent || ""
46+
const editorWrapper = screen.getByTestId("yaml-viewer")
47+
const editorText = editorWrapper.textContent || ""
4848

4949
expect(editorText).toContain("apiVersion")
5050
expect(editorText).toContain("v1")
@@ -65,12 +65,14 @@ describe("YamlViewer", () => {
6565
invalidFunction: () => {},
6666
}
6767

68-
render(<YamlViewer value={invalidData} data-testid="codemirror" />)
68+
render(<YamlViewer value={invalidData} data-testid="yaml-viewer" />)
6969

7070
await waitFor(() => {
7171
// Check if ErrorMessage is rendered (outside editor)
7272
expect(screen.getByText(/Failed to serialize object to YAML/i)).toBeInTheDocument()
73-
expect(screen.queryByTestId("codemirror")).not.toBeInTheDocument()
73+
// expect(screen.queryByTestId("yaml-viewer")).not.toBeInTheDocument()
74+
const editorWrapper = screen.getByTestId("yaml-viewer")
75+
expect(within(editorWrapper).queryByLabelText("YAML data viewer (read-only)")).not.toBeInTheDocument()
7476
})
7577
})
7678
})

apps/greenhouse/src/components/admin/common/YamlViewer.tsx

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,50 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useMemo, useRef } from "react"
7-
import CodeMirror, { EditorView, highlightWhitespace } from "@uiw/react-codemirror"
6+
import React, { useMemo, useRef, useEffect } from "react"
7+
import { EditorView, highlightWhitespace, lineNumbers } from "@codemirror/view"
8+
import { EditorState } from "@codemirror/state"
89
import { yaml } from "@codemirror/lang-yaml"
10+
import { oneDark } from "@codemirror/theme-one-dark"
911
import yamlParser from "js-yaml"
1012
import { ErrorMessage } from "../common/ErrorBoundary/ErrorMessage"
1113

12-
interface YamlViewerProps extends Omit<React.ComponentProps<typeof CodeMirror>, "value"> {
14+
interface YamlViewerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "value"> {
1315
value: object
16+
className?: string
1417
}
1518

16-
export default function YamlViewer({ value, ...props }: YamlViewerProps) {
19+
function createEditorExtensions() {
20+
return [
21+
yaml(),
22+
oneDark,
23+
highlightWhitespace(),
24+
lineNumbers(),
25+
EditorView.editable.of(false),
26+
EditorView.lineWrapping,
27+
EditorView.theme({
28+
".cm-highlightSpace": {
29+
backgroundImage:
30+
"url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='6' height='6'><circle cx='3' cy='3' r='1' fill='%23cccccc' /></svg>\")",
31+
backgroundRepeat: "no-repeat",
32+
backgroundPosition: "center",
33+
backgroundSize: "contain",
34+
opacity: 0.1,
35+
},
36+
".cm-scroller": {
37+
fontFamily: "monospace",
38+
},
39+
}),
40+
EditorView.contentAttributes.of({
41+
"aria-label": "YAML data viewer (read-only)",
42+
"aria-readonly": "true",
43+
}),
44+
]
45+
}
46+
47+
export default function YamlViewer({ value, className = "", ...props }: YamlViewerProps) {
1748
const containerRef = useRef<HTMLDivElement>(null)
49+
const editorViewRef = useRef<EditorView | null>(null)
1850

1951
const { yamlContent, error } = useMemo(() => {
2052
try {
@@ -33,34 +65,54 @@ export default function YamlViewer({ value, ...props }: YamlViewerProps) {
3365
}
3466
}, [value])
3567

68+
// Store initial content in a ref to avoid triggering effect re-runs
69+
const initialContentRef = useRef(yamlContent)
70+
71+
// Create the CodeMirror editor instance once
72+
useEffect(() => {
73+
if (!containerRef.current) return
74+
75+
const state = EditorState.create({
76+
doc: initialContentRef.current,
77+
extensions: createEditorExtensions(),
78+
})
79+
80+
const view = new EditorView({
81+
state,
82+
parent: containerRef.current,
83+
})
84+
85+
editorViewRef.current = view
86+
87+
return () => {
88+
view.destroy()
89+
editorViewRef.current = null
90+
}
91+
}, [])
92+
93+
// Update editor content when yamlContent changes
94+
useEffect(() => {
95+
if (!editorViewRef.current) return
96+
97+
const currentDoc = editorViewRef.current.state.doc.toString()
98+
if (currentDoc !== yamlContent) {
99+
const scrollPos = editorViewRef.current.scrollDOM.scrollTop
100+
101+
editorViewRef.current.dispatch({
102+
changes: {
103+
from: 0,
104+
to: editorViewRef.current.state.doc.length,
105+
insert: yamlContent,
106+
},
107+
})
108+
109+
editorViewRef.current.scrollDOM.scrollTop = scrollPos
110+
}
111+
}, [yamlContent])
112+
36113
return (
37-
<div ref={containerRef} className="overflow-x-auto max-w-full">
38-
{error ? (
39-
<ErrorMessage error={new Error(error)} />
40-
) : (
41-
<CodeMirror
42-
value={yamlContent}
43-
theme="dark"
44-
extensions={[
45-
yaml(),
46-
highlightWhitespace(),
47-
EditorView.theme({
48-
".cm-highlightSpace": {
49-
backgroundImage:
50-
"url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='6' height='6'><circle cx='3' cy='3' r='1' fill='%23cccccc' /></svg>\")",
51-
backgroundRepeat: "no-repeat",
52-
backgroundPosition: "center",
53-
backgroundSize: "contain",
54-
opacity: 0.1,
55-
},
56-
}),
57-
]}
58-
editable={false}
59-
aria-label="YAML data viewer (read-only)"
60-
aria-readonly="true"
61-
{...props}
62-
/>
63-
)}
114+
<div className={`overflow-x-auto max-w-full ${className}`} {...props}>
115+
{error ? <ErrorMessage error={error} /> : <div ref={containerRef} />}
64116
</div>
65117
)
66118
}

0 commit comments

Comments
 (0)