@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
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RefreshCw,
|
|
7
7
|
Pencil,
|
|
8
8
|
Save,
|
|
9
|
+
Eye,
|
|
9
10
|
XCircle,
|
|
10
11
|
AlertTriangle,
|
|
11
12
|
} from 'lucide-react'
|
|
@@ -13,6 +14,8 @@ import { Download } from 'lucide-react'
|
|
|
13
14
|
import { stringify as yamlStringify } from 'yaml'
|
|
14
15
|
import { CodeViewer } from '../ui/CodeViewer'
|
|
15
16
|
import { YamlEditor } from '../ui/YamlEditor'
|
|
17
|
+
import type { YamlSchemaLoader } from '../ui/YamlEditor'
|
|
18
|
+
import { YamlReview, type YamlPreviewResult } from '../ui/YamlReview'
|
|
16
19
|
import { Tooltip } from '../ui/Tooltip'
|
|
17
20
|
import type { SelectedResource } from '../../types'
|
|
18
21
|
import { resourceToYaml } from '../../utils/yaml'
|
|
@@ -40,20 +43,23 @@ export function SaveSuccessAnimation() {
|
|
|
40
43
|
// ============================================================================
|
|
41
44
|
|
|
42
45
|
// Get edit warning for resource types with limited editability
|
|
43
|
-
function getEditWarning(
|
|
46
|
+
function getEditWarning(
|
|
47
|
+
kind: string,
|
|
48
|
+
): { message: string; tip: string; learnMoreUrl?: string } | null {
|
|
44
49
|
const k = kind.toLowerCase()
|
|
45
50
|
if (k === 'pods' || k === 'pod') {
|
|
46
51
|
return {
|
|
47
52
|
message: 'Pods have limited editability.',
|
|
48
53
|
tip: 'Green highlighted lines can be changed. Edit the parent Deployment instead for other fields.',
|
|
49
|
-
learnMoreUrl:
|
|
54
|
+
learnMoreUrl:
|
|
55
|
+
'https://kubernetes.io/docs/concepts/workloads/pods/#pod-update-and-replacement',
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
if (k === 'jobs' || k === 'job') {
|
|
53
59
|
return {
|
|
54
60
|
message: 'Jobs cannot be modified after creation.',
|
|
55
61
|
tip: 'Delete and recreate the Job to make changes.',
|
|
56
|
-
learnMoreUrl: 'https://kubernetes.io/docs/concepts/workloads/controllers/job/'
|
|
62
|
+
learnMoreUrl: 'https://kubernetes.io/docs/concepts/workloads/controllers/job/',
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
return null
|
|
@@ -77,7 +83,7 @@ function formatSaveError(error: string): { summary: string; details?: string } {
|
|
|
77
83
|
const message = errorPart.slice(messageStart, messageEnd).trim()
|
|
78
84
|
return {
|
|
79
85
|
summary: `Cannot update ${target}: ${message}`,
|
|
80
|
-
details: error.length > 200 ? error : undefined
|
|
86
|
+
details: error.length > 200 ? error : undefined,
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
}
|
|
@@ -87,7 +93,7 @@ function formatSaveError(error: string): { summary: string; details?: string } {
|
|
|
87
93
|
if (summary) {
|
|
88
94
|
return {
|
|
89
95
|
summary,
|
|
90
|
-
details: error.length > 200 ? error : undefined
|
|
96
|
+
details: error.length > 200 ? error : undefined,
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
@@ -95,7 +101,7 @@ function formatSaveError(error: string): { summary: string; details?: string } {
|
|
|
95
101
|
if (error.length > 150) {
|
|
96
102
|
return {
|
|
97
103
|
summary: error.substring(0, 150) + '...',
|
|
98
|
-
details: error
|
|
104
|
+
details: error,
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
|
|
@@ -105,16 +111,28 @@ function formatSaveError(error: string): { summary: string; details?: string } {
|
|
|
105
111
|
// Safe sessionStorage wrappers — storage can throw QuotaExceededError or be
|
|
106
112
|
// blocked by browser security policies. Draft persistence is best-effort.
|
|
107
113
|
function safeSessionGet(key: string): string | null {
|
|
108
|
-
try {
|
|
114
|
+
try {
|
|
115
|
+
return sessionStorage.getItem(key)
|
|
116
|
+
} catch {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
109
119
|
}
|
|
110
120
|
function safeSessionSet(key: string, value: string): void {
|
|
111
|
-
try {
|
|
121
|
+
try {
|
|
122
|
+
sessionStorage.setItem(key, value)
|
|
123
|
+
} catch {
|
|
124
|
+
/* best-effort */
|
|
125
|
+
}
|
|
112
126
|
}
|
|
113
127
|
function safeSessionRemove(key: string): void {
|
|
114
|
-
try {
|
|
128
|
+
try {
|
|
129
|
+
sessionStorage.removeItem(key)
|
|
130
|
+
} catch {
|
|
131
|
+
/* best-effort */
|
|
132
|
+
}
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
interface EditableYamlViewProps {
|
|
135
|
+
export interface EditableYamlViewProps {
|
|
118
136
|
resource: SelectedResource
|
|
119
137
|
data: any
|
|
120
138
|
onCopy: (text: string) => void
|
|
@@ -124,11 +142,32 @@ interface EditableYamlViewProps {
|
|
|
124
142
|
/** Called after a successful save so the parent can refetch */
|
|
125
143
|
onSaved?: () => void
|
|
126
144
|
/** Save handler — injected by the platform wrapper */
|
|
127
|
-
onSave?: (params: {
|
|
145
|
+
onSave?: (params: {
|
|
146
|
+
kind: string
|
|
147
|
+
namespace: string
|
|
148
|
+
name: string
|
|
149
|
+
yaml: string
|
|
150
|
+
force: boolean
|
|
151
|
+
reviewedResourceVersion?: string
|
|
152
|
+
reviewedContext?: string
|
|
153
|
+
}) => Promise<void>
|
|
128
154
|
/** Whether a save is in progress */
|
|
129
155
|
isSaving?: boolean
|
|
130
156
|
/** Error message from the last save attempt */
|
|
131
157
|
saveError?: string | null
|
|
158
|
+
onPreview?: (params: {
|
|
159
|
+
yaml: string
|
|
160
|
+
mode: 'update'
|
|
161
|
+
force: boolean
|
|
162
|
+
target: { kind: string; namespace: string; name: string }
|
|
163
|
+
}) => Promise<{
|
|
164
|
+
documents: YamlPreviewResult[]
|
|
165
|
+
nonAtomic: boolean
|
|
166
|
+
context?: string
|
|
167
|
+
}>
|
|
168
|
+
isPreviewing?: boolean
|
|
169
|
+
previewError?: string | null
|
|
170
|
+
schemaLoader?: YamlSchemaLoader
|
|
132
171
|
/** Duplicate handler — opens create dialog with this resource's YAML */
|
|
133
172
|
onDuplicate?: (params: { kind: string; namespace: string; name: string; yaml: string }) => void
|
|
134
173
|
/**
|
|
@@ -138,7 +177,23 @@ interface EditableYamlViewProps {
|
|
|
138
177
|
onDownload?: (content: string, mime: string, filename: string) => void
|
|
139
178
|
}
|
|
140
179
|
|
|
141
|
-
export function EditableYamlView({
|
|
180
|
+
export function EditableYamlView({
|
|
181
|
+
resource,
|
|
182
|
+
data,
|
|
183
|
+
onCopy,
|
|
184
|
+
copied,
|
|
185
|
+
readOnly = false,
|
|
186
|
+
onSaved,
|
|
187
|
+
onSave,
|
|
188
|
+
isSaving,
|
|
189
|
+
saveError,
|
|
190
|
+
onPreview,
|
|
191
|
+
isPreviewing,
|
|
192
|
+
previewError,
|
|
193
|
+
schemaLoader,
|
|
194
|
+
onDuplicate,
|
|
195
|
+
onDownload,
|
|
196
|
+
}: EditableYamlViewProps) {
|
|
142
197
|
const draftKey = `radar_yaml_draft:${resource.kind}/${resource.namespace}/${resource.name}`
|
|
143
198
|
|
|
144
199
|
// Restore draft from sessionStorage (e.g., after session-expiry redirect).
|
|
@@ -152,6 +207,14 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
152
207
|
// Default on: the editor resubmits the full live manifest, so an unforced
|
|
153
208
|
// save would conflict on every field owned by Helm/Flux/Argo/a controller.
|
|
154
209
|
const [force, setForce] = useState(true)
|
|
210
|
+
const [preview, setPreview] = useState<{
|
|
211
|
+
yaml: string
|
|
212
|
+
force: boolean
|
|
213
|
+
originalYaml: string
|
|
214
|
+
documents: YamlPreviewResult[]
|
|
215
|
+
nonAtomic: boolean
|
|
216
|
+
context?: string
|
|
217
|
+
} | null>(null)
|
|
155
218
|
|
|
156
219
|
// Clean up restored draft flag
|
|
157
220
|
useEffect(() => {
|
|
@@ -190,6 +253,7 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
190
253
|
if (readOnly) return
|
|
191
254
|
setEditedYaml(resourceToYaml(data))
|
|
192
255
|
setYamlErrors([])
|
|
256
|
+
setPreview(null)
|
|
193
257
|
setIsEditing(true)
|
|
194
258
|
}, [data, readOnly])
|
|
195
259
|
|
|
@@ -197,12 +261,34 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
197
261
|
setIsEditing(false)
|
|
198
262
|
setEditedYaml('')
|
|
199
263
|
setYamlErrors([])
|
|
264
|
+
setPreview(null)
|
|
200
265
|
}, [])
|
|
201
266
|
|
|
202
267
|
const handleSaveEdit = useCallback(async () => {
|
|
203
268
|
if (yamlErrors.length > 0 || !onSave) return
|
|
204
269
|
|
|
205
270
|
try {
|
|
271
|
+
if (onPreview) {
|
|
272
|
+
const result = await onPreview({
|
|
273
|
+
yaml: editedYaml,
|
|
274
|
+
mode: 'update',
|
|
275
|
+
force,
|
|
276
|
+
target: {
|
|
277
|
+
kind: resource.kind,
|
|
278
|
+
namespace: resource.namespace,
|
|
279
|
+
name: resource.name,
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
setPreview({
|
|
283
|
+
yaml: editedYaml,
|
|
284
|
+
force,
|
|
285
|
+
originalYaml: resourceToYaml(data),
|
|
286
|
+
documents: result.documents,
|
|
287
|
+
nonAtomic: result.nonAtomic,
|
|
288
|
+
context: result.context,
|
|
289
|
+
})
|
|
290
|
+
return
|
|
291
|
+
}
|
|
206
292
|
await onSave({
|
|
207
293
|
kind: resource.kind,
|
|
208
294
|
namespace: resource.namespace,
|
|
@@ -216,7 +302,49 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
216
302
|
} catch {
|
|
217
303
|
// Error handled by caller via saveError prop
|
|
218
304
|
}
|
|
219
|
-
}, [onSave, resource, editedYaml, yamlErrors, onSaved, force])
|
|
305
|
+
}, [onSave, onPreview, resource, data, editedYaml, yamlErrors, onSaved, force])
|
|
306
|
+
|
|
307
|
+
const handleApplyReviewed = useCallback(async () => {
|
|
308
|
+
if (!preview || !onSave) return
|
|
309
|
+
try {
|
|
310
|
+
await onSave({
|
|
311
|
+
kind: resource.kind,
|
|
312
|
+
namespace: resource.namespace,
|
|
313
|
+
name: resource.name,
|
|
314
|
+
yaml: preview.yaml,
|
|
315
|
+
force: preview.force,
|
|
316
|
+
reviewedResourceVersion: preview.documents[0]?.reviewedResourceVersion,
|
|
317
|
+
reviewedContext: preview.context,
|
|
318
|
+
})
|
|
319
|
+
setPreview(null)
|
|
320
|
+
setIsEditing(false)
|
|
321
|
+
setEditedYaml('')
|
|
322
|
+
setTimeout(() => onSaved?.(), 1000)
|
|
323
|
+
} catch {
|
|
324
|
+
if (onPreview) {
|
|
325
|
+
try {
|
|
326
|
+
const refreshed = await onPreview({
|
|
327
|
+
yaml: preview.yaml,
|
|
328
|
+
mode: 'update',
|
|
329
|
+
force: preview.force,
|
|
330
|
+
target: {
|
|
331
|
+
kind: resource.kind,
|
|
332
|
+
namespace: resource.namespace,
|
|
333
|
+
name: resource.name,
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
setPreview({
|
|
337
|
+
...preview,
|
|
338
|
+
documents: refreshed.documents,
|
|
339
|
+
nonAtomic: refreshed.nonAtomic,
|
|
340
|
+
context: refreshed.context,
|
|
341
|
+
})
|
|
342
|
+
} catch {
|
|
343
|
+
// Keep the last review visible when refresh is unavailable.
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}, [preview, onSave, onPreview, resource, onSaved])
|
|
220
348
|
|
|
221
349
|
const handleYamlValidate = useCallback((_isValid: boolean, errors: string[]) => {
|
|
222
350
|
setYamlErrors(errors)
|
|
@@ -225,9 +353,24 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
225
353
|
const yamlContent = yamlStringify(data, { lineWidth: 0, indent: 2 })
|
|
226
354
|
const editWarning = getEditWarning(resource.kind)
|
|
227
355
|
const formattedError = saveError ? formatSaveError(saveError) : null
|
|
228
|
-
const isPending = isSaving ?? false
|
|
356
|
+
const isPending = (isSaving ?? false) || (isPreviewing ?? false)
|
|
229
357
|
|
|
230
358
|
if (isEditing) {
|
|
359
|
+
if (preview) {
|
|
360
|
+
return (
|
|
361
|
+
<YamlReview
|
|
362
|
+
submittedYaml={preview.yaml}
|
|
363
|
+
originalYaml={preview.originalYaml}
|
|
364
|
+
documents={preview.documents}
|
|
365
|
+
nonAtomic={preview.nonAtomic}
|
|
366
|
+
force={preview.force}
|
|
367
|
+
isApplying={isSaving}
|
|
368
|
+
applyError={saveError}
|
|
369
|
+
onBack={() => setPreview(null)}
|
|
370
|
+
onApply={handleApplyReviewed}
|
|
371
|
+
/>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
231
374
|
return (
|
|
232
375
|
<div className="flex flex-col h-full">
|
|
233
376
|
<div className="flex items-center justify-between px-4 py-2 border-b border-theme-border bg-theme-elevated/50">
|
|
@@ -249,10 +392,14 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
249
392
|
<div className="space-y-1.5 max-w-[19rem]">
|
|
250
393
|
<p>Override field ownership (server-side apply).</p>
|
|
251
394
|
<p>
|
|
252
|
-
<span className="font-medium text-theme-text-primary">On (default):</span> your
|
|
395
|
+
<span className="font-medium text-theme-text-primary">On (default):</span> your
|
|
396
|
+
edits overwrite fields owned by Helm/Flux/Argo/kubectl — but an active
|
|
397
|
+
controller may reconcile them back on its next sync.
|
|
253
398
|
</p>
|
|
254
399
|
<p>
|
|
255
|
-
<span className="font-medium text-theme-text-primary">Off:</span> if another
|
|
400
|
+
<span className="font-medium text-theme-text-primary">Off:</span> if another
|
|
401
|
+
manager owns a field you changed, the whole save is rejected with a conflict
|
|
402
|
+
(nothing is applied).
|
|
256
403
|
</p>
|
|
257
404
|
</div>
|
|
258
405
|
}
|
|
@@ -276,10 +423,18 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
276
423
|
>
|
|
277
424
|
{isPending ? (
|
|
278
425
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
|
426
|
+
) : onPreview ? (
|
|
427
|
+
<Eye className="w-3.5 h-3.5" />
|
|
279
428
|
) : (
|
|
280
429
|
<Save className="w-3.5 h-3.5" />
|
|
281
430
|
)}
|
|
282
|
-
{
|
|
431
|
+
{isPreviewing
|
|
432
|
+
? 'Preparing review…'
|
|
433
|
+
: isSaving
|
|
434
|
+
? 'Saving…'
|
|
435
|
+
: onPreview
|
|
436
|
+
? 'Review changes'
|
|
437
|
+
: 'Save'}
|
|
283
438
|
</button>
|
|
284
439
|
</div>
|
|
285
440
|
</div>
|
|
@@ -289,8 +444,12 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
289
444
|
<div className="flex items-start gap-2">
|
|
290
445
|
<AlertTriangle className="w-4 h-4 text-amber-600 dark:text-yellow-300 mt-0.5 shrink-0" />
|
|
291
446
|
<div className="text-xs">
|
|
292
|
-
<span className="font-medium text-amber-700 dark:text-yellow-300">
|
|
293
|
-
|
|
447
|
+
<span className="font-medium text-amber-700 dark:text-yellow-300">
|
|
448
|
+
{editWarning.message}
|
|
449
|
+
</span>
|
|
450
|
+
<span className="text-amber-600 dark:text-yellow-300/80 ml-1">
|
|
451
|
+
{editWarning.tip}
|
|
452
|
+
</span>
|
|
294
453
|
{editWarning.learnMoreUrl && (
|
|
295
454
|
<a
|
|
296
455
|
href={editWarning.learnMoreUrl}
|
|
@@ -319,14 +478,14 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
319
478
|
</div>
|
|
320
479
|
)}
|
|
321
480
|
|
|
322
|
-
{formattedError && (
|
|
481
|
+
{(formattedError || previewError) && (
|
|
323
482
|
<div className="px-4 py-2 bg-red-500/10 border-b border-red-300 dark:border-red-500/30">
|
|
324
483
|
<div className="flex items-start gap-2">
|
|
325
484
|
<AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-300 mt-0.5 shrink-0" />
|
|
326
485
|
<div className="text-xs text-red-600 dark:text-red-300 flex-1">
|
|
327
|
-
<div className="font-medium">Save failed</div>
|
|
328
|
-
<div className="mt-1">{formattedError
|
|
329
|
-
{formattedError
|
|
486
|
+
<div className="font-medium">{previewError ? 'Preview failed' : 'Save failed'}</div>
|
|
487
|
+
<div className="mt-1">{previewError || formattedError?.summary}</div>
|
|
488
|
+
{formattedError?.details && (
|
|
330
489
|
<button
|
|
331
490
|
onClick={() => setShowErrorDetails(!showErrorDetails)}
|
|
332
491
|
className="mt-1 text-red-500 dark:text-red-300 hover:text-red-700 dark:hover:text-red-200 underline"
|
|
@@ -334,7 +493,7 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
334
493
|
{showErrorDetails ? 'Hide details' : 'Show details'}
|
|
335
494
|
</button>
|
|
336
495
|
)}
|
|
337
|
-
{showErrorDetails && formattedError
|
|
496
|
+
{showErrorDetails && formattedError?.details && (
|
|
338
497
|
<pre className="mt-2 p-2 bg-red-500/10 rounded text-[10px] whitespace-pre-wrap break-all max-h-40 overflow-auto">
|
|
339
498
|
{formattedError.details}
|
|
340
499
|
</pre>
|
|
@@ -349,6 +508,7 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
349
508
|
value={editedYaml}
|
|
350
509
|
onChange={setEditedYaml}
|
|
351
510
|
onValidate={handleYamlValidate}
|
|
511
|
+
schemaLoader={schemaLoader}
|
|
352
512
|
height="100%"
|
|
353
513
|
kind={resource.kind}
|
|
354
514
|
/>
|
|
@@ -376,7 +536,11 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
376
536
|
onClick={() => onCopy(yamlContent)}
|
|
377
537
|
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
378
538
|
>
|
|
379
|
-
{copied ?
|
|
539
|
+
{copied ? (
|
|
540
|
+
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
541
|
+
) : (
|
|
542
|
+
<Copy className="w-3.5 h-3.5" />
|
|
543
|
+
)}
|
|
380
544
|
Copy
|
|
381
545
|
</button>
|
|
382
546
|
<Tooltip content="Download manifest as YAML (server-generated fields stripped)">
|
|
@@ -391,7 +555,14 @@ export function EditableYamlView({ resource, data, onCopy, copied, readOnly = fa
|
|
|
391
555
|
{onDuplicate && (
|
|
392
556
|
<Tooltip content="Duplicate as new resource">
|
|
393
557
|
<button
|
|
394
|
-
onClick={() =>
|
|
558
|
+
onClick={() =>
|
|
559
|
+
onDuplicate({
|
|
560
|
+
kind: resource.kind,
|
|
561
|
+
namespace: resource.namespace,
|
|
562
|
+
name: resource.name,
|
|
563
|
+
yaml: yamlContent,
|
|
564
|
+
})
|
|
565
|
+
}
|
|
395
566
|
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
396
567
|
>
|
|
397
568
|
<CopyPlus className="w-3.5 h-3.5" />
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
isBlockingYamlDiagnostic,
|
|
4
|
+
parseFallbackYamlDiagnostics,
|
|
5
|
+
parseYamlDocumentIdentities,
|
|
6
|
+
shouldAutoTriggerYamlSuggestions,
|
|
7
|
+
} from './YamlEditor'
|
|
8
|
+
|
|
9
|
+
describe('parseYamlDocumentIdentities', () => {
|
|
10
|
+
it('preserves ordered identities and document start lines', () => {
|
|
11
|
+
const documents = parseYamlDocumentIdentities(`apiVersion: apps/v1
|
|
12
|
+
kind: Deployment
|
|
13
|
+
metadata:
|
|
14
|
+
name: api
|
|
15
|
+
---
|
|
16
|
+
apiVersion: monitoring.coreos.com/v1
|
|
17
|
+
kind: ServiceMonitor
|
|
18
|
+
metadata:
|
|
19
|
+
name: api
|
|
20
|
+
`)
|
|
21
|
+
expect(documents).toEqual([
|
|
22
|
+
{
|
|
23
|
+
index: 0,
|
|
24
|
+
schemaIndex: 0,
|
|
25
|
+
apiVersion: 'apps/v1',
|
|
26
|
+
kind: 'Deployment',
|
|
27
|
+
startLine: 1,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
index: 1,
|
|
31
|
+
schemaIndex: 1,
|
|
32
|
+
apiVersion: 'monitoring.coreos.com/v1',
|
|
33
|
+
kind: 'ServiceMonitor',
|
|
34
|
+
startLine: 5,
|
|
35
|
+
},
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('keeps incomplete documents addressable without inventing an identity', () => {
|
|
40
|
+
expect(parseYamlDocumentIdentities('apiVersion: v1\nkind:')).toEqual([
|
|
41
|
+
{ index: 0, schemaIndex: 0, apiVersion: 'v1', kind: '', startLine: 1 },
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('compacts empty document segments to match preview and apply indices', () => {
|
|
46
|
+
expect(
|
|
47
|
+
parseYamlDocumentIdentities(`apiVersion: apps/v1
|
|
48
|
+
kind: Deployment
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
apiVersion: v1
|
|
53
|
+
kind: ConfigMap
|
|
54
|
+
`),
|
|
55
|
+
).toEqual([
|
|
56
|
+
{
|
|
57
|
+
index: 0,
|
|
58
|
+
schemaIndex: 0,
|
|
59
|
+
apiVersion: 'apps/v1',
|
|
60
|
+
kind: 'Deployment',
|
|
61
|
+
startLine: 1,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
index: 1,
|
|
65
|
+
schemaIndex: 2,
|
|
66
|
+
apiVersion: 'v1',
|
|
67
|
+
kind: 'ConfigMap',
|
|
68
|
+
startLine: 5,
|
|
69
|
+
},
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('isBlockingYamlDiagnostic', () => {
|
|
75
|
+
it('blocks syntax failures when no schema source is present', () => {
|
|
76
|
+
expect(isBlockingYamlDiagnostic('error')).toBe(true)
|
|
77
|
+
expect(isBlockingYamlDiagnostic('warning', '0', 'Incorrect type. Expected integer.')).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('keeps cluster schema diagnostics advisory', () => {
|
|
81
|
+
expect(
|
|
82
|
+
isBlockingYamlDiagnostic(
|
|
83
|
+
'warning',
|
|
84
|
+
'513',
|
|
85
|
+
'Property contianers is not allowed.',
|
|
86
|
+
'yaml-schema: Kubernetes cluster schema',
|
|
87
|
+
),
|
|
88
|
+
).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('keeps deprecations visible but advisory', () => {
|
|
92
|
+
expect(isBlockingYamlDiagnostic('warning', '2', 'Use spec.newField instead.')).toBe(false)
|
|
93
|
+
expect(
|
|
94
|
+
isBlockingYamlDiagnostic(
|
|
95
|
+
'warning',
|
|
96
|
+
undefined,
|
|
97
|
+
'[radar-advisory:deprecated] Use spec.newField instead.',
|
|
98
|
+
),
|
|
99
|
+
).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('parseFallbackYamlDiagnostics', () => {
|
|
104
|
+
it('accepts valid multi-document YAML', () => {
|
|
105
|
+
expect(
|
|
106
|
+
parseFallbackYamlDiagnostics(`apiVersion: v1
|
|
107
|
+
kind: ConfigMap
|
|
108
|
+
---
|
|
109
|
+
apiVersion: apps/v1
|
|
110
|
+
kind: Deployment
|
|
111
|
+
`),
|
|
112
|
+
).toEqual([])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('reports a blocking syntax error against the correct document', () => {
|
|
116
|
+
const diagnostics = parseFallbackYamlDiagnostics(`apiVersion: v1
|
|
117
|
+
kind: ConfigMap
|
|
118
|
+
---
|
|
119
|
+
apiVersion: v1
|
|
120
|
+
kind: [`)
|
|
121
|
+
|
|
122
|
+
expect(diagnostics).toHaveLength(1)
|
|
123
|
+
expect(diagnostics[0]).toMatchObject({
|
|
124
|
+
severity: 'error',
|
|
125
|
+
line: 5,
|
|
126
|
+
column: 8,
|
|
127
|
+
documentIndex: 1,
|
|
128
|
+
blocking: true,
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('shouldAutoTriggerYamlSuggestions', () => {
|
|
134
|
+
it('triggers after Enter creates an indented blank line', () => {
|
|
135
|
+
expect(shouldAutoTriggerYamlSuggestions(['\n '], ' ', 7)).toBe(true)
|
|
136
|
+
expect(shouldAutoTriggerYamlSuggestions(['\r\n '], ' ', 3)).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not trigger for typing, pasted content, or a non-blank line', () => {
|
|
140
|
+
expect(shouldAutoTriggerYamlSuggestions([' '], ' ', 7)).toBe(false)
|
|
141
|
+
expect(shouldAutoTriggerYamlSuggestions(['\n image:'], ' image:', 13)).toBe(false)
|
|
142
|
+
expect(shouldAutoTriggerYamlSuggestions(['\n '], ' ports:', 13)).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('waits until the cursor is at the end of the new indentation', () => {
|
|
146
|
+
expect(shouldAutoTriggerYamlSuggestions(['\n '], ' ', 4)).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
})
|