@skyhook-io/k8s-ui 1.8.13 → 1.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +6 -3
- package/src/components/resources/get-pod-phase-display.test.ts +204 -0
- package/src/components/resources/resource-utils.ts +101 -0
- package/src/components/shared/CreateResourceDialog.tsx +307 -172
- package/src/components/shared/EditableYamlView.tsx +197 -26
- package/src/components/ui/YamlEditor.test.ts +148 -0
- package/src/components/ui/YamlEditor.tsx +759 -186
- package/src/components/ui/YamlReview.test.ts +45 -0
- package/src/components/ui/YamlReview.tsx +426 -0
- package/src/components/ui/index.ts +9 -0
- package/src/components/ui/monacoRuntime.ts +41 -0
- package/src/components/ui/monacoYaml.worker.ts +1 -0
- package/src/components/ui/yamlMonacoRuntime.ts +110 -0
- package/src/components/workload/WorkloadView.tsx +27 -2
- package/src/monaco-deep.d.ts +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/yaml.test.ts +100 -2
- package/src/utils/yaml.ts +86 -1
|
@@ -1,34 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Editor, {
|
|
3
|
-
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import Editor, {
|
|
3
|
+
DiffEditor,
|
|
4
|
+
type DiffOnMount,
|
|
5
|
+
type Monaco,
|
|
6
|
+
type OnChange,
|
|
7
|
+
type OnMount,
|
|
8
|
+
} from '@monaco-editor/react'
|
|
4
9
|
import type { editor } from 'monaco-editor'
|
|
10
|
+
import { AlertCircle, AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react'
|
|
11
|
+
import { parseAllDocuments, parseDocument } from 'yaml'
|
|
12
|
+
import { isMac } from '../../utils/platform'
|
|
13
|
+
import { splitYamlDocuments } from '../../utils/yaml'
|
|
14
|
+
import { PaneLoader } from './PaneLoader'
|
|
15
|
+
|
|
16
|
+
export interface YamlDocumentIdentity {
|
|
17
|
+
index: number
|
|
18
|
+
schemaIndex: number
|
|
19
|
+
apiVersion: string
|
|
20
|
+
kind: string
|
|
21
|
+
startLine: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface YamlSchemaLoadResult {
|
|
25
|
+
schemas: Array<Record<string, unknown> | null>
|
|
26
|
+
unavailable?: Array<{ index: number; reason: string }>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type YamlSchemaLoader = (documents: YamlDocumentIdentity[]) => Promise<YamlSchemaLoadResult>
|
|
5
30
|
|
|
6
|
-
interface
|
|
31
|
+
export interface YamlDiagnostic {
|
|
32
|
+
severity: 'error' | 'warning' | 'info' | 'hint'
|
|
33
|
+
message: string
|
|
34
|
+
line: number
|
|
35
|
+
column: number
|
|
36
|
+
documentIndex: number
|
|
37
|
+
blocking: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface YamlEditorProps {
|
|
7
41
|
value: string
|
|
8
42
|
onChange?: (value: string) => void
|
|
9
43
|
readOnly?: boolean
|
|
10
44
|
height?: string | number
|
|
11
45
|
onValidate?: (isValid: boolean, errors: string[]) => void
|
|
12
|
-
|
|
46
|
+
onDiagnostics?: (diagnostics: YamlDiagnostic[]) => void
|
|
47
|
+
schemaLoader?: YamlSchemaLoader
|
|
13
48
|
kind?: string
|
|
49
|
+
showProblems?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type SchemaStatus = 'idle' | 'loading' | 'ready' | 'partial' | 'unavailable'
|
|
53
|
+
|
|
54
|
+
export function parseYamlDocumentIdentities(value: string): YamlDocumentIdentity[] {
|
|
55
|
+
return splitYamlDocuments(value).map(({ content, startLine, schemaIndex }, index) => {
|
|
56
|
+
let parsed: unknown
|
|
57
|
+
try {
|
|
58
|
+
parsed = parseDocument(content).toJS({ maxAliasCount: 100 })
|
|
59
|
+
} catch {
|
|
60
|
+
parsed = undefined
|
|
61
|
+
}
|
|
62
|
+
const object = parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {}
|
|
63
|
+
return {
|
|
64
|
+
index,
|
|
65
|
+
schemaIndex,
|
|
66
|
+
apiVersion: typeof object.apiVersion === 'string' ? object.apiVersion : '',
|
|
67
|
+
kind: typeof object.kind === 'string' ? object.kind : '',
|
|
68
|
+
startLine,
|
|
69
|
+
}
|
|
70
|
+
})
|
|
14
71
|
}
|
|
15
72
|
|
|
16
|
-
// Find line numbers for editable fields in a Pod YAML
|
|
17
73
|
function findPodEditableLines(yaml: string): number[] {
|
|
18
74
|
const lines = yaml.split('\n')
|
|
19
75
|
const editableLines: number[] = []
|
|
20
|
-
|
|
21
76
|
let inContainers = false
|
|
22
77
|
let inInitContainers = false
|
|
23
78
|
let inTolerations = false
|
|
24
79
|
let containerIndent = 0
|
|
25
80
|
|
|
26
|
-
for (let i = 0; i < lines.length; i
|
|
81
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
27
82
|
const line = lines[i]
|
|
28
83
|
const trimmed = line.trimStart()
|
|
29
84
|
const indent = line.length - trimmed.length
|
|
30
|
-
|
|
31
|
-
// Track when we enter/exit containers section
|
|
32
85
|
if (trimmed.startsWith('containers:')) {
|
|
33
86
|
inContainers = true
|
|
34
87
|
containerIndent = indent
|
|
@@ -41,44 +94,108 @@ function findPodEditableLines(yaml: string): number[] {
|
|
|
41
94
|
}
|
|
42
95
|
if (trimmed.startsWith('tolerations:')) {
|
|
43
96
|
inTolerations = true
|
|
44
|
-
editableLines.push(i + 1)
|
|
97
|
+
editableLines.push(i + 1)
|
|
45
98
|
containerIndent = indent
|
|
46
99
|
continue
|
|
47
100
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
101
|
+
if (
|
|
102
|
+
(inContainers || inInitContainers || inTolerations) &&
|
|
103
|
+
indent <= containerIndent &&
|
|
104
|
+
trimmed.length > 0 &&
|
|
105
|
+
!trimmed.startsWith('-') &&
|
|
106
|
+
!trimmed.startsWith('#')
|
|
107
|
+
) {
|
|
55
108
|
inContainers = false
|
|
56
109
|
inInitContainers = false
|
|
57
110
|
inTolerations = false
|
|
58
111
|
}
|
|
59
|
-
|
|
60
|
-
// Mark image lines as editable
|
|
61
112
|
if ((inContainers || inInitContainers) && trimmed.startsWith('image:')) {
|
|
62
|
-
editableLines.push(i + 1) // 1-indexed
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Mark toleration lines as editable (whole section)
|
|
66
|
-
if (inTolerations && trimmed.length > 0) {
|
|
67
|
-
editableLines.push(i + 1)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// activeDeadlineSeconds is editable
|
|
71
|
-
if (trimmed.startsWith('activeDeadlineSeconds:')) {
|
|
72
113
|
editableLines.push(i + 1)
|
|
73
114
|
}
|
|
115
|
+
if (inTolerations && trimmed.length > 0) editableLines.push(i + 1)
|
|
116
|
+
if (trimmed.startsWith('activeDeadlineSeconds:')) editableLines.push(i + 1)
|
|
117
|
+
if (trimmed.startsWith('terminationGracePeriodSeconds:')) editableLines.push(i + 1)
|
|
118
|
+
}
|
|
119
|
+
return editableLines
|
|
120
|
+
}
|
|
74
121
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
122
|
+
function documentIndexForLine(documents: YamlDocumentIdentity[], line: number) {
|
|
123
|
+
let index = 0
|
|
124
|
+
for (const document of documents) {
|
|
125
|
+
if (document.startLine > line) break
|
|
126
|
+
index = document.index
|
|
79
127
|
}
|
|
128
|
+
return index
|
|
129
|
+
}
|
|
80
130
|
|
|
81
|
-
|
|
131
|
+
export function parseFallbackYamlDiagnostics(value: string): YamlDiagnostic[] {
|
|
132
|
+
const documents = parseYamlDocumentIdentities(value)
|
|
133
|
+
return parseAllDocuments(value).flatMap((document) =>
|
|
134
|
+
document.errors.map((error) => {
|
|
135
|
+
const start = error.linePos?.[0]
|
|
136
|
+
const line = start?.line ?? 1
|
|
137
|
+
return {
|
|
138
|
+
severity: 'error',
|
|
139
|
+
message: error.message,
|
|
140
|
+
line,
|
|
141
|
+
column: start?.col ?? 1,
|
|
142
|
+
documentIndex: documentIndexForLine(documents, line),
|
|
143
|
+
blocking: true,
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function useDocumentMonacoTheme() {
|
|
150
|
+
const readTheme = () =>
|
|
151
|
+
typeof document !== 'undefined' && document.documentElement.classList.contains('dark')
|
|
152
|
+
const [dark, setDark] = useState(readTheme)
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (typeof document === 'undefined') return
|
|
155
|
+
const observer = new MutationObserver(() => setDark(readTheme()))
|
|
156
|
+
observer.observe(document.documentElement, {
|
|
157
|
+
attributes: true,
|
|
158
|
+
attributeFilter: ['class'],
|
|
159
|
+
})
|
|
160
|
+
return () => observer.disconnect()
|
|
161
|
+
}, [])
|
|
162
|
+
return dark ? ('vs-dark' as const) : ('vs' as const)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function markerSeverity(value: number, monaco: Monaco): YamlDiagnostic['severity'] {
|
|
166
|
+
if (value === monaco.MarkerSeverity.Error) return 'error'
|
|
167
|
+
if (value === monaco.MarkerSeverity.Warning) return 'warning'
|
|
168
|
+
if (value === monaco.MarkerSeverity.Info) return 'info'
|
|
169
|
+
return 'hint'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function isBlockingYamlDiagnostic(
|
|
173
|
+
severity: YamlDiagnostic['severity'],
|
|
174
|
+
code?: string,
|
|
175
|
+
message?: string,
|
|
176
|
+
source?: string,
|
|
177
|
+
) {
|
|
178
|
+
if (source?.startsWith('yaml-schema:')) return false
|
|
179
|
+
if (severity === 'error') return true
|
|
180
|
+
if (severity !== 'warning') return false
|
|
181
|
+
return code !== '2' && !message?.startsWith('[radar-advisory:deprecated]')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function shouldAutoTriggerYamlSuggestions(
|
|
185
|
+
insertedTexts: readonly string[],
|
|
186
|
+
lineContent: string,
|
|
187
|
+
cursorColumn: number,
|
|
188
|
+
) {
|
|
189
|
+
const insertedBlankLine = insertedTexts.some(
|
|
190
|
+
(text) => /\r?\n/.test(text) && text.trim().length === 0,
|
|
191
|
+
)
|
|
192
|
+
return (
|
|
193
|
+
insertedBlankLine && lineContent.trim().length === 0 && cursorColumn === lineContent.length + 1
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function displayDiagnosticMessage(message: string) {
|
|
198
|
+
return message.replace(/^\[radar-advisory:deprecated\]\s*/, '')
|
|
82
199
|
}
|
|
83
200
|
|
|
84
201
|
export function YamlEditor({
|
|
@@ -87,172 +204,517 @@ export function YamlEditor({
|
|
|
87
204
|
readOnly = false,
|
|
88
205
|
height = '100%',
|
|
89
206
|
onValidate,
|
|
207
|
+
onDiagnostics,
|
|
208
|
+
schemaLoader,
|
|
90
209
|
kind,
|
|
210
|
+
showProblems = true,
|
|
91
211
|
}: YamlEditorProps) {
|
|
92
212
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
|
93
213
|
const decorationsRef = useRef<string[]>([])
|
|
214
|
+
const markerSubscriptionRef = useRef<{ dispose(): void } | null>(null)
|
|
215
|
+
const suggestionSubscriptionRef = useRef<{ dispose(): void } | null>(null)
|
|
216
|
+
const suggestionTimerRef = useRef<number | null>(null)
|
|
217
|
+
const schemaDisposerRef = useRef<(() => void) | null>(null)
|
|
94
218
|
const monacoRef = useRef<Monaco | null>(null)
|
|
219
|
+
const schemaRequestRef = useRef(0)
|
|
220
|
+
const schemaLoadedOnceRef = useRef(false)
|
|
221
|
+
const schemaStatusRef = useRef<SchemaStatus>(schemaLoader ? 'loading' : 'idle')
|
|
222
|
+
const schemaLoaderRef = useRef(schemaLoader)
|
|
223
|
+
const documentsRef = useRef<YamlDocumentIdentity[]>([])
|
|
224
|
+
const onDiagnosticsRef = useRef(onDiagnostics)
|
|
225
|
+
const onValidateRef = useRef(onValidate)
|
|
226
|
+
const [runtimeReady, setRuntimeReady] = useState(false)
|
|
227
|
+
const [runtimeError, setRuntimeError] = useState(false)
|
|
228
|
+
const [runtimeAttempt, setRuntimeAttempt] = useState(0)
|
|
229
|
+
const [diagnostics, setDiagnostics] = useState<YamlDiagnostic[]>([])
|
|
230
|
+
const [problemsOpen, setProblemsOpen] = useState(false)
|
|
231
|
+
const [schemaStatus, setSchemaStatus] = useState<SchemaStatus>(schemaLoader ? 'loading' : 'idle')
|
|
232
|
+
const [schemaMessage, setSchemaMessage] = useState('')
|
|
233
|
+
const [schemaUnavailable, setSchemaUnavailable] = useState<
|
|
234
|
+
Array<{ index: number; reason: string }>
|
|
235
|
+
>([])
|
|
236
|
+
const editorId = useId()
|
|
237
|
+
const modelPath = useMemo(
|
|
238
|
+
() => `radar://yaml/${editorId.replace(/[^a-zA-Z0-9_-]/g, '') || 'editor'}.yaml`,
|
|
239
|
+
[editorId],
|
|
240
|
+
)
|
|
241
|
+
const theme = useDocumentMonacoTheme()
|
|
242
|
+
const documents = useMemo(() => parseYamlDocumentIdentities(value), [value])
|
|
243
|
+
const identitySignature = documents
|
|
244
|
+
.map(
|
|
245
|
+
({ schemaIndex, apiVersion, kind: documentKind }) =>
|
|
246
|
+
`${schemaIndex}:${apiVersion}|${documentKind}`,
|
|
247
|
+
)
|
|
248
|
+
.join('\n')
|
|
249
|
+
documentsRef.current = documents
|
|
250
|
+
schemaStatusRef.current = schemaStatus
|
|
251
|
+
schemaLoaderRef.current = schemaLoader
|
|
252
|
+
onDiagnosticsRef.current = onDiagnostics
|
|
253
|
+
onValidateRef.current = onValidate
|
|
254
|
+
const suggestionShortcut = isMac() ? '⌃Space' : 'Ctrl+Space'
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
let active = true
|
|
258
|
+
setRuntimeReady(false)
|
|
259
|
+
setRuntimeError(false)
|
|
260
|
+
import('./yamlMonacoRuntime')
|
|
261
|
+
.then(({ ensureYamlMonaco }) => ensureYamlMonaco())
|
|
262
|
+
.then(() => {
|
|
263
|
+
if (active) setRuntimeReady(true)
|
|
264
|
+
})
|
|
265
|
+
.catch(() => {
|
|
266
|
+
if (active) setRuntimeError(true)
|
|
267
|
+
})
|
|
268
|
+
return () => {
|
|
269
|
+
active = false
|
|
270
|
+
}
|
|
271
|
+
}, [runtimeAttempt])
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (!runtimeError) return
|
|
275
|
+
const next = parseFallbackYamlDiagnostics(value)
|
|
276
|
+
setDiagnostics(next)
|
|
277
|
+
if (next.length > 0) setProblemsOpen(true)
|
|
278
|
+
onDiagnosticsRef.current?.(next)
|
|
279
|
+
onValidateRef.current?.(
|
|
280
|
+
next.length === 0,
|
|
281
|
+
next.map((diagnostic) => `Line ${diagnostic.line}: ${diagnostic.message}`),
|
|
282
|
+
)
|
|
283
|
+
}, [runtimeError, value])
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
return () => {
|
|
287
|
+
markerSubscriptionRef.current?.dispose()
|
|
288
|
+
suggestionSubscriptionRef.current?.dispose()
|
|
289
|
+
if (suggestionTimerRef.current !== null) {
|
|
290
|
+
window.clearTimeout(suggestionTimerRef.current)
|
|
291
|
+
}
|
|
292
|
+
schemaDisposerRef.current?.()
|
|
293
|
+
const editorWindow = window as typeof window & {
|
|
294
|
+
__radarMonacoEditor?: unknown
|
|
295
|
+
}
|
|
296
|
+
if (editorWindow.__radarMonacoEditor === editorRef.current) {
|
|
297
|
+
delete editorWindow.__radarMonacoEditor
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}, [])
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
schemaDisposerRef.current?.()
|
|
304
|
+
schemaDisposerRef.current = null
|
|
305
|
+
if (!schemaLoaderRef.current) {
|
|
306
|
+
setSchemaStatus('idle')
|
|
307
|
+
setSchemaMessage('')
|
|
308
|
+
setSchemaUnavailable([])
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
const completeDocuments = documents.filter((document) => document.apiVersion && document.kind)
|
|
312
|
+
if (completeDocuments.length !== documents.length || documents.length === 0) {
|
|
313
|
+
setSchemaStatus('unavailable')
|
|
314
|
+
setSchemaMessage('Add apiVersion and kind to enable cluster-aware guidance.')
|
|
315
|
+
setSchemaUnavailable(
|
|
316
|
+
documents
|
|
317
|
+
.filter((document) => !document.apiVersion || !document.kind)
|
|
318
|
+
.map((document) => ({
|
|
319
|
+
index: document.index,
|
|
320
|
+
reason: 'Add apiVersion and kind to enable cluster-aware guidance.',
|
|
321
|
+
})),
|
|
322
|
+
)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const request = ++schemaRequestRef.current
|
|
327
|
+
setSchemaStatus('loading')
|
|
328
|
+
setSchemaMessage('Loading schemas from this cluster…')
|
|
329
|
+
setSchemaUnavailable([])
|
|
330
|
+
const delay = schemaLoadedOnceRef.current ? 250 : 0
|
|
331
|
+
schemaLoadedOnceRef.current = true
|
|
332
|
+
const timer = window.setTimeout(() => {
|
|
333
|
+
const loadSchemas = schemaLoaderRef.current
|
|
334
|
+
if (!loadSchemas) return
|
|
335
|
+
loadSchemas(completeDocuments)
|
|
336
|
+
.then(async (result) => {
|
|
337
|
+
if (request !== schemaRequestRef.current) return
|
|
338
|
+
const { registerYamlSchema } = await import('./yamlMonacoRuntime')
|
|
339
|
+
if (request !== schemaRequestRef.current) return
|
|
340
|
+
const schemaSequence = Array.from(
|
|
341
|
+
{
|
|
342
|
+
length: Math.max(...documents.map((document) => document.schemaIndex)) + 1,
|
|
343
|
+
},
|
|
344
|
+
() => null as Record<string, unknown> | null,
|
|
345
|
+
)
|
|
346
|
+
documents.forEach((document, index) => {
|
|
347
|
+
schemaSequence[document.schemaIndex] = result.schemas[index] ?? null
|
|
348
|
+
})
|
|
349
|
+
const disposeSchema = await registerYamlSchema(modelPath, schemaSequence)
|
|
350
|
+
if (request !== schemaRequestRef.current) {
|
|
351
|
+
disposeSchema()
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
schemaDisposerRef.current = disposeSchema
|
|
355
|
+
const unavailable = result.unavailable ?? []
|
|
356
|
+
setSchemaUnavailable(unavailable)
|
|
357
|
+
setSchemaStatus(
|
|
358
|
+
unavailable.length === 0
|
|
359
|
+
? 'ready'
|
|
360
|
+
: unavailable.length < documents.length
|
|
361
|
+
? 'partial'
|
|
362
|
+
: 'unavailable',
|
|
363
|
+
)
|
|
364
|
+
setSchemaMessage(
|
|
365
|
+
unavailable.length === 0
|
|
366
|
+
? `${documents.length} cluster schema${documents.length === 1 ? '' : 's'} active`
|
|
367
|
+
: unavailable.length === documents.length
|
|
368
|
+
? unavailable[0]?.reason || 'Cluster schemas are unavailable.'
|
|
369
|
+
: `${documents.length - unavailable.length} of ${documents.length} cluster schemas active`,
|
|
370
|
+
)
|
|
371
|
+
})
|
|
372
|
+
.catch((error: unknown) => {
|
|
373
|
+
if (request !== schemaRequestRef.current) return
|
|
374
|
+
const reason = error instanceof Error ? error.message : 'Cluster schemas are unavailable.'
|
|
375
|
+
setSchemaStatus('unavailable')
|
|
376
|
+
setSchemaMessage(reason)
|
|
377
|
+
setSchemaUnavailable(documents.map((document) => ({ index: document.index, reason })))
|
|
378
|
+
})
|
|
379
|
+
}, delay)
|
|
380
|
+
return () => {
|
|
381
|
+
window.clearTimeout(timer)
|
|
382
|
+
schemaRequestRef.current += 1
|
|
383
|
+
}
|
|
384
|
+
}, [Boolean(schemaLoader), identitySignature, modelPath])
|
|
95
385
|
|
|
96
|
-
// Apply decorations for editable fields
|
|
97
386
|
const applyDecorations = useCallback(() => {
|
|
98
|
-
const
|
|
387
|
+
const mountedEditor = editorRef.current
|
|
99
388
|
const monaco = monacoRef.current
|
|
100
|
-
if (!
|
|
101
|
-
|
|
389
|
+
if (!mountedEditor || !monaco || !kind) return
|
|
102
390
|
const isPod = kind.toLowerCase() === 'pods' || kind.toLowerCase() === 'pod'
|
|
103
391
|
if (!isPod) {
|
|
104
|
-
// Clear decorations for non-pods
|
|
105
392
|
if (decorationsRef.current.length > 0) {
|
|
106
|
-
decorationsRef.current =
|
|
393
|
+
decorationsRef.current = mountedEditor.deltaDecorations(decorationsRef.current, [])
|
|
107
394
|
}
|
|
108
395
|
return
|
|
109
396
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
endLineNumber: lineNumber,
|
|
118
|
-
endColumn: 1,
|
|
119
|
-
},
|
|
120
|
-
options: {
|
|
121
|
-
isWholeLine: true,
|
|
122
|
-
className: 'editable-line-highlight',
|
|
123
|
-
glyphMarginClassName: 'editable-line-glyph',
|
|
124
|
-
overviewRuler: {
|
|
125
|
-
color: 'rgba(34, 197, 94, 0.5)',
|
|
126
|
-
position: monaco.editor.OverviewRulerLane.Left,
|
|
397
|
+
const decorations: editor.IModelDeltaDecoration[] = findPodEditableLines(value).map(
|
|
398
|
+
(lineNumber) => ({
|
|
399
|
+
range: {
|
|
400
|
+
startLineNumber: lineNumber,
|
|
401
|
+
startColumn: 1,
|
|
402
|
+
endLineNumber: lineNumber,
|
|
403
|
+
endColumn: 1,
|
|
127
404
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
405
|
+
options: {
|
|
406
|
+
isWholeLine: true,
|
|
407
|
+
className: 'editable-line-highlight',
|
|
408
|
+
glyphMarginClassName: 'editable-line-glyph',
|
|
409
|
+
overviewRuler: {
|
|
410
|
+
color: 'rgba(34, 197, 94, 0.5)',
|
|
411
|
+
position: monaco.editor.OverviewRulerLane.Left,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
)
|
|
416
|
+
decorationsRef.current = mountedEditor.deltaDecorations(decorationsRef.current, decorations)
|
|
132
417
|
}, [value, kind])
|
|
133
418
|
|
|
134
|
-
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
applyDecorations()
|
|
137
|
-
}, [applyDecorations])
|
|
419
|
+
useEffect(() => applyDecorations(), [applyDecorations])
|
|
138
420
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
421
|
+
const publishDiagnostics = useCallback(
|
|
422
|
+
(mountedEditor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
|
423
|
+
const model = mountedEditor.getModel()
|
|
424
|
+
if (!model) return
|
|
425
|
+
const next = monaco.editor.getModelMarkers({ resource: model.uri }).map((marker) => {
|
|
426
|
+
const severity = markerSeverity(marker.severity, monaco)
|
|
427
|
+
return {
|
|
428
|
+
severity,
|
|
429
|
+
message: displayDiagnosticMessage(marker.message),
|
|
430
|
+
line: marker.startLineNumber,
|
|
431
|
+
column: marker.startColumn,
|
|
432
|
+
documentIndex: documentIndexForLine(documentsRef.current, marker.startLineNumber),
|
|
433
|
+
blocking: isBlockingYamlDiagnostic(
|
|
434
|
+
severity,
|
|
435
|
+
typeof marker.code === 'string' ? marker.code : undefined,
|
|
436
|
+
marker.message,
|
|
437
|
+
marker.source,
|
|
438
|
+
),
|
|
439
|
+
} satisfies YamlDiagnostic
|
|
440
|
+
})
|
|
441
|
+
setDiagnostics(next)
|
|
442
|
+
if (next.some((diagnostic) => diagnostic.blocking)) setProblemsOpen(true)
|
|
443
|
+
const blocking = next.filter((diagnostic) => diagnostic.blocking)
|
|
444
|
+
onDiagnosticsRef.current?.(next)
|
|
445
|
+
onValidateRef.current?.(
|
|
446
|
+
blocking.length === 0,
|
|
447
|
+
blocking.map((diagnostic) => `Line ${diagnostic.line}: ${diagnostic.message}`),
|
|
448
|
+
)
|
|
449
|
+
},
|
|
450
|
+
[],
|
|
451
|
+
)
|
|
143
452
|
|
|
144
|
-
const handleEditorMount: OnMount = useCallback(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
453
|
+
const handleEditorMount: OnMount = useCallback(
|
|
454
|
+
(mountedEditor, monaco) => {
|
|
455
|
+
editorRef.current = mountedEditor
|
|
456
|
+
monacoRef.current = monaco
|
|
457
|
+
;(window as typeof window & { __radarMonacoEditor?: unknown }).__radarMonacoEditor =
|
|
458
|
+
mountedEditor
|
|
459
|
+
ensureEditableLineStyles()
|
|
460
|
+
mountedEditor.updateOptions({
|
|
461
|
+
minimap: { enabled: false },
|
|
462
|
+
lineNumbers: 'on',
|
|
463
|
+
scrollBeyondLastLine: false,
|
|
464
|
+
wordWrap: 'on',
|
|
465
|
+
wrappingStrategy: 'advanced',
|
|
466
|
+
folding: true,
|
|
467
|
+
foldingStrategy: 'indentation',
|
|
468
|
+
renderLineHighlight: 'line',
|
|
469
|
+
selectOnLineNumbers: true,
|
|
470
|
+
roundedSelection: true,
|
|
471
|
+
cursorStyle: 'line',
|
|
472
|
+
automaticLayout: true,
|
|
473
|
+
tabSize: 2,
|
|
474
|
+
insertSpaces: true,
|
|
475
|
+
fontSize: 13,
|
|
476
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
477
|
+
padding: { top: 12, bottom: 12 },
|
|
478
|
+
glyphMargin: true,
|
|
479
|
+
ariaLabel: 'YAML editor',
|
|
480
|
+
})
|
|
481
|
+
markerSubscriptionRef.current?.dispose()
|
|
482
|
+
markerSubscriptionRef.current = monaco.editor.onDidChangeMarkers((uris) => {
|
|
483
|
+
if (uris.some((uri) => uri.toString() === mountedEditor.getModel()?.uri.toString())) {
|
|
484
|
+
publishDiagnostics(mountedEditor, monaco)
|
|
158
485
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
486
|
+
})
|
|
487
|
+
suggestionSubscriptionRef.current?.dispose()
|
|
488
|
+
suggestionSubscriptionRef.current = mountedEditor.onDidChangeModelContent((event) => {
|
|
489
|
+
if (
|
|
490
|
+
!schemaLoaderRef.current ||
|
|
491
|
+
!event.changes.some(({ text }) => /\r?\n/.test(text) && text.trim().length === 0)
|
|
492
|
+
) {
|
|
493
|
+
return
|
|
164
494
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Configure YAML diagnostics (yaml property added by monaco-yaml plugin when available)
|
|
170
|
-
;(monaco.languages as any).yaml?.yamlDefaults?.setDiagnosticsOptions({
|
|
171
|
-
enableSchemaRequest: false,
|
|
172
|
-
validate: true,
|
|
173
|
-
format: true,
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
// Set editor options
|
|
177
|
-
editor.updateOptions({
|
|
178
|
-
minimap: { enabled: false },
|
|
179
|
-
lineNumbers: 'on',
|
|
180
|
-
scrollBeyondLastLine: false,
|
|
181
|
-
wordWrap: 'on',
|
|
182
|
-
wrappingStrategy: 'advanced',
|
|
183
|
-
folding: true,
|
|
184
|
-
foldingStrategy: 'indentation',
|
|
185
|
-
renderLineHighlight: 'line',
|
|
186
|
-
selectOnLineNumbers: true,
|
|
187
|
-
roundedSelection: true,
|
|
188
|
-
cursorStyle: 'line',
|
|
189
|
-
automaticLayout: true,
|
|
190
|
-
tabSize: 2,
|
|
191
|
-
insertSpaces: true,
|
|
192
|
-
fontSize: 13,
|
|
193
|
-
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
194
|
-
padding: { top: 12, bottom: 12 },
|
|
195
|
-
glyphMargin: true,
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
// Listen for validation markers
|
|
199
|
-
if (onValidate) {
|
|
200
|
-
monaco.editor.onDidChangeMarkers((uris: readonly { toString(): string }[]) => {
|
|
201
|
-
const uri = uris[0]
|
|
202
|
-
if (uri && uri.toString() === editor.getModel()?.uri.toString()) {
|
|
203
|
-
const markers = monaco.editor.getModelMarkers({ resource: uri as Parameters<typeof monaco.editor.getModelMarkers>[0]['resource'] })
|
|
204
|
-
const errors = markers
|
|
205
|
-
.filter((m: { severity: number }) => m.severity === monaco.MarkerSeverity.Error)
|
|
206
|
-
.map((m: { startLineNumber: number; message: string }) => `Line ${m.startLineNumber}: ${m.message}`)
|
|
207
|
-
onValidate(errors.length === 0, errors)
|
|
495
|
+
if (suggestionTimerRef.current !== null) {
|
|
496
|
+
window.clearTimeout(suggestionTimerRef.current)
|
|
208
497
|
}
|
|
498
|
+
const insertedTexts = event.changes.map(({ text }) => text)
|
|
499
|
+
suggestionTimerRef.current = window.setTimeout(() => {
|
|
500
|
+
suggestionTimerRef.current = null
|
|
501
|
+
if (
|
|
502
|
+
!mountedEditor.hasTextFocus() ||
|
|
503
|
+
mountedEditor.getOption(monaco.editor.EditorOption.readOnly) ||
|
|
504
|
+
!['ready', 'partial'].includes(schemaStatusRef.current)
|
|
505
|
+
) {
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
const model = mountedEditor.getModel()
|
|
509
|
+
const position = mountedEditor.getPosition()
|
|
510
|
+
if (
|
|
511
|
+
!model ||
|
|
512
|
+
!position ||
|
|
513
|
+
!shouldAutoTriggerYamlSuggestions(
|
|
514
|
+
insertedTexts,
|
|
515
|
+
model.getLineContent(position.lineNumber),
|
|
516
|
+
position.column,
|
|
517
|
+
)
|
|
518
|
+
) {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
mountedEditor.trigger('radar-yaml', 'editor.action.triggerSuggest', { auto: true })
|
|
522
|
+
}, 120)
|
|
209
523
|
})
|
|
210
|
-
|
|
524
|
+
publishDiagnostics(mountedEditor, monaco)
|
|
525
|
+
window.setTimeout(applyDecorations, 100)
|
|
526
|
+
},
|
|
527
|
+
[applyDecorations, publishDiagnostics],
|
|
528
|
+
)
|
|
211
529
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
530
|
+
const handleChange: OnChange = useCallback(
|
|
531
|
+
(nextValue) => {
|
|
532
|
+
if (onChange && nextValue !== undefined) onChange(nextValue)
|
|
533
|
+
},
|
|
534
|
+
[onChange],
|
|
535
|
+
)
|
|
215
536
|
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
537
|
+
const focusDiagnostic = useCallback((diagnostic: YamlDiagnostic) => {
|
|
538
|
+
const mountedEditor = editorRef.current
|
|
539
|
+
if (!mountedEditor) return
|
|
540
|
+
mountedEditor.setPosition({
|
|
541
|
+
lineNumber: diagnostic.line,
|
|
542
|
+
column: diagnostic.column,
|
|
543
|
+
})
|
|
544
|
+
mountedEditor.revealLineInCenter(diagnostic.line)
|
|
545
|
+
mountedEditor.focus()
|
|
546
|
+
}, [])
|
|
547
|
+
|
|
548
|
+
const blockingCount = diagnostics.filter((diagnostic) => diagnostic.blocking).length
|
|
549
|
+
const problemSummary =
|
|
550
|
+
blockingCount > 0
|
|
551
|
+
? `${blockingCount} blocking`
|
|
552
|
+
: diagnostics.length > 0
|
|
553
|
+
? `${diagnostics.length} advisory`
|
|
554
|
+
: schemaUnavailable.length > 0
|
|
555
|
+
? `${schemaUnavailable.length} schema unavailable`
|
|
556
|
+
: 'None'
|
|
221
557
|
|
|
222
558
|
return (
|
|
223
|
-
<div
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
559
|
+
<div
|
|
560
|
+
className="flex flex-col rounded-lg overflow-hidden border border-theme-border bg-theme-surface"
|
|
561
|
+
style={{ height }}
|
|
562
|
+
>
|
|
563
|
+
<div className="flex-1 min-h-0">
|
|
564
|
+
{runtimeReady ? (
|
|
565
|
+
<Editor
|
|
566
|
+
path={modelPath}
|
|
567
|
+
defaultLanguage="yaml"
|
|
568
|
+
value={value}
|
|
569
|
+
onChange={handleChange}
|
|
570
|
+
onMount={handleEditorMount}
|
|
571
|
+
theme={theme}
|
|
572
|
+
options={{
|
|
573
|
+
readOnly,
|
|
574
|
+
domReadOnly: readOnly,
|
|
575
|
+
suggest: {
|
|
576
|
+
selectionMode: 'never',
|
|
577
|
+
},
|
|
578
|
+
}}
|
|
579
|
+
loading={<PaneLoader label="Loading YAML editor…" className="h-full" />}
|
|
580
|
+
/>
|
|
581
|
+
) : runtimeError ? (
|
|
582
|
+
<div className="flex h-full min-h-0 flex-col bg-theme-base">
|
|
583
|
+
<div
|
|
584
|
+
role="alert"
|
|
585
|
+
className="flex shrink-0 items-center gap-2 border-b border-theme-border bg-theme-elevated px-3 py-2 text-xs text-theme-text-secondary"
|
|
586
|
+
>
|
|
587
|
+
<AlertTriangle className="h-4 w-4 shrink-0 text-warning-text" />
|
|
588
|
+
<span>
|
|
589
|
+
Rich YAML editing is unavailable. Basic editing and syntax validation remain
|
|
590
|
+
available without completion or cluster schema guidance.
|
|
591
|
+
</span>
|
|
592
|
+
<button
|
|
593
|
+
type="button"
|
|
594
|
+
onClick={() => setRuntimeAttempt((attempt) => attempt + 1)}
|
|
595
|
+
className="ml-auto rounded border border-theme-border bg-theme-surface px-2 py-1 text-theme-text-primary hover:bg-theme-hover"
|
|
596
|
+
>
|
|
597
|
+
Retry rich editor
|
|
598
|
+
</button>
|
|
599
|
+
</div>
|
|
600
|
+
<textarea
|
|
601
|
+
aria-label="YAML editor fallback"
|
|
602
|
+
value={value}
|
|
603
|
+
onChange={(event) => onChange?.(event.target.value)}
|
|
604
|
+
readOnly={readOnly}
|
|
605
|
+
spellCheck={false}
|
|
606
|
+
className="min-h-0 flex-1 resize-none bg-theme-base p-3 font-mono text-xs leading-5 text-theme-text-primary outline-none"
|
|
607
|
+
/>
|
|
237
608
|
</div>
|
|
238
|
-
|
|
239
|
-
|
|
609
|
+
) : (
|
|
610
|
+
<PaneLoader label="Loading YAML editor…" className="h-full" />
|
|
611
|
+
)}
|
|
612
|
+
</div>
|
|
613
|
+
{showProblems && (
|
|
614
|
+
<div className="shrink-0 border-t border-theme-border bg-theme-elevated/60">
|
|
615
|
+
<button
|
|
616
|
+
type="button"
|
|
617
|
+
onClick={() => setProblemsOpen((open) => !open)}
|
|
618
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-theme-text-secondary hover:bg-theme-hover"
|
|
619
|
+
aria-expanded={problemsOpen}
|
|
620
|
+
>
|
|
621
|
+
{problemsOpen ? (
|
|
622
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
623
|
+
) : (
|
|
624
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
625
|
+
)}
|
|
626
|
+
<span className="font-medium text-theme-text-primary">Problems</span>
|
|
627
|
+
<span>{problemSummary}</span>
|
|
628
|
+
<span className="ml-auto flex min-w-0 items-center gap-3 text-theme-text-tertiary">
|
|
629
|
+
<span className="truncate" aria-live="polite">
|
|
630
|
+
{schemaStatus === 'loading' ? 'Loading cluster schemas…' : schemaMessage}
|
|
631
|
+
</span>
|
|
632
|
+
{!readOnly && (schemaStatus === 'ready' || schemaStatus === 'partial') && (
|
|
633
|
+
<span
|
|
634
|
+
className="flex shrink-0 items-center gap-1.5"
|
|
635
|
+
aria-label={`Select a suggestion with arrow keys, accept with Enter or Tab; show suggestions with ${suggestionShortcut}`}
|
|
636
|
+
>
|
|
637
|
+
<span>Accept selected</span>
|
|
638
|
+
<kbd className="rounded border border-theme-border bg-theme-surface px-1 font-mono text-theme-text-secondary">
|
|
639
|
+
Enter / Tab
|
|
640
|
+
</kbd>
|
|
641
|
+
<span>Suggestions</span>
|
|
642
|
+
<kbd className="rounded border border-theme-border bg-theme-surface px-1 font-mono text-theme-text-secondary">
|
|
643
|
+
{suggestionShortcut}
|
|
644
|
+
</kbd>
|
|
645
|
+
</span>
|
|
646
|
+
)}
|
|
647
|
+
</span>
|
|
648
|
+
</button>
|
|
649
|
+
{problemsOpen && (
|
|
650
|
+
<div className="max-h-36 overflow-auto border-t border-theme-border py-1">
|
|
651
|
+
{diagnostics.length === 0 && schemaUnavailable.length === 0 ? (
|
|
652
|
+
<div className="px-3 py-2 text-xs text-theme-text-tertiary">
|
|
653
|
+
{schemaStatus === 'ready'
|
|
654
|
+
? 'No syntax or schema problems found.'
|
|
655
|
+
: schemaMessage || 'No syntax problems found.'}
|
|
656
|
+
</div>
|
|
657
|
+
) : (
|
|
658
|
+
diagnostics.map((diagnostic, index) => {
|
|
659
|
+
const document = documents[diagnostic.documentIndex]
|
|
660
|
+
const Icon =
|
|
661
|
+
diagnostic.severity === 'error'
|
|
662
|
+
? AlertCircle
|
|
663
|
+
: diagnostic.severity === 'warning'
|
|
664
|
+
? AlertTriangle
|
|
665
|
+
: Info
|
|
666
|
+
return (
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
key={`${diagnostic.line}:${diagnostic.column}:${index}`}
|
|
670
|
+
onClick={() => focusDiagnostic(diagnostic)}
|
|
671
|
+
className="flex w-full items-start gap-2 px-3 py-1.5 text-left text-xs hover:bg-theme-hover"
|
|
672
|
+
>
|
|
673
|
+
<Icon
|
|
674
|
+
className={`mt-0.5 h-3.5 w-3.5 shrink-0 ${diagnostic.blocking ? 'text-red-500' : 'text-theme-text-tertiary'}`}
|
|
675
|
+
/>
|
|
676
|
+
<span className="min-w-0 flex-1 text-theme-text-secondary">
|
|
677
|
+
{diagnostic.message}
|
|
678
|
+
</span>
|
|
679
|
+
<span className="shrink-0 text-theme-text-tertiary">
|
|
680
|
+
{document?.kind ? `${document.kind} · ` : ''}Ln {diagnostic.line}
|
|
681
|
+
</span>
|
|
682
|
+
</button>
|
|
683
|
+
)
|
|
684
|
+
})
|
|
685
|
+
)}
|
|
686
|
+
{schemaUnavailable.map(({ index, reason }) => {
|
|
687
|
+
const document = documents[index]
|
|
688
|
+
return (
|
|
689
|
+
<div
|
|
690
|
+
key={`schema:${index}`}
|
|
691
|
+
className="flex items-start gap-2 px-3 py-1.5 text-xs text-theme-text-secondary"
|
|
692
|
+
>
|
|
693
|
+
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning-text" />
|
|
694
|
+
<span className="min-w-0 flex-1">{reason}</span>
|
|
695
|
+
<span className="shrink-0 text-theme-text-tertiary">
|
|
696
|
+
{document?.kind || `Document ${index + 1}`}
|
|
697
|
+
</span>
|
|
698
|
+
</div>
|
|
699
|
+
)
|
|
700
|
+
})}
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
</div>
|
|
704
|
+
)}
|
|
240
705
|
</div>
|
|
241
706
|
)
|
|
242
707
|
}
|
|
243
708
|
|
|
244
|
-
interface YamlDiffEditorProps {
|
|
709
|
+
export interface YamlDiffEditorProps {
|
|
245
710
|
original: string
|
|
246
711
|
modified: string
|
|
247
712
|
height?: string | number
|
|
248
|
-
/** When true, render unified (single column) instead of side-by-side. */
|
|
249
713
|
unified?: boolean
|
|
250
|
-
/** When true, only diff regions stay rendered — unchanged sections collapse. */
|
|
251
714
|
hideUnchanged?: boolean
|
|
252
|
-
/** Caller-controlled theme. Defaults to dark to match YamlEditor. */
|
|
253
715
|
theme?: 'vs-dark' | 'vs'
|
|
254
|
-
/** When true, drops the rounded border so the editor reaches its container's edges. */
|
|
255
716
|
bleed?: boolean
|
|
717
|
+
onReadyChange?: (ready: boolean) => void
|
|
256
718
|
}
|
|
257
719
|
|
|
258
720
|
export function YamlDiffEditor({
|
|
@@ -261,33 +723,144 @@ export function YamlDiffEditor({
|
|
|
261
723
|
height = '100%',
|
|
262
724
|
unified = false,
|
|
263
725
|
hideUnchanged = false,
|
|
264
|
-
theme
|
|
726
|
+
theme,
|
|
265
727
|
bleed = false,
|
|
728
|
+
onReadyChange,
|
|
266
729
|
}: YamlDiffEditorProps) {
|
|
730
|
+
const [runtimeReady, setRuntimeReady] = useState(false)
|
|
731
|
+
const [runtimeError, setRuntimeError] = useState(false)
|
|
732
|
+
const [runtimeAttempt, setRuntimeAttempt] = useState(0)
|
|
733
|
+
const documentTheme = useDocumentMonacoTheme()
|
|
734
|
+
const diffModelsRef = useRef<editor.IDiffEditorModel | null>(null)
|
|
735
|
+
const onReadyChangeRef = useRef(onReadyChange)
|
|
736
|
+
onReadyChangeRef.current = onReadyChange
|
|
737
|
+
const handleDiffMount = useCallback<DiffOnMount>((diffEditor) => {
|
|
738
|
+
diffModelsRef.current = diffEditor.getModel()
|
|
739
|
+
onReadyChangeRef.current?.(true)
|
|
740
|
+
}, [])
|
|
741
|
+
useEffect(() => {
|
|
742
|
+
let active = true
|
|
743
|
+
setRuntimeReady(false)
|
|
744
|
+
setRuntimeError(false)
|
|
745
|
+
onReadyChangeRef.current?.(false)
|
|
746
|
+
import('./monacoRuntime')
|
|
747
|
+
.then(({ ensureMonaco }) => ensureMonaco())
|
|
748
|
+
.then(() => {
|
|
749
|
+
if (active) setRuntimeReady(true)
|
|
750
|
+
})
|
|
751
|
+
.catch(() => {
|
|
752
|
+
if (active) setRuntimeError(true)
|
|
753
|
+
})
|
|
754
|
+
return () => {
|
|
755
|
+
active = false
|
|
756
|
+
}
|
|
757
|
+
}, [runtimeAttempt])
|
|
758
|
+
useEffect(
|
|
759
|
+
() => () => {
|
|
760
|
+
const models = diffModelsRef.current
|
|
761
|
+
diffModelsRef.current = null
|
|
762
|
+
window.setTimeout(() => {
|
|
763
|
+
models?.original.dispose()
|
|
764
|
+
models?.modified.dispose()
|
|
765
|
+
})
|
|
766
|
+
},
|
|
767
|
+
[],
|
|
768
|
+
)
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
if (runtimeError) onReadyChangeRef.current?.(true)
|
|
771
|
+
}, [runtimeError])
|
|
267
772
|
return (
|
|
268
|
-
<div
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
773
|
+
<div
|
|
774
|
+
className={
|
|
775
|
+
bleed ? 'overflow-hidden' : 'rounded-lg overflow-hidden border border-theme-border'
|
|
776
|
+
}
|
|
777
|
+
style={{ height }}
|
|
778
|
+
>
|
|
779
|
+
{runtimeReady ? (
|
|
780
|
+
<DiffEditor
|
|
781
|
+
original={original}
|
|
782
|
+
modified={modified}
|
|
783
|
+
language="yaml"
|
|
784
|
+
theme={theme ?? documentTheme}
|
|
785
|
+
keepCurrentOriginalModel
|
|
786
|
+
keepCurrentModifiedModel
|
|
787
|
+
onMount={handleDiffMount}
|
|
788
|
+
options={{
|
|
789
|
+
readOnly: true,
|
|
790
|
+
renderSideBySide: !unified,
|
|
791
|
+
useInlineViewWhenSpaceIsLimited: false,
|
|
792
|
+
hideUnchangedRegions: { enabled: hideUnchanged },
|
|
793
|
+
minimap: { enabled: false },
|
|
794
|
+
lineNumbers: 'on',
|
|
795
|
+
scrollBeyondLastLine: false,
|
|
796
|
+
wordWrap: 'on',
|
|
797
|
+
fontSize: 13,
|
|
798
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
799
|
+
padding: { top: 12, bottom: 12 },
|
|
800
|
+
renderOverviewRuler: true,
|
|
801
|
+
ignoreTrimWhitespace: false,
|
|
802
|
+
automaticLayout: true,
|
|
803
|
+
}}
|
|
804
|
+
loading={<PaneLoader label="Loading diff…" className="h-full" />}
|
|
805
|
+
/>
|
|
806
|
+
) : runtimeError ? (
|
|
807
|
+
<div className="flex h-full min-h-0 flex-col bg-theme-base">
|
|
808
|
+
<div
|
|
809
|
+
role="alert"
|
|
810
|
+
className="flex shrink-0 items-center gap-2 border-b border-theme-border bg-theme-elevated px-3 py-2 text-xs text-theme-text-secondary"
|
|
811
|
+
>
|
|
812
|
+
<AlertTriangle className="h-4 w-4 shrink-0 text-warning-text" />
|
|
813
|
+
<span>Rich diff unavailable. Review the plain YAML below.</span>
|
|
814
|
+
<button
|
|
815
|
+
type="button"
|
|
816
|
+
onClick={() => setRuntimeAttempt((attempt) => attempt + 1)}
|
|
817
|
+
className="ml-auto rounded border border-theme-border bg-theme-surface px-2 py-1 text-theme-text-primary hover:bg-theme-hover"
|
|
818
|
+
>
|
|
819
|
+
Retry rich diff
|
|
820
|
+
</button>
|
|
821
|
+
</div>
|
|
822
|
+
<div className="grid min-h-0 flex-1 grid-cols-1 divide-y divide-theme-border overflow-hidden md:grid-cols-2 md:divide-x md:divide-y-0">
|
|
823
|
+
<section className="flex min-h-0 flex-col">
|
|
824
|
+
<div className="shrink-0 border-b border-theme-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
825
|
+
Before
|
|
826
|
+
</div>
|
|
827
|
+
<pre className="min-h-0 flex-1 overflow-auto whitespace-pre p-3 font-mono text-xs text-theme-text-primary">
|
|
828
|
+
{original}
|
|
829
|
+
</pre>
|
|
830
|
+
</section>
|
|
831
|
+
<section className="flex min-h-0 flex-col">
|
|
832
|
+
<div className="shrink-0 border-b border-theme-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
833
|
+
After
|
|
834
|
+
</div>
|
|
835
|
+
<pre className="min-h-0 flex-1 overflow-auto whitespace-pre p-3 font-mono text-xs text-theme-text-primary">
|
|
836
|
+
{modified}
|
|
837
|
+
</pre>
|
|
838
|
+
</section>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
) : (
|
|
842
|
+
<PaneLoader label="Loading diff…" className="h-full" />
|
|
843
|
+
)}
|
|
291
844
|
</div>
|
|
292
845
|
)
|
|
293
846
|
}
|
|
847
|
+
|
|
848
|
+
function ensureEditableLineStyles() {
|
|
849
|
+
const styleId = 'yaml-editor-styles'
|
|
850
|
+
if (document.getElementById(styleId)) return
|
|
851
|
+
const style = document.createElement('style')
|
|
852
|
+
style.id = styleId
|
|
853
|
+
style.textContent = `
|
|
854
|
+
.editable-line-highlight {
|
|
855
|
+
background-color: rgba(34, 197, 94, 0.1) !important;
|
|
856
|
+
border-left: 3px solid rgba(34, 197, 94, 0.6) !important;
|
|
857
|
+
}
|
|
858
|
+
.editable-line-glyph {
|
|
859
|
+
background-color: rgba(34, 197, 94, 0.6);
|
|
860
|
+
width: 4px !important;
|
|
861
|
+
margin-left: 3px;
|
|
862
|
+
border-radius: 2px;
|
|
863
|
+
}
|
|
864
|
+
`
|
|
865
|
+
document.head.appendChild(style)
|
|
866
|
+
}
|