@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,476 @@
1
+ import { useState } from 'react'
2
+ import { Info, CheckCircle, XCircle, AlertCircle, ChevronRight, ChevronDown, FileCode, AlertTriangle, Layers } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
+
6
+ interface GenericRendererProps {
7
+ data: any
8
+ }
9
+
10
+ export function GenericRenderer({ data }: GenericRendererProps) {
11
+ const spec = data.spec || {}
12
+ const status = data.status || {}
13
+ const conditions = status.conditions
14
+
15
+ // Determine health status from conditions or status fields
16
+ const healthStatus = getHealthStatus(status, conditions)
17
+
18
+ // Extract important fields from spec and status
19
+ const statusFields = getImportantFields(status, 'status')
20
+ const specFields = getImportantFields(spec, 'spec')
21
+
22
+ // Get nested objects for separate display
23
+ const nestedSpecObjects = getNestedObjects(spec)
24
+ const nestedStatusObjects = getNestedObjects(status)
25
+
26
+ // Check if resource has meaningful content
27
+ const hasStatusContent = Object.keys(statusFields).length > 0 || (conditions && conditions.length > 0)
28
+ const hasSpecContent = Object.keys(specFields).length > 0 || nestedSpecObjects.length > 0
29
+
30
+ return (
31
+ <>
32
+ {/* Health Status Banner (if determinable) */}
33
+ {healthStatus && (
34
+ <div className={clsx(
35
+ 'mb-4 p-3 rounded-lg border flex items-center gap-3',
36
+ healthStatus.type === 'healthy' && 'bg-green-500/10 border-green-500/30',
37
+ healthStatus.type === 'degraded' && 'bg-yellow-500/10 border-yellow-500/30',
38
+ healthStatus.type === 'unhealthy' && 'bg-red-500/10 border-red-500/30',
39
+ healthStatus.type === 'unknown' && 'bg-slate-500/10 border-slate-500/30'
40
+ )}>
41
+ {healthStatus.type === 'healthy' && <CheckCircle className="w-5 h-5 text-green-400 shrink-0" />}
42
+ {healthStatus.type === 'degraded' && <AlertCircle className="w-5 h-5 text-yellow-400 shrink-0" />}
43
+ {healthStatus.type === 'unhealthy' && <XCircle className="w-5 h-5 text-red-400 shrink-0" />}
44
+ {healthStatus.type === 'unknown' && <Info className="w-5 h-5 text-slate-400 shrink-0" />}
45
+ <div>
46
+ <div className={clsx(
47
+ 'text-sm font-medium',
48
+ healthStatus.type === 'healthy' && 'text-green-400',
49
+ healthStatus.type === 'degraded' && 'text-yellow-400',
50
+ healthStatus.type === 'unhealthy' && 'text-red-400',
51
+ healthStatus.type === 'unknown' && 'text-slate-400'
52
+ )}>
53
+ {healthStatus.label}
54
+ </div>
55
+ {healthStatus.message && (
56
+ <div className="text-xs text-theme-text-secondary mt-0.5">{healthStatus.message}</div>
57
+ )}
58
+ </div>
59
+ </div>
60
+ )}
61
+
62
+ {/* Empty state hint */}
63
+ {!hasStatusContent && !hasSpecContent && (
64
+ <div className="mb-4 p-4 bg-slate-500/10 border border-slate-500/20 rounded-lg">
65
+ <div className="flex items-start gap-3">
66
+ <FileCode className="w-5 h-5 text-slate-400 mt-0.5 shrink-0" />
67
+ <div>
68
+ <div className="text-sm font-medium text-theme-text-secondary">Custom Resource</div>
69
+ <div className="text-xs text-theme-text-tertiary mt-1">
70
+ This resource type doesn&apos;t have a dedicated view. Use the YAML view (code icon in header) to see the full resource definition.
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ )}
76
+
77
+ {/* Status Section */}
78
+ {Object.keys(statusFields).length > 0 && (
79
+ <Section title="Status" icon={Info} defaultExpanded>
80
+ <PropertyList>
81
+ {Object.entries(statusFields).map(([key, value]) => (
82
+ <Property key={key} label={formatFieldName(key)} value={formatValue(value)} />
83
+ ))}
84
+ </PropertyList>
85
+ </Section>
86
+ )}
87
+
88
+ {/* Conditions (common in CRDs) */}
89
+ {conditions && Array.isArray(conditions) && conditions.length > 0 && (
90
+ <GenericConditionsSection conditions={conditions} />
91
+ )}
92
+
93
+ {/* Nested Status Objects */}
94
+ {nestedStatusObjects.length > 0 && (
95
+ <Section title="Status Details" icon={Layers} defaultExpanded={false}>
96
+ <div className="space-y-2">
97
+ {nestedStatusObjects.map(({ key, value }) => (
98
+ <NestedObjectViewer key={key} name={formatFieldName(key)} value={value} />
99
+ ))}
100
+ </div>
101
+ </Section>
102
+ )}
103
+
104
+ {/* Spec Section */}
105
+ {Object.keys(specFields).length > 0 && (
106
+ <Section title="Specification" defaultExpanded={Object.keys(specFields).length <= 8}>
107
+ <PropertyList>
108
+ {Object.entries(specFields).map(([key, value]) => (
109
+ <Property key={key} label={formatFieldName(key)} value={formatValue(value)} />
110
+ ))}
111
+ </PropertyList>
112
+ </Section>
113
+ )}
114
+
115
+ {/* Nested Spec Objects */}
116
+ {nestedSpecObjects.length > 0 && (
117
+ <Section title="Specification Details" icon={Layers} defaultExpanded={false}>
118
+ <div className="space-y-2">
119
+ {nestedSpecObjects.map(({ key, value }) => (
120
+ <NestedObjectViewer key={key} name={formatFieldName(key)} value={value} />
121
+ ))}
122
+ </div>
123
+ </Section>
124
+ )}
125
+ </>
126
+ )
127
+ }
128
+
129
+ // ============================================================================
130
+ // HEALTH STATUS DETECTION
131
+ // ============================================================================
132
+
133
+ type HealthType = 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
134
+
135
+ interface HealthStatusResult {
136
+ type: HealthType
137
+ label: string
138
+ message?: string
139
+ }
140
+
141
+ function getHealthStatus(status: any, conditions?: any[]): HealthStatusResult | null {
142
+ if (!status) return null
143
+
144
+ // Check phase first (common pattern)
145
+ if (status.phase) {
146
+ const phase = String(status.phase)
147
+ const healthyPhases = ['Running', 'Active', 'Succeeded', 'Ready', 'Healthy', 'Available', 'Bound', 'Complete']
148
+ const degradedPhases = ['Pending', 'Progressing', 'Unknown', 'Terminating', 'Waiting']
149
+ const unhealthyPhases = ['Failed', 'Error', 'CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull']
150
+
151
+ if (healthyPhases.includes(phase)) {
152
+ return { type: 'healthy', label: phase }
153
+ }
154
+ if (degradedPhases.includes(phase)) {
155
+ return { type: 'degraded', label: phase, message: status.message || status.reason }
156
+ }
157
+ if (unhealthyPhases.includes(phase)) {
158
+ return { type: 'unhealthy', label: phase, message: status.message || status.reason }
159
+ }
160
+ }
161
+
162
+ // Check conditions
163
+ if (conditions && Array.isArray(conditions) && conditions.length > 0) {
164
+ // Look for Ready or Available condition
165
+ const readyCondition = conditions.find((c: any) =>
166
+ c.type === 'Ready' || c.type === 'Available' || c.type === 'Healthy'
167
+ )
168
+ if (readyCondition) {
169
+ if (readyCondition.status === 'True') {
170
+ return { type: 'healthy', label: 'Ready' }
171
+ }
172
+ if (readyCondition.status === 'False') {
173
+ return {
174
+ type: 'unhealthy',
175
+ label: 'Not Ready',
176
+ message: readyCondition.message || readyCondition.reason
177
+ }
178
+ }
179
+ }
180
+
181
+ // Check for any False conditions that indicate problems
182
+ const problemConditions = conditions.filter((c: any) =>
183
+ c.status === 'False' && ['Ready', 'Available', 'Healthy', 'Initialized'].includes(c.type)
184
+ )
185
+ if (problemConditions.length > 0) {
186
+ const problem = problemConditions[0]
187
+ return {
188
+ type: 'unhealthy',
189
+ label: `${problem.type}: False`,
190
+ message: problem.message || problem.reason
191
+ }
192
+ }
193
+
194
+ // Check for warning conditions
195
+ const warningConditions = conditions.filter((c: any) =>
196
+ c.status === 'True' && ['Degraded', 'Warning', 'ScalingLimited'].includes(c.type)
197
+ )
198
+ if (warningConditions.length > 0) {
199
+ const warning = warningConditions[0]
200
+ return {
201
+ type: 'degraded',
202
+ label: warning.type,
203
+ message: warning.message || warning.reason
204
+ }
205
+ }
206
+ }
207
+
208
+ // Check replica-based status
209
+ if (status.replicas !== undefined) {
210
+ const desired = status.replicas
211
+ const ready = status.readyReplicas || status.availableReplicas || 0
212
+ if (desired > 0 && ready >= desired) {
213
+ return { type: 'healthy', label: `${ready}/${desired} Ready` }
214
+ }
215
+ if (desired > 0 && ready > 0) {
216
+ return { type: 'degraded', label: `${ready}/${desired} Ready` }
217
+ }
218
+ if (desired > 0 && ready === 0) {
219
+ return { type: 'unhealthy', label: `0/${desired} Ready` }
220
+ }
221
+ }
222
+
223
+ return null
224
+ }
225
+
226
+ // ============================================================================
227
+ // FIELD EXTRACTION
228
+ // ============================================================================
229
+
230
+ function getImportantFields(obj: any, context: string): Record<string, any> {
231
+ const result: Record<string, any> = {}
232
+ if (!obj || typeof obj !== 'object') return result
233
+
234
+ // Priority fields to show first
235
+ const priorityFields = context === 'status'
236
+ ? ['phase', 'state', 'ready', 'available', 'replicas', 'currentReplicas', 'readyReplicas',
237
+ 'availableReplicas', 'updatedReplicas', 'observedGeneration', 'message', 'reason']
238
+ : ['replicas', 'selector', 'schedule', 'suspend', 'concurrencyPolicy', 'parallelism',
239
+ 'completions', 'backoffLimit', 'image', 'command', 'args', 'ports', 'resources']
240
+
241
+ // First add priority fields
242
+ for (const field of priorityFields) {
243
+ if (obj[field] !== undefined && !isComplexObject(obj[field])) {
244
+ result[field] = obj[field]
245
+ }
246
+ }
247
+
248
+ // Then add other simple fields
249
+ for (const [key, value] of Object.entries(obj)) {
250
+ if (result[key] !== undefined) continue
251
+ if (key === 'conditions') continue
252
+ if (key === 'managedFields') continue
253
+ if (isComplexObject(value)) continue
254
+
255
+ result[key] = value
256
+ }
257
+
258
+ return result
259
+ }
260
+
261
+ function getNestedObjects(obj: any): Array<{ key: string; value: any }> {
262
+ const result: Array<{ key: string; value: any }> = []
263
+ if (!obj || typeof obj !== 'object') return result
264
+
265
+ for (const [key, value] of Object.entries(obj)) {
266
+ if (key === 'conditions') continue
267
+ if (key === 'managedFields') continue
268
+ if (isComplexObject(value)) {
269
+ result.push({ key, value })
270
+ }
271
+ }
272
+
273
+ return result
274
+ }
275
+
276
+ function isComplexObject(value: any): boolean {
277
+ if (value === null || value === undefined) return false
278
+ if (typeof value !== 'object') return false
279
+ if (Array.isArray(value)) {
280
+ return value.some(item => typeof item === 'object' && item !== null)
281
+ }
282
+ return true
283
+ }
284
+
285
+ function formatFieldName(name: string): string {
286
+ return name
287
+ .replace(/([A-Z])/g, ' $1')
288
+ .replace(/^./, str => str.toUpperCase())
289
+ .trim()
290
+ }
291
+
292
+ function formatValue(value: any): string {
293
+ if (value === null || value === undefined) return '-'
294
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No'
295
+ if (Array.isArray(value)) return value.join(', ')
296
+ if (typeof value === 'object') return JSON.stringify(value)
297
+ return String(value)
298
+ }
299
+
300
+ // ============================================================================
301
+ // NESTED OBJECT VIEWER
302
+ // ============================================================================
303
+
304
+ interface NestedObjectViewerProps {
305
+ name: string
306
+ value: any
307
+ depth?: number
308
+ }
309
+
310
+ function NestedObjectViewer({ name, value, depth = 0 }: NestedObjectViewerProps) {
311
+ const [expanded, setExpanded] = useState(depth < 1)
312
+
313
+ if (Array.isArray(value)) {
314
+ return (
315
+ <div className="text-sm">
316
+ <button
317
+ onClick={() => setExpanded(!expanded)}
318
+ className="flex items-center gap-1 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
319
+ >
320
+ {expanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
321
+ <span className="font-medium">{name}</span>
322
+ <span className="text-xs text-theme-text-tertiary">({value.length} items)</span>
323
+ </button>
324
+ {expanded && (
325
+ <div className="ml-5 mt-1 space-y-1">
326
+ {value.map((item, i) => {
327
+ if (typeof item === 'object' && item !== null) {
328
+ return <NestedObjectViewer key={i} name={`[${i}]`} value={item} depth={depth + 1} />
329
+ }
330
+ return (
331
+ <div key={i} className="text-xs text-theme-text-secondary">
332
+ [{i}]: {String(item)}
333
+ </div>
334
+ )
335
+ })}
336
+ </div>
337
+ )}
338
+ </div>
339
+ )
340
+ }
341
+
342
+ if (typeof value === 'object' && value !== null) {
343
+ const entries = Object.entries(value)
344
+ const simpleEntries = entries.filter(([, v]) => !isComplexObject(v))
345
+ const complexEntries = entries.filter(([, v]) => isComplexObject(v))
346
+
347
+ return (
348
+ <div className="text-sm">
349
+ <button
350
+ onClick={() => setExpanded(!expanded)}
351
+ className="flex items-center gap-1 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
352
+ >
353
+ {expanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
354
+ <span className="font-medium">{name}</span>
355
+ <span className="text-xs text-theme-text-tertiary">({entries.length} fields)</span>
356
+ </button>
357
+ {expanded && (
358
+ <div className="ml-5 mt-1 space-y-1">
359
+ {simpleEntries.map(([k, v]) => (
360
+ <div key={k} className="flex items-start gap-2 text-xs">
361
+ <span className="text-theme-text-tertiary w-28 shrink-0">{formatFieldName(k)}</span>
362
+ <span className="text-theme-text-secondary break-all">{formatValue(v)}</span>
363
+ </div>
364
+ ))}
365
+ {complexEntries.map(([k, v]) => (
366
+ <NestedObjectViewer key={k} name={formatFieldName(k)} value={v} depth={depth + 1} />
367
+ ))}
368
+ </div>
369
+ )}
370
+ </div>
371
+ )
372
+ }
373
+
374
+ return (
375
+ <div className="text-xs text-theme-text-secondary">
376
+ {name}: {String(value)}
377
+ </div>
378
+ )
379
+ }
380
+
381
+ // ============================================================================
382
+ // CONDITIONS SECTION
383
+ // ============================================================================
384
+
385
+ function GenericConditionsSection({ conditions }: { conditions: any[] }) {
386
+ // Sort conditions: problematic first, then alphabetically
387
+ const sortedConditions = [...conditions].sort((a, b) => {
388
+ const aProblematic = a.status === 'False' || (a.status === 'True' && ['Degraded', 'Warning'].includes(a.type))
389
+ const bProblematic = b.status === 'False' || (b.status === 'True' && ['Degraded', 'Warning'].includes(b.type))
390
+ if (aProblematic && !bProblematic) return -1
391
+ if (!aProblematic && bProblematic) return 1
392
+ return (a.type || '').localeCompare(b.type || '')
393
+ })
394
+
395
+ // Check for any problems
396
+ const hasProblems = sortedConditions.some(c =>
397
+ c.status === 'False' || (c.status === 'True' && ['Degraded', 'Warning'].includes(c.type))
398
+ )
399
+
400
+ return (
401
+ <Section title={`Conditions (${conditions.length})`} defaultExpanded={hasProblems || conditions.length <= 5}>
402
+ <div className="space-y-2">
403
+ {sortedConditions.map((cond: any, index: number) => {
404
+ const isTrue = cond.status === 'True'
405
+ const isFalse = cond.status === 'False'
406
+ const isWarningType = ['Degraded', 'Warning', 'ScalingLimited'].includes(cond.type)
407
+ const isPositiveType = ['Ready', 'Available', 'Healthy', 'Initialized', 'Complete'].includes(cond.type)
408
+
409
+ // Determine icon color based on condition semantics
410
+ let iconColor = 'text-yellow-400'
411
+ if (isTrue && isPositiveType) iconColor = 'text-green-400'
412
+ else if (isTrue && isWarningType) iconColor = 'text-yellow-400'
413
+ else if (isTrue) iconColor = 'text-green-400'
414
+ else if (isFalse && isPositiveType) iconColor = 'text-red-400'
415
+ else if (isFalse) iconColor = 'text-slate-400'
416
+
417
+ return (
418
+ <div key={`${cond.type}-${index}`} className="flex items-start gap-2 text-sm">
419
+ <span className={clsx('w-5 h-5 rounded-full flex items-center justify-center shrink-0 mt-0.5', iconColor)}>
420
+ {isTrue && isPositiveType ? <CheckCircle className="w-4 h-4" /> :
421
+ isTrue && isWarningType ? <AlertTriangle className="w-4 h-4" /> :
422
+ isTrue ? <CheckCircle className="w-4 h-4" /> :
423
+ isFalse && isPositiveType ? <XCircle className="w-4 h-4" /> :
424
+ isFalse ? <AlertCircle className="w-4 h-4" /> :
425
+ <AlertCircle className="w-4 h-4" />}
426
+ </span>
427
+ <div className="flex-1 min-w-0">
428
+ <div className="flex items-center gap-2 flex-wrap">
429
+ <span className="text-theme-text-primary font-medium">{cond.type}</span>
430
+ <span className={clsx(
431
+ 'px-1.5 py-0.5 rounded text-xs font-medium',
432
+ isTrue ? 'bg-theme-elevated text-theme-text-secondary' : 'bg-red-500/20 text-red-400'
433
+ )}>
434
+ {cond.status}
435
+ </span>
436
+ {cond.reason && (
437
+ <span className="text-xs text-theme-text-tertiary">({cond.reason})</span>
438
+ )}
439
+ </div>
440
+ {cond.message && (
441
+ <div className="text-xs text-theme-text-secondary mt-0.5 break-words">
442
+ {cond.message}
443
+ </div>
444
+ )}
445
+ {cond.lastTransitionTime && (
446
+ <div className="text-xs text-theme-text-tertiary mt-0.5">
447
+ Last transition: {formatTimestamp(cond.lastTransitionTime)}
448
+ </div>
449
+ )}
450
+ </div>
451
+ </div>
452
+ )
453
+ })}
454
+ </div>
455
+ </Section>
456
+ )
457
+ }
458
+
459
+ function formatTimestamp(timestamp: string): string {
460
+ try {
461
+ const date = new Date(timestamp)
462
+ const now = new Date()
463
+ const diffMs = now.getTime() - date.getTime()
464
+ const diffMins = Math.floor(diffMs / 60000)
465
+ const diffHours = Math.floor(diffMins / 60)
466
+ const diffDays = Math.floor(diffHours / 24)
467
+
468
+ if (diffMins < 1) return 'just now'
469
+ if (diffMins < 60) return `${diffMins}m ago`
470
+ if (diffHours < 24) return `${diffHours}h ago`
471
+ if (diffDays < 7) return `${diffDays}d ago`
472
+ return date.toLocaleDateString()
473
+ } catch {
474
+ return timestamp
475
+ }
476
+ }
@@ -0,0 +1,154 @@
1
+ import { GitBranch, AlertTriangle, FolderGit, CheckCircle2 } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
4
+ import { formatAge } from '../resource-utils'
5
+ import { formatBytes } from '../../../utils/format'
6
+ import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
7
+ import { fluxConditionsToGitOpsStatus, type FluxCondition } from '../../../types/gitops'
8
+
9
+ interface GitRepositoryRendererProps {
10
+ data: any
11
+ }
12
+
13
+ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
14
+ const status = data.status || {}
15
+ const spec = data.spec || {}
16
+ const conditions = (status.conditions || []) as FluxCondition[]
17
+ const artifact = status.artifact || {}
18
+
19
+ // Convert to unified GitOps status
20
+ const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true)
21
+
22
+ // Problem detection
23
+ const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
24
+
25
+ if (gitOpsStatus.suspended) {
26
+ problems.push({ color: 'yellow', message: 'GitRepository is suspended' })
27
+ }
28
+
29
+ if (gitOpsStatus.health === 'Degraded' && gitOpsStatus.message) {
30
+ problems.push({ color: 'red', message: gitOpsStatus.message })
31
+ }
32
+
33
+ // Extract repository info
34
+ const url = spec.url || ''
35
+ const ref = spec.ref || {}
36
+ const branch = ref.branch || ref.tag || ref.semver || ref.commit || 'default'
37
+
38
+ return (
39
+ <>
40
+ {/* Problem alerts */}
41
+ {problems.map((problem, i) => (
42
+ <div
43
+ key={i}
44
+ className={clsx(
45
+ 'mb-4 p-3 border rounded-lg',
46
+ problem.color === 'red'
47
+ ? 'bg-red-500/10 border-red-500/30'
48
+ : 'bg-yellow-500/10 border-yellow-500/30'
49
+ )}
50
+ >
51
+ <div className="flex items-start gap-2">
52
+ <AlertTriangle
53
+ className={clsx(
54
+ 'w-4 h-4 mt-0.5 shrink-0',
55
+ problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
56
+ )}
57
+ />
58
+ <div className="flex-1 min-w-0">
59
+ <div
60
+ className={clsx(
61
+ 'text-sm font-medium',
62
+ problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
63
+ )}
64
+ >
65
+ {problem.color === 'red' ? 'Issue Detected' : 'Warning'}
66
+ </div>
67
+ <div
68
+ className={clsx(
69
+ 'text-xs mt-1',
70
+ problem.color === 'red' ? 'text-red-300/80' : 'text-yellow-300/80'
71
+ )}
72
+ >
73
+ {problem.message}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ ))}
79
+
80
+ {/* Status section */}
81
+ <Section title="Status">
82
+ <div className="space-y-3">
83
+ <GitOpsStatusBadge status={gitOpsStatus} showHealth={false} />
84
+ {spec.interval && (
85
+ <SyncCountdown
86
+ interval={spec.interval}
87
+ lastSyncTime={status.lastHandledReconcileAt}
88
+ suspended={gitOpsStatus.suspended}
89
+ />
90
+ )}
91
+ </div>
92
+ </Section>
93
+
94
+ {/* Source section */}
95
+ <Section title="Source" icon={FolderGit}>
96
+ <PropertyList>
97
+ <Property label="URL" value={url} />
98
+ <Property
99
+ label="Reference"
100
+ value={
101
+ <span className="flex items-center gap-1">
102
+ <GitBranch className="w-3.5 h-3.5" />
103
+ {branch}
104
+ </span>
105
+ }
106
+ />
107
+ {ref.branch && <Property label="Branch" value={ref.branch} />}
108
+ {ref.tag && <Property label="Tag" value={ref.tag} />}
109
+ {ref.semver && <Property label="Semver" value={ref.semver} />}
110
+ {ref.commit && <Property label="Commit" value={ref.commit} />}
111
+ {spec.secretRef?.name && (
112
+ <Property label="Secret" value={spec.secretRef.name} />
113
+ )}
114
+ {spec.ignore && <Property label="Ignore" value={spec.ignore} />}
115
+ </PropertyList>
116
+ </Section>
117
+
118
+ {/* Artifact section (last fetched source) */}
119
+ {artifact.revision && (
120
+ <Section title="Latest Artifact" icon={CheckCircle2}>
121
+ <PropertyList>
122
+ <Property label="Revision" value={artifact.revision} />
123
+ <Property label="Digest" value={artifact.digest} />
124
+ <Property
125
+ label="Last Updated"
126
+ value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
127
+ />
128
+ {artifact.size && (
129
+ <Property label="Size" value={formatBytes(artifact.size)} />
130
+ )}
131
+ </PropertyList>
132
+ </Section>
133
+ )}
134
+
135
+ {/* Additional Status Info */}
136
+ {status.observedGeneration !== undefined && (
137
+ <Section title="Additional Info" defaultExpanded={false}>
138
+ <PropertyList>
139
+ <Property label="Observed Generation" value={status.observedGeneration} />
140
+ {status.lastHandledReconcileAt && (
141
+ <Property
142
+ label="Last Reconciled"
143
+ value={formatAge(status.lastHandledReconcileAt)}
144
+ />
145
+ )}
146
+ </PropertyList>
147
+ </Section>
148
+ )}
149
+
150
+ {/* Conditions section */}
151
+ <ConditionsSection conditions={conditions} />
152
+ </>
153
+ )
154
+ }