@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,202 @@
1
+ import { Play, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
4
+ import { formatAge, formatDuration } from '../resource-utils'
5
+
6
+ interface WorkflowRendererProps {
7
+ data: any
8
+ }
9
+
10
+ interface WorkflowStep {
11
+ id: string
12
+ displayName: string
13
+ phase: string
14
+ startedAt: string | null
15
+ finishedAt: string | null
16
+ }
17
+
18
+ function getStepDuration(step: WorkflowStep): string | null {
19
+ if (!step.startedAt) return null
20
+ const start = new Date(step.startedAt)
21
+ const end = step.finishedAt ? new Date(step.finishedAt) : new Date()
22
+ return formatDuration(end.getTime() - start.getTime(), true)
23
+ }
24
+
25
+ function StepStatusIcon({ phase }: { phase: string }) {
26
+ switch (phase) {
27
+ case 'Succeeded':
28
+ return <CheckCircle className="w-4 h-4 text-green-400 shrink-0" />
29
+ case 'Failed':
30
+ return <XCircle className="w-4 h-4 text-red-400 shrink-0" />
31
+ case 'Running':
32
+ return <Loader2 className="w-4 h-4 text-yellow-400 shrink-0 animate-spin" />
33
+ default:
34
+ return <Clock className="w-4 h-4 text-theme-text-tertiary shrink-0" />
35
+ }
36
+ }
37
+
38
+ function getPhaseBadgeClass(phase: string): string {
39
+ switch (phase) {
40
+ case 'Succeeded':
41
+ return 'status-healthy'
42
+ case 'Running':
43
+ return 'status-degraded'
44
+ case 'Failed':
45
+ case 'Error':
46
+ return 'status-unhealthy'
47
+ case 'Pending':
48
+ return 'status-degraded'
49
+ default:
50
+ return 'status-unknown'
51
+ }
52
+ }
53
+
54
+ function formatEstimatedDuration(seconds: number): string {
55
+ if (seconds < 60) return `${seconds}s`
56
+ const minutes = Math.floor(seconds / 60)
57
+ const remainingSeconds = seconds % 60
58
+ if (minutes < 60) {
59
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`
60
+ }
61
+ const hours = Math.floor(minutes / 60)
62
+ const remainingMinutes = minutes % 60
63
+ return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`
64
+ }
65
+
66
+ function getWorkflowProblems(data: any): string[] {
67
+ const problems: string[] = []
68
+ const status = data.status || {}
69
+ const phase = status.phase
70
+
71
+ if (phase === 'Failed') {
72
+ problems.push(status.message || 'Workflow failed')
73
+ } else if (phase === 'Error') {
74
+ problems.push(status.message || 'Workflow error')
75
+ }
76
+
77
+ // Check for failed nodes
78
+ const nodes = status.nodes || {}
79
+ const failedSteps = Object.values(nodes)
80
+ .filter((node: any) => node.type === 'Pod' && node.phase === 'Failed')
81
+ .map((node: any) => node.displayName)
82
+
83
+ if (failedSteps.length > 0) {
84
+ problems.push(`Failed steps: ${failedSteps.join(', ')}`)
85
+ }
86
+
87
+ return problems
88
+ }
89
+
90
+ function extractSteps(data: any): WorkflowStep[] {
91
+ const nodes = data.status?.nodes || {}
92
+ const steps: WorkflowStep[] = Object.entries(nodes)
93
+ .filter(([, node]: [string, any]) => node.type === 'Pod')
94
+ .map(([id, node]: [string, any]) => ({
95
+ id,
96
+ displayName: node.displayName || id,
97
+ phase: node.phase || 'Pending',
98
+ startedAt: node.startedAt || null,
99
+ finishedAt: node.finishedAt || null,
100
+ }))
101
+
102
+ steps.sort((a, b) => {
103
+ if (!a.startedAt && !b.startedAt) return 0
104
+ if (!a.startedAt) return 1
105
+ if (!b.startedAt) return -1
106
+ return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
107
+ })
108
+
109
+ return steps
110
+ }
111
+
112
+ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
113
+ const status = data.status || {}
114
+ const spec = data.spec || {}
115
+ const phase = status.phase || 'Unknown'
116
+
117
+ // Compute duration
118
+ const startedAt = status.startedAt ? new Date(status.startedAt) : null
119
+ const finishedAt = status.finishedAt ? new Date(status.finishedAt) : null
120
+ const duration = startedAt && finishedAt
121
+ ? formatDuration(finishedAt.getTime() - startedAt.getTime(), true)
122
+ : startedAt
123
+ ? formatDuration(Date.now() - startedAt.getTime(), true) + ' (running)'
124
+ : null
125
+
126
+ // Extract problems
127
+ const problems = getWorkflowProblems(data)
128
+ const hasProblems = problems.length > 0
129
+
130
+ // Extract steps
131
+ const steps = extractSteps(data)
132
+
133
+ // Arguments
134
+ const parameters = spec.arguments?.parameters || []
135
+
136
+ // Template reference
137
+ const templateName = spec.workflowTemplateRef?.name || null
138
+
139
+ // Estimated duration
140
+ const estimatedDuration = status.estimatedDuration
141
+
142
+ return (
143
+ <>
144
+ {/* Problems alert */}
145
+ {hasProblems && (
146
+ <AlertBanner variant="error" title="Workflow Issues" items={problems} />
147
+ )}
148
+
149
+ {/* Success banner */}
150
+ {phase === 'Succeeded' && !hasProblems && (
151
+ <AlertBanner variant="success" title="Workflow Completed Successfully" />
152
+ )}
153
+
154
+ {/* Status section */}
155
+ <Section title="Status" icon={Play}>
156
+ <PropertyList>
157
+ <Property label="Phase" value={
158
+ <span className={clsx('px-2 py-0.5 rounded text-xs', getPhaseBadgeClass(phase))}>
159
+ {phase}
160
+ </span>
161
+ } />
162
+ {duration && <Property label="Duration" value={duration} />}
163
+ {status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
164
+ <Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
165
+ {templateName && <Property label="Template" value={templateName} />}
166
+ {estimatedDuration != null && (
167
+ <Property label="Estimated Duration" value={formatEstimatedDuration(estimatedDuration)} />
168
+ )}
169
+ </PropertyList>
170
+ </Section>
171
+
172
+ {/* Steps section */}
173
+ {steps.length > 0 && (
174
+ <Section title={`Steps (${steps.length})`} defaultExpanded>
175
+ <div className="space-y-1.5">
176
+ {steps.map(step => (
177
+ <div key={step.id} className="flex items-center gap-2 text-sm bg-theme-elevated/30 rounded px-3 py-2">
178
+ <StepStatusIcon phase={step.phase} />
179
+ <span className="flex-1 text-theme-text-primary">{step.displayName}</span>
180
+ <span className="text-xs text-theme-text-secondary">{getStepDuration(step) || '-'}</span>
181
+ </div>
182
+ ))}
183
+ </div>
184
+ </Section>
185
+ )}
186
+
187
+ {/* Arguments section */}
188
+ {parameters.length > 0 && (
189
+ <Section title={`Arguments (${parameters.length})`} defaultExpanded={parameters.length <= 5}>
190
+ <PropertyList>
191
+ {parameters.map((param: any) => (
192
+ <Property key={param.name} label={param.name} value={param.value} />
193
+ ))}
194
+ </PropertyList>
195
+ </Section>
196
+ )}
197
+
198
+ {/* Conditions section */}
199
+ <ConditionsSection conditions={status.conditions} />
200
+ </>
201
+ )
202
+ }
@@ -0,0 +1,91 @@
1
+ import { Play, FileText, Key, Lock } from 'lucide-react'
2
+ import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
+
4
+ interface WorkflowTemplateRendererProps {
5
+ data: any
6
+ }
7
+
8
+ function getTemplateType(template: any): string {
9
+ if (template.steps) return 'steps'
10
+ if (template.dag) return 'dag'
11
+ if (template.script) return 'script'
12
+ if (template.container) return 'container'
13
+ if (template.resource) return 'resource'
14
+ return 'unknown'
15
+ }
16
+
17
+ function getTemplateImage(template: any): string | null {
18
+ if (template.container?.image) return template.container.image
19
+ if (template.script?.image) return template.script.image
20
+ return null
21
+ }
22
+
23
+ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps) {
24
+ const spec = data.spec || {}
25
+ const templates = spec.templates || []
26
+ const parameters = spec.arguments?.parameters || []
27
+ const imagePullSecrets = spec.imagePullSecrets || []
28
+
29
+ return (
30
+ <>
31
+ {/* Overview section */}
32
+ <Section title="Overview" icon={Play}>
33
+ <PropertyList>
34
+ <Property label="Entrypoint" value={spec.entrypoint} />
35
+ <Property label="Templates" value={templates.length} />
36
+ <Property label="Service Account" value={spec.serviceAccountName} />
37
+ </PropertyList>
38
+ </Section>
39
+
40
+ {/* Templates section */}
41
+ {templates.length > 0 && (
42
+ <Section title={`Templates (${templates.length})`} icon={FileText} defaultExpanded>
43
+ <div className="space-y-1.5">
44
+ {templates.map((template: any) => {
45
+ const type = getTemplateType(template)
46
+ const image = getTemplateImage(template)
47
+ return (
48
+ <div key={template.name} className="bg-theme-elevated/30 rounded px-3 py-2 text-sm">
49
+ <div className="font-medium text-theme-text-primary">{template.name}</div>
50
+ <div className="text-xs text-theme-text-secondary mt-0.5">{type}</div>
51
+ {image && (
52
+ <div className="text-xs text-theme-text-tertiary mt-0.5 truncate" title={image}>
53
+ {image}
54
+ </div>
55
+ )}
56
+ </div>
57
+ )
58
+ })}
59
+ </div>
60
+ </Section>
61
+ )}
62
+
63
+ {/* Arguments section */}
64
+ {parameters.length > 0 && (
65
+ <Section title={`Arguments (${parameters.length})`} icon={Key} defaultExpanded={parameters.length <= 5}>
66
+ <PropertyList>
67
+ {parameters.map((param: any) => (
68
+ <Property key={param.name} label={param.name} value={param.value} />
69
+ ))}
70
+ </PropertyList>
71
+ </Section>
72
+ )}
73
+
74
+ {/* Image Pull Secrets section */}
75
+ {imagePullSecrets.length > 0 && (
76
+ <Section title={`Image Pull Secrets (${imagePullSecrets.length})`} icon={Lock}>
77
+ <div className="flex flex-wrap gap-1">
78
+ {imagePullSecrets.map((secret: any) => (
79
+ <span
80
+ key={secret.name}
81
+ className="px-2 py-0.5 bg-theme-elevated rounded text-xs text-theme-text-secondary"
82
+ >
83
+ {secret.name}
84
+ </span>
85
+ ))}
86
+ </div>
87
+ </Section>
88
+ )}
89
+ </>
90
+ )
91
+ }
@@ -0,0 +1,311 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Server, ExternalLink, Scale, Minus, Plus, Loader2 } from 'lucide-react'
3
+ import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+
5
+ interface WorkloadRendererProps {
6
+ kind: string
7
+ data: any
8
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
9
+ onViewPods?: () => void
10
+ onScale?: (replicas: number) => Promise<void>
11
+ isScalePending?: boolean
12
+ onRequestRefresh?: () => void
13
+ }
14
+
15
+ // Check if the workload is actively progressing (scaling, rolling update)
16
+ function isWorkloadProgressing(status: any): boolean {
17
+ const conditions = status.conditions || []
18
+ const progressing = conditions.find((c: any) => c.type === 'Progressing')
19
+ return progressing?.status === 'True' && progressing?.reason !== 'ProgressDeadlineExceeded'
20
+ }
21
+
22
+ // Extract real problems from workload status (excludes normal rollout progress)
23
+ function getWorkloadProblems(status: any, spec: any, kind: string): string[] {
24
+ const problems: string[] = []
25
+ const progressing = isWorkloadProgressing(status)
26
+ const isDaemonSet = kind === 'daemonsets'
27
+
28
+ // Check replica/pod counts — only flag as problem if NOT actively progressing
29
+ if (!progressing) {
30
+ if (isDaemonSet) {
31
+ const ready = status.numberReady || 0
32
+ const desired = status.desiredNumberScheduled || 0
33
+ if (desired > 0 && ready < desired) {
34
+ problems.push(`${desired - ready} of ${desired} pods are not ready`)
35
+ }
36
+ if (status.numberUnavailable > 0) {
37
+ problems.push(`${status.numberUnavailable} pods are unavailable`)
38
+ }
39
+ } else {
40
+ const ready = status.readyReplicas || 0
41
+ const desired = spec.replicas || 0
42
+ if (desired > 0 && ready < desired) {
43
+ problems.push(`${desired - ready} of ${desired} replicas are not ready`)
44
+ }
45
+ if (status.unavailableReplicas > 0) {
46
+ problems.push(`${status.unavailableReplicas} replicas are unavailable`)
47
+ }
48
+ }
49
+ }
50
+
51
+ // Check conditions — real failures always shown
52
+ const conditions = status.conditions || []
53
+ for (const cond of conditions) {
54
+ if (cond.status === 'True' && cond.type === 'ReplicaFailure' && cond.message) {
55
+ problems.push(cond.message)
56
+ }
57
+ // Show condition failures, but skip Available=False during active rollout (that's expected)
58
+ if (cond.status === 'False' && cond.message) {
59
+ if (progressing && cond.type === 'Available') continue
60
+ problems.push(`${cond.type}: ${cond.message}`)
61
+ }
62
+ }
63
+
64
+ return problems
65
+ }
66
+
67
+ // Get progress info for active rollouts
68
+ function getWorkloadProgress(status: any, spec: any, kind: string): string | null {
69
+ if (!isWorkloadProgressing(status)) return null
70
+
71
+ const isDaemonSet = kind === 'daemonsets'
72
+ if (isDaemonSet) {
73
+ const ready = status.numberReady || 0
74
+ const desired = status.desiredNumberScheduled || 0
75
+ if (desired > 0 && ready < desired) {
76
+ return `${ready} of ${desired} pods ready`
77
+ }
78
+ } else {
79
+ const ready = status.readyReplicas || 0
80
+ const desired = spec.replicas || 0
81
+ if (desired > 0 && ready < desired) {
82
+ return `${ready} of ${desired} replicas ready`
83
+ }
84
+ }
85
+ return null
86
+ }
87
+
88
+ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, onRequestRefresh }: WorkloadRendererProps) {
89
+ const status = data.status || {}
90
+ const spec = data.spec || {}
91
+ const metadata = data.metadata || {}
92
+
93
+ const isDaemonSet = kind === 'daemonsets'
94
+ const isStatefulSet = kind === 'statefulsets'
95
+ const isScalable = (kind === 'deployments' || kind === 'statefulsets') && !!onScale
96
+
97
+ // Scale dialog state
98
+ const [showScaleDialog, setShowScaleDialog] = useState(false)
99
+ const [targetReplicas, setTargetReplicas] = useState(spec.replicas || 0)
100
+ const [scaledTo, setScaledTo] = useState<number | null>(null)
101
+
102
+ // Clear scaledTo once the backend data catches up
103
+ useEffect(() => {
104
+ if (scaledTo !== null && spec.replicas === scaledTo) {
105
+ setScaledTo(null)
106
+ }
107
+ }, [spec.replicas, scaledTo])
108
+
109
+ // Check for problems and progress
110
+ const problems = getWorkloadProblems(status, spec, kind)
111
+ const hasProblems = problems.length > 0
112
+ const progressMessage = getWorkloadProgress(status, spec, kind)
113
+
114
+ // Request data refresh while scaling is in progress
115
+ const isScaling = scaledTo !== null || progressMessage !== null
116
+ useEffect(() => {
117
+ if (!isScaling || !onRequestRefresh) return
118
+ const interval = setInterval(() => {
119
+ onRequestRefresh()
120
+ }, 2000)
121
+ return () => clearInterval(interval)
122
+ }, [isScaling, onRequestRefresh])
123
+
124
+ const handleScale = async () => {
125
+ if (!onScale) return
126
+ try {
127
+ await onScale(targetReplicas)
128
+ setScaledTo(targetReplicas)
129
+ setShowScaleDialog(false)
130
+ } catch {
131
+ // Stay on the dialog if scale failed
132
+ }
133
+ }
134
+
135
+ const openScaleDialog = () => {
136
+ setTargetReplicas(spec.replicas || 0)
137
+ setShowScaleDialog(true)
138
+ }
139
+
140
+ return (
141
+ <>
142
+ {/* Scaling in progress banner */}
143
+ {(scaledTo !== null || progressMessage) && !hasProblems && (
144
+ <div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
145
+ <div className="flex items-center gap-2">
146
+ <Loader2 className="w-4 h-4 text-blue-400 animate-spin shrink-0" />
147
+ <div className="text-sm text-blue-300">
148
+ {progressMessage || `Scaling to ${scaledTo} replicas...`}
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )}
153
+
154
+ {/* Problems alert - shown at top when there are real issues */}
155
+ {hasProblems && (
156
+ <AlertBanner variant="error" title="Issues Detected" items={problems}>
157
+ <div className="flex items-center justify-between mt-2">
158
+ <div className="text-xs text-red-400/60">
159
+ Check Events below for details, or view individual pods for logs.
160
+ </div>
161
+ {onViewPods && (
162
+ <button
163
+ onClick={onViewPods}
164
+ className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-red-400 hover:text-red-300 bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 rounded transition-colors"
165
+ >
166
+ <ExternalLink className="w-3 h-3" />
167
+ View Pods
168
+ </button>
169
+ )}
170
+ </div>
171
+ </AlertBanner>
172
+ )}
173
+
174
+ <Section title="Status" icon={Server}>
175
+ <PropertyList>
176
+ {isDaemonSet ? (
177
+ <>
178
+ <Property label="Desired" value={status.desiredNumberScheduled} />
179
+ <Property label="Current" value={status.currentNumberScheduled} />
180
+ <Property label="Ready" value={status.numberReady} />
181
+ <Property label="Up-to-date" value={status.updatedNumberScheduled} />
182
+ <Property label="Available" value={status.numberAvailable} />
183
+ </>
184
+ ) : (
185
+ <>
186
+ <Property label="Replicas" value={`${status.readyReplicas || 0}/${spec.replicas || 0}`} />
187
+ <Property label="Updated" value={status.updatedReplicas} />
188
+ <Property label="Available" value={status.availableReplicas} />
189
+ <Property label="Unavailable" value={status.unavailableReplicas} />
190
+ </>
191
+ )}
192
+ </PropertyList>
193
+ <div className="mt-3 pt-3 border-t border-theme-border flex items-center gap-2">
194
+ {onViewPods && (
195
+ <button
196
+ onClick={onViewPods}
197
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-400 hover:text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded transition-colors"
198
+ >
199
+ <ExternalLink className="w-3 h-3" />
200
+ View Managed Pods
201
+ </button>
202
+ )}
203
+ {isScalable && (
204
+ <button
205
+ onClick={openScaleDialog}
206
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-emerald-400 hover:text-emerald-300 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 rounded transition-colors"
207
+ >
208
+ <Scale className="w-3 h-3" />
209
+ Scale
210
+ </button>
211
+ )}
212
+ </div>
213
+ </Section>
214
+
215
+ {/* Scale Dialog */}
216
+ {showScaleDialog && (
217
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
218
+ <div className="bg-theme-surface border border-theme-border rounded-lg shadow-xl w-80 p-4">
219
+ <h3 className="text-sm font-medium text-theme-text-primary mb-4">
220
+ Scale {metadata.name}
221
+ </h3>
222
+
223
+ <div className="flex items-center justify-center gap-4 mb-4">
224
+ <button
225
+ onClick={() => setTargetReplicas(Math.max(0, targetReplicas - 1))}
226
+ className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
227
+ disabled={targetReplicas <= 0}
228
+ >
229
+ <Minus className="w-5 h-5" />
230
+ </button>
231
+
232
+ <input
233
+ type="number"
234
+ min="0"
235
+ max="10000"
236
+ value={targetReplicas}
237
+ onChange={(e) => setTargetReplicas(Math.min(10000, Math.max(0, parseInt(e.target.value) || 0)))}
238
+ className="w-20 text-center text-2xl font-semibold bg-theme-elevated border border-theme-border rounded-lg py-2 text-theme-text-primary focus:outline-none focus:border-blue-500"
239
+ />
240
+
241
+ <button
242
+ onClick={() => setTargetReplicas(Math.min(10000, targetReplicas + 1))}
243
+ disabled={targetReplicas >= 10000}
244
+ className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
245
+ >
246
+ <Plus className="w-5 h-5" />
247
+ </button>
248
+ </div>
249
+
250
+ <div className="text-xs text-theme-text-tertiary text-center mb-4">
251
+ Current: {spec.replicas || 0} replicas
252
+ {targetReplicas !== (spec.replicas || 0) && (
253
+ <span className="text-theme-text-secondary">
254
+ {' '}→ {targetReplicas}
255
+ </span>
256
+ )}
257
+ </div>
258
+
259
+ <div className="flex gap-2">
260
+ <button
261
+ onClick={() => setShowScaleDialog(false)}
262
+ className="flex-1 px-3 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary bg-theme-elevated hover:bg-theme-hover border border-theme-border rounded-lg transition-colors"
263
+ >
264
+ Cancel
265
+ </button>
266
+ <button
267
+ onClick={handleScale}
268
+ disabled={isScalePending || targetReplicas === (spec.replicas || 0)}
269
+ className="flex-1 px-3 py-2 text-sm text-white bg-blue-600 hover:bg-blue-500 disabled:bg-blue-600/50 disabled:cursor-not-allowed rounded-lg transition-colors"
270
+ >
271
+ {isScalePending ? 'Scaling...' : 'Apply'}
272
+ </button>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ )}
277
+
278
+ <Section title="Strategy">
279
+ <PropertyList>
280
+ {isDaemonSet || isStatefulSet ? (
281
+ <Property label="Update Strategy" value={spec.updateStrategy?.type} />
282
+ ) : (
283
+ <>
284
+ <Property label="Strategy" value={spec.strategy?.type} />
285
+ {spec.strategy?.rollingUpdate && (
286
+ <>
287
+ <Property label="Max Surge" value={spec.strategy.rollingUpdate.maxSurge} />
288
+ <Property label="Max Unavailable" value={spec.strategy.rollingUpdate.maxUnavailable} />
289
+ </>
290
+ )}
291
+ </>
292
+ )}
293
+ {isStatefulSet && (
294
+ <>
295
+ <Property label="Service Name" value={
296
+ spec.serviceName ? <ResourceLink name={spec.serviceName} kind="services" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} /> : undefined
297
+ } />
298
+ <Property label="Pod Management" value={spec.podManagementPolicy || 'OrderedReady'} />
299
+ </>
300
+ )}
301
+ </PropertyList>
302
+ </Section>
303
+
304
+ <Section title="Pod Template" defaultExpanded={false}>
305
+ <PodTemplateSection template={spec.template} />
306
+ </Section>
307
+
308
+ <ConditionsSection conditions={status.conditions} />
309
+ </>
310
+ )
311
+ }