@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.
- package/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- 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
|
+
}
|