@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,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import { AlertTriangle, Check, ChevronDown, ChevronRight, Loader2, X } from 'lucide-react'
|
|
3
3
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
4
|
-
import { YamlEditor } from '../ui/YamlEditor'
|
|
4
|
+
import { YamlEditor, type YamlSchemaLoader } from '../ui/YamlEditor'
|
|
5
|
+
import {
|
|
6
|
+
reviewedResourceVersionsForPreview,
|
|
7
|
+
YamlReview,
|
|
8
|
+
type YamlPreviewResult,
|
|
9
|
+
} from '../ui/YamlReview'
|
|
5
10
|
import { Tooltip } from '../ui/Tooltip'
|
|
6
11
|
import { formatApplyError } from '../../utils/k8s-errors'
|
|
7
12
|
|
|
@@ -17,14 +22,39 @@ export interface CreateResourceDialogProps {
|
|
|
17
22
|
onClose: () => void
|
|
18
23
|
initialYaml?: string
|
|
19
24
|
title?: string
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
onApply: (params: {
|
|
26
|
+
yaml: string
|
|
27
|
+
mode: 'apply' | 'create'
|
|
28
|
+
dryRun: boolean
|
|
29
|
+
force: boolean
|
|
30
|
+
reviewedResourceVersions?: Record<number, string>
|
|
31
|
+
reviewedContext?: string
|
|
32
|
+
}) => Promise<ApplyResult[]>
|
|
22
33
|
isApplying: boolean
|
|
23
|
-
|
|
34
|
+
onPreview?: (params: { yaml: string; mode: 'apply' | 'create'; force: boolean }) => Promise<{
|
|
35
|
+
documents: YamlPreviewResult[]
|
|
36
|
+
nonAtomic: boolean
|
|
37
|
+
context?: string
|
|
38
|
+
}>
|
|
39
|
+
isPreviewing?: boolean
|
|
40
|
+
previewError?: string | null
|
|
41
|
+
schemaLoader?: YamlSchemaLoader
|
|
24
42
|
onCreated?: (result: ApplyResult) => void
|
|
25
43
|
}
|
|
26
44
|
|
|
27
|
-
export function CreateResourceDialog({
|
|
45
|
+
export function CreateResourceDialog({
|
|
46
|
+
open,
|
|
47
|
+
onClose,
|
|
48
|
+
initialYaml = '',
|
|
49
|
+
title,
|
|
50
|
+
onApply,
|
|
51
|
+
isApplying,
|
|
52
|
+
onPreview,
|
|
53
|
+
isPreviewing = false,
|
|
54
|
+
previewError,
|
|
55
|
+
schemaLoader,
|
|
56
|
+
onCreated,
|
|
57
|
+
}: CreateResourceDialogProps) {
|
|
28
58
|
const [yaml, setYaml] = useState(initialYaml)
|
|
29
59
|
const [mode, setMode] = useState<'apply' | 'create'>('apply')
|
|
30
60
|
const [dryRun, setDryRun] = useState(false)
|
|
@@ -32,31 +62,66 @@ export function CreateResourceDialog({ open, onClose, initialYaml = '', title, o
|
|
|
32
62
|
const [yamlValid, setYamlValid] = useState(true)
|
|
33
63
|
const [error, setError] = useState<string | null>(null)
|
|
34
64
|
const [success, setSuccess] = useState<string | null>(null)
|
|
65
|
+
const [preview, setPreview] = useState<{
|
|
66
|
+
yaml: string
|
|
67
|
+
mode: 'apply' | 'create'
|
|
68
|
+
force: boolean
|
|
69
|
+
documents: YamlPreviewResult[]
|
|
70
|
+
nonAtomic: boolean
|
|
71
|
+
context?: string
|
|
72
|
+
} | null>(null)
|
|
35
73
|
|
|
36
|
-
// Reset state when dialog opens or initialYaml changes
|
|
37
74
|
useEffect(() => {
|
|
38
|
-
if (open)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
75
|
+
if (!open) return
|
|
76
|
+
setYaml(initialYaml)
|
|
77
|
+
setMode('apply')
|
|
78
|
+
setDryRun(false)
|
|
79
|
+
setForce(false)
|
|
80
|
+
setYamlValid(true)
|
|
81
|
+
setError(null)
|
|
82
|
+
setSuccess(null)
|
|
83
|
+
setPreview(null)
|
|
46
84
|
}, [open, initialYaml])
|
|
47
85
|
|
|
48
|
-
const
|
|
49
|
-
|
|
86
|
+
const pending = isApplying || isPreviewing
|
|
87
|
+
const closeNow = useCallback(() => {
|
|
50
88
|
setError(null)
|
|
51
89
|
setSuccess(null)
|
|
90
|
+
setPreview(null)
|
|
52
91
|
setYaml('')
|
|
53
92
|
onClose()
|
|
54
|
-
}, [onClose
|
|
93
|
+
}, [onClose])
|
|
94
|
+
const handleClose = useCallback(() => {
|
|
95
|
+
if (!pending) closeNow()
|
|
96
|
+
}, [closeNow, pending])
|
|
55
97
|
|
|
56
98
|
const handleValidate = useCallback((_isValid: boolean, errors: string[]) => {
|
|
57
99
|
setYamlValid(errors.length === 0)
|
|
58
100
|
}, [])
|
|
59
101
|
|
|
102
|
+
const finishApply = useCallback(
|
|
103
|
+
(results: ApplyResult[], appliedMode: 'apply' | 'create', wasDryRun: boolean) => {
|
|
104
|
+
const action = appliedMode === 'create' ? 'Created' : 'Applied'
|
|
105
|
+
const dryRunLabel = wasDryRun ? ' (dry run)' : ''
|
|
106
|
+
if (results.length === 1) {
|
|
107
|
+
const result = results[0]
|
|
108
|
+
setSuccess(
|
|
109
|
+
`${action} ${result.kind} ${result.namespace ? `${result.namespace}/` : ''}${result.name}${dryRunLabel}`,
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
setSuccess(`${action} ${results.length} resources${dryRunLabel}`)
|
|
113
|
+
}
|
|
114
|
+
if (wasDryRun) return
|
|
115
|
+
if (onCreated && results.length > 0) {
|
|
116
|
+
closeNow()
|
|
117
|
+
onCreated(results[0])
|
|
118
|
+
} else {
|
|
119
|
+
window.setTimeout(closeNow, 1200)
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
[closeNow, onCreated],
|
|
123
|
+
)
|
|
124
|
+
|
|
60
125
|
const handleSubmit = useCallback(async () => {
|
|
61
126
|
if (!yaml.trim()) {
|
|
62
127
|
setError('YAML content is required')
|
|
@@ -64,167 +129,238 @@ export function CreateResourceDialog({ open, onClose, initialYaml = '', title, o
|
|
|
64
129
|
}
|
|
65
130
|
setError(null)
|
|
66
131
|
setSuccess(null)
|
|
67
|
-
|
|
68
132
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
if (onPreview) {
|
|
134
|
+
const reviewedForce = mode === 'apply' && force
|
|
135
|
+
const result = await onPreview({ yaml, mode, force: reviewedForce })
|
|
136
|
+
setPreview({
|
|
137
|
+
yaml,
|
|
138
|
+
mode,
|
|
139
|
+
force: reviewedForce,
|
|
140
|
+
documents: result.documents,
|
|
141
|
+
nonAtomic: result.nonAtomic,
|
|
142
|
+
context: result.context,
|
|
143
|
+
})
|
|
144
|
+
return
|
|
78
145
|
}
|
|
146
|
+
const results = await onApply({
|
|
147
|
+
yaml,
|
|
148
|
+
mode,
|
|
149
|
+
dryRun,
|
|
150
|
+
force: mode === 'apply' && force,
|
|
151
|
+
})
|
|
152
|
+
finishApply(results, mode, dryRun)
|
|
153
|
+
} catch (caught) {
|
|
154
|
+
setError(caught instanceof Error ? caught.message : 'Unknown error')
|
|
155
|
+
}
|
|
156
|
+
}, [yaml, mode, dryRun, force, onApply, onPreview, finishApply])
|
|
79
157
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
158
|
+
const handleApplyReviewed = useCallback(async () => {
|
|
159
|
+
if (!preview) return
|
|
160
|
+
setError(null)
|
|
161
|
+
try {
|
|
162
|
+
const reviewedResourceVersions = reviewedResourceVersionsForPreview(preview.documents)
|
|
163
|
+
const results = await onApply({
|
|
164
|
+
yaml: preview.yaml,
|
|
165
|
+
mode: preview.mode,
|
|
166
|
+
dryRun: false,
|
|
167
|
+
force: preview.force,
|
|
168
|
+
reviewedResourceVersions,
|
|
169
|
+
reviewedContext: preview.context,
|
|
170
|
+
})
|
|
171
|
+
finishApply(results, preview.mode, false)
|
|
172
|
+
} catch (caught) {
|
|
173
|
+
const message = caught instanceof Error ? caught.message : 'Unknown error'
|
|
174
|
+
const appliedResults =
|
|
175
|
+
caught instanceof Error &&
|
|
176
|
+
'appliedResults' in caught &&
|
|
177
|
+
Array.isArray(caught.appliedResults)
|
|
178
|
+
? caught.appliedResults
|
|
179
|
+
: []
|
|
180
|
+
if (preview.mode === 'create' && appliedResults.length > 0) {
|
|
181
|
+
setYaml(preview.yaml)
|
|
182
|
+
setMode('apply')
|
|
183
|
+
setPreview(null)
|
|
184
|
+
setError(
|
|
185
|
+
`${message} Radar switched to Apply mode so you can review and continue from the current cluster state.`,
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
setError(message)
|
|
190
|
+
if (onPreview) {
|
|
191
|
+
try {
|
|
192
|
+
const refreshed = await onPreview({
|
|
193
|
+
yaml: preview.yaml,
|
|
194
|
+
mode: preview.mode,
|
|
195
|
+
force: preview.force,
|
|
196
|
+
})
|
|
197
|
+
setPreview({
|
|
198
|
+
...preview,
|
|
199
|
+
documents: refreshed.documents,
|
|
200
|
+
nonAtomic: refreshed.nonAtomic,
|
|
201
|
+
context: refreshed.context,
|
|
202
|
+
})
|
|
203
|
+
} catch {
|
|
204
|
+
// Keep the last review visible when refresh is unavailable.
|
|
87
205
|
}
|
|
88
206
|
}
|
|
89
|
-
} catch (err) {
|
|
90
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
91
207
|
}
|
|
92
|
-
}, [
|
|
208
|
+
}, [preview, onApply, onPreview, finishApply])
|
|
93
209
|
|
|
94
210
|
const dialogTitle = title || 'Create Resource'
|
|
95
|
-
const submitLabel = mode === 'create' ? 'Create' : 'Apply'
|
|
211
|
+
const submitLabel = onPreview ? 'Review' : mode === 'create' ? 'Create' : 'Apply'
|
|
96
212
|
|
|
97
213
|
return (
|
|
98
|
-
<DialogPortal
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
214
|
+
<DialogPortal
|
|
215
|
+
open={open}
|
|
216
|
+
onClose={handleClose}
|
|
217
|
+
closable={!pending}
|
|
218
|
+
className={
|
|
219
|
+
preview
|
|
220
|
+
? 'w-[min(1200px,calc(100vw-2rem))] h-[min(860px,calc(100vh-2rem))] flex flex-col'
|
|
221
|
+
: 'w-[700px] max-h-[85vh] flex flex-col'
|
|
222
|
+
}
|
|
223
|
+
>
|
|
224
|
+
{preview ? (
|
|
225
|
+
<YamlReview
|
|
226
|
+
submittedYaml={preview.yaml}
|
|
227
|
+
documents={preview.documents}
|
|
228
|
+
nonAtomic={preview.nonAtomic}
|
|
229
|
+
force={preview.force}
|
|
230
|
+
isApplying={isApplying}
|
|
231
|
+
applyError={error}
|
|
232
|
+
applyLabel={
|
|
233
|
+
preview.mode === 'create' ? 'Create reviewed resources' : 'Apply reviewed changes'
|
|
234
|
+
}
|
|
235
|
+
onClose={handleClose}
|
|
236
|
+
onBack={() => {
|
|
237
|
+
setError(null)
|
|
238
|
+
setPreview(null)
|
|
239
|
+
}}
|
|
240
|
+
onApply={handleApplyReviewed}
|
|
119
241
|
/>
|
|
120
|
-
|
|
242
|
+
) : (
|
|
243
|
+
<>
|
|
244
|
+
<div className="flex shrink-0 items-center justify-between border-b border-theme-border px-5 py-3.5">
|
|
245
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">{dialogTitle}</h2>
|
|
246
|
+
<Tooltip content="Close">
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={handleClose}
|
|
250
|
+
className="rounded p-1 text-theme-text-secondary transition-colors hover:bg-theme-hover"
|
|
251
|
+
>
|
|
252
|
+
<X className="h-4 w-4" />
|
|
253
|
+
</button>
|
|
254
|
+
</Tooltip>
|
|
255
|
+
</div>
|
|
121
256
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
257
|
+
<div className="min-h-0 flex-1 px-5 py-3">
|
|
258
|
+
<YamlEditor
|
|
259
|
+
value={yaml}
|
|
260
|
+
onChange={setYaml}
|
|
261
|
+
height="400px"
|
|
262
|
+
onValidate={handleValidate}
|
|
263
|
+
schemaLoader={schemaLoader}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{(error || previewError) && (
|
|
268
|
+
<ApplyErrorBanner error={error || previewError || 'Preview failed'} />
|
|
269
|
+
)}
|
|
270
|
+
{success && (
|
|
271
|
+
<div className="mx-5 mb-2 flex items-center gap-2 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-600 dark:text-emerald-400">
|
|
272
|
+
<Check className="h-3.5 w-3.5 shrink-0" />
|
|
273
|
+
<span>{success}</span>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
<div className="flex shrink-0 items-center justify-between border-t border-theme-border px-5 py-3">
|
|
278
|
+
<div className="flex items-center gap-3">
|
|
279
|
+
<Tooltip
|
|
280
|
+
content="Apply: create or update (idempotent). Create: fail if exists."
|
|
281
|
+
position="bottom"
|
|
282
|
+
>
|
|
283
|
+
<div
|
|
284
|
+
className="flex items-center rounded-md border border-theme-border bg-theme-base p-0.5"
|
|
285
|
+
role="radiogroup"
|
|
286
|
+
aria-label="Apply mode"
|
|
287
|
+
>
|
|
288
|
+
{(['apply', 'create'] as const).map((option) => (
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
key={option}
|
|
292
|
+
onClick={() => setMode(option)}
|
|
293
|
+
role="radio"
|
|
294
|
+
aria-checked={mode === option}
|
|
295
|
+
className={`rounded px-2.5 py-1 text-xs font-medium capitalize transition-colors ${
|
|
296
|
+
mode === option
|
|
297
|
+
? 'bg-theme-elevated text-theme-text-primary shadow-theme-sm'
|
|
298
|
+
: 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
299
|
+
}`}
|
|
300
|
+
>
|
|
301
|
+
{option}
|
|
302
|
+
</button>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
</Tooltip>
|
|
306
|
+
|
|
307
|
+
{!onPreview && (
|
|
308
|
+
<Tooltip
|
|
309
|
+
content="Validate against the cluster without persisting changes"
|
|
310
|
+
position="bottom"
|
|
311
|
+
>
|
|
312
|
+
<label className="flex cursor-pointer items-center gap-1.5 text-xs text-theme-text-secondary">
|
|
313
|
+
<input
|
|
314
|
+
type="checkbox"
|
|
315
|
+
checked={dryRun}
|
|
316
|
+
onChange={(event) => setDryRun(event.target.checked)}
|
|
317
|
+
className="h-3.5 w-3.5 rounded border-theme-border bg-theme-base"
|
|
318
|
+
/>
|
|
319
|
+
Dry run
|
|
320
|
+
</label>
|
|
321
|
+
</Tooltip>
|
|
322
|
+
)}
|
|
130
323
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
324
|
+
<Tooltip
|
|
325
|
+
content="Override field ownership conflicts. An active controller may reconcile those fields back."
|
|
326
|
+
position="bottom"
|
|
327
|
+
>
|
|
328
|
+
<label
|
|
329
|
+
className={`flex items-center gap-1.5 text-xs ${mode === 'apply' ? 'cursor-pointer text-theme-text-secondary' : 'cursor-not-allowed text-theme-text-tertiary'}`}
|
|
330
|
+
>
|
|
331
|
+
<input
|
|
332
|
+
type="checkbox"
|
|
333
|
+
checked={mode === 'apply' && force}
|
|
334
|
+
disabled={mode !== 'apply'}
|
|
335
|
+
onChange={(event) => setForce(event.target.checked)}
|
|
336
|
+
className="h-3.5 w-3.5 rounded border-theme-border bg-theme-base"
|
|
337
|
+
/>
|
|
338
|
+
Force
|
|
339
|
+
</label>
|
|
340
|
+
</Tooltip>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div className="flex items-center gap-2">
|
|
137
344
|
<button
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors ${
|
|
142
|
-
mode === 'apply'
|
|
143
|
-
? 'bg-theme-elevated text-theme-text-primary shadow-sm'
|
|
144
|
-
: 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
145
|
-
}`}
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={handleClose}
|
|
347
|
+
className="rounded-lg px-3 py-1.5 text-xs text-theme-text-secondary transition-colors hover:bg-theme-hover"
|
|
146
348
|
>
|
|
147
|
-
|
|
349
|
+
Cancel
|
|
148
350
|
</button>
|
|
149
351
|
<button
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
className=
|
|
154
|
-
mode === 'create'
|
|
155
|
-
? 'bg-theme-elevated text-theme-text-primary shadow-sm'
|
|
156
|
-
: 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
157
|
-
}`}
|
|
352
|
+
type="button"
|
|
353
|
+
onClick={handleSubmit}
|
|
354
|
+
disabled={pending || !yaml.trim() || !yamlValid}
|
|
355
|
+
className="btn-brand flex items-center gap-1.5 rounded-lg px-4 py-1.5 text-xs font-medium disabled:cursor-not-allowed"
|
|
158
356
|
>
|
|
159
|
-
|
|
357
|
+
{pending && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
|
358
|
+
{isPreviewing ? 'Previewing…' : isApplying ? `${submitLabel}…` : submitLabel}
|
|
160
359
|
</button>
|
|
161
360
|
</div>
|
|
162
|
-
</
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<Tooltip content="Validate against the cluster without persisting changes" position="bottom">
|
|
166
|
-
<label className="flex items-center gap-1.5 text-xs text-theme-text-secondary cursor-pointer">
|
|
167
|
-
<input
|
|
168
|
-
type="checkbox"
|
|
169
|
-
checked={dryRun}
|
|
170
|
-
onChange={(e) => setDryRun(e.target.checked)}
|
|
171
|
-
className="w-3.5 h-3.5 rounded border-theme-border bg-theme-base"
|
|
172
|
-
/>
|
|
173
|
-
Dry run
|
|
174
|
-
</label>
|
|
175
|
-
</Tooltip>
|
|
176
|
-
|
|
177
|
-
{/* Force checkbox — only meaningful for server-side apply */}
|
|
178
|
-
<Tooltip
|
|
179
|
-
content={
|
|
180
|
-
<div className="space-y-1.5 max-w-[19rem]">
|
|
181
|
-
<p>Override field ownership (server-side apply).</p>
|
|
182
|
-
<p>
|
|
183
|
-
<span className="font-medium text-theme-text-primary">Off (default):</span> if a field is owned by Helm/Flux/Argo/kubectl, the whole apply is rejected with a conflict instead of overwriting.
|
|
184
|
-
</p>
|
|
185
|
-
<p>
|
|
186
|
-
<span className="font-medium text-theme-text-primary">On:</span> your manifest overwrites those fields — but an active controller may reconcile them back on its next sync.
|
|
187
|
-
</p>
|
|
188
|
-
</div>
|
|
189
|
-
}
|
|
190
|
-
position="bottom"
|
|
191
|
-
>
|
|
192
|
-
<label className={`flex items-center gap-1.5 text-xs cursor-pointer ${mode === 'apply' ? 'text-theme-text-secondary' : 'text-theme-text-tertiary cursor-not-allowed'}`}>
|
|
193
|
-
<input
|
|
194
|
-
type="checkbox"
|
|
195
|
-
checked={mode === 'apply' && force}
|
|
196
|
-
disabled={mode !== 'apply'}
|
|
197
|
-
onChange={(e) => setForce(e.target.checked)}
|
|
198
|
-
className="w-3.5 h-3.5 rounded border-theme-border bg-theme-base"
|
|
199
|
-
/>
|
|
200
|
-
Force
|
|
201
|
-
</label>
|
|
202
|
-
</Tooltip>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<div className="flex items-center gap-2">
|
|
206
|
-
<button
|
|
207
|
-
onClick={handleClose}
|
|
208
|
-
className="px-3 py-1.5 text-xs rounded-lg hover:bg-theme-hover text-theme-text-secondary transition-colors"
|
|
209
|
-
>
|
|
210
|
-
Cancel
|
|
211
|
-
</button>
|
|
212
|
-
<button
|
|
213
|
-
onClick={handleSubmit}
|
|
214
|
-
disabled={isApplying || !yaml.trim() || !yamlValid}
|
|
215
|
-
className="px-4 py-1.5 text-xs rounded-lg btn-brand font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
|
216
|
-
>
|
|
217
|
-
{isApplying ? (
|
|
218
|
-
<>
|
|
219
|
-
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
220
|
-
{submitLabel === 'Apply' ? 'Applying...' : 'Creating...'}
|
|
221
|
-
</>
|
|
222
|
-
) : (
|
|
223
|
-
submitLabel
|
|
224
|
-
)}
|
|
225
|
-
</button>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</>
|
|
363
|
+
)}
|
|
228
364
|
</DialogPortal>
|
|
229
365
|
)
|
|
230
366
|
}
|
|
@@ -232,31 +368,30 @@ export function CreateResourceDialog({ open, onClose, initialYaml = '', title, o
|
|
|
232
368
|
function ApplyErrorBanner({ error }: { error: string }) {
|
|
233
369
|
const [expanded, setExpanded] = useState(false)
|
|
234
370
|
const parsed = formatApplyError(error)
|
|
235
|
-
const hasFriendly =
|
|
236
|
-
|
|
371
|
+
const hasFriendly = Boolean(parsed.suggestion)
|
|
237
372
|
return (
|
|
238
|
-
<div className="mx-5 mb-2 rounded-md
|
|
239
|
-
<div className="px-3 py-2
|
|
240
|
-
<AlertTriangle className="
|
|
373
|
+
<div className="mx-5 mb-2 rounded-md border border-red-500/30 bg-red-500/10 text-xs">
|
|
374
|
+
<div className="flex items-start gap-2 px-3 py-2 text-red-600 dark:text-red-400">
|
|
375
|
+
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
241
376
|
<div className="min-w-0 flex-1">
|
|
242
377
|
<span className="font-medium">{parsed.summary}</span>
|
|
243
378
|
{parsed.suggestion && (
|
|
244
|
-
<p className="mt-1 text-red-400/80">{parsed.suggestion}</p>
|
|
379
|
+
<p className="mt-1 text-red-500/80 dark:text-red-400/80">{parsed.suggestion}</p>
|
|
245
380
|
)}
|
|
246
381
|
</div>
|
|
247
382
|
</div>
|
|
248
383
|
{hasFriendly && (
|
|
249
384
|
<button
|
|
250
385
|
type="button"
|
|
251
|
-
onClick={() => setExpanded(!
|
|
252
|
-
className="flex items-center gap-1 px-3 pb-2 text-red-400/60 hover:text-red-400/80
|
|
386
|
+
onClick={() => setExpanded((value) => !value)}
|
|
387
|
+
className="flex items-center gap-1 px-3 pb-2 text-red-500/60 hover:text-red-500/80 dark:text-red-400/60 dark:hover:text-red-400/80"
|
|
253
388
|
>
|
|
254
|
-
{expanded ? <ChevronDown className="
|
|
255
|
-
|
|
389
|
+
{expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
390
|
+
Details
|
|
256
391
|
</button>
|
|
257
392
|
)}
|
|
258
|
-
{
|
|
259
|
-
<div className="px-3 pb-2 text-red-
|
|
393
|
+
{expanded && hasFriendly && (
|
|
394
|
+
<div className="break-all px-3 pb-2 font-mono leading-relaxed text-red-500/60 dark:text-red-400/60">
|
|
260
395
|
{parsed.raw}
|
|
261
396
|
</div>
|
|
262
397
|
)}
|