@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 { useState, useMemo } from 'react'
2
+ import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
+ import { formatAge } from '../resource-utils'
6
+ import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER, TrivyAlertBanner } from './trivy-shared'
7
+
8
+ interface ConfigAuditReportRendererProps {
9
+ data: any
10
+ }
11
+
12
+ interface GroupedCheck {
13
+ checkID: string
14
+ title: string
15
+ severity: string
16
+ success: boolean
17
+ description: string
18
+ remediation: string
19
+ messages: string[]
20
+ count: number
21
+ }
22
+
23
+ function groupChecks(checks: any[]): GroupedCheck[] {
24
+ const map = new Map<string, GroupedCheck>()
25
+
26
+ for (const check of checks) {
27
+ const id = check.checkID || 'unknown'
28
+ const existing = map.get(id)
29
+ if (existing) {
30
+ existing.count++
31
+ if (!check.success) existing.success = false
32
+ if ((SEVERITY_ORDER[check.severity] ?? 99) < (SEVERITY_ORDER[existing.severity] ?? 99)) {
33
+ existing.severity = check.severity
34
+ }
35
+ if (Array.isArray(check.messages)) {
36
+ for (const msg of check.messages) {
37
+ if (msg && !existing.messages.includes(msg)) {
38
+ existing.messages.push(msg)
39
+ }
40
+ }
41
+ }
42
+ } else {
43
+ map.set(id, {
44
+ checkID: id,
45
+ title: check.title || '-',
46
+ severity: check.severity || 'LOW',
47
+ success: check.success,
48
+ description: check.description || '',
49
+ remediation: check.remediation || '',
50
+ messages: Array.isArray(check.messages) ? [...check.messages] : [],
51
+ count: 1,
52
+ })
53
+ }
54
+ }
55
+
56
+ const grouped = Array.from(map.values())
57
+ grouped.sort((a, b) => {
58
+ if (a.success !== b.success) return a.success ? 1 : -1
59
+ return (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)
60
+ })
61
+ return grouped
62
+ }
63
+
64
+ export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererProps) {
65
+ const [expandedSection, setExpandedSection] = useState(true)
66
+ const [expandedChecks, setExpandedChecks] = useState<Set<string>>(new Set())
67
+
68
+ const report = data.report || {}
69
+ const summary = report.summary || {}
70
+ const checks = Array.isArray(report.checks) ? report.checks : []
71
+ const scanner = report.scanner || {}
72
+
73
+ const critical = summary.criticalCount || 0
74
+ const high = summary.highCount || 0
75
+ const medium = summary.mediumCount || 0
76
+ const low = summary.lowCount || 0
77
+
78
+ const grouped = useMemo(() => groupChecks(checks), [checks])
79
+ const passCount = grouped.filter(g => g.success).length
80
+ const failCount = grouped.length - passCount
81
+
82
+ const containerCount = useMemo(() => {
83
+ const names = new Set<string>()
84
+ for (const check of checks) {
85
+ if (Array.isArray(check.messages)) {
86
+ for (const msg of check.messages) {
87
+ const match = msg?.match(/Container '([^']+)'/)
88
+ if (match) names.add(match[1])
89
+ }
90
+ }
91
+ }
92
+ return names.size
93
+ }, [checks])
94
+
95
+ const toggleCheck = (checkID: string) => {
96
+ setExpandedChecks(prev => {
97
+ const next = new Set(prev)
98
+ if (next.has(checkID)) next.delete(checkID)
99
+ else next.add(checkID)
100
+ return next
101
+ })
102
+ }
103
+
104
+ return (
105
+ <>
106
+ <TrivyAlertBanner critical={critical} high={high} noun="finding" />
107
+
108
+ {/* Report Overview */}
109
+ <Section title="Report Overview" icon={ShieldCheck}>
110
+ <PropertyList>
111
+ <Property label="Scanner" value={scanner.name ? `${scanner.name} ${scanner.version || ''}`.trim() : '-'} />
112
+ <Property label="Scanned" value={report.updateTimestamp ? formatAge(report.updateTimestamp) + ' ago' : '-'} />
113
+ </PropertyList>
114
+ </Section>
115
+
116
+ {/* Summary */}
117
+ <Section title="Summary">
118
+ <div className="flex items-center gap-4 mb-3">
119
+ <div className="flex items-center gap-1.5">
120
+ <CheckCircle2 className="w-4 h-4 text-green-400" />
121
+ <span className="text-sm font-medium text-green-400">{passCount}</span>
122
+ <span className="text-xs text-theme-text-tertiary">passed</span>
123
+ </div>
124
+ <div className="flex items-center gap-1.5">
125
+ <XCircle className="w-4 h-4 text-red-400" />
126
+ <span className="text-sm font-medium text-red-400">{failCount}</span>
127
+ <span className="text-xs text-theme-text-tertiary">failed</span>
128
+ </div>
129
+ </div>
130
+ <div className="flex flex-wrap gap-2">
131
+ {[
132
+ { label: 'Critical', count: critical, key: 'CRITICAL' },
133
+ { label: 'High', count: high, key: 'HIGH' },
134
+ { label: 'Medium', count: medium, key: 'MEDIUM' },
135
+ { label: 'Low', count: low, key: 'LOW' },
136
+ ].filter(({ count }) => count > 0).map(({ label, count, key }) => (
137
+ <span key={key} className={clsx('px-2 py-0.5 rounded text-xs font-medium', SEVERITY_BADGE_COLORS[key])}>
138
+ {count} {label}
139
+ </span>
140
+ ))}
141
+ {failCount === 0 && (
142
+ <span className="px-2 py-0.5 rounded text-xs font-medium bg-green-500/20 text-green-400">All checks passed</span>
143
+ )}
144
+ </div>
145
+ </Section>
146
+
147
+ {/* Checks - grouped by checkID */}
148
+ {grouped.length > 0 && (
149
+ <Section title="Checks">
150
+ <button
151
+ onClick={() => setExpandedSection(!expandedSection)}
152
+ className="flex items-center gap-1 text-xs text-theme-text-secondary hover:text-theme-text-primary mb-2"
153
+ >
154
+ {expandedSection ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
155
+ {grouped.length} unique checks{containerCount > 0 ? ` (${containerCount} containers scanned)` : ''}
156
+ </button>
157
+ {expandedSection && (
158
+ <div className="space-y-0.5">
159
+ {grouped.map((group) => {
160
+ const isExpanded = expandedChecks.has(group.checkID)
161
+ return (
162
+ <div key={group.checkID}>
163
+ <button
164
+ onClick={() => toggleCheck(group.checkID)}
165
+ className="w-full flex items-center gap-2 py-1.5 px-1 rounded hover:bg-theme-hover/50 text-left"
166
+ >
167
+ {group.success ? (
168
+ <CheckCircle2 className="w-3.5 h-3.5 text-green-400 shrink-0" />
169
+ ) : (
170
+ <XCircle className="w-3.5 h-3.5 text-red-400 shrink-0" />
171
+ )}
172
+ <span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', SEVERITY_BADGE_COLORS[group.severity] || 'bg-gray-500/20 text-gray-400')}>
173
+ {group.severity}
174
+ </span>
175
+ <span className="text-xs text-theme-text-secondary truncate flex-1">{group.title}</span>
176
+ {group.count > 1 && (
177
+ <span className="text-[10px] text-theme-text-tertiary shrink-0">{group.count} containers</span>
178
+ )}
179
+ {isExpanded ? (
180
+ <ChevronDown className="w-3 h-3 text-theme-text-tertiary shrink-0" />
181
+ ) : (
182
+ <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
183
+ )}
184
+ </button>
185
+ {isExpanded && (
186
+ <div className="ml-6 mr-1 mb-2 p-2.5 bg-theme-elevated/50 rounded border border-theme-border/50 space-y-2">
187
+ <div className="text-[10px] font-mono text-theme-text-tertiary">{group.checkID}</div>
188
+ {group.description && (
189
+ <div>
190
+ <div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-0.5">Description</div>
191
+ <div className="text-xs text-theme-text-secondary">{group.description}</div>
192
+ </div>
193
+ )}
194
+ {group.messages.length > 0 && (
195
+ <div>
196
+ <div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-0.5">Affected</div>
197
+ <div className="space-y-0.5">
198
+ {group.messages.map((msg, i) => (
199
+ <div key={i} className="text-xs text-theme-text-secondary">{msg}</div>
200
+ ))}
201
+ </div>
202
+ </div>
203
+ )}
204
+ {group.remediation && (
205
+ <div>
206
+ <div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-0.5">Remediation</div>
207
+ <div className="text-xs text-theme-text-secondary">{group.remediation}</div>
208
+ </div>
209
+ )}
210
+ </div>
211
+ )}
212
+ </div>
213
+ )
214
+ })}
215
+ </div>
216
+ )}
217
+ </Section>
218
+ )}
219
+ </>
220
+ )
221
+ }
@@ -0,0 +1,124 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { AlertTriangle, Copy, Check } from 'lucide-react'
3
+ import { Section, ExpandableSection } from '../../ui/drawer-components'
4
+ import { Tooltip } from '../../ui/Tooltip'
5
+ import { formatBytes } from '../../../utils/format'
6
+
7
+ interface ConfigMapRendererProps {
8
+ data: any
9
+ }
10
+
11
+ // Short = single-line and under 120 chars — render inline
12
+ function isShortValue(value: string): boolean {
13
+ return !value.includes('\n') && value.length <= 120
14
+ }
15
+
16
+ function TruncatedKey({ text }: { text: string }) {
17
+ const ref = useRef<HTMLSpanElement>(null)
18
+ const [isTruncated, setIsTruncated] = useState(false)
19
+
20
+ useEffect(() => {
21
+ const el = ref.current
22
+ if (el) setIsTruncated(el.scrollWidth > el.clientWidth)
23
+ }, [text])
24
+
25
+ return (
26
+ <Tooltip content={text} delay={100} position="top" disabled={!isTruncated}>
27
+ <span ref={ref} className="truncate block">{text}</span>
28
+ </Tooltip>
29
+ )
30
+ }
31
+
32
+ function CopyButton({ text }: { text: string }) {
33
+ const [copied, setCopied] = useState(false)
34
+ return (
35
+ <button
36
+ onClick={() => {
37
+ navigator.clipboard.writeText(text)
38
+ setCopied(true)
39
+ setTimeout(() => setCopied(false), 1500)
40
+ }}
41
+ className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary opacity-0 group-hover/row:opacity-100 transition-opacity shrink-0"
42
+ title="Copy value"
43
+ >
44
+ {copied ? <Check className="w-3 h-3 text-green-400" /> : <Copy className="w-3 h-3" />}
45
+ </button>
46
+ )
47
+ }
48
+
49
+ export function ConfigMapRenderer({ data }: ConfigMapRendererProps) {
50
+ const dataEntries = Object.entries((data.data || {}) as Record<string, string>)
51
+ const binaryDataKeys = Object.keys(data.binaryData || {})
52
+
53
+ const shortEntries = dataEntries.filter(([, v]) => isShortValue(v))
54
+ const longEntries = dataEntries.filter(([, v]) => !isShortValue(v))
55
+
56
+ // Dynamic value column width: give values as much room as they need (30-55%)
57
+ const maxValueLen = shortEntries.reduce((max, [, v]) => Math.max(max, v.length), 0)
58
+ // Short values (< 10 chars like "true", "5m") → values get 30%, keys get 70%
59
+ // Long values (URLs, addresses) → values get up to 55%
60
+ const valuePercent = Math.min(55, Math.max(30, Math.round(maxValueLen * 1.5 + 10)))
61
+ const keyPercent = 100 - valuePercent
62
+
63
+ return (
64
+ <>
65
+ <Section title={`Data (${dataEntries.length + binaryDataKeys.length} keys)`} defaultExpanded>
66
+ <div className="space-y-3">
67
+ {/* Short values: compact table */}
68
+ {shortEntries.length > 0 && (
69
+ <table className="w-full bg-theme-base rounded overflow-hidden text-xs border-separate border-spacing-0 table-fixed">
70
+ <colgroup>
71
+ <col style={{ width: `${keyPercent}%` }} />
72
+ <col style={{ width: `${valuePercent}%` }} />
73
+ <col style={{ width: '20px' }} />
74
+ </colgroup>
75
+ <tbody>
76
+ {shortEntries.map(([key, value], i) => (
77
+ <tr key={key} className={`group/row ${i % 2 === 0 ? '' : 'bg-theme-elevated/30'}`}>
78
+ <td className="px-2.5 py-1.5 text-theme-text-secondary font-medium truncate align-top">
79
+ <TruncatedKey text={key} />
80
+ </td>
81
+ <td className="px-2.5 py-1.5 text-theme-text-primary font-mono break-all align-top">
82
+ {value}
83
+ </td>
84
+ <td className="py-1.5 pr-2 align-top">
85
+ <CopyButton text={value} />
86
+ </td>
87
+ </tr>
88
+ ))}
89
+ </tbody>
90
+ </table>
91
+ )}
92
+
93
+ {/* Long/multiline values: expandable code blocks */}
94
+ {longEntries.map(([key, value]) => {
95
+ const isLarge = value.length > 500
96
+ return (
97
+ <ExpandableSection key={key} title={`${key} (${formatBytes(value.length)})`} defaultExpanded={!isLarge}>
98
+ <pre className="bg-theme-base rounded p-2 text-xs text-theme-text-secondary overflow-x-auto max-h-60 whitespace-pre-wrap">
99
+ {value}
100
+ </pre>
101
+ </ExpandableSection>
102
+ )
103
+ })}
104
+
105
+ {binaryDataKeys.map((key) => (
106
+ <div key={key} className="text-sm text-theme-text-secondary">
107
+ {key} <span className="text-xs">(binary)</span>
108
+ </div>
109
+ ))}
110
+ {dataEntries.length === 0 && binaryDataKeys.length === 0 && (
111
+ <div className="text-sm text-theme-text-tertiary">No data</div>
112
+ )}
113
+ </div>
114
+ </Section>
115
+
116
+ {data.immutable && (
117
+ <div className="flex items-center gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded text-yellow-400 text-sm">
118
+ <AlertTriangle className="w-4 h-4" />
119
+ Immutable ConfigMap
120
+ </div>
121
+ )}
122
+ </>
123
+ )
124
+ }
@@ -0,0 +1,86 @@
1
+ import { Clock, Pause } from 'lucide-react'
2
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { formatAge, cronToHuman } from '../resource-utils'
4
+
5
+ interface CronJobRendererProps {
6
+ data: any
7
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
8
+ }
9
+
10
+ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
11
+ const status = data.status || {}
12
+ const spec = data.spec || {}
13
+
14
+ // Check for issues or notable states
15
+ const isSuspended = spec.suspend === true
16
+ const hasNeverRun = !status.lastScheduleTime
17
+
18
+ // Calculate time since last success vs last schedule
19
+ const lastScheduleAge = status.lastScheduleTime ? new Date().getTime() - new Date(status.lastScheduleTime).getTime() : 0
20
+ const lastSuccessAge = status.lastSuccessfulTime ? new Date().getTime() - new Date(status.lastSuccessfulTime).getTime() : 0
21
+ const recentFailures = lastScheduleAge > 0 && lastSuccessAge > lastScheduleAge
22
+
23
+ return (
24
+ <>
25
+ {/* Suspended warning */}
26
+ {isSuspended && (
27
+ <AlertBanner
28
+ variant="warning"
29
+ icon={Pause}
30
+ title="CronJob Suspended"
31
+ message="No new jobs will be scheduled until this CronJob is resumed."
32
+ />
33
+ )}
34
+
35
+ {/* Never run warning */}
36
+ {hasNeverRun && !isSuspended && (
37
+ <AlertBanner
38
+ variant="info"
39
+ title="Never Scheduled"
40
+ message="This CronJob has never run. Check the schedule and starting deadline settings."
41
+ />
42
+ )}
43
+
44
+ {/* Recent failures warning */}
45
+ {recentFailures && (
46
+ <AlertBanner
47
+ variant="error"
48
+ title="Recent Jobs Failing"
49
+ message={<>Jobs have been scheduled but haven't succeeded recently. Last success: {formatAge(status.lastSuccessfulTime)}. Check job history and pod logs.</>}
50
+ />
51
+ )}
52
+
53
+ <Section title="Schedule" icon={Clock}>
54
+ <PropertyList>
55
+ <Property label="Schedule" value={spec.schedule} />
56
+ <Property label="Human" value={cronToHuman(spec.schedule)} />
57
+ <Property label="Suspend" value={spec.suspend ? 'Yes' : 'No'} />
58
+ <Property label="Last Schedule" value={status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never'} />
59
+ <Property label="Last Success" value={status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : 'Never'} />
60
+ <Property label="Active Jobs" value={status.active?.length || 0} />
61
+ </PropertyList>
62
+ </Section>
63
+
64
+ <Section title="Configuration">
65
+ <PropertyList>
66
+ <Property label="Concurrency" value={spec.concurrencyPolicy || 'Allow'} />
67
+ <Property label="Starting Deadline" value={spec.startingDeadlineSeconds ? `${spec.startingDeadlineSeconds}s` : 'None'} />
68
+ <Property label="Success History" value={spec.successfulJobsHistoryLimit ?? 3} />
69
+ <Property label="Failed History" value={spec.failedJobsHistoryLimit ?? 1} />
70
+ </PropertyList>
71
+ </Section>
72
+
73
+ {status.active?.length > 0 && (
74
+ <Section title="Active Jobs">
75
+ <div className="space-y-1">
76
+ {status.active.map((job: any) => (
77
+ <div key={job.name} className="text-sm">
78
+ <ResourceLink name={job.name} kind="jobs" namespace={job.namespace || data.metadata?.namespace || ''} onNavigate={onNavigate} />
79
+ </div>
80
+ ))}
81
+ </div>
82
+ </Section>
83
+ )}
84
+ </>
85
+ )
86
+ }
@@ -0,0 +1,175 @@
1
+ import { AlertTriangle, Info, Clock, Target, Server, Hash } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property } from '../../ui/drawer-components'
4
+ import { formatAge } from '../resource-utils'
5
+ import type { NavigateToResource } from '../../../utils/navigation'
6
+ import { kindToPlural } from '../../../utils/navigation'
7
+
8
+ interface EventRendererProps {
9
+ data: any
10
+ onNavigate?: NavigateToResource
11
+ }
12
+
13
+ export function EventRenderer({ data, onNavigate }: EventRendererProps) {
14
+ const eventType = data.type || 'Normal'
15
+ const isWarning = eventType === 'Warning'
16
+ const reason = data.reason || ''
17
+ const message = data.message || ''
18
+ const count = data.count || 1
19
+ const involvedObject = data.involvedObject || {}
20
+ const source = data.source || {}
21
+ const firstTimestamp = data.firstTimestamp
22
+ const lastTimestamp = data.lastTimestamp || data.metadata?.creationTimestamp
23
+
24
+ return (
25
+ <>
26
+ {/* Event Type Banner */}
27
+ <div className={clsx(
28
+ 'mb-4 p-4 rounded-lg border',
29
+ isWarning
30
+ ? 'bg-amber-500/10 border-amber-500/30'
31
+ : 'bg-blue-500/10 border-blue-500/30'
32
+ )}>
33
+ <div className="flex items-start gap-3">
34
+ {isWarning ? (
35
+ <AlertTriangle className="w-5 h-5 text-amber-400 mt-0.5 shrink-0" />
36
+ ) : (
37
+ <Info className="w-5 h-5 text-blue-400 mt-0.5 shrink-0" />
38
+ )}
39
+ <div className="flex-1 min-w-0">
40
+ <div className="flex items-center gap-2 mb-1">
41
+ <span className={clsx(
42
+ 'px-2 py-0.5 rounded text-xs font-medium',
43
+ isWarning ? 'bg-amber-500/20 text-amber-400' : 'bg-blue-500/20 text-blue-400'
44
+ )}>
45
+ {eventType}
46
+ </span>
47
+ {reason && (
48
+ <span className={clsx(
49
+ 'text-sm font-semibold',
50
+ isWarning ? 'text-amber-300' : 'text-blue-300'
51
+ )}>
52
+ {reason}
53
+ </span>
54
+ )}
55
+ {count > 1 && (
56
+ <span className="text-xs text-theme-text-tertiary">
57
+ ({count} times)
58
+ </span>
59
+ )}
60
+ </div>
61
+ {message && (
62
+ <p className={clsx(
63
+ 'text-sm break-all',
64
+ isWarning ? 'text-amber-200/90' : 'text-blue-200/90'
65
+ )}>
66
+ {message}
67
+ </p>
68
+ )}
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ {/* Involved Object */}
74
+ {involvedObject.kind && (
75
+ <Section title="Involved Object" icon={Target} defaultExpanded>
76
+ <div className="space-y-2">
77
+ <div className="flex items-center gap-2">
78
+ <span className="text-theme-text-tertiary text-sm w-24">Kind</span>
79
+ <span className="text-theme-text-primary text-sm font-medium">{involvedObject.kind}</span>
80
+ </div>
81
+ <div className="flex items-center gap-2">
82
+ <span className="text-theme-text-tertiary text-sm w-24">Name</span>
83
+ {onNavigate ? (
84
+ <button
85
+ onClick={() => {
86
+ // Parse API group from apiVersion (format: "group/version" or "v1" for core)
87
+ const apiVersion = involvedObject.apiVersion || ''
88
+ const group = apiVersion.includes('/') ? apiVersion.split('/')[0] : undefined
89
+ onNavigate({
90
+ kind: kindToPlural(involvedObject.kind),
91
+ namespace: involvedObject.namespace || '',
92
+ name: involvedObject.name,
93
+ group,
94
+ })
95
+ }}
96
+ className="text-blue-400 hover:text-blue-300 text-sm font-medium hover:underline"
97
+ >
98
+ {involvedObject.name}
99
+ </button>
100
+ ) : (
101
+ <span className="text-theme-text-primary text-sm">{involvedObject.name}</span>
102
+ )}
103
+ </div>
104
+ {involvedObject.namespace && (
105
+ <div className="flex items-center gap-2">
106
+ <span className="text-theme-text-tertiary text-sm w-24">Namespace</span>
107
+ <span className="text-theme-text-secondary text-sm">{involvedObject.namespace}</span>
108
+ </div>
109
+ )}
110
+ {involvedObject.uid && (
111
+ <div className="flex items-center gap-2">
112
+ <span className="text-theme-text-tertiary text-sm w-24">UID</span>
113
+ <span className="text-theme-text-tertiary text-xs font-mono">{involvedObject.uid}</span>
114
+ </div>
115
+ )}
116
+ {involvedObject.fieldPath && (
117
+ <div className="flex items-center gap-2">
118
+ <span className="text-theme-text-tertiary text-sm w-24">Field</span>
119
+ <span className="text-theme-text-secondary text-sm font-mono">{involvedObject.fieldPath}</span>
120
+ </div>
121
+ )}
122
+ </div>
123
+ </Section>
124
+ )}
125
+
126
+ {/* Timing */}
127
+ <Section title="Timing" icon={Clock} defaultExpanded>
128
+ <PropertyList>
129
+ {count > 1 && <Property label="Count" value={count} />}
130
+ {firstTimestamp && (
131
+ <Property label="First Seen" value={formatAge(firstTimestamp)} />
132
+ )}
133
+ {lastTimestamp && (
134
+ <Property label="Last Seen" value={formatAge(lastTimestamp)} />
135
+ )}
136
+ {firstTimestamp && lastTimestamp && firstTimestamp !== lastTimestamp && (
137
+ <Property label="Duration" value={calculateDuration(firstTimestamp, lastTimestamp)} />
138
+ )}
139
+ </PropertyList>
140
+ </Section>
141
+
142
+ {/* Source */}
143
+ {(source.component || source.host) && (
144
+ <Section title="Source" icon={Server} defaultExpanded={false}>
145
+ <PropertyList>
146
+ {source.component && <Property label="Component" value={source.component} />}
147
+ {source.host && <Property label="Host" value={source.host} />}
148
+ </PropertyList>
149
+ </Section>
150
+ )}
151
+
152
+ {/* Resource Version Info */}
153
+ {involvedObject.resourceVersion && (
154
+ <Section title="Resource Version" icon={Hash} defaultExpanded={false}>
155
+ <PropertyList>
156
+ <Property label="Object Version" value={involvedObject.resourceVersion} />
157
+ {involvedObject.apiVersion && <Property label="API Version" value={involvedObject.apiVersion} />}
158
+ </PropertyList>
159
+ </Section>
160
+ )}
161
+ </>
162
+ )
163
+ }
164
+
165
+ function calculateDuration(first: string, last: string): string {
166
+ const firstDate = new Date(first)
167
+ const lastDate = new Date(last)
168
+ const diffMs = lastDate.getTime() - firstDate.getTime()
169
+
170
+ if (diffMs < 1000) return 'instant'
171
+ if (diffMs < 60000) return `${Math.floor(diffMs / 1000)}s`
172
+ if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m`
173
+ if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h ${Math.floor((diffMs % 3600000) / 60000)}m`
174
+ return `${Math.floor(diffMs / 86400000)}d ${Math.floor((diffMs % 86400000) / 3600000)}h`
175
+ }