@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,164 @@
1
+ import { Cpu, AlertTriangle } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
4
+ import { formatAge } from '../resource-utils'
5
+
6
+ interface HPARendererProps {
7
+ data: any
8
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
9
+ }
10
+
11
+ // Extract problems from HPA conditions
12
+ function getHPAProblems(data: any): string[] {
13
+ const problems: string[] = []
14
+ const conditions = data.status?.conditions || []
15
+ const status = data.status || {}
16
+ const spec = data.spec || {}
17
+
18
+ // Check conditions for issues
19
+ for (const cond of conditions) {
20
+ // AbleToScale = False means target not found or can't scale
21
+ if (cond.type === 'AbleToScale' && cond.status === 'False') {
22
+ problems.push(`Cannot scale: ${cond.reason}${cond.message ? ' - ' + cond.message : ''}`)
23
+ }
24
+
25
+ // ScalingActive = False means metrics unavailable
26
+ if (cond.type === 'ScalingActive' && cond.status === 'False') {
27
+ if (cond.reason === 'FailedGetResourceMetric') {
28
+ problems.push('Metrics unavailable — is metrics-server running?')
29
+ } else {
30
+ problems.push(`Scaling inactive: ${cond.reason}${cond.message ? ' - ' + cond.message : ''}`)
31
+ }
32
+ }
33
+
34
+ // ScalingLimited = True means at min/max bound
35
+ if (cond.type === 'ScalingLimited' && cond.status === 'True') {
36
+ if (cond.reason === 'TooFewReplicas') {
37
+ problems.push(`At minimum replicas (${spec.minReplicas || 1}) — cannot scale down further`)
38
+ } else if (cond.reason === 'TooManyReplicas') {
39
+ problems.push(`At maximum replicas (${spec.maxReplicas}) — cannot scale up further`)
40
+ }
41
+ }
42
+ }
43
+
44
+ // Check for desired != current (scaling in progress or stuck)
45
+ if (status.currentReplicas !== undefined && status.desiredReplicas !== undefined) {
46
+ if (status.currentReplicas !== status.desiredReplicas) {
47
+ const direction = status.desiredReplicas > status.currentReplicas ? 'up' : 'down'
48
+ problems.push(`Scaling ${direction}: ${status.currentReplicas} → ${status.desiredReplicas} replicas`)
49
+ }
50
+ }
51
+
52
+ return problems
53
+ }
54
+
55
+ export function HPARenderer({ data, onNavigate }: HPARendererProps) {
56
+ const status = data.status || {}
57
+ const spec = data.spec || {}
58
+ const metrics = status.currentMetrics || []
59
+
60
+ // Check for problems
61
+ const problems = getHPAProblems(data)
62
+ const hasProblems = problems.length > 0
63
+
64
+ // Determine if these are errors (red) or warnings (yellow)
65
+ const hasErrors = problems.some(p =>
66
+ p.includes('Cannot scale') || p.includes('unavailable') || p.includes('inactive')
67
+ )
68
+
69
+ return (
70
+ <>
71
+ {/* Problems/warnings alert */}
72
+ {hasProblems && (
73
+ <div className={clsx(
74
+ 'mb-4 p-3 rounded-lg border',
75
+ hasErrors
76
+ ? 'bg-red-500/10 border-red-500/30'
77
+ : 'bg-yellow-500/10 border-yellow-500/30'
78
+ )}>
79
+ <div className="flex items-start gap-2">
80
+ <AlertTriangle className={clsx(
81
+ 'w-4 h-4 mt-0.5 shrink-0',
82
+ hasErrors ? 'text-red-400' : 'text-yellow-400'
83
+ )} />
84
+ <div className="flex-1 min-w-0">
85
+ <div className={clsx(
86
+ 'text-sm font-medium mb-1',
87
+ hasErrors ? 'text-red-400' : 'text-yellow-400'
88
+ )}>
89
+ {hasErrors ? 'Scaling Issues' : 'Scaling Status'}
90
+ </div>
91
+ <ul className={clsx(
92
+ 'text-xs space-y-1',
93
+ hasErrors ? 'text-red-300' : 'text-yellow-300'
94
+ )}>
95
+ {problems.map((problem, i) => (
96
+ <li key={i} className="flex items-start gap-1.5">
97
+ <span className={clsx(
98
+ 'mt-0.5',
99
+ hasErrors ? 'text-red-400/60' : 'text-yellow-400/60'
100
+ )}>•</span>
101
+ <span>{problem}</span>
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ )}
109
+
110
+ <Section title="Scaling" icon={Cpu}>
111
+ <PropertyList>
112
+ <Property label="Target" value={
113
+ spec.scaleTargetRef?.name ? (
114
+ <ResourceLink
115
+ name={spec.scaleTargetRef.name}
116
+ kind={(spec.scaleTargetRef.kind || 'Deployment').toLowerCase() + 's'}
117
+ namespace={data.metadata?.namespace || ''}
118
+ label={`${spec.scaleTargetRef.kind}/${spec.scaleTargetRef.name}`}
119
+ onNavigate={onNavigate}
120
+ />
121
+ ) : undefined
122
+ } />
123
+ <Property label="Current" value={status.currentReplicas} />
124
+ <Property label="Desired" value={status.desiredReplicas} />
125
+ <Property label="Min" value={spec.minReplicas || 1} />
126
+ <Property label="Max" value={spec.maxReplicas} />
127
+ {status.lastScaleTime && <Property label="Last Scale" value={formatAge(status.lastScaleTime)} />}
128
+ </PropertyList>
129
+ </Section>
130
+
131
+ {metrics.length > 0 && (
132
+ <Section title="Metrics" defaultExpanded>
133
+ <div className="space-y-3">
134
+ {metrics.map((metric: any, i: number) => {
135
+ const current = metric.resource?.current?.averageUtilization || metric.resource?.current?.averageValue
136
+ const target = spec.metrics?.[i]?.resource?.target?.averageUtilization || spec.metrics?.[i]?.resource?.target?.averageValue
137
+ return (
138
+ <div key={i} className="bg-theme-elevated/30 rounded p-2">
139
+ <div className="flex items-center justify-between text-sm">
140
+ <span className="text-theme-text-primary">{metric.resource?.name || metric.type}</span>
141
+ <span className="text-theme-text-secondary">{current}{typeof current === 'number' ? '%' : ''} / {target}{typeof target === 'number' ? '%' : ''}</span>
142
+ </div>
143
+ {typeof current === 'number' && typeof target === 'number' && (
144
+ <div className="mt-2 h-2 bg-theme-hover rounded overflow-hidden">
145
+ <div
146
+ className={clsx(
147
+ 'h-full transition-all',
148
+ current > target ? 'bg-red-500' : current > target * 0.8 ? 'bg-yellow-500' : 'bg-green-500'
149
+ )}
150
+ style={{ width: `${Math.min(100, (current / target) * 100)}%` }}
151
+ />
152
+ </div>
153
+ )}
154
+ </div>
155
+ )
156
+ })}
157
+ </div>
158
+ </Section>
159
+ )}
160
+
161
+ <ConditionsSection conditions={status.conditions} />
162
+ </>
163
+ )
164
+ }
@@ -0,0 +1,303 @@
1
+ import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge } from '../../ui/drawer-components'
4
+ import type { ResourceRef } from '../../../types'
5
+
6
+ interface HTTPRouteRendererProps {
7
+ data: any
8
+ onNavigate?: (ref: ResourceRef) => void
9
+ }
10
+
11
+ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps) {
12
+ const spec = data.spec || {}
13
+ const status = data.status || {}
14
+ const parentRefs = spec.parentRefs || []
15
+ const hostnames = spec.hostnames || []
16
+ const rules = spec.rules || []
17
+ const parentStatuses = status.parents || []
18
+ const routeNs = data.metadata?.namespace || ''
19
+
20
+ const notAcceptedParents = parentStatuses.filter((p: any) =>
21
+ (p.conditions || []).some((c: any) => c.type === 'Accepted' && c.status === 'False')
22
+ )
23
+ const unresolvedRefsParents = parentStatuses.filter((p: any) =>
24
+ (p.conditions || []).some((c: any) => c.type === 'ResolvedRefs' && c.status === 'False')
25
+ )
26
+
27
+ const firstParentConditions = parentStatuses.length > 0
28
+ ? parentStatuses[0].conditions
29
+ : undefined
30
+
31
+ function toGatewayRef(ref: any): ResourceRef {
32
+ return {
33
+ kind: 'Gateway',
34
+ namespace: ref.namespace || routeNs,
35
+ name: ref.name,
36
+ group: 'gateway.networking.k8s.io',
37
+ }
38
+ }
39
+
40
+ function toServiceRef(backend: any): ResourceRef {
41
+ return {
42
+ kind: 'Service',
43
+ namespace: backend.namespace || routeNs,
44
+ name: backend.name,
45
+ }
46
+ }
47
+
48
+ return (
49
+ <>
50
+ {notAcceptedParents.length > 0 && (
51
+ <AlertBanner
52
+ variant="error"
53
+ title="Route Not Accepted"
54
+ message={notAcceptedParents.map((p: any) => {
55
+ const cond = (p.conditions || []).find((c: any) => c.type === 'Accepted' && c.status === 'False')
56
+ const gwName = p.parentRef?.name || 'unknown'
57
+ return cond?.reason
58
+ ? `Gateway "${gwName}": ${cond.reason}${cond.message ? ' — ' + cond.message : ''}`
59
+ : `Gateway "${gwName}" has not accepted this route.`
60
+ }).join('; ')}
61
+ />
62
+ )}
63
+
64
+ {unresolvedRefsParents.length > 0 && (
65
+ <AlertBanner
66
+ variant="warning"
67
+ title="Unresolved References"
68
+ message="Some backend references could not be resolved. Check that the target services exist and are accessible."
69
+ />
70
+ )}
71
+
72
+ <Section title="Status" icon={Globe}>
73
+ <PropertyList>
74
+ <Property
75
+ label="Hostnames"
76
+ value={
77
+ hostnames.length > 0 ? (
78
+ <div className="flex flex-wrap gap-1">
79
+ {hostnames.map((h: string) => (
80
+ <span key={h} className="px-2 py-0.5 bg-theme-elevated rounded text-xs text-theme-text-secondary">{h}</span>
81
+ ))}
82
+ </div>
83
+ ) : (
84
+ <span className="text-xs text-theme-text-tertiary">Any</span>
85
+ )
86
+ }
87
+ />
88
+ <Property
89
+ label="Parent Gateways"
90
+ value={
91
+ parentRefs.length > 0 ? (
92
+ <div className="flex flex-wrap gap-1">
93
+ {parentRefs.map((ref: any, i: number) => (
94
+ <ResourceRefBadge key={`${ref.namespace || ''}-${ref.name}-${i}`} resourceRef={toGatewayRef(ref)} onClick={onNavigate} />
95
+ ))}
96
+ </div>
97
+ ) : 'None'
98
+ }
99
+ />
100
+ </PropertyList>
101
+ </Section>
102
+
103
+ <Section title={`Rules (${rules.length})`} icon={Network} defaultExpanded>
104
+ <div className="space-y-1.5">
105
+ {rules.map((rule: any, ruleIdx: number) => {
106
+ const matches = rule.matches || []
107
+ const backendRefs = rule.backendRefs || []
108
+ const filters = rule.filters || []
109
+ const totalWeight = backendRefs.reduce((sum: number, b: any) => sum + (b.weight ?? 1), 0)
110
+ const hasWeights = backendRefs.length > 1 && backendRefs.some((b: any) => b.weight !== undefined)
111
+ const multiBackend = backendRefs.length > 1
112
+
113
+ return (
114
+ <div key={ruleIdx} className="bg-theme-elevated/30 rounded p-2.5">
115
+ <div className="text-xs font-medium text-theme-text-tertiary mb-1.5">
116
+ Rule {ruleIdx + 1}
117
+ </div>
118
+
119
+ {/* Matches → backends inline */}
120
+ <div className="space-y-1">
121
+ {matches.length === 0 && backendRefs.length === 0 && (
122
+ <div className="text-xs text-theme-text-secondary italic">Match all</div>
123
+ )}
124
+ {matches.length === 0 && backendRefs.length > 0 && !multiBackend && (
125
+ <div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
126
+ <span className="italic text-theme-text-tertiary">all</span>
127
+ <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
128
+ <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
129
+ {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
130
+ </div>
131
+ )}
132
+ {matches.length === 0 && multiBackend && (
133
+ <>
134
+ <div className="text-xs text-theme-text-secondary italic">all</div>
135
+ {backendRefs.map((b: any, bi: number) => {
136
+ const pct = hasWeights ? Math.round(((b.weight ?? 1) / totalWeight) * 100) : null
137
+ return (
138
+ <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
139
+ <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
140
+ <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
141
+ {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
142
+ {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
143
+ </div>
144
+ )
145
+ })}
146
+ </>
147
+ )}
148
+ {matches.map((match: any, matchIdx: number) => (
149
+ <div key={matchIdx} className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
150
+ {match.method && (
151
+ <span className="px-1.5 py-0.5 bg-blue-500/20 text-blue-400 rounded font-medium">
152
+ {match.method}
153
+ </span>
154
+ )}
155
+ {match.path && (
156
+ <span>
157
+ <span className="text-theme-text-tertiary">{match.path.type || 'PathPrefix'}:</span>{' '}
158
+ <span className="text-theme-text-primary">{match.path.value}</span>
159
+ </span>
160
+ )}
161
+ {match.headers && match.headers.length > 0 && (
162
+ <span className="text-theme-text-tertiary">
163
+ headers: [{match.headers.map((h: any) => `${h.name}=${h.value}`).join(', ')}]
164
+ </span>
165
+ )}
166
+ {match.queryParams && match.queryParams.length > 0 && (
167
+ <span className="text-theme-text-tertiary">
168
+ query: [{match.queryParams.map((q: any) => `${q.name}=${q.value}`).join(', ')}]
169
+ </span>
170
+ )}
171
+ {/* Single backend inline on first match */}
172
+ {matchIdx === 0 && backendRefs.length === 1 && (
173
+ <>
174
+ <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
175
+ <ResourceRefBadge resourceRef={toServiceRef(backendRefs[0])} onClick={onNavigate} />
176
+ {backendRefs[0].port && <span className="text-theme-text-tertiary">:{backendRefs[0].port}</span>}
177
+ </>
178
+ )}
179
+ </div>
180
+ ))}
181
+ {/* Multiple backends as aligned column below matches */}
182
+ {matches.length > 0 && multiBackend && (
183
+ backendRefs.map((b: any, bi: number) => {
184
+ const pct = hasWeights ? Math.round(((b.weight ?? 1) / totalWeight) * 100) : null
185
+ return (
186
+ <div key={bi} className="text-xs text-theme-text-secondary flex items-center gap-1.5 pl-1">
187
+ <ArrowRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
188
+ <ResourceRefBadge resourceRef={toServiceRef(b)} onClick={onNavigate} />
189
+ {b.port && <span className="text-theme-text-tertiary">:{b.port}</span>}
190
+ {pct !== null && <span className="text-theme-text-tertiary text-[10px]">{pct}%</span>}
191
+ </div>
192
+ )
193
+ })
194
+ )}
195
+ </div>
196
+
197
+ {/* Filters inline */}
198
+ {filters.length > 0 && (
199
+ <div className="mt-1.5">
200
+ <div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
201
+ <Filter className="w-3 h-3 text-theme-text-tertiary" />
202
+ {filters.map((filter: any, filterIdx: number) => (
203
+ <span key={filterIdx} className="inline-flex items-center gap-1">
204
+ <span className="px-1.5 py-0.5 bg-amber-500/20 text-amber-400 rounded">
205
+ {filter.type}
206
+ </span>
207
+ {filter.type === 'RequestHeaderModifier' && filter.requestHeaderModifier && (
208
+ <span className="text-theme-text-tertiary">
209
+ {summarizeHeaderModifier(filter.requestHeaderModifier)}
210
+ </span>
211
+ )}
212
+ {filter.type === 'ResponseHeaderModifier' && filter.responseHeaderModifier && (
213
+ <span className="text-theme-text-tertiary">
214
+ {summarizeHeaderModifier(filter.responseHeaderModifier)}
215
+ </span>
216
+ )}
217
+ {filter.type === 'RequestRedirect' && filter.requestRedirect && (
218
+ <span className="text-theme-text-tertiary">
219
+ {filter.requestRedirect.scheme && `${filter.requestRedirect.scheme}://`}
220
+ {filter.requestRedirect.hostname || ''}
221
+ {filter.requestRedirect.port ? `:${filter.requestRedirect.port}` : ''}
222
+ {filter.requestRedirect.statusCode ? ` (${filter.requestRedirect.statusCode})` : ''}
223
+ </span>
224
+ )}
225
+ {filter.type === 'URLRewrite' && filter.urlRewrite && (
226
+ <span className="text-theme-text-tertiary">
227
+ {filter.urlRewrite.hostname && `host: ${filter.urlRewrite.hostname}`}
228
+ {filter.urlRewrite.path?.replacePrefixMatch && ` path: ${filter.urlRewrite.path.replacePrefixMatch}`}
229
+ </span>
230
+ )}
231
+ {filter.type === 'RequestMirror' && filter.requestMirror && (
232
+ <span className="text-theme-text-tertiary">
233
+ {filter.requestMirror.backendRef?.name || 'unknown'}
234
+ {filter.requestMirror.backendRef?.port ? `:${filter.requestMirror.backendRef.port}` : ''}
235
+ </span>
236
+ )}
237
+ </span>
238
+ ))}
239
+ </div>
240
+ </div>
241
+ )}
242
+ </div>
243
+ )
244
+ })}
245
+ </div>
246
+ </Section>
247
+
248
+ {/* Parent Status */}
249
+ {parentStatuses.length > 0 && (
250
+ <Section title={`Parent Status (${parentStatuses.length})`} defaultExpanded>
251
+ <div className="space-y-2">
252
+ {parentStatuses.map((parent: any, idx: number) => {
253
+ const ref = parent.parentRef || {}
254
+ const conditions = parent.conditions || []
255
+ const accepted = conditions.find((c: any) => c.type === 'Accepted')
256
+ const resolved = conditions.find((c: any) => c.type === 'ResolvedRefs')
257
+
258
+ return (
259
+ <div key={`${ref.namespace || ''}-${ref.name}-${idx}`} className="bg-theme-elevated/30 rounded p-2.5">
260
+ <div className="text-sm font-medium text-theme-text-primary mb-1.5">
261
+ {ref.namespace ? `${ref.namespace}/` : ''}{ref.name || 'unknown'}
262
+ {ref.sectionName ? ` (${ref.sectionName})` : ''}
263
+ </div>
264
+ <div className="flex flex-wrap gap-2">
265
+ {accepted && (
266
+ <span className={clsx(
267
+ 'px-2 py-0.5 rounded text-xs font-medium',
268
+ accepted.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
269
+ )}>
270
+ {accepted.status === 'True' ? 'Accepted' : 'Not Accepted'}
271
+ </span>
272
+ )}
273
+ {resolved && (
274
+ <span className={clsx(
275
+ 'px-2 py-0.5 rounded text-xs font-medium',
276
+ resolved.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
277
+ )}>
278
+ {resolved.status === 'True' ? 'Refs Resolved' : 'Unresolved Refs'}
279
+ </span>
280
+ )}
281
+ </div>
282
+ {accepted?.message && accepted.status === 'False' && (
283
+ <div className="text-xs text-theme-text-tertiary mt-1">{accepted.message}</div>
284
+ )}
285
+ </div>
286
+ )
287
+ })}
288
+ </div>
289
+ </Section>
290
+ )}
291
+
292
+ <ConditionsSection conditions={firstParentConditions} />
293
+ </>
294
+ )
295
+ }
296
+
297
+ function summarizeHeaderModifier(modifier: any): string {
298
+ const parts: string[] = []
299
+ if (modifier.set?.length) parts.push(`set: ${modifier.set.map((h: any) => h.name).join(', ')}`)
300
+ if (modifier.add?.length) parts.push(`add: ${modifier.add.map((h: any) => h.name).join(', ')}`)
301
+ if (modifier.remove?.length) parts.push(`remove: ${modifier.remove.join(', ')}`)
302
+ return parts.join('; ')
303
+ }
@@ -0,0 +1,167 @@
1
+ import { Database, AlertTriangle, CheckCircle2, Shield } 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 HelmRepositoryRendererProps {
10
+ data: any
11
+ }
12
+
13
+ export function HelmRepositoryRenderer({ data }: HelmRepositoryRendererProps) {
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
+ // Determine repository type
23
+ const isOCI = spec.type === 'oci'
24
+
25
+ // Problem detection
26
+ const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
27
+
28
+ if (gitOpsStatus.suspended) {
29
+ problems.push({ color: 'yellow', message: 'HelmRepository is suspended' })
30
+ }
31
+
32
+ if (gitOpsStatus.health === 'Degraded' && gitOpsStatus.message) {
33
+ problems.push({ color: 'red', message: gitOpsStatus.message })
34
+ }
35
+
36
+ return (
37
+ <>
38
+ {/* Problem alerts */}
39
+ {problems.map((problem, i) => (
40
+ <div
41
+ key={i}
42
+ className={clsx(
43
+ 'mb-4 p-3 border rounded-lg',
44
+ problem.color === 'red'
45
+ ? 'bg-red-500/10 border-red-500/30'
46
+ : 'bg-yellow-500/10 border-yellow-500/30'
47
+ )}
48
+ >
49
+ <div className="flex items-start gap-2">
50
+ <AlertTriangle
51
+ className={clsx(
52
+ 'w-4 h-4 mt-0.5 shrink-0',
53
+ problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
54
+ )}
55
+ />
56
+ <div className="flex-1 min-w-0">
57
+ <div
58
+ className={clsx(
59
+ 'text-sm font-medium',
60
+ problem.color === 'red' ? 'text-red-400' : 'text-yellow-400'
61
+ )}
62
+ >
63
+ {problem.color === 'red' ? 'Issue Detected' : 'Warning'}
64
+ </div>
65
+ <div
66
+ className={clsx(
67
+ 'text-xs mt-1',
68
+ problem.color === 'red' ? 'text-red-300/80' : 'text-yellow-300/80'
69
+ )}
70
+ >
71
+ {problem.message}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ ))}
77
+
78
+ {/* Status section */}
79
+ <Section title="Status">
80
+ <div className="space-y-3">
81
+ <GitOpsStatusBadge status={gitOpsStatus} showHealth={false} />
82
+ {spec.interval && (
83
+ <SyncCountdown
84
+ interval={spec.interval}
85
+ lastSyncTime={status.lastHandledReconcileAt}
86
+ suspended={gitOpsStatus.suspended}
87
+ />
88
+ )}
89
+ </div>
90
+ </Section>
91
+
92
+ {/* Repository section */}
93
+ <Section title="Repository" icon={Database}>
94
+ <PropertyList>
95
+ <Property label="URL" value={spec.url} />
96
+ <Property
97
+ label="Type"
98
+ value={
99
+ <span className={clsx(
100
+ 'px-2 py-0.5 rounded text-xs font-medium',
101
+ isOCI ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
102
+ )}>
103
+ {isOCI ? 'OCI' : 'HTTP'}
104
+ </span>
105
+ }
106
+ />
107
+ {spec.provider && <Property label="Provider" value={spec.provider} />}
108
+ </PropertyList>
109
+ </Section>
110
+
111
+ {/* Authentication section */}
112
+ {(spec.secretRef?.name || spec.certSecretRef?.name || spec.passCredentials) && (
113
+ <Section title="Authentication" icon={Shield} defaultExpanded={false}>
114
+ <PropertyList>
115
+ {spec.secretRef?.name && (
116
+ <Property label="Credentials Secret" value={spec.secretRef.name} />
117
+ )}
118
+ {spec.certSecretRef?.name && (
119
+ <Property label="TLS Secret" value={spec.certSecretRef.name} />
120
+ )}
121
+ {spec.passCredentials && (
122
+ <Property label="Pass Credentials" value="Yes" />
123
+ )}
124
+ {spec.insecure && (
125
+ <Property label="Insecure" value="Yes (TLS verification disabled)" />
126
+ )}
127
+ </PropertyList>
128
+ </Section>
129
+ )}
130
+
131
+ {/* Artifact/Index section */}
132
+ {artifact.revision && (
133
+ <Section title="Index" icon={CheckCircle2}>
134
+ <PropertyList>
135
+ <Property label="Revision" value={artifact.revision} />
136
+ <Property label="Digest" value={artifact.digest} />
137
+ <Property
138
+ label="Last Updated"
139
+ value={artifact.lastUpdateTime ? formatAge(artifact.lastUpdateTime) : '-'}
140
+ />
141
+ {artifact.size && (
142
+ <Property label="Size" value={formatBytes(artifact.size)} />
143
+ )}
144
+ </PropertyList>
145
+ </Section>
146
+ )}
147
+
148
+ {/* Additional Info */}
149
+ {status.observedGeneration !== undefined && (
150
+ <Section title="Additional Info" defaultExpanded={false}>
151
+ <PropertyList>
152
+ <Property label="Observed Generation" value={status.observedGeneration} />
153
+ {status.lastHandledReconcileAt && (
154
+ <Property
155
+ label="Last Reconciled"
156
+ value={formatAge(status.lastHandledReconcileAt)}
157
+ />
158
+ )}
159
+ </PropertyList>
160
+ </Section>
161
+ )}
162
+
163
+ {/* Conditions section */}
164
+ <ConditionsSection conditions={conditions} />
165
+ </>
166
+ )
167
+ }