@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,165 @@
1
+ import { ReactNode, useState, useRef, useEffect, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { clsx } from 'clsx'
4
+
5
+ interface TooltipProps {
6
+ content: ReactNode
7
+ children: ReactNode
8
+ /** Delay before showing tooltip in ms (default: 300) */
9
+ delay?: number
10
+ /** Position of tooltip (default: 'top') */
11
+ position?: 'top' | 'bottom' | 'left' | 'right'
12
+ /** Additional class for the tooltip content */
13
+ className?: string
14
+ /** Whether tooltip is disabled */
15
+ disabled?: boolean
16
+ /** Additional class for the wrapper span (useful for positioning) */
17
+ wrapperClassName?: string
18
+ /** Inline styles for the wrapper span (useful for absolute positioning) */
19
+ wrapperStyle?: React.CSSProperties
20
+ }
21
+
22
+ export function Tooltip({
23
+ content,
24
+ children,
25
+ delay = 300,
26
+ position = 'top',
27
+ className,
28
+ disabled = false,
29
+ wrapperClassName,
30
+ wrapperStyle,
31
+ }: TooltipProps) {
32
+ const [isVisible, setIsVisible] = useState(false)
33
+ const [coords, setCoords] = useState({ top: 0, left: 0 })
34
+ const triggerRef = useRef<HTMLSpanElement>(null)
35
+ const tooltipRef = useRef<HTMLSpanElement>(null)
36
+ const timeoutRef = useRef<number | null>(null)
37
+
38
+ const updatePosition = useCallback(() => {
39
+ if (!triggerRef.current) return
40
+
41
+ const rect = triggerRef.current.getBoundingClientRect()
42
+ const tooltipRect = tooltipRef.current?.getBoundingClientRect()
43
+ const tooltipWidth = tooltipRect?.width || 0
44
+ const tooltipHeight = tooltipRect?.height || 0
45
+
46
+ let top = 0
47
+ let left = 0
48
+
49
+ switch (position) {
50
+ case 'top':
51
+ top = rect.top - tooltipHeight - 6
52
+ left = rect.left + rect.width / 2 - tooltipWidth / 2
53
+ break
54
+ case 'bottom':
55
+ top = rect.bottom + 6
56
+ left = rect.left + rect.width / 2 - tooltipWidth / 2
57
+ break
58
+ case 'left':
59
+ top = rect.top + rect.height / 2 - tooltipHeight / 2
60
+ left = rect.left - tooltipWidth - 6
61
+ break
62
+ case 'right':
63
+ top = rect.top + rect.height / 2 - tooltipHeight / 2
64
+ left = rect.right + 6
65
+ break
66
+ }
67
+
68
+ // Keep tooltip within viewport
69
+ const padding = 8
70
+ if (left < padding) left = padding
71
+ if (left + tooltipWidth > window.innerWidth - padding) {
72
+ left = window.innerWidth - tooltipWidth - padding
73
+ }
74
+ if (top < padding) top = rect.bottom + 6 // flip to bottom
75
+ if (top + tooltipHeight > window.innerHeight - padding) {
76
+ top = rect.top - tooltipHeight - 6 // flip to top
77
+ }
78
+
79
+ setCoords({ top, left })
80
+ }, [position])
81
+
82
+ const showTooltip = () => {
83
+ if (disabled || !content) return
84
+ timeoutRef.current = window.setTimeout(() => {
85
+ setIsVisible(true)
86
+ }, delay)
87
+ }
88
+
89
+ const hideTooltip = () => {
90
+ if (timeoutRef.current) {
91
+ clearTimeout(timeoutRef.current)
92
+ timeoutRef.current = null
93
+ }
94
+ setIsVisible(false)
95
+ }
96
+
97
+ useEffect(() => {
98
+ if (isVisible) {
99
+ // Small delay to let the tooltip render before measuring
100
+ requestAnimationFrame(updatePosition)
101
+ }
102
+ }, [isVisible, updatePosition])
103
+
104
+ useEffect(() => {
105
+ return () => {
106
+ if (timeoutRef.current) {
107
+ clearTimeout(timeoutRef.current)
108
+ }
109
+ }
110
+ }, [])
111
+
112
+ if (disabled || !content) {
113
+ return <>{children}</>
114
+ }
115
+
116
+ return (
117
+ <>
118
+ <span
119
+ ref={triggerRef}
120
+ className={clsx('inline-flex max-w-full', wrapperClassName)}
121
+ style={wrapperStyle}
122
+ onMouseEnter={showTooltip}
123
+ onMouseLeave={hideTooltip}
124
+ onFocus={showTooltip}
125
+ onBlur={hideTooltip}
126
+ >
127
+ {children}
128
+ </span>
129
+ {isVisible &&
130
+ createPortal(
131
+ <span
132
+ ref={tooltipRef}
133
+ className={clsx(
134
+ 'fixed z-[9999] px-2 py-1 text-xs text-theme-text-primary bg-theme-base rounded shadow-lg border border-theme-border',
135
+ 'whitespace-nowrap pointer-events-none',
136
+ className
137
+ )}
138
+ style={{ top: coords.top, left: coords.left }}
139
+ role="tooltip"
140
+ >
141
+ {content}
142
+ </span>,
143
+ document.body
144
+ )}
145
+ </>
146
+ )
147
+ }
148
+
149
+ /** Simple wrapper that adds tooltip to any element - use for quick migrations from title="" */
150
+ export function WithTooltip({
151
+ tip,
152
+ children,
153
+ delay = 300,
154
+ }: {
155
+ tip: string | undefined | null
156
+ children: ReactNode
157
+ delay?: number
158
+ }) {
159
+ if (!tip) return <>{children}</>
160
+ return (
161
+ <Tooltip content={tip} delay={delay}>
162
+ {children}
163
+ </Tooltip>
164
+ )
165
+ }
@@ -0,0 +1,280 @@
1
+ import { useRef, useCallback, useEffect } from 'react'
2
+ import Editor, { OnMount, OnChange, type Monaco } from '@monaco-editor/react'
3
+ import type { editor } from 'monaco-editor'
4
+
5
+ interface YamlEditorProps {
6
+ value: string
7
+ onChange?: (value: string) => void
8
+ readOnly?: boolean
9
+ height?: string | number
10
+ onValidate?: (isValid: boolean, errors: string[]) => void
11
+ /** Resource kind - used to highlight editable fields for restricted resources like Pods */
12
+ kind?: string
13
+ }
14
+
15
+ // Find line numbers for editable fields in a Pod YAML
16
+ function findPodEditableLines(yaml: string): number[] {
17
+ const lines = yaml.split('\n')
18
+ const editableLines: number[] = []
19
+
20
+ let inContainers = false
21
+ let inInitContainers = false
22
+ let inTolerations = false
23
+ let containerIndent = 0
24
+
25
+ for (let i = 0; i < lines.length; i++) {
26
+ const line = lines[i]
27
+ const trimmed = line.trimStart()
28
+ const indent = line.length - trimmed.length
29
+
30
+ // Track when we enter/exit containers section
31
+ if (trimmed.startsWith('containers:')) {
32
+ inContainers = true
33
+ containerIndent = indent
34
+ continue
35
+ }
36
+ if (trimmed.startsWith('initContainers:')) {
37
+ inInitContainers = true
38
+ containerIndent = indent
39
+ continue
40
+ }
41
+ if (trimmed.startsWith('tolerations:')) {
42
+ inTolerations = true
43
+ editableLines.push(i + 1) // 1-indexed
44
+ containerIndent = indent
45
+ continue
46
+ }
47
+
48
+ // Exit section when we hit same or lower indent level with a new key
49
+ if ((inContainers || inInitContainers || inTolerations) &&
50
+ indent <= containerIndent &&
51
+ trimmed.length > 0 &&
52
+ !trimmed.startsWith('-') &&
53
+ !trimmed.startsWith('#')) {
54
+ inContainers = false
55
+ inInitContainers = false
56
+ inTolerations = false
57
+ }
58
+
59
+ // Mark image lines as editable
60
+ if ((inContainers || inInitContainers) && trimmed.startsWith('image:')) {
61
+ editableLines.push(i + 1) // 1-indexed
62
+ }
63
+
64
+ // Mark toleration lines as editable (whole section)
65
+ if (inTolerations && trimmed.length > 0) {
66
+ editableLines.push(i + 1)
67
+ }
68
+
69
+ // activeDeadlineSeconds is editable
70
+ if (trimmed.startsWith('activeDeadlineSeconds:')) {
71
+ editableLines.push(i + 1)
72
+ }
73
+
74
+ // terminationGracePeriodSeconds is editable (with restrictions)
75
+ if (trimmed.startsWith('terminationGracePeriodSeconds:')) {
76
+ editableLines.push(i + 1)
77
+ }
78
+ }
79
+
80
+ return editableLines
81
+ }
82
+
83
+ export function YamlEditor({
84
+ value,
85
+ onChange,
86
+ readOnly = false,
87
+ height = '100%',
88
+ onValidate,
89
+ kind,
90
+ }: YamlEditorProps) {
91
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
92
+ const decorationsRef = useRef<string[]>([])
93
+ const monacoRef = useRef<Monaco | null>(null)
94
+
95
+ // Apply decorations for editable fields
96
+ const applyDecorations = useCallback(() => {
97
+ const editor = editorRef.current
98
+ const monaco = monacoRef.current
99
+ if (!editor || !monaco || !kind) return
100
+
101
+ const isPod = kind.toLowerCase() === 'pods' || kind.toLowerCase() === 'pod'
102
+ if (!isPod) {
103
+ // Clear decorations for non-pods
104
+ if (decorationsRef.current.length > 0) {
105
+ decorationsRef.current = editor.deltaDecorations(decorationsRef.current, [])
106
+ }
107
+ return
108
+ }
109
+
110
+ const editableLines = findPodEditableLines(value)
111
+
112
+ const decorations: editor.IModelDeltaDecoration[] = editableLines.map(lineNumber => ({
113
+ range: {
114
+ startLineNumber: lineNumber,
115
+ startColumn: 1,
116
+ endLineNumber: lineNumber,
117
+ endColumn: 1,
118
+ },
119
+ options: {
120
+ isWholeLine: true,
121
+ className: 'editable-line-highlight',
122
+ glyphMarginClassName: 'editable-line-glyph',
123
+ overviewRuler: {
124
+ color: 'rgba(34, 197, 94, 0.5)',
125
+ position: monaco.editor.OverviewRulerLane.Left,
126
+ },
127
+ },
128
+ }))
129
+
130
+ decorationsRef.current = editor.deltaDecorations(decorationsRef.current, decorations)
131
+ }, [value, kind])
132
+
133
+ // Re-apply decorations when value changes
134
+ useEffect(() => {
135
+ applyDecorations()
136
+ }, [applyDecorations])
137
+
138
+ // Expose editor globally for desktop clipboard interception (see main.tsx).
139
+ useEffect(() => {
140
+ return () => { delete (window as any).__radarMonacoEditor }
141
+ }, [])
142
+
143
+ const handleEditorMount: OnMount = useCallback((editor, monaco) => {
144
+ editorRef.current = editor
145
+ monacoRef.current = monaco
146
+ ;(window as any).__radarMonacoEditor = editor
147
+
148
+ // Add CSS for editable line highlighting
149
+ const styleId = 'yaml-editor-styles'
150
+ if (!document.getElementById(styleId)) {
151
+ const style = document.createElement('style')
152
+ style.id = styleId
153
+ style.textContent = `
154
+ .editable-line-highlight {
155
+ background-color: rgba(34, 197, 94, 0.1) !important;
156
+ border-left: 3px solid rgba(34, 197, 94, 0.6) !important;
157
+ }
158
+ .editable-line-glyph {
159
+ background-color: rgba(34, 197, 94, 0.6);
160
+ width: 4px !important;
161
+ margin-left: 3px;
162
+ border-radius: 2px;
163
+ }
164
+ `
165
+ document.head.appendChild(style)
166
+ }
167
+
168
+ // Configure YAML diagnostics (yaml property added by monaco-yaml plugin when available)
169
+ ;(monaco.languages as any).yaml?.yamlDefaults?.setDiagnosticsOptions({
170
+ enableSchemaRequest: false,
171
+ validate: true,
172
+ format: true,
173
+ })
174
+
175
+ // Set editor options
176
+ editor.updateOptions({
177
+ minimap: { enabled: false },
178
+ lineNumbers: 'on',
179
+ scrollBeyondLastLine: false,
180
+ wordWrap: 'on',
181
+ wrappingStrategy: 'advanced',
182
+ folding: true,
183
+ foldingStrategy: 'indentation',
184
+ renderLineHighlight: 'line',
185
+ selectOnLineNumbers: true,
186
+ roundedSelection: true,
187
+ cursorStyle: 'line',
188
+ automaticLayout: true,
189
+ tabSize: 2,
190
+ insertSpaces: true,
191
+ fontSize: 13,
192
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
193
+ padding: { top: 12, bottom: 12 },
194
+ glyphMargin: true,
195
+ })
196
+
197
+ // Listen for validation markers
198
+ if (onValidate) {
199
+ monaco.editor.onDidChangeMarkers((uris: readonly { toString(): string }[]) => {
200
+ const uri = uris[0]
201
+ if (uri && uri.toString() === editor.getModel()?.uri.toString()) {
202
+ const markers = monaco.editor.getModelMarkers({ resource: uri as Parameters<typeof monaco.editor.getModelMarkers>[0]['resource'] })
203
+ const errors = markers
204
+ .filter((m: { severity: number }) => m.severity === monaco.MarkerSeverity.Error)
205
+ .map((m: { startLineNumber: number; message: string }) => `Line ${m.startLineNumber}: ${m.message}`)
206
+ onValidate(errors.length === 0, errors)
207
+ }
208
+ })
209
+ }
210
+
211
+ // Apply initial decorations
212
+ setTimeout(applyDecorations, 100)
213
+ }, [onValidate, applyDecorations])
214
+
215
+ const handleChange: OnChange = useCallback((newValue) => {
216
+ if (onChange && newValue !== undefined) {
217
+ onChange(newValue)
218
+ }
219
+ }, [onChange])
220
+
221
+ return (
222
+ <div className="rounded-lg overflow-hidden border border-theme-border" style={{ height }}>
223
+ <Editor
224
+ defaultLanguage="yaml"
225
+ value={value}
226
+ onChange={handleChange}
227
+ onMount={handleEditorMount}
228
+ theme="vs-dark"
229
+ options={{
230
+ readOnly,
231
+ domReadOnly: readOnly,
232
+ }}
233
+ loading={
234
+ <div className="flex items-center justify-center h-full bg-theme-surface text-theme-text-secondary">
235
+ Loading editor...
236
+ </div>
237
+ }
238
+ />
239
+ </div>
240
+ )
241
+ }
242
+
243
+ // Diff editor for showing changes before apply
244
+ interface YamlDiffEditorProps {
245
+ original: string
246
+ modified: string
247
+ height?: string | number
248
+ }
249
+
250
+ export function YamlDiffEditor({
251
+ original: _original,
252
+ modified,
253
+ height = '100%',
254
+ }: YamlDiffEditorProps) {
255
+ // TODO: Use Monaco DiffEditor to show actual diff
256
+ void _original
257
+ return (
258
+ <div className="rounded-lg overflow-hidden border border-theme-border" style={{ height }}>
259
+ <Editor
260
+ defaultLanguage="yaml"
261
+ value={modified}
262
+ theme="vs-dark"
263
+ options={{
264
+ readOnly: true,
265
+ minimap: { enabled: false },
266
+ lineNumbers: 'on',
267
+ scrollBeyondLastLine: false,
268
+ fontSize: 13,
269
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
270
+ padding: { top: 12, bottom: 12 },
271
+ }}
272
+ loading={
273
+ <div className="flex items-center justify-center h-full bg-theme-surface text-theme-text-secondary">
274
+ Loading diff...
275
+ </div>
276
+ }
277
+ />
278
+ </div>
279
+ )
280
+ }