@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,221 @@
1
+ import { clsx } from 'clsx'
2
+ import { CheckCircle2, AlertCircle, Loader2, Pause, HelpCircle, XCircle } from 'lucide-react'
3
+ import type { GitOpsStatus, SyncStatus, GitOpsHealthStatus } from '../../types/gitops'
4
+
5
+ interface GitOpsStatusBadgeProps {
6
+ status: GitOpsStatus
7
+ showHealth?: boolean
8
+ compact?: boolean
9
+ }
10
+
11
+ /**
12
+ * Unified status badge for GitOps resources (FluxCD and ArgoCD)
13
+ * Shows sync status with optional health indicator
14
+ */
15
+ export function GitOpsStatusBadge({ status, showHealth = true, compact = false }: GitOpsStatusBadgeProps) {
16
+ const Icon = getStatusIcon(status)
17
+ const colorClass = getStatusColorClass(status)
18
+ const label = getStatusLabel(status)
19
+
20
+ if (compact) {
21
+ return (
22
+ <span
23
+ className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', colorClass)}
24
+ title={status.message}
25
+ >
26
+ <Icon className="w-3 h-3" />
27
+ {label}
28
+ </span>
29
+ )
30
+ }
31
+
32
+ // Don't show health indicator if the main badge already shows health status
33
+ // (i.e., when the main label is 'Degraded', 'Progressing', or 'Suspended')
34
+ const mainLabelShowsHealth = label === 'Degraded' || label === 'Progressing' || label === 'Suspended'
35
+
36
+ return (
37
+ <div className="flex items-center gap-2">
38
+ <span
39
+ className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', colorClass)}
40
+ title={status.message}
41
+ >
42
+ <Icon className="w-3.5 h-3.5" />
43
+ {label}
44
+ </span>
45
+ {showHealth && !mainLabelShowsHealth && status.health !== 'Unknown' && status.health !== getHealthFromSync(status.sync) && (
46
+ <HealthIndicator health={status.health} />
47
+ )}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ function getHealthFromSync(sync: SyncStatus): GitOpsHealthStatus {
53
+ if (sync === 'Synced') return 'Healthy'
54
+ if (sync === 'Reconciling') return 'Progressing'
55
+ return 'Unknown'
56
+ }
57
+
58
+ function getStatusIcon(status: GitOpsStatus) {
59
+ if (status.suspended) return Pause
60
+ if (status.sync === 'Reconciling') return Loader2
61
+ if (status.sync === 'Synced' && status.health === 'Healthy') return CheckCircle2
62
+ if (status.health === 'Degraded') return XCircle
63
+ if (status.sync === 'OutOfSync') return AlertCircle
64
+ if (status.health === 'Progressing') return Loader2
65
+ return HelpCircle
66
+ }
67
+
68
+ function getStatusColorClass(status: GitOpsStatus): string {
69
+ if (status.suspended) {
70
+ return 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30'
71
+ }
72
+ if (status.sync === 'Synced' && status.health === 'Healthy') {
73
+ return 'bg-green-500/20 text-green-400 border border-green-500/30'
74
+ }
75
+ if (status.health === 'Degraded') {
76
+ return 'bg-red-500/20 text-red-400 border border-red-500/30'
77
+ }
78
+ if (status.sync === 'OutOfSync') {
79
+ return 'bg-orange-500/20 text-orange-400 border border-orange-500/30'
80
+ }
81
+ if (status.sync === 'Reconciling' || status.health === 'Progressing') {
82
+ return 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
83
+ }
84
+ return 'bg-gray-500/20 text-gray-400 border border-gray-500/30'
85
+ }
86
+
87
+ function getStatusLabel(status: GitOpsStatus): string {
88
+ if (status.suspended) return 'Suspended'
89
+ if (status.sync === 'Reconciling') return 'Syncing'
90
+ if (status.sync === 'Synced' && status.health === 'Healthy') return 'Synced'
91
+ if (status.health === 'Degraded') return 'Degraded'
92
+ if (status.sync === 'OutOfSync') return 'OutOfSync'
93
+ if (status.health === 'Progressing') return 'Progressing'
94
+ return 'Unknown'
95
+ }
96
+
97
+ interface HealthIndicatorProps {
98
+ health: GitOpsHealthStatus
99
+ }
100
+
101
+ function HealthIndicator({ health }: HealthIndicatorProps) {
102
+ const { icon: Icon, color, label } = getHealthInfo(health)
103
+
104
+ return (
105
+ <span
106
+ className={clsx('px-1.5 py-0.5 rounded text-xs inline-flex items-center gap-1', color)}
107
+ title={`Health: ${label}`}
108
+ >
109
+ <Icon className="w-3 h-3" />
110
+ {label}
111
+ </span>
112
+ )
113
+ }
114
+
115
+ function getHealthInfo(health: GitOpsHealthStatus) {
116
+ switch (health) {
117
+ case 'Healthy':
118
+ return {
119
+ icon: CheckCircle2,
120
+ color: 'bg-green-500/10 text-green-400',
121
+ label: 'Healthy',
122
+ }
123
+ case 'Progressing':
124
+ return {
125
+ icon: Loader2,
126
+ color: 'bg-blue-500/10 text-blue-400',
127
+ label: 'Progressing',
128
+ }
129
+ case 'Degraded':
130
+ return {
131
+ icon: XCircle,
132
+ color: 'bg-red-500/10 text-red-400',
133
+ label: 'Degraded',
134
+ }
135
+ case 'Suspended':
136
+ return {
137
+ icon: Pause,
138
+ color: 'bg-yellow-500/10 text-yellow-400',
139
+ label: 'Suspended',
140
+ }
141
+ case 'Missing':
142
+ return {
143
+ icon: AlertCircle,
144
+ color: 'bg-orange-500/10 text-orange-400',
145
+ label: 'Missing',
146
+ }
147
+ default:
148
+ return {
149
+ icon: HelpCircle,
150
+ color: 'bg-gray-500/10 text-gray-400',
151
+ label: 'Unknown',
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Simple sync status badge without health indicator
158
+ */
159
+ export function SyncStatusBadge({ sync, suspended }: { sync: SyncStatus; suspended?: boolean }) {
160
+ if (suspended) {
161
+ return (
162
+ <span className="px-2 py-0.5 rounded text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 inline-flex items-center gap-1">
163
+ <Pause className="w-3 h-3" />
164
+ Suspended
165
+ </span>
166
+ )
167
+ }
168
+
169
+ const config = getSyncConfig(sync)
170
+ const Icon = config.icon
171
+
172
+ return (
173
+ <span className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', config.color)}>
174
+ <Icon className={clsx('w-3 h-3', sync === 'Reconciling' && 'animate-spin')} />
175
+ {config.label}
176
+ </span>
177
+ )
178
+ }
179
+
180
+ function getSyncConfig(sync: SyncStatus) {
181
+ switch (sync) {
182
+ case 'Synced':
183
+ return {
184
+ icon: CheckCircle2,
185
+ color: 'bg-green-500/20 text-green-400 border border-green-500/30',
186
+ label: 'Synced',
187
+ }
188
+ case 'OutOfSync':
189
+ return {
190
+ icon: AlertCircle,
191
+ color: 'bg-orange-500/20 text-orange-400 border border-orange-500/30',
192
+ label: 'OutOfSync',
193
+ }
194
+ case 'Reconciling':
195
+ return {
196
+ icon: Loader2,
197
+ color: 'bg-blue-500/20 text-blue-400 border border-blue-500/30',
198
+ label: 'Syncing',
199
+ }
200
+ default:
201
+ return {
202
+ icon: HelpCircle,
203
+ color: 'bg-gray-500/20 text-gray-400 border border-gray-500/30',
204
+ label: 'Unknown',
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Simple health status badge
211
+ */
212
+ export function HealthStatusBadge({ health }: { health: GitOpsHealthStatus }) {
213
+ const { icon: Icon, color, label } = getHealthInfo(health)
214
+
215
+ return (
216
+ <span className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', color)}>
217
+ <Icon className={clsx('w-3 h-3', health === 'Progressing' && 'animate-spin')} />
218
+ {label}
219
+ </span>
220
+ )
221
+ }
@@ -0,0 +1,210 @@
1
+ import { useState } from 'react'
2
+ import { ChevronDown, ChevronRight, Package, ExternalLink } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import type { ManagedResource, GitOpsHealthStatus } from '../../types/gitops'
5
+ import { groupManagedResourcesByKind } from '../../types/gitops'
6
+
7
+ interface ManagedResourcesListProps {
8
+ resources: ManagedResource[]
9
+ /** Callback when a resource is clicked (for navigation) */
10
+ onResourceClick?: (resource: ManagedResource) => void
11
+ /** Maximum resources to show before collapsing */
12
+ maxVisible?: number
13
+ /** Title for the section */
14
+ title?: string
15
+ /** Whether to show health status for each resource */
16
+ showHealth?: boolean
17
+ }
18
+
19
+ /**
20
+ * Displays a list of managed resources grouped by kind
21
+ * Used for showing Flux inventory or ArgoCD managed resources
22
+ */
23
+ export function ManagedResourcesList({
24
+ resources,
25
+ onResourceClick,
26
+ maxVisible = 20,
27
+ title = 'Managed Resources',
28
+ showHealth = false,
29
+ }: ManagedResourcesListProps) {
30
+ const [expanded, setExpanded] = useState(resources.length <= 10)
31
+ const [showAll, setShowAll] = useState(false)
32
+
33
+ if (!resources || resources.length === 0) {
34
+ return null
35
+ }
36
+
37
+ const grouped = groupManagedResourcesByKind(resources)
38
+ const sortedGroups = Array.from(grouped.entries()).sort((a, b) => a[0].localeCompare(b[0]))
39
+ const hasMore = resources.length > maxVisible && !showAll
40
+
41
+ return (
42
+ <div className="border-b-subtle pb-4 last:border-0">
43
+ <button
44
+ onClick={() => setExpanded(!expanded)}
45
+ className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
46
+ >
47
+ {expanded ? (
48
+ <ChevronDown className="w-4 h-4 text-theme-text-tertiary" />
49
+ ) : (
50
+ <ChevronRight className="w-4 h-4 text-theme-text-tertiary" />
51
+ )}
52
+ <Package className="w-4 h-4 text-theme-text-secondary" />
53
+ <span className="text-sm font-medium text-theme-text-secondary">
54
+ {title} ({resources.length})
55
+ </span>
56
+ </button>
57
+
58
+ {expanded && (
59
+ <div className="pl-6 space-y-3">
60
+ {sortedGroups.map(([kind, kindResources]) => (
61
+ <ResourceKindGroup
62
+ key={kind}
63
+ kind={kind}
64
+ resources={showAll ? kindResources : kindResources.slice(0, Math.ceil(maxVisible / sortedGroups.length))}
65
+ onResourceClick={onResourceClick}
66
+ showHealth={showHealth}
67
+ />
68
+ ))}
69
+
70
+ {hasMore && (
71
+ <button
72
+ onClick={() => setShowAll(true)}
73
+ className="text-xs text-blue-400 hover:text-blue-300 transition-colors"
74
+ >
75
+ Show all {resources.length} resources...
76
+ </button>
77
+ )}
78
+ </div>
79
+ )}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ interface ResourceKindGroupProps {
85
+ kind: string
86
+ resources: ManagedResource[]
87
+ onResourceClick?: (resource: ManagedResource) => void
88
+ showHealth?: boolean
89
+ }
90
+
91
+ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: ResourceKindGroupProps) {
92
+ const [expanded, setExpanded] = useState(resources.length <= 5)
93
+
94
+ return (
95
+ <div>
96
+ <button
97
+ onClick={() => setExpanded(!expanded)}
98
+ className="flex items-center gap-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors mb-1"
99
+ >
100
+ {expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
101
+ <span className="font-medium">{kind}</span>
102
+ <span className="text-theme-text-tertiary">({resources.length})</span>
103
+ </button>
104
+
105
+ {expanded && (
106
+ <div className="ml-4 space-y-0.5">
107
+ {resources.map((resource, idx) => (
108
+ <ResourceItem
109
+ key={`${resource.namespace}-${resource.name}-${idx}`}
110
+ resource={resource}
111
+ onClick={onResourceClick}
112
+ showHealth={showHealth}
113
+ />
114
+ ))}
115
+ </div>
116
+ )}
117
+ </div>
118
+ )
119
+ }
120
+
121
+ interface ResourceItemProps {
122
+ resource: ManagedResource
123
+ onClick?: (resource: ManagedResource) => void
124
+ showHealth?: boolean
125
+ }
126
+
127
+ function ResourceItem({ resource, onClick, showHealth }: ResourceItemProps) {
128
+ const displayName = resource.namespace
129
+ ? `${resource.namespace}/${resource.name}`
130
+ : resource.name
131
+
132
+ const content = (
133
+ <div className="flex items-center gap-2 py-0.5">
134
+ {showHealth && resource.health && (
135
+ <HealthDot health={resource.health} />
136
+ )}
137
+ <span className="text-xs text-theme-text-secondary truncate" title={displayName}>
138
+ {displayName}
139
+ </span>
140
+ {onClick && (
141
+ <ExternalLink className="w-3 h-3 text-theme-text-tertiary opacity-0 group-hover:opacity-100 transition-opacity" />
142
+ )}
143
+ </div>
144
+ )
145
+
146
+ if (onClick) {
147
+ return (
148
+ <button
149
+ onClick={() => onClick(resource)}
150
+ className="group w-full text-left hover:bg-theme-elevated/30 rounded px-1 -mx-1 transition-colors"
151
+ >
152
+ {content}
153
+ </button>
154
+ )
155
+ }
156
+
157
+ return <div className="px-1 -mx-1">{content}</div>
158
+ }
159
+
160
+ function HealthDot({ health }: { health: GitOpsHealthStatus }) {
161
+ const colorClass = getHealthDotColor(health)
162
+ return (
163
+ <span
164
+ className={clsx('w-1.5 h-1.5 rounded-full shrink-0', colorClass)}
165
+ title={health}
166
+ />
167
+ )
168
+ }
169
+
170
+ function getHealthDotColor(health: GitOpsHealthStatus): string {
171
+ switch (health) {
172
+ case 'Healthy':
173
+ return 'bg-green-400'
174
+ case 'Progressing':
175
+ return 'bg-blue-400'
176
+ case 'Degraded':
177
+ return 'bg-red-400'
178
+ case 'Suspended':
179
+ return 'bg-yellow-400'
180
+ case 'Missing':
181
+ return 'bg-orange-400'
182
+ default:
183
+ return 'bg-gray-400'
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Compact inventory count display for table cells
189
+ */
190
+ export function InventoryCount({ count, healthy, unhealthy }: { count: number; healthy?: number; unhealthy?: number }) {
191
+ if (count === 0) {
192
+ return <span className="text-theme-text-tertiary">-</span>
193
+ }
194
+
195
+ if (healthy !== undefined && unhealthy !== undefined) {
196
+ return (
197
+ <span className="text-xs">
198
+ <span className="text-green-400">{healthy}</span>
199
+ <span className="text-theme-text-tertiary">/</span>
200
+ <span className={unhealthy > 0 ? 'text-red-400' : 'text-theme-text-secondary'}>{count}</span>
201
+ </span>
202
+ )
203
+ }
204
+
205
+ return (
206
+ <span className="text-xs text-theme-text-secondary">
207
+ {count} {count === 1 ? 'resource' : 'resources'}
208
+ </span>
209
+ )
210
+ }
@@ -0,0 +1,146 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Clock, RefreshCw } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+
5
+ interface SyncCountdownProps {
6
+ /** The reconciliation interval (e.g., "5m", "1h", "30s") */
7
+ interval: string
8
+ /** Last reconciliation time (ISO string) */
9
+ lastSyncTime?: string
10
+ /** Whether the resource is suspended */
11
+ suspended?: boolean
12
+ /** Whether to show a compact version */
13
+ compact?: boolean
14
+ }
15
+
16
+ /**
17
+ * Displays a countdown to the next reconciliation
18
+ */
19
+ export function SyncCountdown({ interval, lastSyncTime, suspended, compact = false }: SyncCountdownProps) {
20
+ const [secondsRemaining, setSecondsRemaining] = useState<number | null>(null)
21
+
22
+ const intervalSeconds = parseInterval(interval)
23
+
24
+ useEffect(() => {
25
+ if (suspended || !lastSyncTime || intervalSeconds === null) {
26
+ setSecondsRemaining(null)
27
+ return
28
+ }
29
+
30
+ const calculateRemaining = () => {
31
+ const lastSync = new Date(lastSyncTime).getTime()
32
+ const nextSync = lastSync + intervalSeconds * 1000
33
+ const now = Date.now()
34
+ const remaining = Math.max(0, Math.floor((nextSync - now) / 1000))
35
+ return remaining
36
+ }
37
+
38
+ setSecondsRemaining(calculateRemaining())
39
+
40
+ const timer = setInterval(() => {
41
+ setSecondsRemaining(calculateRemaining())
42
+ }, 1000)
43
+
44
+ return () => clearInterval(timer)
45
+ }, [interval, lastSyncTime, suspended, intervalSeconds])
46
+
47
+ if (suspended) {
48
+ return (
49
+ <div className={clsx('flex items-center gap-1.5', compact ? 'text-xs' : 'text-sm')}>
50
+ <Clock className={clsx('text-yellow-400', compact ? 'w-3 h-3' : 'w-3.5 h-3.5')} />
51
+ <span className="text-yellow-400">Paused</span>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ if (secondsRemaining === null) {
57
+ return (
58
+ <div className={clsx('flex items-center gap-1.5 text-theme-text-tertiary', compact ? 'text-xs' : 'text-sm')}>
59
+ <Clock className={compact ? 'w-3 h-3' : 'w-3.5 h-3.5'} />
60
+ <span>{interval}</span>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ const isImminent = secondsRemaining < 30
66
+ const formatted = formatSecondsRemaining(secondsRemaining)
67
+
68
+ return (
69
+ <div
70
+ className={clsx(
71
+ 'flex items-center gap-1.5',
72
+ compact ? 'text-xs' : 'text-sm',
73
+ isImminent ? 'text-blue-400' : 'text-theme-text-secondary'
74
+ )}
75
+ title={`Interval: ${interval}`}
76
+ >
77
+ {isImminent ? (
78
+ <RefreshCw className={clsx('animate-pulse', compact ? 'w-3 h-3' : 'w-3.5 h-3.5')} />
79
+ ) : (
80
+ <Clock className={compact ? 'w-3 h-3' : 'w-3.5 h-3.5'} />
81
+ )}
82
+ <span>
83
+ {secondsRemaining === 0 ? 'Now' : `in ${formatted}`}
84
+ </span>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Parses a Kubernetes duration string (e.g., "5m", "1h30m", "30s") into seconds
91
+ */
92
+ function parseInterval(interval: string): number | null {
93
+ if (!interval) return null
94
+
95
+ let totalSeconds = 0
96
+ const regex = /(\d+)(h|m|s)/g
97
+ let match
98
+
99
+ while ((match = regex.exec(interval)) !== null) {
100
+ const value = parseInt(match[1], 10)
101
+ const unit = match[2]
102
+
103
+ switch (unit) {
104
+ case 'h':
105
+ totalSeconds += value * 3600
106
+ break
107
+ case 'm':
108
+ totalSeconds += value * 60
109
+ break
110
+ case 's':
111
+ totalSeconds += value
112
+ break
113
+ }
114
+ }
115
+
116
+ return totalSeconds > 0 ? totalSeconds : null
117
+ }
118
+
119
+ /**
120
+ * Formats seconds into a human-readable duration
121
+ */
122
+ function formatSecondsRemaining(seconds: number): string {
123
+ if (seconds < 60) {
124
+ return `${seconds}s`
125
+ }
126
+ if (seconds < 3600) {
127
+ const mins = Math.floor(seconds / 60)
128
+ const secs = seconds % 60
129
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
130
+ }
131
+ const hours = Math.floor(seconds / 3600)
132
+ const mins = Math.floor((seconds % 3600) / 60)
133
+ return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
134
+ }
135
+
136
+ /**
137
+ * Simple interval display without countdown
138
+ */
139
+ export function IntervalDisplay({ interval, compact = false }: { interval: string; compact?: boolean }) {
140
+ return (
141
+ <div className={clsx('flex items-center gap-1.5 text-theme-text-secondary', compact ? 'text-xs' : 'text-sm')}>
142
+ <Clock className={compact ? 'w-3 h-3' : 'w-3.5 h-3.5'} />
143
+ <span>{interval}</span>
144
+ </div>
145
+ )
146
+ }
@@ -0,0 +1,4 @@
1
+ export { GitOpsStatusBadge, SyncStatusBadge, HealthStatusBadge } from './GitOpsStatusBadge'
2
+ export { SyncCountdown, IntervalDisplay } from './SyncCountdown'
3
+ export { ManagedResourcesList, InventoryCount } from './ManagedResourcesList'
4
+ export { GitOpsActions, SyncButton, SuspendToggle } from './GitOpsActions'
@@ -0,0 +1,90 @@
1
+ import { useState, useMemo } from 'react'
2
+ import { ChevronRight, ChevronDown } from 'lucide-react'
3
+ import { getLevelColor } from '../../utils/log-format'
4
+ import type { LogLevel } from './useLogBuffer'
5
+
6
+ interface JsonLogLineProps {
7
+ content: string
8
+ level: LogLevel
9
+ wordWrap: boolean
10
+ }
11
+
12
+ export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
13
+ const [expanded, setExpanded] = useState(false)
14
+ const levelColor = getLevelColor(level)
15
+
16
+ const parsed = useMemo(() => {
17
+ try {
18
+ return JSON.parse(content.trim())
19
+ } catch {
20
+ return null
21
+ }
22
+ }, [content])
23
+
24
+ if (!parsed) {
25
+ return (
26
+ <span className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${levelColor}`}>
27
+ {content}
28
+ </span>
29
+ )
30
+ }
31
+
32
+ const summary = buildSummary(parsed)
33
+ const fieldCount = Object.keys(parsed).length
34
+
35
+ return (
36
+ <span className={levelColor}>
37
+ <button
38
+ onClick={() => setExpanded(!expanded)}
39
+ className="inline-flex items-center gap-0.5 hover:bg-theme-surface/50 rounded px-0.5 -ml-0.5 align-middle"
40
+ >
41
+ {expanded
42
+ ? <ChevronDown className="w-3 h-3 shrink-0 text-theme-text-tertiary" />
43
+ : <ChevronRight className="w-3 h-3 shrink-0 text-theme-text-tertiary" />
44
+ }
45
+ </button>
46
+ {!expanded ? (
47
+ <span className={wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}>
48
+ {summary}
49
+ <span className="text-theme-text-tertiary ml-1">{`{${fieldCount} fields}`}</span>
50
+ </span>
51
+ ) : (
52
+ <span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
53
+ {formatJsonExpanded(parsed)}
54
+ </span>
55
+ )}
56
+ </span>
57
+ )
58
+ }
59
+
60
+ function buildSummary(obj: Record<string, unknown>): string {
61
+ const parts: string[] = []
62
+
63
+ // Level
64
+ const lvl = obj.level || obj.severity || obj.lvl
65
+ if (typeof lvl === 'string') {
66
+ parts.push(lvl.toUpperCase())
67
+ }
68
+
69
+ // Message
70
+ const msg = obj.msg || obj.message
71
+ if (typeof msg === 'string') {
72
+ parts.push(msg)
73
+ }
74
+
75
+ // Error
76
+ const err = obj.error || obj.err
77
+ if (typeof err === 'string') {
78
+ parts.push(`error=${err}`)
79
+ }
80
+
81
+ return parts.join(' ')
82
+ }
83
+
84
+ function formatJsonExpanded(obj: Record<string, unknown>): string {
85
+ try {
86
+ return JSON.stringify(obj, null, 2)
87
+ } catch {
88
+ return String(obj)
89
+ }
90
+ }