@skyhook-io/k8s-ui 1.8.10 → 1.8.12

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 (168) hide show
  1. package/package.json +7 -6
  2. package/src/assets/gitops/argocd.png +0 -0
  3. package/src/assets/gitops/flux.svg +7 -0
  4. package/src/components/applications/AppChips.tsx +10 -0
  5. package/src/components/applications/ApplicationDetail.test.tsx +697 -0
  6. package/src/components/applications/ApplicationDetail.tsx +3269 -474
  7. package/src/components/applications/ApplicationsView.tsx +62 -10
  8. package/src/components/applications/applications-view.test.tsx +47 -0
  9. package/src/components/applications/index.ts +1 -1
  10. package/src/components/audit/AuditAlerts.tsx +14 -10
  11. package/src/components/checks/ChecksView.tsx +2 -3
  12. package/src/components/checks/severity.ts +14 -10
  13. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  14. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  15. package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +13 -6
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
  21. package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
  22. package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
  23. package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
  24. package/src/components/gitops/insights/index.ts +1 -0
  25. package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
  26. package/src/components/gitops/insights/insights-helpers.ts +74 -1
  27. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
  28. package/src/components/issues/IssuesView.tsx +91 -52
  29. package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
  30. package/src/components/issues/ResourceIssuesSection.tsx +46 -144
  31. package/src/components/issues/index.ts +3 -0
  32. package/src/components/issues/issue-timing.ts +53 -0
  33. package/src/components/issues/issues.test.ts +102 -1
  34. package/src/components/issues/severity.ts +3 -0
  35. package/src/components/logs/LogCore.tsx +14 -4
  36. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  37. package/src/components/logs/useLogStream.ts +8 -1
  38. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  39. package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
  40. package/src/components/resources/ResourcesSidebar.tsx +46 -46
  41. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  42. package/src/components/resources/ResourcesView.tsx +233 -51
  43. package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
  44. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
  45. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
  46. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
  47. package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
  48. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
  49. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
  50. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  51. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
  53. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
  54. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
  55. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
  56. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
  57. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
  58. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
  59. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  60. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
  61. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  62. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
  63. package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
  64. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
  65. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
  66. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
  67. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
  68. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
  69. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  70. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
  71. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
  72. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
  73. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
  75. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
  76. package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
  77. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  78. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  79. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
  80. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  81. package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
  82. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  83. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
  84. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  85. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  86. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  87. package/src/components/resources/renderers/index.ts +1 -0
  88. package/src/components/resources/resource-utils-argo.test.ts +18 -1
  89. package/src/components/resources/resource-utils-argo.ts +8 -0
  90. package/src/components/resources/resource-utils-capi.ts +1 -1
  91. package/src/components/resources/resource-utils.ts +31 -0
  92. package/src/components/shared/DetailShell.tsx +2 -2
  93. package/src/components/shared/ResourceActionsBar.tsx +142 -111
  94. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  95. package/src/components/shared/UnifiedDiff.tsx +51 -0
  96. package/src/components/shared/index.ts +1 -0
  97. package/src/components/timeline/TimelineList.tsx +375 -354
  98. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  99. package/src/components/timeline/TimelineStrip.tsx +836 -0
  100. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  101. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  102. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  103. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  104. package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
  105. package/src/components/timeline/index.ts +23 -0
  106. package/src/components/timeline/scrubber-math.test.ts +235 -0
  107. package/src/components/timeline/scrubber-math.ts +249 -0
  108. package/src/components/timeline/shared.test.tsx +219 -0
  109. package/src/components/timeline/shared.tsx +232 -447
  110. package/src/components/timeline/timeline-filters.test.ts +196 -0
  111. package/src/components/timeline/timeline-filters.ts +180 -0
  112. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  113. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  114. package/src/components/timeline/timeline-live.test.ts +131 -0
  115. package/src/components/timeline/timeline-live.ts +77 -0
  116. package/src/components/topology/GroupNode.tsx +9 -2
  117. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  118. package/src/components/topology/K8sResourceNode.tsx +387 -243
  119. package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
  120. package/src/components/topology/TopologyControls.tsx +3 -1
  121. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  122. package/src/components/topology/TopologyGraph.tsx +149 -90
  123. package/src/components/topology/TopologyOverlayBar.tsx +30 -0
  124. package/src/components/topology/TopologySearch.tsx +25 -17
  125. package/src/components/topology/index.ts +2 -0
  126. package/src/components/topology/layout.ts +10 -2
  127. package/src/components/topology/topology.css +0 -92
  128. package/src/components/ui/Badge.tsx +6 -1
  129. package/src/components/ui/CodeViewer.tsx +2 -2
  130. package/src/components/ui/Collapse.tsx +59 -0
  131. package/src/components/ui/ConfirmDialog.tsx +7 -3
  132. package/src/components/ui/DialogPortal.tsx +31 -9
  133. package/src/components/ui/Input.tsx +24 -0
  134. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  135. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  136. package/src/components/ui/PaneLoader.tsx +10 -2
  137. package/src/components/ui/ResourceBar.tsx +68 -25
  138. package/src/components/ui/SearchBox.tsx +18 -4
  139. package/src/components/ui/SearchPillInput.tsx +2 -2
  140. package/src/components/ui/SelectMenu.tsx +81 -0
  141. package/src/components/ui/drawer-components.tsx +48 -32
  142. package/src/components/ui/index.ts +6 -0
  143. package/src/components/ui/severity-tone.ts +11 -4
  144. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  145. package/src/components/workload/WorkloadView.tsx +2706 -475
  146. package/src/components/workload/index.ts +1 -1
  147. package/src/theme/tailwind-theme.css +1 -0
  148. package/src/theme/variables.css +2 -1
  149. package/src/types/core.ts +57 -4
  150. package/src/types/gitops-insights.ts +69 -1
  151. package/src/types/gitops.ts +3 -3
  152. package/src/utils/application-history.test.ts +538 -0
  153. package/src/utils/application-history.ts +257 -0
  154. package/src/utils/application-topology.test.ts +198 -0
  155. package/src/utils/application-topology.ts +261 -0
  156. package/src/utils/applications.test.ts +201 -1
  157. package/src/utils/applications.ts +1182 -393
  158. package/src/utils/index.ts +3 -0
  159. package/src/utils/navigation.test.ts +32 -1
  160. package/src/utils/navigation.ts +70 -0
  161. package/src/utils/resource-hierarchy.test.ts +1024 -1
  162. package/src/utils/resource-hierarchy.ts +925 -267
  163. package/src/utils/resource-icons.ts +2 -0
  164. package/src/utils/topology-neighborhood.test.ts +202 -1
  165. package/src/utils/topology-neighborhood.ts +194 -13
  166. package/src/utils/workflow-execution.test.ts +556 -0
  167. package/src/utils/workflow-execution.ts +814 -0
  168. package/src/utils/workload-colors.ts +2 -3
@@ -1,23 +1,16 @@
1
1
  import { Play, Clock, CheckCircle, XCircle, Loader2, SkipForward, PauseCircle } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
4
  import { formatAge, formatDuration } from '../resource-utils'
5
+ import { buildWorkflowExecutionModel, flattenWorkflowExecution, WorkflowExecutionModel, WorkflowExecutionNode, WorkflowTemplateReference } from '../../../utils/workflow-execution'
6
+ import { SEVERITY_TEXT } from '../../../utils/badge-colors'
5
7
 
6
8
  interface WorkflowRendererProps {
7
9
  data: any
10
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
8
11
  }
9
12
 
10
- interface WorkflowStep {
11
- id: string
12
- displayName: string
13
- phase: string
14
- startedAt: string | null
15
- finishedAt: string | null
16
- message: string | null
17
- nodeType: string
18
- }
19
-
20
- function getStepDuration(step: WorkflowStep): string | null {
13
+ function getStepDuration(step: WorkflowExecutionNode): string | null {
21
14
  if (!step.startedAt) return null
22
15
  const start = new Date(step.startedAt)
23
16
  const end = step.finishedAt ? new Date(step.finishedAt) : new Date()
@@ -29,15 +22,16 @@ function StepStatusIcon({ phase, nodeType }: { phase: string; nodeType?: string
29
22
  return <SkipForward className="w-4 h-4 text-theme-text-tertiary shrink-0" />
30
23
  }
31
24
  if (nodeType === 'Suspend' && phase !== 'Succeeded') {
32
- return <PauseCircle className="w-4 h-4 text-yellow-400 shrink-0" />
25
+ return <PauseCircle className={clsx('h-4 w-4 shrink-0', SEVERITY_TEXT.warning)} />
33
26
  }
34
27
  switch (phase) {
35
28
  case 'Succeeded':
36
- return <CheckCircle className="w-4 h-4 text-green-400 shrink-0" />
29
+ return <CheckCircle className={clsx('h-4 w-4 shrink-0', SEVERITY_TEXT.success)} />
37
30
  case 'Failed':
38
- return <XCircle className="w-4 h-4 text-red-400 shrink-0" />
31
+ case 'Error':
32
+ return <XCircle className={clsx('h-4 w-4 shrink-0', SEVERITY_TEXT.error)} />
39
33
  case 'Running':
40
- return <Loader2 className="w-4 h-4 text-yellow-400 shrink-0 animate-spin" />
34
+ return <Loader2 className={clsx('h-4 w-4 shrink-0 animate-spin', SEVERITY_TEXT.warning)} />
41
35
  default:
42
36
  return <Clock className="w-4 h-4 text-theme-text-tertiary shrink-0" />
43
37
  }
@@ -74,66 +68,41 @@ function formatEstimatedDuration(seconds: number): string {
74
68
  return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`
75
69
  }
76
70
 
77
- function getWorkflowProblems(data: any): string[] {
71
+ function getWorkflowProblems(data: any, execution: WorkflowExecutionModel): string[] {
78
72
  const problems: string[] = []
79
73
  const status = data.status || {}
80
74
  const phase = status.phase
81
75
 
82
76
  if (phase === 'Failed') {
83
- problems.push(status.message || 'Workflow failed')
77
+ problems.push(workflowProblemSummary(status.message || 'Workflow failed'))
84
78
  } else if (phase === 'Error') {
85
- problems.push(status.message || 'Workflow error')
79
+ problems.push(workflowProblemSummary(status.message || 'Workflow error'))
86
80
  }
87
81
 
88
- // Check for failed nodes — include error messages when available
89
- const nodes = status.nodes || {}
90
- const failedNodes = Object.values(nodes)
91
- .filter((node: any) => (node.type === 'Pod' || node.type === 'Suspend' || node.type === 'Skipped') && (node.phase === 'Failed' || node.phase === 'Error'))
82
+ const failedNodes = phase === 'Failed' || phase === 'Error'
83
+ ? execution.focusPaths.map((path) => path.terminal).filter((node, index, nodes) =>
84
+ (node.phase === 'Failed' || node.phase === 'Error') && nodes.findIndex((candidate) => candidate.id === node.id) === index,
85
+ )
86
+ : []
92
87
 
93
- if (failedNodes.length > 0) {
94
- const withMessages = failedNodes.filter((node: any) => node.message)
95
- if (withMessages.length > 0) {
96
- for (const n of withMessages as any[]) {
97
- problems.push(`${n.displayName}: ${n.message}`)
98
- }
99
- } else {
100
- problems.push(`Failed steps: ${failedNodes.map((n: any) => n.displayName).join(', ')}`)
101
- }
88
+ for (const node of failedNodes) {
89
+ problems.push(workflowProblemSummary(node.message ? `${node.displayLabel}: ${node.message}` : `${node.displayLabel} failed`))
102
90
  }
103
91
 
104
92
  return problems
105
93
  }
106
94
 
107
- const VISIBLE_NODE_TYPES = new Set(['Pod', 'Skipped', 'Suspend'])
108
-
109
- function extractSteps(data: any): WorkflowStep[] {
110
- const nodes = data.status?.nodes || {}
111
- const steps: WorkflowStep[] = Object.entries(nodes)
112
- .filter(([, node]: [string, any]) => VISIBLE_NODE_TYPES.has(node.type))
113
- .map(([id, node]: [string, any]) => ({
114
- id,
115
- displayName: node.displayName || id,
116
- phase: node.phase || (node.type === 'Skipped' ? 'Skipped' : 'Pending'),
117
- startedAt: node.startedAt || null,
118
- finishedAt: node.finishedAt || null,
119
- message: node.message || null,
120
- nodeType: node.type,
121
- }))
122
-
123
- steps.sort((a, b) => {
124
- if (!a.startedAt && !b.startedAt) return 0
125
- if (!a.startedAt) return 1
126
- if (!b.startedAt) return -1
127
- return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
128
- })
129
-
130
- return steps
95
+ function workflowProblemSummary(message: string): string {
96
+ return message.length > 300 ? `${message.slice(0, 299)}…` : message
131
97
  }
132
98
 
133
- export function WorkflowRenderer({ data }: WorkflowRendererProps) {
99
+ export function WorkflowRenderer({ data, onNavigate }: WorkflowRendererProps) {
134
100
  const status = data.status || {}
135
101
  const spec = data.spec || {}
136
102
  const phase = status.phase || 'Unknown'
103
+ const execution = buildWorkflowExecutionModel(data)
104
+ const executionNodes = execution.visibleSteps
105
+ const stepRows = flattenWorkflowExecution(execution)
137
106
 
138
107
  // Compute duration
139
108
  const startedAt = status.startedAt ? new Date(status.startedAt) : null
@@ -144,18 +113,13 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
144
113
  ? formatDuration(Date.now() - startedAt.getTime(), true) + ' (running)'
145
114
  : null
146
115
 
147
- // Extract problems
148
- const problems = getWorkflowProblems(data)
116
+ const problems = [...new Set(getWorkflowProblems(data, execution))]
149
117
  const hasProblems = problems.length > 0
150
118
 
151
- // Extract steps
152
- const steps = extractSteps(data)
153
-
154
119
  // Arguments
155
120
  const parameters = spec.arguments?.parameters || []
156
121
 
157
- // Template reference
158
- const templateName = spec.workflowTemplateRef?.name || null
122
+ const workflowTemplateRef = execution.templateRefs.find((ref) => ref.source === 'workflow')
159
123
 
160
124
  // Estimated duration
161
125
  const estimatedDuration = status.estimatedDuration
@@ -183,7 +147,7 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
183
147
  {duration && <Property label="Duration" value={duration} />}
184
148
  {status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
185
149
  <Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
186
- {templateName && <Property label="Template" value={templateName} />}
150
+ {workflowTemplateRef && <Property label="Definition" value={<TemplateRefLink refInfo={workflowTemplateRef} onNavigate={onNavigate} />} />}
187
151
  {status.progress && (
188
152
  <Property label="Progress" value={status.progress} />
189
153
  )}
@@ -203,33 +167,38 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
203
167
  </PropertyList>
204
168
  </Section>
205
169
 
206
- {/* Steps section */}
207
- {steps.length > 0 && (
208
- <Section title={`Steps (${steps.length})`} defaultExpanded>
170
+ {executionNodes.length > 0 && (
171
+ <Section title={`Execution (${executionNodes.length} ${executionNodes.length === 1 ? 'node' : 'nodes'})`} defaultExpanded>
209
172
  <div className="space-y-1.5">
210
- {steps.map(step => {
173
+ {stepRows.map(({ node: step, depth }) => {
211
174
  const isFailed = step.phase === 'Failed' || step.phase === 'Error'
212
- const isSkipped = step.nodeType === 'Skipped' || step.phase === 'Skipped'
213
- const isSuspend = step.nodeType === 'Suspend'
175
+ const isSkipped = step.type === 'Skipped' || step.phase === 'Skipped'
176
+ const isSuspend = step.type === 'Suspend'
214
177
  return (
215
178
  <div key={step.id} className={clsx(
216
179
  'text-sm card-inner px-3 py-2',
217
- isFailed && 'border-l-2 border-red-500'
218
- )}>
180
+ isFailed && 'border-l-2 border-l-[var(--color-error)]'
181
+ )} style={{ marginLeft: `${Math.min(depth, 6) * 12}px` }}>
219
182
  <div className="flex items-center gap-2">
220
- <StepStatusIcon phase={step.phase} nodeType={step.nodeType} />
183
+ <StepStatusIcon phase={step.phase} nodeType={step.type} />
221
184
  <span className={clsx(
222
185
  'flex-1',
223
- isSkipped ? 'text-theme-text-tertiary' : isSuspend ? 'text-yellow-400' : 'text-theme-text-primary'
186
+ isSkipped ? 'text-theme-text-tertiary' : isSuspend ? SEVERITY_TEXT.warning : 'text-theme-text-primary'
224
187
  )}>
225
- {step.displayName}
188
+ {step.displayLabel}
226
189
  {isSkipped && <span className="ml-1 text-xs text-theme-text-tertiary">(skipped)</span>}
227
- {isSuspend && <span className="ml-1 text-xs text-yellow-400/70">(suspend)</span>}
190
+ {isSuspend && <span className={clsx('ml-1 text-xs opacity-70', SEVERITY_TEXT.warning)}>(suspend)</span>}
228
191
  </span>
192
+ <span className="text-xs text-theme-text-tertiary">{step.displayType}</span>
229
193
  <span className="text-xs text-theme-text-secondary">{getStepDuration(step) || '-'}</span>
230
194
  </div>
231
195
  {isFailed && step.message && (
232
- <div className="text-xs text-red-400 mt-1 ml-6 break-all">{step.message}</div>
196
+ <div className={clsx('mt-1 ml-6 line-clamp-2 break-all text-xs', SEVERITY_TEXT.error)} title={step.message}>{step.message}</div>
197
+ )}
198
+ {step.templateRef && (
199
+ <div className="mt-1 ml-6 text-xs text-theme-text-tertiary">
200
+ Uses <TemplateRefLink refInfo={step.templateRef} onNavigate={onNavigate} />
201
+ </div>
233
202
  )}
234
203
  </div>
235
204
  )
@@ -254,3 +223,16 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
254
223
  </>
255
224
  )
256
225
  }
226
+
227
+ function TemplateRefLink({ refInfo, onNavigate }: { refInfo: WorkflowTemplateReference; onNavigate?: WorkflowRendererProps['onNavigate'] }) {
228
+ return (
229
+ <ResourceLink
230
+ name={refInfo.name}
231
+ kind={refInfo.resourceKind}
232
+ namespace={refInfo.namespace}
233
+ group="argoproj.io"
234
+ label={refInfo.clusterScope ? `${refInfo.name} (cluster)` : refInfo.name}
235
+ onNavigate={onNavigate}
236
+ />
237
+ )
238
+ }
@@ -23,6 +23,7 @@ export * from './CNPGScheduledBackupRenderer'
23
23
  export * from './ConfigAuditReportRenderer'
24
24
  export * from './ConfigMapRenderer'
25
25
  export * from './CronJobRenderer'
26
+ export * from './CronWorkflowRenderer'
26
27
  export * from './eso-cells'
27
28
  export * from './EventRenderer'
28
29
  export * from './EndpointSliceRenderer'
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { getArgoApplicationStatus } from './resource-utils-argo'
2
+ import { getArgoApplicationStatus, isArgoOperationInProgress } from './resource-utils-argo'
3
3
 
4
4
  describe('getArgoApplicationStatus', () => {
5
5
  // A Suspended Argo app is intentionally paused — neutral (sky), matching the
@@ -20,4 +20,21 @@ describe('getArgoApplicationStatus', () => {
20
20
  const badge = getArgoApplicationStatus({ status: { health: { status: 'Degraded' }, sync: { status: 'Synced' } } })
21
21
  expect(badge.level).toBe('unhealthy')
22
22
  })
23
+
24
+ it('surfaces an operation being terminated', () => {
25
+ const badge = getArgoApplicationStatus({ status: { operationState: { phase: 'Terminating' } } })
26
+ expect(badge.level).toBe('degraded')
27
+ expect(badge.text).toBe('Terminating')
28
+ })
29
+ })
30
+
31
+ describe('isArgoOperationInProgress', () => {
32
+ it.each([
33
+ [{ status: { operationState: { phase: 'Running' } } }, true],
34
+ [{ status: { operationState: { phase: 'Terminating' } } }, true],
35
+ [{ operation: { sync: {} }, status: { operationState: { phase: 'Succeeded' } } }, true],
36
+ [{ status: { operationState: { phase: 'Succeeded' } } }, false],
37
+ ])('maps %o to %s', (app, expected) => {
38
+ expect(isArgoOperationInProgress(app)).toBe(expected)
39
+ })
23
40
  })
@@ -33,6 +33,9 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
33
33
  if (opPhase === 'Running') {
34
34
  return { text: 'Syncing', color: healthColors.degraded, level: 'degraded' }
35
35
  }
36
+ if (opPhase === 'Terminating') {
37
+ return { text: 'Terminating', color: healthColors.degraded, level: 'degraded' }
38
+ }
36
39
 
37
40
  // Failed operation
38
41
  if (opPhase === 'Failed' || opPhase === 'Error') {
@@ -61,6 +64,11 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
61
64
  return { text: health || sync || 'Unknown', color: healthColors.unknown, level: 'unknown' }
62
65
  }
63
66
 
67
+ export function isArgoOperationInProgress(app: any): boolean {
68
+ const phase = app?.status?.operationState?.phase
69
+ return app?.operation != null || phase === 'Running' || phase === 'Terminating'
70
+ }
71
+
64
72
  // Radar suspends an Argo Application by clearing spec.syncPolicy.automated and
65
73
  // recording the prior prune/selfHeal flags in annotations so Resume can restore
66
74
  // them. The *presence* of any of these annotations marks the app as suspended —
@@ -300,6 +300,6 @@ export function getClusterProvider(resource: any): string {
300
300
  /** Parse CAPI compound condition messages ("* Foo: bar * Baz: qux") into a structured list */
301
301
  export function parseCAPIConditionMessage(message: string): string[] | null {
302
302
  if (!message || !message.includes('*')) return null
303
- const items = message.split(/\s*\*\s*/).filter(Boolean).map(s => s.trim())
303
+ const items = message.split('*').map(s => s.trim()).filter(Boolean)
304
304
  return items.length > 1 ? items : null
305
305
  }
@@ -1406,6 +1406,36 @@ export function getWorkflowTemplate(workflow: any): string | null {
1406
1406
  return workflow.spec?.workflowTemplateRef?.name || null
1407
1407
  }
1408
1408
 
1409
+ export function getCronWorkflowStatus(cwf: any): StatusBadge {
1410
+ const spec = cwf.spec || {}
1411
+ const status = cwf.status || {}
1412
+ const active = Array.isArray(status.active) ? status.active.length : 0
1413
+ if (spec.suspend === true) {
1414
+ return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
1415
+ }
1416
+ if (active > 0) {
1417
+ return { text: `${active} active`, color: healthColors.neutral, level: 'neutral' }
1418
+ }
1419
+ if (status.lastScheduledTime) {
1420
+ return { text: 'Idle', color: healthColors.neutral, level: 'neutral' }
1421
+ }
1422
+ return { text: 'Never run', color: healthColors.unknown, level: 'unknown' }
1423
+ }
1424
+
1425
+ export function getCronWorkflowSchedule(cwf: any): string {
1426
+ const schedules = cwf.spec?.schedules
1427
+ if (Array.isArray(schedules) && schedules.length > 0) return schedules.join(', ')
1428
+ return cwf.spec?.schedule || '-'
1429
+ }
1430
+
1431
+ export function getCronWorkflowLastRun(cwf: any): string {
1432
+ return cwf.status?.lastScheduledTime ? formatAge(cwf.status.lastScheduledTime) : 'Never'
1433
+ }
1434
+
1435
+ export function getCronWorkflowTemplate(cwf: any): string {
1436
+ return cwf.spec?.workflowSpec?.workflowTemplateRef?.name || cwf.spec?.workflowSpec?.entrypoint || '-'
1437
+ }
1438
+
1409
1439
  // ============================================================================
1410
1440
  // CERT-MANAGER UTILITIES — re-exported from resource-utils-certmanager.ts
1411
1441
  // ============================================================================
@@ -1866,6 +1896,7 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
1866
1896
  if (kindLower === 'persistentvolumes') return getPVStatus(resource).text
1867
1897
  if (kindLower === 'rollouts') return getRolloutStatus(resource).text
1868
1898
  if (kindLower === 'workflows') return getWorkflowStatus(resource).text
1899
+ if (kindLower === 'cronworkflows') return getCronWorkflowStatus(resource).text
1869
1900
  if (kindLower === 'hpas' || kindLower === 'horizontalpodautoscalers') return getHPAStatus(resource).text
1870
1901
  if (kindLower === 'gateways') return getGatewayStatus(resource).text
1871
1902
  if (kindLower === 'gatewayclasses') return getGatewayClassStatus(resource).text
@@ -52,9 +52,9 @@ export function DetailShell<TId extends string = string>({
52
52
  const visibleTabs = tabs.filter((t) => !t.hidden)
53
53
 
54
54
  return (
55
- <div className="flex flex-col h-full w-full bg-theme-surface">
55
+ <div className="flex flex-col h-full w-full bg-theme-base">
56
56
  {/* Header */}
57
- <div className="shrink-0 border-b border-theme-border bg-theme-surface">
57
+ <div className="shrink-0 border-b border-theme-border bg-theme-base">
58
58
  {!compactHeader && (
59
59
  <>
60
60
  {breadcrumb && <div className="px-6 pt-2.5">{breadcrumb}</div>}