@skyhook-io/k8s-ui 1.0.0

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.
Files changed (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. package/tsconfig.json +24 -0
@@ -0,0 +1,302 @@
1
+ import { useState, useCallback } from 'react'
2
+ import {
3
+ Copy,
4
+ Check,
5
+ RefreshCw,
6
+ Pencil,
7
+ Save,
8
+ XCircle,
9
+ AlertTriangle,
10
+ } from 'lucide-react'
11
+ import { stringify as yamlStringify } from 'yaml'
12
+ import { CodeViewer } from '../ui/CodeViewer'
13
+ import { YamlEditor } from '../ui/YamlEditor'
14
+ import type { SelectedResource } from '../../types'
15
+
16
+ // ============================================================================
17
+ // SUCCESS ANIMATION
18
+ // ============================================================================
19
+
20
+ export function SaveSuccessAnimation() {
21
+ return (
22
+ <div className="absolute top-0 left-0 right-0 z-50 pointer-events-none">
23
+ <div className="flex justify-center animate-fade-in-out">
24
+ <div className="mt-2 px-4 py-2 bg-green-600/90 dark:bg-green-500/90 backdrop-blur-sm rounded-lg shadow-lg flex items-center gap-2">
25
+ <Check className="w-4 h-4 text-white" />
26
+ <span className="text-white text-sm font-medium">Saved</span>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ // ============================================================================
34
+ // EDITABLE YAML VIEW
35
+ // ============================================================================
36
+
37
+ // Get edit warning for resource types with limited editability
38
+ function getEditWarning(kind: string): { message: string; tip: string; learnMoreUrl?: string } | null {
39
+ const k = kind.toLowerCase()
40
+ if (k === 'pods' || k === 'pod') {
41
+ return {
42
+ message: 'Pods have limited editability.',
43
+ tip: 'Green highlighted lines can be changed. Edit the parent Deployment instead for other fields.',
44
+ learnMoreUrl: 'https://kubernetes.io/docs/concepts/workloads/pods/#pod-update-and-replacement'
45
+ }
46
+ }
47
+ if (k === 'jobs' || k === 'job') {
48
+ return {
49
+ message: 'Jobs cannot be modified after creation.',
50
+ tip: 'Delete and recreate the Job to make changes.',
51
+ learnMoreUrl: 'https://kubernetes.io/docs/concepts/workloads/controllers/job/'
52
+ }
53
+ }
54
+ return null
55
+ }
56
+
57
+ // Parse and simplify Kubernetes error messages
58
+ function formatSaveError(error: string): { summary: string; details?: string } {
59
+ if (error.includes('is invalid:')) {
60
+ const parts = error.split('is invalid:')
61
+ const errorPart = parts[1]?.trim() || ''
62
+
63
+ if (errorPart.includes('Forbidden:')) {
64
+ const forbiddenMatch = errorPart.match(/([^:]+):\s*Forbidden:\s*([^.{]+)/)
65
+ if (forbiddenMatch) {
66
+ return {
67
+ summary: `Cannot update ${forbiddenMatch[1]}: ${forbiddenMatch[2].trim()}`,
68
+ details: error.length > 200 ? error : undefined
69
+ }
70
+ }
71
+ }
72
+
73
+ const summaryMatch = errorPart.match(/^([^{]+)/)
74
+ if (summaryMatch) {
75
+ return {
76
+ summary: summaryMatch[1].trim(),
77
+ details: error.length > 200 ? error : undefined
78
+ }
79
+ }
80
+ }
81
+
82
+ if (error.length > 150) {
83
+ return {
84
+ summary: error.substring(0, 150) + '...',
85
+ details: error
86
+ }
87
+ }
88
+
89
+ return { summary: error }
90
+ }
91
+
92
+ interface EditableYamlViewProps {
93
+ resource: SelectedResource
94
+ data: any
95
+ onCopy: (text: string) => void
96
+ copied: boolean
97
+ /** Called after a successful save so the parent can refetch */
98
+ onSaved?: () => void
99
+ /** Save handler — injected by the platform wrapper */
100
+ onSave?: (params: { kind: string; namespace: string; name: string; yaml: string }) => Promise<void>
101
+ /** Whether a save is in progress */
102
+ isSaving?: boolean
103
+ /** Error message from the last save attempt */
104
+ saveError?: string | null
105
+ }
106
+
107
+ export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSave, isSaving, saveError }: EditableYamlViewProps) {
108
+ const [isEditing, setIsEditing] = useState(false)
109
+ const [editedYaml, setEditedYaml] = useState('')
110
+ const [yamlErrors, setYamlErrors] = useState<string[]>([])
111
+ const [showErrorDetails, setShowErrorDetails] = useState(false)
112
+
113
+ // Convert resource to YAML for editing
114
+ const convertToYaml = useCallback((d: any) => {
115
+ if (!d) return ''
116
+ const cleaned = { ...d }
117
+ delete cleaned.status
118
+ if (cleaned.metadata) {
119
+ delete cleaned.metadata.managedFields
120
+ delete cleaned.metadata.resourceVersion
121
+ delete cleaned.metadata.uid
122
+ delete cleaned.metadata.creationTimestamp
123
+ delete cleaned.metadata.generation
124
+ }
125
+ return yamlStringify(cleaned, { lineWidth: 0, indent: 2 })
126
+ }, [])
127
+
128
+ const handleStartEdit = useCallback(() => {
129
+ setEditedYaml(convertToYaml(data))
130
+ setYamlErrors([])
131
+ setIsEditing(true)
132
+ }, [data, convertToYaml])
133
+
134
+ const handleCancelEdit = useCallback(() => {
135
+ setIsEditing(false)
136
+ setEditedYaml('')
137
+ setYamlErrors([])
138
+ }, [])
139
+
140
+ const handleSaveEdit = useCallback(async () => {
141
+ if (yamlErrors.length > 0 || !onSave) return
142
+
143
+ try {
144
+ await onSave({
145
+ kind: resource.kind,
146
+ namespace: resource.namespace,
147
+ name: resource.name,
148
+ yaml: editedYaml,
149
+ })
150
+ setIsEditing(false)
151
+ setEditedYaml('')
152
+ setTimeout(() => onSaved?.(), 1000)
153
+ } catch {
154
+ // Error handled by caller via saveError prop
155
+ }
156
+ }, [onSave, resource, editedYaml, yamlErrors, onSaved])
157
+
158
+ const handleYamlValidate = useCallback((_isValid: boolean, errors: string[]) => {
159
+ setYamlErrors(errors)
160
+ }, [])
161
+
162
+ const yamlContent = yamlStringify(data, { lineWidth: 0, indent: 2 })
163
+ const editWarning = getEditWarning(resource.kind)
164
+ const formattedError = saveError ? formatSaveError(saveError) : null
165
+ const isPending = isSaving ?? false
166
+
167
+ if (isEditing) {
168
+ return (
169
+ <div className="flex flex-col h-full">
170
+ <div className="flex items-center justify-between px-4 py-2 border-b border-theme-border bg-theme-elevated/50">
171
+ <div className="flex items-center gap-2">
172
+ <Pencil className="w-4 h-4 text-blue-400" />
173
+ <span className="text-sm font-medium text-theme-text-primary">Editing Resource</span>
174
+ </div>
175
+ <div className="flex items-center gap-2">
176
+ <button
177
+ onClick={handleCancelEdit}
178
+ disabled={isPending}
179
+ className="flex items-center gap-1 px-3 py-1.5 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-surface rounded border border-theme-border disabled:opacity-50"
180
+ >
181
+ <XCircle className="w-3.5 h-3.5" />
182
+ Cancel
183
+ </button>
184
+ <button
185
+ onClick={handleSaveEdit}
186
+ disabled={isPending || yamlErrors.length > 0}
187
+ className="flex items-center gap-1 px-3 py-1.5 text-xs text-white bg-blue-600 hover:bg-blue-700 rounded disabled:opacity-50 disabled:cursor-not-allowed"
188
+ >
189
+ {isPending ? (
190
+ <RefreshCw className="w-3.5 h-3.5 animate-spin" />
191
+ ) : (
192
+ <Save className="w-3.5 h-3.5" />
193
+ )}
194
+ {isPending ? 'Saving...' : 'Save'}
195
+ </button>
196
+ </div>
197
+ </div>
198
+
199
+ {editWarning && (
200
+ <div className="px-4 py-2.5 bg-amber-500/10 dark:bg-yellow-500/10 border-b border-amber-300 dark:border-yellow-500/30">
201
+ <div className="flex items-start gap-2">
202
+ <AlertTriangle className="w-4 h-4 text-amber-600 dark:text-yellow-300 mt-0.5 shrink-0" />
203
+ <div className="text-xs">
204
+ <span className="font-medium text-amber-700 dark:text-yellow-300">{editWarning.message}</span>
205
+ <span className="text-amber-600 dark:text-yellow-300/80 ml-1">{editWarning.tip}</span>
206
+ {editWarning.learnMoreUrl && (
207
+ <a
208
+ href={editWarning.learnMoreUrl}
209
+ target="_blank"
210
+ rel="noopener noreferrer"
211
+ className="ml-1.5 text-blue-600 dark:text-blue-300 hover:underline"
212
+ >
213
+ Learn more →
214
+ </a>
215
+ )}
216
+ </div>
217
+ </div>
218
+ </div>
219
+ )}
220
+
221
+ {yamlErrors.length > 0 && (
222
+ <div className="px-4 py-2 bg-red-500/10 border-b border-red-300 dark:border-red-500/30">
223
+ <div className="flex items-start gap-2">
224
+ <AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-300 mt-0.5 shrink-0" />
225
+ <div className="text-xs text-red-600 dark:text-red-300">
226
+ {yamlErrors.map((err, i) => (
227
+ <div key={i}>{err}</div>
228
+ ))}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ )}
233
+
234
+ {formattedError && (
235
+ <div className="px-4 py-2 bg-red-500/10 border-b border-red-300 dark:border-red-500/30">
236
+ <div className="flex items-start gap-2">
237
+ <AlertTriangle className="w-4 h-4 text-red-600 dark:text-red-300 mt-0.5 shrink-0" />
238
+ <div className="text-xs text-red-600 dark:text-red-300 flex-1">
239
+ <div className="font-medium">Save failed</div>
240
+ <div className="mt-1">{formattedError.summary}</div>
241
+ {formattedError.details && (
242
+ <button
243
+ onClick={() => setShowErrorDetails(!showErrorDetails)}
244
+ className="mt-1 text-red-500 dark:text-red-300 hover:text-red-700 dark:hover:text-red-200 underline"
245
+ >
246
+ {showErrorDetails ? 'Hide details' : 'Show details'}
247
+ </button>
248
+ )}
249
+ {showErrorDetails && formattedError.details && (
250
+ <pre className="mt-2 p-2 bg-red-500/10 rounded text-[10px] whitespace-pre-wrap break-all max-h-40 overflow-auto">
251
+ {formattedError.details}
252
+ </pre>
253
+ )}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ )}
258
+
259
+ <div className="flex-1 min-h-0">
260
+ <YamlEditor
261
+ value={editedYaml}
262
+ onChange={setEditedYaml}
263
+ onValidate={handleYamlValidate}
264
+ height="100%"
265
+ kind={resource.kind}
266
+ />
267
+ </div>
268
+ </div>
269
+ )
270
+ }
271
+
272
+ // Read-only mode
273
+ return (
274
+ <div className="p-4">
275
+ <div className="flex items-center justify-between mb-2">
276
+ <span className="text-sm font-medium text-theme-text-secondary">YAML</span>
277
+ <div className="flex items-center gap-2">
278
+ <button
279
+ onClick={handleStartEdit}
280
+ className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:text-blue-300 hover:bg-theme-elevated rounded"
281
+ >
282
+ <Pencil className="w-3.5 h-3.5" />
283
+ Edit
284
+ </button>
285
+ <button
286
+ onClick={() => onCopy(yamlContent)}
287
+ 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"
288
+ >
289
+ {copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
290
+ Copy
291
+ </button>
292
+ </div>
293
+ </div>
294
+ <CodeViewer
295
+ code={yamlContent}
296
+ language="yaml"
297
+ showLineNumbers
298
+ maxHeight="calc(100vh - 250px)"
299
+ />
300
+ </div>
301
+ )
302
+ }