@skyhook-io/k8s-ui 1.3.2 → 1.4.1

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 (78) hide show
  1. package/package.json +2 -2
  2. package/src/components/audit/AuditAlerts.tsx +88 -0
  3. package/src/components/audit/AuditCard.tsx +130 -0
  4. package/src/components/audit/AuditFindingsTable.tsx +526 -0
  5. package/src/components/audit/index.ts +3 -0
  6. package/src/components/dock/DockContext.tsx +5 -2
  7. package/src/components/dock/LocalTerminalTab.tsx +11 -0
  8. package/src/components/resources/ResourcesView.tsx +126 -49
  9. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  10. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  11. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  12. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  13. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  14. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  15. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
  16. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  17. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  18. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  19. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
  20. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  21. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  22. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  23. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  24. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  25. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  26. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  27. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  28. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  29. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  30. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  31. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  32. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
  33. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
  34. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  35. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  36. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  37. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  38. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  39. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  41. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  42. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  43. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  44. package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
  45. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  46. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  47. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  48. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  49. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  50. package/src/components/resources/renderers/index.ts +3 -0
  51. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  52. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  53. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  54. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  55. package/src/components/resources/resource-utils.ts +131 -33
  56. package/src/components/shared/CreateResourceDialog.tsx +236 -0
  57. package/src/components/shared/EditableYamlView.tsx +16 -1
  58. package/src/components/shared/ResourceActionsBar.tsx +9 -7
  59. package/src/components/shared/ResourceRendererDispatch.tsx +13 -6
  60. package/src/components/shared/index.ts +1 -0
  61. package/src/components/timeline/TimelineList.tsx +11 -4
  62. package/src/components/topology/K8sResourceNode.tsx +15 -0
  63. package/src/components/topology/TopologyControls.tsx +21 -1
  64. package/src/components/topology/TopologyGraph.tsx +1 -1
  65. package/src/components/ui/Badge.tsx +8 -0
  66. package/src/components/ui/drawer-components.tsx +68 -5
  67. package/src/components/workload/WorkloadView.tsx +55 -29
  68. package/src/index.ts +3 -0
  69. package/src/theme/variables.css +1 -1
  70. package/src/types/core.ts +2 -1
  71. package/src/types/gitops.ts +8 -0
  72. package/src/utils/badge-colors.ts +7 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/k8s-errors.ts +142 -0
  75. package/src/utils/navigation.test.ts +29 -2
  76. package/src/utils/navigation.ts +45 -24
  77. package/src/utils/resource-icons.ts +3 -0
  78. package/src/utils/skeleton-yaml.ts +302 -0
@@ -1,5 +1,5 @@
1
- import { useState } from 'react'
2
- import { Package, ChevronDown, ChevronRight } from 'lucide-react'
1
+ import { useState, useMemo } from 'react'
2
+ import { Package, ChevronDown, ChevronRight, Search } from 'lucide-react'
3
3
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
4
4
  import { formatAge } from '../resource-utils'
5
5
  import { formatTrivyImage } from './trivy-shared'
@@ -13,6 +13,7 @@ const INITIAL_SHOW_COUNT = 100
13
13
  export function SbomReportRenderer({ data }: SbomReportRendererProps) {
14
14
  const [showAll, setShowAll] = useState(false)
15
15
  const [expanded, setExpanded] = useState(true)
16
+ const [searchTerm, setSearchTerm] = useState('')
16
17
 
17
18
  const report = data.report || {}
18
19
  const scanner = report.scanner || {}
@@ -29,7 +30,18 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
29
30
  const bomFormat = components.bomFormat || '-'
30
31
  const specVersion = components.specVersion || '-'
31
32
 
32
- const displayedComponents = showAll ? bom : bom.slice(0, INITIAL_SHOW_COUNT)
33
+ const filteredBom = useMemo(() => {
34
+ if (!searchTerm) return bom
35
+ const term = searchTerm.toLowerCase()
36
+ return bom.filter((comp: any) =>
37
+ (comp.name || '').toLowerCase().includes(term) ||
38
+ (comp.purl || '').toLowerCase().includes(term) ||
39
+ (comp.type || '').toLowerCase().includes(term)
40
+ )
41
+ }, [bom, searchTerm])
42
+
43
+ const hasActiveFilter = searchTerm !== ''
44
+ const displayedComponents = showAll ? filteredBom : filteredBom.slice(0, INITIAL_SHOW_COUNT)
33
45
 
34
46
  return (
35
47
  <>
@@ -58,6 +70,30 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
58
70
  </button>
59
71
  {expanded && (
60
72
  <div className="overflow-x-auto -mx-1">
73
+ {/* Search */}
74
+ <div className="flex items-center gap-2 mb-2 px-1">
75
+ <div className="relative flex-1">
76
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
77
+ <input
78
+ type="text"
79
+ placeholder="Filter by name, type, or purl..."
80
+ value={searchTerm}
81
+ onChange={(e) => { setSearchTerm(e.target.value); setShowAll(false) }}
82
+ className="w-full pl-6 pr-2 py-1 text-xs bg-theme-bg border border-theme-border rounded text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
83
+ />
84
+ </div>
85
+ </div>
86
+ {hasActiveFilter && (
87
+ <div className="flex items-center gap-1 mb-2 px-1 text-xs text-theme-text-tertiary">
88
+ Showing {filteredBom.length} of {bom.length}
89
+ <button
90
+ onClick={() => { setSearchTerm(''); setShowAll(false) }}
91
+ className="ml-1 text-blue-400 hover:text-blue-300 hover:underline"
92
+ >
93
+ Clear
94
+ </button>
95
+ </div>
96
+ )}
61
97
  <table className="w-full text-xs">
62
98
  <thead>
63
99
  <tr className="border-b border-theme-border text-theme-text-tertiary">
@@ -81,12 +117,17 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
81
117
  })}
82
118
  </tbody>
83
119
  </table>
84
- {!showAll && bom.length > INITIAL_SHOW_COUNT && (
120
+ {displayedComponents.length === 0 && (
121
+ <div className="py-4 text-center text-xs text-theme-text-tertiary">
122
+ No components match the current filter.
123
+ </div>
124
+ )}
125
+ {!showAll && filteredBom.length > INITIAL_SHOW_COUNT && (
85
126
  <button
86
127
  onClick={() => setShowAll(true)}
87
128
  className="mt-2 text-xs text-blue-400 hover:text-blue-300 hover:underline"
88
129
  >
89
- Show all {bom.length} components
130
+ Show all {filteredBom.length} components
90
131
  </button>
91
132
  )}
92
133
  </div>
@@ -1,9 +1,10 @@
1
1
  import { Lock, Key, FileText } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
4
 
5
5
  interface SealedSecretRendererProps {
6
6
  data: any
7
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
7
8
  }
8
9
 
9
10
  function getScope(annotations: Record<string, string> | undefined): string {
@@ -13,7 +14,7 @@ function getScope(annotations: Record<string, string> | undefined): string {
13
14
  return 'strict'
14
15
  }
15
16
 
16
- export function SealedSecretRenderer({ data }: SealedSecretRendererProps) {
17
+ export function SealedSecretRenderer({ data, onNavigate }: SealedSecretRendererProps) {
17
18
  const spec = data.spec || {}
18
19
  const status = data.status || {}
19
20
  const conditions = status.conditions || []
@@ -65,6 +66,14 @@ export function SealedSecretRenderer({ data }: SealedSecretRendererProps) {
65
66
  </span>
66
67
  }
67
68
  />
69
+ <Property label="Target Secret" value={
70
+ <ResourceLink
71
+ name={data.spec?.template?.metadata?.name || data.metadata?.name || ''}
72
+ kind="secrets"
73
+ namespace={data.metadata?.namespace || ''}
74
+ onNavigate={onNavigate}
75
+ />
76
+ } />
68
77
  <Property label="Secret Type" value={secretType} />
69
78
  <Property label="Scope" value={scope} />
70
79
  <Property label="Observed Gen" value={status.observedGeneration} />
@@ -1,5 +1,5 @@
1
1
  import { Radio } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, KeyValueBadgeList } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, LabelSelectorDisplay } from '../../ui/drawer-components'
3
3
  import {
4
4
  getServiceMonitorEndpoints,
5
5
  getServiceMonitorJobLabel,
@@ -12,7 +12,7 @@ interface ServiceMonitorRendererProps {
12
12
 
13
13
  export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
14
14
  const endpoints = getServiceMonitorEndpoints(data)
15
- const selector = data.spec?.selector?.matchLabels
15
+ const selector = data.spec?.selector
16
16
  const conditions = data.status?.conditions
17
17
 
18
18
  return (
@@ -53,11 +53,9 @@ export function ServiceMonitorRenderer({ data }: ServiceMonitorRendererProps) {
53
53
  </Section>
54
54
  )}
55
55
 
56
- {selector && Object.keys(selector).length > 0 && (
57
- <Section title="Selector">
58
- <KeyValueBadgeList items={selector} />
59
- </Section>
60
- )}
56
+ <Section title="Selector">
57
+ <LabelSelectorDisplay selector={selector} emptyText="No selector" />
58
+ </Section>
61
59
 
62
60
  <ConditionsSection conditions={conditions} />
63
61
  </>
@@ -16,11 +16,10 @@ export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: Serv
16
16
  const namespace = data.metadata?.namespace
17
17
  const serviceName = data.metadata?.name
18
18
 
19
- // Check for issues
20
19
  const isLoadBalancer = spec.type === 'LoadBalancer'
20
+ const isExternalName = spec.type === 'ExternalName'
21
21
  const lbPending = isLoadBalancer && lbIngress.length === 0
22
22
  const hasNoSelector = !spec.selector || Object.keys(spec.selector).length === 0
23
- const isExternalName = spec.type === 'ExternalName'
24
23
 
25
24
  return (
26
25
  <>
@@ -46,47 +45,60 @@ export function ServiceRenderer({ data, onCopy, copied, renderPortAction }: Serv
46
45
  <Section title="Service" icon={Globe}>
47
46
  <PropertyList>
48
47
  <Property label="Type" value={spec.type || 'ClusterIP'} />
49
- <Property label="Cluster IP" value={spec.clusterIP} copyable onCopy={onCopy} copied={copied} />
48
+ {isExternalName ? (
49
+ <Property label="External Name" value={spec.externalName} copyable onCopy={onCopy} copied={copied} />
50
+ ) : (
51
+ <Property label="Cluster IP" value={spec.clusterIP} copyable onCopy={onCopy} copied={copied} />
52
+ )}
50
53
  {spec.externalIPs?.length > 0 && (
51
54
  <Property label="External IPs" value={spec.externalIPs.join(', ')} copyable onCopy={onCopy} copied={copied} />
52
55
  )}
53
- {lbIngress.length > 0 && (
56
+ {lbIngress.map((ing: any, i: number) => (
54
57
  <Property
55
- label="Load Balancer"
56
- value={lbIngress[0].ip || lbIngress[0].hostname}
58
+ key={i}
59
+ label={lbIngress.length > 1 ? `Load Balancer ${i + 1}` : 'Load Balancer'}
60
+ value={ing.ip || ing.hostname}
57
61
  copyable
58
62
  onCopy={onCopy}
59
63
  copied={copied}
60
64
  />
61
- )}
65
+ ))}
62
66
  <Property label="Session Affinity" value={spec.sessionAffinity} />
63
67
  <Property label="External Traffic" value={spec.externalTrafficPolicy} />
68
+ <Property label="Internal Traffic" value={spec.internalTrafficPolicy} />
69
+ {spec.ipFamilyPolicy && <Property label="IP Family Policy" value={spec.ipFamilyPolicy} />}
70
+ {spec.ipFamilies?.length > 0 && <Property label="IP Families" value={spec.ipFamilies.join(', ')} />}
64
71
  </PropertyList>
65
72
  </Section>
66
73
 
67
- <Section title="Ports" defaultExpanded>
68
- <div className="space-y-2">
69
- {ports.map((port: any, i: number) => (
70
- <div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
71
- <div className="flex items-center justify-between">
72
- <span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
73
- <div className="flex items-center gap-2">
74
- {renderPortAction?.({
75
- namespace,
76
- serviceName,
77
- port: port.port,
78
- protocol: port.protocol || 'TCP',
79
- })}
74
+ {ports.length > 0 && (
75
+ <Section title="Ports" defaultExpanded>
76
+ <div className="space-y-2">
77
+ {ports.map((port: any, i: number) => (
78
+ <div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
79
+ <div className="flex items-center justify-between">
80
+ <div className="flex items-center gap-2">
81
+ <span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
82
+ <span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
83
+ </div>
84
+ <div className="flex items-center gap-2">
85
+ {renderPortAction?.({
86
+ namespace,
87
+ serviceName,
88
+ port: port.port,
89
+ protocol: port.protocol || 'TCP',
90
+ })}
91
+ </div>
92
+ </div>
93
+ <div className="text-xs text-theme-text-secondary mt-1">
94
+ {port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
95
+ {port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
80
96
  </div>
81
97
  </div>
82
- <div className="text-xs text-theme-text-secondary mt-1">
83
- {port.port} {port.targetPort !== port.port && `→ ${port.targetPort}`}
84
- {port.nodePort && ` (NodePort: ${port.nodePort})`}
85
- </div>
86
- </div>
87
- ))}
88
- </div>
89
- </Section>
98
+ ))}
99
+ </div>
100
+ </Section>
101
+ )}
90
102
 
91
103
  {spec.selector && (
92
104
  <Section title="Selector">
@@ -56,7 +56,7 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
56
56
  <AlertBanner
57
57
  variant="error"
58
58
  title={isFailed ? 'Backup Failed' : 'Backup Partially Failed'}
59
- message={`${errors} error(s) occurred during backup.`}
59
+ message={status.failureReason || `${errors} error(s) occurred during backup.`}
60
60
  items={validationErrors.length > 0 ? validationErrors : undefined}
61
61
  />
62
62
  )}
@@ -84,8 +84,16 @@ export function VeleroBackupRenderer({ data }: VeleroBackupRendererProps) {
84
84
  )}
85
85
  <Property label="Duration" value={getBackupDuration(data)} />
86
86
  <Property label="Expiration" value={getBackupExpiry(data)} />
87
- {errors > 0 && <Property label="Errors" value={String(errors)} />}
88
- {warnings > 0 && <Property label="Warnings" value={String(warnings)} />}
87
+ <Property label="Errors" value={
88
+ errors > 0
89
+ ? <span className="text-red-500 dark:text-red-400 font-medium">{errors}</span>
90
+ : '0'
91
+ } />
92
+ <Property label="Warnings" value={
93
+ warnings > 0
94
+ ? <span className="text-amber-500 dark:text-amber-400 font-medium">{warnings}</span>
95
+ : '0'
96
+ } />
89
97
  </PropertyList>
90
98
  </Section>
91
99
 
@@ -49,7 +49,7 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
49
49
  <AlertBanner
50
50
  variant="error"
51
51
  title={isFailed ? 'Restore Failed' : 'Restore Partially Failed'}
52
- message={`${errors} error(s) occurred during restore.`}
52
+ message={status.failureReason || `${errors} error(s) occurred during restore.`}
53
53
  />
54
54
  )}
55
55
  {warnings > 0 && !isFailed && (
@@ -76,8 +76,16 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
76
76
  <Property label="Completed" value={formatAge(status.completionTimestamp) + ' ago'} />
77
77
  )}
78
78
  <Property label="Duration" value={getRestoreDuration(data)} />
79
- {errors > 0 && <Property label="Errors" value={String(errors)} />}
80
- {warnings > 0 && <Property label="Warnings" value={String(warnings)} />}
79
+ <Property label="Errors" value={
80
+ errors > 0
81
+ ? <span className="text-red-500 dark:text-red-400 font-medium">{errors}</span>
82
+ : '0'
83
+ } />
84
+ <Property label="Warnings" value={
85
+ warnings > 0
86
+ ? <span className="text-amber-500 dark:text-amber-400 font-medium">{warnings}</span>
87
+ : '0'
88
+ } />
81
89
  </PropertyList>
82
90
  </Section>
83
91
 
@@ -1,6 +1,6 @@
1
1
  import { Shield } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
3
+ import { Section, PropertyList, Property, AlertBanner, LabelSelectorDisplay } from '../../ui/drawer-components'
4
4
 
5
5
  interface WebhookConfigRendererProps {
6
6
  data: any
@@ -65,8 +65,8 @@ export function WebhookConfigRenderer({ data, isMutating }: WebhookConfigRendere
65
65
  ? `URL: ${url}`
66
66
  : 'No target'
67
67
 
68
- const nsLabels = wh.namespaceSelector?.matchLabels
69
- const objLabels = wh.objectSelector?.matchLabels
68
+ const nsSelector = wh.namespaceSelector
69
+ const objSelector = wh.objectSelector
70
70
 
71
71
  return (
72
72
  <div key={i} className="card-inner-lg">
@@ -119,26 +119,18 @@ export function WebhookConfigRenderer({ data, isMutating }: WebhookConfigRendere
119
119
  )}
120
120
 
121
121
  {/* Selectors */}
122
- {(nsLabels || objLabels) && (
122
+ {(nsSelector || objSelector) && (
123
123
  <div className="mt-2 space-y-1">
124
- {nsLabels && Object.keys(nsLabels).length > 0 && (
124
+ {nsSelector && (
125
125
  <div>
126
126
  <div className="text-xs text-theme-text-tertiary mb-1">Namespace Selector</div>
127
- <div className="flex flex-wrap gap-1">
128
- {Object.entries(nsLabels).map(([k, v]) => (
129
- <span key={k} className="badge-sm bg-theme-elevated text-theme-text-secondary">{k}={String(v)}</span>
130
- ))}
131
- </div>
127
+ <LabelSelectorDisplay selector={nsSelector} />
132
128
  </div>
133
129
  )}
134
- {objLabels && Object.keys(objLabels).length > 0 && (
130
+ {objSelector && (
135
131
  <div>
136
132
  <div className="text-xs text-theme-text-tertiary mb-1">Object Selector</div>
137
- <div className="flex flex-wrap gap-1">
138
- {Object.entries(objLabels).map(([k, v]) => (
139
- <span key={k} className="badge-sm bg-theme-elevated text-theme-text-secondary">{k}={String(v)}</span>
140
- ))}
141
- </div>
133
+ <LabelSelectorDisplay selector={objSelector} />
142
134
  </div>
143
135
  )}
144
136
  </div>
@@ -1,4 +1,4 @@
1
- import { Play, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react'
1
+ import { Play, Clock, CheckCircle, XCircle, Loader2, SkipForward, PauseCircle } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
4
4
  import { formatAge, formatDuration } from '../resource-utils'
@@ -13,6 +13,8 @@ interface WorkflowStep {
13
13
  phase: string
14
14
  startedAt: string | null
15
15
  finishedAt: string | null
16
+ message: string | null
17
+ nodeType: string
16
18
  }
17
19
 
18
20
  function getStepDuration(step: WorkflowStep): string | null {
@@ -22,7 +24,13 @@ function getStepDuration(step: WorkflowStep): string | null {
22
24
  return formatDuration(end.getTime() - start.getTime(), true)
23
25
  }
24
26
 
25
- function StepStatusIcon({ phase }: { phase: string }) {
27
+ function StepStatusIcon({ phase, nodeType }: { phase: string; nodeType?: string }) {
28
+ if (nodeType === 'Skipped' || phase === 'Skipped') {
29
+ return <SkipForward className="w-4 h-4 text-theme-text-tertiary shrink-0" />
30
+ }
31
+ if (nodeType === 'Suspend' && phase !== 'Succeeded') {
32
+ return <PauseCircle className="w-4 h-4 text-yellow-400 shrink-0" />
33
+ }
26
34
  switch (phase) {
27
35
  case 'Succeeded':
28
36
  return <CheckCircle className="w-4 h-4 text-green-400 shrink-0" />
@@ -46,6 +54,9 @@ function getPhaseBadgeClass(phase: string): string {
46
54
  return 'status-unhealthy'
47
55
  case 'Pending':
48
56
  return 'status-degraded'
57
+ case 'Skipped':
58
+ case 'Omitted':
59
+ return 'status-unknown'
49
60
  default:
50
61
  return 'status-unknown'
51
62
  }
@@ -74,29 +85,39 @@ function getWorkflowProblems(data: any): string[] {
74
85
  problems.push(status.message || 'Workflow error')
75
86
  }
76
87
 
77
- // Check for failed nodes
88
+ // Check for failed nodes — include error messages when available
78
89
  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)
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
92
 
83
- if (failedSteps.length > 0) {
84
- problems.push(`Failed steps: ${failedSteps.join(', ')}`)
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
+ }
85
102
  }
86
103
 
87
104
  return problems
88
105
  }
89
106
 
107
+ const VISIBLE_NODE_TYPES = new Set(['Pod', 'Skipped', 'Suspend'])
108
+
90
109
  function extractSteps(data: any): WorkflowStep[] {
91
110
  const nodes = data.status?.nodes || {}
92
111
  const steps: WorkflowStep[] = Object.entries(nodes)
93
- .filter(([, node]: [string, any]) => node.type === 'Pod')
112
+ .filter(([, node]: [string, any]) => VISIBLE_NODE_TYPES.has(node.type))
94
113
  .map(([id, node]: [string, any]) => ({
95
114
  id,
96
115
  displayName: node.displayName || id,
97
- phase: node.phase || 'Pending',
116
+ phase: node.phase || (node.type === 'Skipped' ? 'Skipped' : 'Pending'),
98
117
  startedAt: node.startedAt || null,
99
118
  finishedAt: node.finishedAt || null,
119
+ message: node.message || null,
120
+ nodeType: node.type,
100
121
  }))
101
122
 
102
123
  steps.sort((a, b) => {
@@ -163,9 +184,22 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
163
184
  {status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
164
185
  <Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
165
186
  {templateName && <Property label="Template" value={templateName} />}
187
+ {status.progress && (
188
+ <Property label="Progress" value={status.progress} />
189
+ )}
166
190
  {estimatedDuration != null && (
167
191
  <Property label="Estimated Duration" value={formatEstimatedDuration(estimatedDuration)} />
168
192
  )}
193
+ {status.resourcesDuration && (
194
+ <Property
195
+ label="Resource Usage"
196
+ value={
197
+ Object.entries(status.resourcesDuration as Record<string, number>)
198
+ .map(([resource, seconds]) => `${resource}: ${seconds}s`)
199
+ .join(', ')
200
+ }
201
+ />
202
+ )}
169
203
  </PropertyList>
170
204
  </Section>
171
205
 
@@ -173,13 +207,33 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
173
207
  {steps.length > 0 && (
174
208
  <Section title={`Steps (${steps.length})`} defaultExpanded>
175
209
  <div className="space-y-1.5">
176
- {steps.map(step => (
177
- <div key={step.id} className="flex items-center gap-2 text-sm card-inner 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
- ))}
210
+ {steps.map(step => {
211
+ const isFailed = step.phase === 'Failed' || step.phase === 'Error'
212
+ const isSkipped = step.nodeType === 'Skipped' || step.phase === 'Skipped'
213
+ const isSuspend = step.nodeType === 'Suspend'
214
+ return (
215
+ <div key={step.id} className={clsx(
216
+ 'text-sm card-inner px-3 py-2',
217
+ isFailed && 'border-l-2 border-red-500'
218
+ )}>
219
+ <div className="flex items-center gap-2">
220
+ <StepStatusIcon phase={step.phase} nodeType={step.nodeType} />
221
+ <span className={clsx(
222
+ 'flex-1',
223
+ isSkipped ? 'text-theme-text-tertiary' : isSuspend ? 'text-yellow-400' : 'text-theme-text-primary'
224
+ )}>
225
+ {step.displayName}
226
+ {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>}
228
+ </span>
229
+ <span className="text-xs text-theme-text-secondary">{getStepDuration(step) || '-'}</span>
230
+ </div>
231
+ {isFailed && step.message && (
232
+ <div className="text-xs text-red-400 mt-1 ml-6 break-all">{step.message}</div>
233
+ )}
234
+ </div>
235
+ )
236
+ })}
183
237
  </div>
184
238
  </Section>
185
239
  )}
@@ -1,4 +1,4 @@
1
- import { Play, FileText, Key, Lock } from 'lucide-react'
1
+ import { Play, FileText, Key, Lock, File, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'
2
2
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
3
 
4
4
  interface WorkflowTemplateRendererProps {
@@ -44,6 +44,11 @@ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps
44
44
  {templates.map((template: any) => {
45
45
  const type = getTemplateType(template)
46
46
  const image = getTemplateImage(template)
47
+ const inputParams: Array<{ name: string }> = template.inputs?.parameters || []
48
+ const outputParams: Array<{ name: string }> = template.outputs?.parameters || []
49
+ const outputArtifacts: Array<{ name: string }> = template.outputs?.artifacts || []
50
+ const inputArtifacts: Array<{ name: string }> = template.inputs?.artifacts || []
51
+ const hasIO = inputParams.length > 0 || outputParams.length > 0 || outputArtifacts.length > 0 || inputArtifacts.length > 0
47
52
  return (
48
53
  <div key={template.name} className="card-inner px-3 py-2 text-sm">
49
54
  <div className="font-medium text-theme-text-primary">{template.name}</div>
@@ -53,6 +58,46 @@ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps
53
58
  {image}
54
59
  </div>
55
60
  )}
61
+ {hasIO && (
62
+ <div className="mt-2 space-y-1.5">
63
+ {(inputParams.length > 0 || inputArtifacts.length > 0) && (
64
+ <div className="flex items-start gap-1.5">
65
+ <ArrowDownToLine className="w-3 h-3 text-theme-text-tertiary mt-0.5 shrink-0" />
66
+ <div className="flex flex-wrap gap-1">
67
+ {inputParams.map((p) => (
68
+ <span key={p.name} className="badge-sm bg-theme-elevated text-theme-text-secondary">
69
+ {p.name}
70
+ </span>
71
+ ))}
72
+ {inputArtifacts.map((a) => (
73
+ <span key={a.name} className="badge-sm bg-theme-elevated text-theme-text-secondary flex items-center gap-0.5">
74
+ <File className="w-2.5 h-2.5" />
75
+ {a.name}
76
+ </span>
77
+ ))}
78
+ </div>
79
+ </div>
80
+ )}
81
+ {(outputParams.length > 0 || outputArtifacts.length > 0) && (
82
+ <div className="flex items-start gap-1.5">
83
+ <ArrowUpFromLine className="w-3 h-3 text-theme-text-tertiary mt-0.5 shrink-0" />
84
+ <div className="flex flex-wrap gap-1">
85
+ {outputParams.map((p) => (
86
+ <span key={p.name} className="badge-sm bg-theme-elevated text-theme-text-secondary">
87
+ {p.name}
88
+ </span>
89
+ ))}
90
+ {outputArtifacts.map((a) => (
91
+ <span key={a.name} className="badge-sm bg-theme-elevated text-theme-text-secondary flex items-center gap-0.5">
92
+ <File className="w-2.5 h-2.5" />
93
+ {a.name}
94
+ </span>
95
+ ))}
96
+ </div>
97
+ </div>
98
+ )}
99
+ </div>
100
+ )}
56
101
  </div>
57
102
  )
58
103
  })}
@@ -62,6 +62,9 @@ export * from './kyverno-cells'
62
62
  export * from './KyvernoPolicyReportRenderer'
63
63
  export * from './LeaseRenderer'
64
64
  export * from './NetworkPolicyRenderer'
65
+ export * from './NetworkPolicyDiagram'
66
+ export * from './CiliumNetworkPolicyRenderer'
67
+ export * from './ClusterNetworkPolicyRenderer'
65
68
  export * from './NodeRenderer'
66
69
  export * from './OCIRepositoryRenderer'
67
70
  export * from './OrderRenderer'
@@ -3,6 +3,7 @@
3
3
  import { clsx } from 'clsx'
4
4
  import { Tooltip } from '../../ui/Tooltip'
5
5
  import {
6
+ CAPACITY_TYPE_BADGE,
6
7
  getNodePoolStatus,
7
8
  getNodePoolNodeClassRef,
8
9
  getNodePoolLimits,
@@ -22,9 +23,11 @@ export function NodePoolCell({ resource, column }: { resource: any; column: stri
22
23
  case 'status': {
23
24
  const status = getNodePoolStatus(resource)
24
25
  return (
25
- <span className={clsx('badge', status.color)}>
26
- {status.text}
27
- </span>
26
+ <Tooltip content={status.text}>
27
+ <span className={clsx('badge truncate max-w-[140px]', status.color)}>
28
+ {status.text}
29
+ </span>
30
+ </Tooltip>
28
31
  )
29
32
  }
30
33
  case 'nodeClass': {
@@ -70,6 +73,18 @@ export function NodeClaimCell({ resource, column }: { resource: any; column: str
70
73
  const pool = getNodeClaimNodePoolRef(resource)
71
74
  return <span className="text-sm text-theme-text-secondary">{pool}</span>
72
75
  }
76
+ case 'capacityType': {
77
+ const ct = resource.metadata?.labels?.['karpenter.sh/capacity-type'] || '-'
78
+ return (
79
+ <span className={clsx('badge-sm', CAPACITY_TYPE_BADGE[ct] || '')}>
80
+ {ct}
81
+ </span>
82
+ )
83
+ }
84
+ case 'zone': {
85
+ const zone = resource.metadata?.labels?.['topology.kubernetes.io/zone'] || '-'
86
+ return <span className="text-sm text-theme-text-secondary">{zone}</span>
87
+ }
73
88
  default:
74
89
  return <span className="text-sm text-theme-text-tertiary">-</span>
75
90
  }