@skyhook-io/k8s-ui 1.8.9 → 1.8.11

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 (130) hide show
  1. package/package.json +5 -4
  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 +569 -0
  6. package/src/components/applications/ApplicationDetail.tsx +2517 -450
  7. package/src/components/applications/ApplicationsList.tsx +4 -1
  8. package/src/components/applications/ApplicationsView.tsx +101 -18
  9. package/src/components/applications/applications-view.test.tsx +47 -0
  10. package/src/components/applications/index.ts +1 -1
  11. package/src/components/audit/AuditAlerts.tsx +14 -10
  12. package/src/components/checks/ChecksView.tsx +81 -51
  13. package/src/components/checks/severity.ts +39 -45
  14. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  15. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +14 -59
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
  22. package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
  23. package/src/components/gitops/insights/insights-helpers.ts +69 -1
  24. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
  25. package/src/components/issues/IssuesView.tsx +219 -143
  26. package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
  27. package/src/components/issues/ResourceIssuesSection.tsx +28 -128
  28. package/src/components/issues/index.ts +3 -2
  29. package/src/components/issues/issue-timing.ts +53 -0
  30. package/src/components/issues/issues.test.ts +146 -4
  31. package/src/components/issues/severity.ts +29 -32
  32. package/src/components/logs/LogCore.tsx +14 -4
  33. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  34. package/src/components/logs/useLogStream.ts +8 -1
  35. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  36. package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
  37. package/src/components/resources/ResourcesSidebar.tsx +70 -52
  38. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  39. package/src/components/resources/ResourcesView.tsx +243 -53
  40. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  42. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  43. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
  44. package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
  45. package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
  46. package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
  47. package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
  48. package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
  49. package/src/components/resources/renderers/PodRenderer.tsx +81 -70
  50. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  51. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  52. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  53. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  54. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  55. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  56. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  57. package/src/components/resources/renderers/index.ts +1 -0
  58. package/src/components/resources/resource-utils-capi.ts +1 -1
  59. package/src/components/resources/resource-utils.ts +31 -0
  60. package/src/components/shared/DetailShell.tsx +2 -2
  61. package/src/components/shared/ResourceActionsBar.tsx +140 -108
  62. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  63. package/src/components/timeline/TimelineList.tsx +371 -354
  64. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  65. package/src/components/timeline/TimelineStrip.tsx +836 -0
  66. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  67. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  68. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  69. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  70. package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
  71. package/src/components/timeline/index.ts +23 -0
  72. package/src/components/timeline/scrubber-math.test.ts +235 -0
  73. package/src/components/timeline/scrubber-math.ts +249 -0
  74. package/src/components/timeline/shared.test.tsx +219 -0
  75. package/src/components/timeline/shared.tsx +232 -447
  76. package/src/components/timeline/timeline-filters.test.ts +196 -0
  77. package/src/components/timeline/timeline-filters.ts +180 -0
  78. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  79. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  80. package/src/components/timeline/timeline-live.test.ts +131 -0
  81. package/src/components/timeline/timeline-live.ts +77 -0
  82. package/src/components/topology/GroupNode.tsx +9 -2
  83. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  84. package/src/components/topology/K8sResourceNode.tsx +387 -243
  85. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  86. package/src/components/topology/TopologyGraph.tsx +89 -16
  87. package/src/components/topology/TopologySearch.tsx +2 -2
  88. package/src/components/topology/layout.ts +10 -2
  89. package/src/components/topology/topology.css +0 -92
  90. package/src/components/ui/Badge.tsx +5 -0
  91. package/src/components/ui/BoardSkeleton.tsx +47 -0
  92. package/src/components/ui/CardSection.tsx +117 -0
  93. package/src/components/ui/CodeViewer.tsx +2 -2
  94. package/src/components/ui/Collapse.tsx +59 -0
  95. package/src/components/ui/ConfirmDialog.tsx +7 -3
  96. package/src/components/ui/DialogPortal.tsx +31 -9
  97. package/src/components/ui/Input.tsx +24 -0
  98. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  99. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  100. package/src/components/ui/PaneLoader.tsx +10 -2
  101. package/src/components/ui/ResourceBar.tsx +68 -25
  102. package/src/components/ui/SearchBox.tsx +18 -4
  103. package/src/components/ui/SearchPillInput.tsx +2 -2
  104. package/src/components/ui/Toast.tsx +1 -1
  105. package/src/components/ui/drawer-components.tsx +39 -26
  106. package/src/components/ui/index.ts +6 -0
  107. package/src/components/ui/severity-tone.ts +63 -0
  108. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  109. package/src/components/workload/WorkloadView.tsx +2682 -472
  110. package/src/components/workload/index.ts +1 -1
  111. package/src/theme/variables.css +15 -1
  112. package/src/types/core.ts +57 -4
  113. package/src/utils/api-resources.test.ts +54 -0
  114. package/src/utils/api-resources.ts +68 -1
  115. package/src/utils/application-topology.test.ts +198 -0
  116. package/src/utils/application-topology.ts +261 -0
  117. package/src/utils/applications.test.ts +167 -1
  118. package/src/utils/applications.ts +1157 -393
  119. package/src/utils/index.ts +2 -0
  120. package/src/utils/navigation.test.ts +32 -1
  121. package/src/utils/navigation.ts +70 -0
  122. package/src/utils/resource-hierarchy.test.ts +990 -1
  123. package/src/utils/resource-hierarchy.ts +905 -267
  124. package/src/utils/resource-icons.ts +2 -0
  125. package/src/utils/topology-neighborhood.test.ts +202 -1
  126. package/src/utils/topology-neighborhood.ts +194 -13
  127. package/src/utils/workflow-execution.test.ts +556 -0
  128. package/src/utils/workflow-execution.ts +814 -0
  129. package/src/utils/workload-colors.ts +2 -3
  130. package/tsconfig.json +2 -2
@@ -217,6 +217,8 @@ export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecr
217
217
  ref={textareaRef}
218
218
  value={editValue}
219
219
  onChange={(e) => setEditValue(e.target.value)}
220
+ autoCorrect="off"
221
+ autoCapitalize="off"
220
222
  className="w-full bg-theme-base rounded p-2 text-xs text-theme-text-secondary font-mono border border-blue-500/50 focus:border-blue-500 focus:outline-none resize-none overflow-hidden whitespace-pre-wrap"
221
223
  style={{ minHeight: '60px' }}
222
224
  disabled={isSaving}
@@ -3,6 +3,15 @@ import { Globe, Clock, Radio } from 'lucide-react'
3
3
  import { Section, PropertyList, Property, KeyValueBadgeList, CopyHandler, AlertBanner } from '../../ui/drawer-components'
4
4
  import type { ResourceRef } from '../../../types'
5
5
 
6
+ export interface ServicePortRenderProps {
7
+ namespace: string
8
+ serviceName: string
9
+ port: number
10
+ protocol: string
11
+ name?: string
12
+ appProtocol?: string
13
+ }
14
+
6
15
  interface ServiceRendererProps {
7
16
  data: any
8
17
  onCopy: CopyHandler
@@ -10,11 +19,8 @@ interface ServiceRendererProps {
10
19
  endpointSlices?: any[]
11
20
  endpointSlicesLoading?: boolean
12
21
  onNavigate?: (ref: ResourceRef) => void
13
- renderPortAction?: (props: { namespace: string; serviceName: string; port: number; protocol: string; name?: string; appProtocol?: string }) => ReactNode
14
- /** Optional full-width content rendered inside a port's card, below its header
15
- * (e.g. an inline probe panel). Lets a host attach a port-scoped panel in the
16
- * drawer flow rather than as a separate overlay. */
17
- renderPortPanel?: (props: { namespace: string; serviceName: string; port: number; protocol: string; name?: string; appProtocol?: string }) => ReactNode
22
+ renderPortAction?: (props: ServicePortRenderProps) => ReactNode
23
+ renderPortPanel?: (props: ServicePortRenderProps) => ReactNode
18
24
  }
19
25
 
20
26
  function endpointSliceAddressCount(slice: any): number {
@@ -37,7 +43,6 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
37
43
  const ports = spec.ports || []
38
44
  const lbIngress = data.status?.loadBalancer?.ingress || []
39
45
  const namespace = data.metadata?.namespace
40
- const serviceName = data.metadata?.name
41
46
 
42
47
  const isLoadBalancer = spec.type === 'LoadBalancer'
43
48
  const isExternalName = spec.type === 'ExternalName'
@@ -96,40 +101,7 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
96
101
 
97
102
  {ports.length > 0 && (
98
103
  <Section title="Ports" defaultExpanded>
99
- <div className="space-y-2">
100
- {ports.map((port: any, i: number) => (
101
- <div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
102
- <div className="flex items-center justify-between gap-2">
103
- <div className="flex items-baseline gap-x-2 gap-y-0.5 min-w-0 flex-wrap">
104
- <span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
105
- <span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
106
- <span className="text-xs text-theme-text-secondary font-mono">
107
- {port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
108
- {port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
109
- </span>
110
- </div>
111
- <div className="flex items-center gap-2 shrink-0">
112
- {renderPortAction?.({
113
- namespace,
114
- serviceName,
115
- port: port.port,
116
- protocol: port.protocol || 'TCP',
117
- name: port.name,
118
- appProtocol: port.appProtocol,
119
- })}
120
- </div>
121
- </div>
122
- {renderPortPanel?.({
123
- namespace,
124
- serviceName,
125
- port: port.port,
126
- protocol: port.protocol || 'TCP',
127
- name: port.name,
128
- appProtocol: port.appProtocol,
129
- })}
130
- </div>
131
- ))}
132
- </div>
104
+ <ServicePortCards service={data} renderPortAction={renderPortAction} renderPortPanel={renderPortPanel} />
133
105
  </Section>
134
106
  )}
135
107
 
@@ -184,3 +156,49 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
184
156
  </>
185
157
  )
186
158
  }
159
+
160
+ export function ServicePortCards({
161
+ service,
162
+ renderPortAction,
163
+ renderPortPanel,
164
+ }: {
165
+ service: any
166
+ renderPortAction?: (props: ServicePortRenderProps) => ReactNode
167
+ renderPortPanel?: (props: ServicePortRenderProps) => ReactNode
168
+ }) {
169
+ const ports = service?.spec?.ports || []
170
+ const namespace = service?.metadata?.namespace || ''
171
+ const serviceName = service?.metadata?.name || ''
172
+ return (
173
+ <div className="space-y-2">
174
+ {ports.map((port: any, i: number) => {
175
+ const props: ServicePortRenderProps = {
176
+ namespace,
177
+ serviceName,
178
+ port: port.port,
179
+ protocol: port.protocol || 'TCP',
180
+ name: port.name,
181
+ appProtocol: port.appProtocol,
182
+ }
183
+ return (
184
+ <div key={`${serviceName}-${port.name || i}-${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
185
+ <div className="flex items-center justify-between gap-2">
186
+ <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
187
+ <span className="font-medium text-theme-text-primary">{port.name || `port-${i + 1}`}</span>
188
+ <span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
189
+ <span className="font-mono text-xs text-theme-text-secondary">
190
+ {port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
191
+ {port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
192
+ </span>
193
+ </div>
194
+ <div className="flex shrink-0 items-center gap-2">
195
+ {renderPortAction?.(props)}
196
+ </div>
197
+ </div>
198
+ {renderPortPanel?.(props)}
199
+ </div>
200
+ )
201
+ })}
202
+ </div>
203
+ )
204
+ }
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react'
2
2
  import { Shield, Package, ChevronDown, ChevronRight, ChevronUp, ExternalLink, Search, ArrowUpDown, Wrench } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
+ import { Input } from '../../ui/Input'
5
6
  import { formatAge } from '../resource-utils'
6
7
  import { SEVERITY_BADGE_COLORS, SEVERITY_BAR_COLORS, SEVERITY_TEXT_COLORS, SEVERITY_ORDER, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
7
8
 
@@ -222,8 +223,7 @@ export function VulnerabilityReportRenderer({ data }: VulnerabilityReportRendere
222
223
  <div className="flex items-center gap-2 mb-2 px-1">
223
224
  <div className="relative flex-1">
224
225
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
225
- <input
226
- type="text"
226
+ <Input
227
227
  placeholder="Filter by CVE or package..."
228
228
  value={searchTerm}
229
229
  onChange={(e) => { setSearchTerm(e.target.value); setShowAll(false) }}
@@ -0,0 +1,43 @@
1
+ import { renderToString } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { WorkflowRenderer } from './WorkflowRenderer'
5
+
6
+ describe('WorkflowRenderer', () => {
7
+ it('does not turn retained failed attempts into a failed workflow', () => {
8
+ const html = renderToString(<WorkflowRenderer data={{
9
+ metadata: { name: 'retry-workflow' },
10
+ status: {
11
+ phase: 'Succeeded',
12
+ nodes: {
13
+ root: { name: 'retry-workflow', displayName: 'retry-workflow', templateName: 'main', type: 'Retry', phase: 'Succeeded', children: ['attempt'] },
14
+ attempt: { displayName: 'retry-workflow(0)', type: 'Pod', phase: 'Failed', message: 'first attempt failed' },
15
+ },
16
+ },
17
+ }} />)
18
+
19
+ expect(html).toContain('Workflow Completed Successfully')
20
+ expect(html).not.toContain('Workflow Issues')
21
+ expect(html).toContain('Execution (2 nodes)')
22
+ })
23
+
24
+ it('lists every failed node and bounds long problem summaries', () => {
25
+ const longMessage = 'x'.repeat(500)
26
+ const html = renderToString(<WorkflowRenderer data={{
27
+ metadata: { name: 'failed-workflow' },
28
+ status: {
29
+ phase: 'Failed',
30
+ message: 'workflow failed',
31
+ nodes: {
32
+ root: { name: 'failed-workflow', displayName: 'failed-workflow', templateName: 'main', type: 'DAG', phase: 'Failed', children: ['with-message', 'without-message'] },
33
+ 'with-message': { displayName: 'prepare', type: 'Pod', phase: 'Failed', message: longMessage },
34
+ 'without-message': { displayName: 'publish', type: 'Pod', phase: 'Failed' },
35
+ },
36
+ },
37
+ }} />)
38
+
39
+ expect(html).toContain('Workflow Issues')
40
+ expect(html).toContain('publish failed')
41
+ expect(html).toContain(`prepare: ${'x'.repeat(290)}…`)
42
+ })
43
+ })
@@ -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'
@@ -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>}