@skyhook-io/k8s-ui 1.14.5 → 1.14.7

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 (104) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.tsx +36 -4
  3. package/src/components/applications/ApplicationsView.tsx +2 -1
  4. package/src/components/audit/AuditAlerts.tsx +35 -37
  5. package/src/components/audit/AuditCard.tsx +18 -7
  6. package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
  7. package/src/components/audit/AuditFindingsTable.tsx +23 -27
  8. package/src/components/charts/AreaChart.test.tsx +13 -0
  9. package/src/components/charts/AreaChart.tsx +28 -25
  10. package/src/components/charts/PrometheusChartsView.tsx +41 -10
  11. package/src/components/charts/axis.test.ts +16 -0
  12. package/src/components/charts/axis.ts +3 -1
  13. package/src/components/charts/format.ts +4 -0
  14. package/src/components/charts/nearestSample.test.ts +31 -0
  15. package/src/components/charts/nearestSample.ts +15 -0
  16. package/src/components/checks/ChecksView.tsx +76 -28
  17. package/src/components/gitops/GitOpsDetailLayout.tsx +25 -9
  18. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
  19. package/src/components/gitops/health-provenance.test.ts +54 -0
  20. package/src/components/gitops/health-provenance.ts +24 -0
  21. package/src/components/gitops/index.ts +2 -0
  22. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
  23. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
  24. package/src/components/gitops/tree/merge.test.ts +34 -0
  25. package/src/components/gitops/tree/merge.ts +7 -0
  26. package/src/components/issues/IssuesView.tsx +35 -54
  27. package/src/components/logs/LogCore.tsx +40 -9
  28. package/src/components/resources/ResourcesSidebar.tsx +66 -78
  29. package/src/components/resources/ResourcesView.tsx +67 -2
  30. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  31. package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
  32. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
  33. package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
  36. package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
  37. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
  38. package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
  39. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
  40. package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
  41. package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
  42. package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
  43. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
  44. package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
  46. package/src/components/resources/renderers/PodRenderer.tsx +11 -0
  47. package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
  48. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
  49. package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
  50. package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
  51. package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
  52. package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
  53. package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
  54. package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
  55. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
  56. package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
  57. package/src/components/resources/renderers/index.ts +3 -0
  58. package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
  59. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +123 -0
  60. package/src/components/resources/resource-utils.ts +18 -9
  61. package/src/components/shared/CreateResourceDialog.tsx +237 -14
  62. package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
  63. package/src/components/shared/DrainPlanDialog.tsx +297 -0
  64. package/src/components/shared/ResourceActionsBar.tsx +53 -26
  65. package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
  66. package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
  67. package/src/components/shared/index.ts +4 -0
  68. package/src/components/timeline/TimelineList.tsx +14 -11
  69. package/src/components/timeline/TimelineStrip.tsx +26 -4
  70. package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
  71. package/src/components/timeline/TimelineToolbar.tsx +43 -10
  72. package/src/components/topology/TopologyControls.tsx +27 -8
  73. package/src/components/topology/TopologyGraph.tsx +14 -5
  74. package/src/components/trace/ReachabilityView.tsx +17 -16
  75. package/src/components/ui/CardSection.tsx +79 -35
  76. package/src/components/ui/Collapse.test.tsx +74 -0
  77. package/src/components/ui/Collapse.tsx +115 -29
  78. package/src/components/ui/DialogPortal.tsx +7 -2
  79. package/src/components/ui/FetchResult.test.tsx +20 -2
  80. package/src/components/ui/FetchResult.tsx +33 -3
  81. package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
  82. package/src/components/ui/FreshnessControl.tsx +8 -2
  83. package/src/components/ui/RestrictedState.tsx +7 -4
  84. package/src/components/ui/RowActionMenu.tsx +20 -8
  85. package/src/components/ui/SelectMenu.tsx +18 -6
  86. package/src/components/ui/YamlEditor.tsx +7 -9
  87. package/src/components/ui/drawer-components.tsx +14 -21
  88. package/src/components/ui/index.ts +30 -22
  89. package/src/components/workload/WorkloadView.tsx +8 -12
  90. package/src/hooks/useAnimatedUnmount.ts +21 -3
  91. package/src/theme/components.css +20 -1
  92. package/src/types/core.ts +15 -1
  93. package/src/types/gitops-insights.ts +23 -0
  94. package/src/types/gitops-tree.ts +29 -0
  95. package/src/utils/animation.ts +93 -13
  96. package/src/utils/index.ts +1 -0
  97. package/src/utils/navigation.test.ts +20 -1
  98. package/src/utils/navigation.ts +25 -0
  99. package/src/utils/network-policy.test.ts +42 -0
  100. package/src/utils/network-policy.ts +59 -0
  101. package/src/utils/reflector.ts +139 -0
  102. package/src/utils/yaml-file-import.test.ts +189 -0
  103. package/src/utils/yaml-file-import.ts +95 -0
  104. package/src/components/logs/JsonLogLine.tsx +0 -93
@@ -0,0 +1,199 @@
1
+ import { Check, X, AlertTriangle, Minus, Clock } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { canaryStepLabel, canaryStepTemplateRefs } from '../RolloutRenderer'
4
+ import { analysisPhaseLevel, healthColors, type HealthLevel } from '../../resource-utils'
5
+
6
+ export interface StepAnalysisStatus {
7
+ name?: string
8
+ status?: string
9
+ message?: string
10
+ }
11
+
12
+ interface CanaryStepTimelineProps {
13
+ steps: any[]
14
+ currentStepIndex?: number
15
+ // The live "Step analysis" slot from rolloutAnalysisRuns(status) — shown
16
+ // inline on the current step when it's an analysis step, instead of only
17
+ // in the separate Analysis section above.
18
+ stepAnalysisStatus?: StepAnalysisStatus
19
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
20
+ namespace?: string
21
+ }
22
+
23
+ type StepState = 'completed' | 'current' | 'pending'
24
+
25
+ // Only 'unhealthy' (Failed/Error) and 'alert' (Inconclusive) colour the dot.
26
+ // A Successful or still-running analysis deliberately gets no special colour:
27
+ // the step itself may still be paused or in progress regardless, so it keeps
28
+ // the same "current" tone as any other in-progress step rather than reading
29
+ // as done.
30
+ function stepDotTone(state: StepState, analysisLevel?: HealthLevel) {
31
+ if (state === 'current' && (analysisLevel === 'unhealthy' || analysisLevel === 'alert')) {
32
+ return healthColors[analysisLevel]
33
+ }
34
+ switch (state) {
35
+ case 'completed':
36
+ return healthColors.healthy
37
+ case 'current':
38
+ return healthColors.neutral
39
+ default:
40
+ return 'bg-theme-hover text-theme-text-tertiary'
41
+ }
42
+ }
43
+
44
+ function StepDot({ state, analysisLevel }: { state: StepState; analysisLevel?: HealthLevel }) {
45
+ const tone = stepDotTone(state, analysisLevel)
46
+ let Icon = Minus
47
+ if (state === 'current' && analysisLevel === 'unhealthy') Icon = X
48
+ else if (state === 'current' && analysisLevel === 'alert') Icon = AlertTriangle
49
+ else if (state === 'completed') Icon = Check
50
+ else if (state === 'current') Icon = Clock
51
+ return (
52
+ <span className={clsx('z-10 mt-1 flex h-3 w-3 shrink-0 items-center justify-center rounded-full ring-2 ring-theme-surface', tone)}>
53
+ <Icon className="h-2 w-2" strokeWidth={4} />
54
+ </span>
55
+ )
56
+ }
57
+
58
+ // Vertical connected stepper for a Rollout's canary steps — a linear
59
+ // sequence (no branching), so a simple top-to-bottom timeline reads more
60
+ // naturally than a graph. Same connecting-line visual as ConditionsSection.
61
+ export function CanaryStepTimeline({ steps, currentStepIndex, stepAnalysisStatus, onNavigate, namespace }: CanaryStepTimelineProps) {
62
+ if (steps.length === 0) return null
63
+
64
+ return (
65
+ <div className="relative">
66
+ <div className="absolute bottom-2 left-[9px] top-2 w-px bg-theme-border" />
67
+ <div className="space-y-0.5">
68
+ {steps.map((step, index) => {
69
+ const state: StepState =
70
+ currentStepIndex === undefined ? 'pending' : index < currentStepIndex ? 'completed' : index === currentStepIndex ? 'current' : 'pending'
71
+ const isCurrent = state === 'current'
72
+ const label = canaryStepLabel(step)
73
+ const templateRefs = step.analysis ? canaryStepTemplateRefs(step) : []
74
+ const showAnalysisStatus = isCurrent && step.analysis && stepAnalysisStatus?.status
75
+ const analysisLevel: HealthLevel | undefined = showAnalysisStatus
76
+ ? analysisPhaseLevel(stepAnalysisStatus!.status)
77
+ : undefined
78
+
79
+ return (
80
+ <div key={index} className="relative flex items-start gap-2 py-1.5 pr-1 text-sm">
81
+ <StepDot state={state} analysisLevel={analysisLevel} />
82
+ <div className="min-w-0 flex-1 pl-1">
83
+ <div className="flex items-center gap-2">
84
+ <span className="text-[11px] text-theme-text-tertiary">#{index}</span>
85
+ <span className={clsx(isCurrent ? 'font-medium text-theme-text-primary' : state === 'completed' ? 'text-theme-text-secondary' : 'text-theme-text-tertiary')}>
86
+ {label}
87
+ </span>
88
+ {showAnalysisStatus && analysisLevel && (
89
+ onNavigate && stepAnalysisStatus!.name ? (
90
+ <button
91
+ onClick={() =>
92
+ onNavigate({ kind: 'AnalysisRun', namespace: namespace ?? '', name: stepAnalysisStatus!.name! })
93
+ }
94
+ className={clsx('badge-sm hover:underline', healthColors[analysisLevel])}
95
+ title={stepAnalysisStatus!.name}
96
+ >
97
+ {stepAnalysisStatus!.status}
98
+ </button>
99
+ ) : (
100
+ <span className={clsx('badge-sm', healthColors[analysisLevel])}>
101
+ {stepAnalysisStatus!.status}
102
+ </span>
103
+ )
104
+ )}
105
+ </div>
106
+ {templateRefs.length > 0 && (
107
+ <div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5">
108
+ {templateRefs.map((ref) => (
109
+ <button
110
+ key={ref.name}
111
+ onClick={() =>
112
+ onNavigate?.({
113
+ kind: ref.clusterScoped ? 'ClusterAnalysisTemplate' : 'AnalysisTemplate',
114
+ namespace: ref.clusterScoped ? '' : namespace ?? '',
115
+ name: ref.name,
116
+ })
117
+ }
118
+ disabled={!onNavigate}
119
+ className={clsx(
120
+ 'font-mono text-[11px]',
121
+ onNavigate ? 'text-brand hover:underline' : 'text-theme-text-tertiary'
122
+ )}
123
+ >
124
+ {ref.name}
125
+ </button>
126
+ ))}
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ )
132
+ })}
133
+ </div>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ export interface BlueGreenPhase {
139
+ label: string
140
+ state: 'completed' | 'current' | 'pending'
141
+ analysis?: { name?: string; status?: string }
142
+ }
143
+
144
+ // Same connected-dot visual as CanaryStepTimeline, for blueGreenPhases()'s
145
+ // derived phase list — blueGreen has no steps[] array to iterate, so there's
146
+ // nothing to reuse structurally, but the "linear sequence, top to bottom"
147
+ // visual language should match.
148
+ export function BlueGreenTimeline({
149
+ phases,
150
+ onNavigate,
151
+ namespace,
152
+ }: {
153
+ phases: BlueGreenPhase[]
154
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
155
+ namespace?: string
156
+ }) {
157
+ if (phases.length === 0) return null
158
+ return (
159
+ <div className="relative">
160
+ <div className="absolute bottom-2 left-[9px] top-2 w-px bg-theme-border" />
161
+ <div className="space-y-0.5">
162
+ {phases.map((phase, index) => {
163
+ const level = phase.analysis?.status ? analysisPhaseLevel(phase.analysis.status) : undefined
164
+ return (
165
+ <div key={index} className="relative flex items-start gap-2 py-1.5 pr-1 text-sm">
166
+ <StepDot state={phase.state} analysisLevel={level} />
167
+ <span
168
+ className={clsx(
169
+ 'pl-1',
170
+ phase.state === 'current'
171
+ ? 'font-medium text-theme-text-primary'
172
+ : phase.state === 'completed'
173
+ ? 'text-theme-text-secondary'
174
+ : 'text-theme-text-tertiary'
175
+ )}
176
+ >
177
+ {phase.label}
178
+ </span>
179
+ {level &&
180
+ (onNavigate && phase.analysis?.name ? (
181
+ <button
182
+ onClick={() =>
183
+ onNavigate({ kind: 'AnalysisRun', namespace: namespace ?? '', name: phase.analysis!.name! })
184
+ }
185
+ className={clsx('badge-sm hover:underline', healthColors[level])}
186
+ title={phase.analysis.name}
187
+ >
188
+ {phase.analysis.status}
189
+ </button>
190
+ ) : (
191
+ <span className={clsx('badge-sm', healthColors[level])}>{phase.analysis!.status}</span>
192
+ ))}
193
+ </div>
194
+ )
195
+ })}
196
+ </div>
197
+ </div>
198
+ )
199
+ }
@@ -0,0 +1,123 @@
1
+ import { useState, useId } from 'react'
2
+ import { Box } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { formatAge } from '../../resource-utils'
5
+ import { Collapse, CollapseChevron, disclosurePanelId } from '../../../ui/Collapse'
6
+ import { revisionRoleBadges } from '../../../shared/ResourceActionsBar'
7
+ import type { WorkloadRevision, WorkloadPodInfo } from '../../../../types'
8
+
9
+ interface ReplicaSetProgressionProps {
10
+ revisions: WorkloadRevision[]
11
+ pods?: WorkloadPodInfo[]
12
+ isRollout: true
13
+ namespace: string
14
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
15
+ }
16
+
17
+ function imageTag(image: string): string {
18
+ const lastColon = image.lastIndexOf(':')
19
+ const lastSlash = image.lastIndexOf('/')
20
+ return lastColon > lastSlash ? image.slice(lastColon + 1) : image
21
+ }
22
+
23
+ function podToneClass(pod: WorkloadPodInfo): string {
24
+ switch (pod.healthLevel) {
25
+ case 'healthy':
26
+ return 'status-healthy'
27
+ case 'degraded':
28
+ return 'status-degraded'
29
+ case 'unhealthy':
30
+ return 'status-unhealthy'
31
+ default:
32
+ return 'status-unknown'
33
+ }
34
+ }
35
+
36
+ // Read-only revision tree: Rollout -> ReplicaSet revision -> pods, mirroring
37
+ // `kubectl argo rollouts get rollout`'s tree output. Newest first, matching
38
+ // the existing rollback-history dialog's ordering. Rollback stays exclusive
39
+ // to that dialog — this is purely informational.
40
+ export function ReplicaSetProgression({ revisions, pods, namespace, onNavigate }: ReplicaSetProgressionProps) {
41
+ const [expanded, setExpanded] = useState<Set<number>>(new Set())
42
+ // Revision rows are mapped inline, so they can't each call useDisclosure;
43
+ // one generated prefix plus the revision number keeps aria-controls unique.
44
+ const panelBase = useId()
45
+
46
+ const podsByHash = new Map<string, WorkloadPodInfo[]>()
47
+ for (const pod of pods ?? []) {
48
+ if (!pod.revisionIdentity) continue
49
+ const list = podsByHash.get(pod.revisionIdentity) ?? []
50
+ list.push(pod)
51
+ podsByHash.set(pod.revisionIdentity, list)
52
+ }
53
+
54
+ const toggle = (number: number) =>
55
+ setExpanded((prev) => {
56
+ const next = new Set(prev)
57
+ if (next.has(number)) next.delete(number)
58
+ else next.add(number)
59
+ return next
60
+ })
61
+
62
+ return (
63
+ <div className="space-y-1">
64
+ {revisions.map((rev) => {
65
+ const revPods = rev.podHash ? podsByHash.get(rev.podHash) ?? [] : []
66
+ // One predicate for header, caret and panel: no pods, nothing to open.
67
+ const isOpen = expanded.has(rev.number) && revPods.length > 0
68
+ return (
69
+ <div key={rev.number} className="rounded border border-theme-border/50">
70
+ <button
71
+ type="button"
72
+ aria-expanded={isOpen}
73
+ aria-controls={disclosurePanelId(panelBase, rev.number)}
74
+ onClick={() => toggle(rev.number)}
75
+ className="flex w-full items-center gap-2 px-2 py-1.5 text-left hover:bg-theme-hover"
76
+ disabled={revPods.length === 0}
77
+ >
78
+ {revPods.length > 0 ? (
79
+ <CollapseChevron open={isOpen} className="h-3.5 w-3.5" />
80
+ ) : (
81
+ <span className="w-3.5 shrink-0" />
82
+ )}
83
+ <span className="font-mono text-xs text-theme-text-primary">#{rev.number}</span>
84
+ <span className="min-w-0 flex-1 truncate font-mono text-xs text-theme-text-secondary">{imageTag(rev.image)}</span>
85
+ <div className="flex shrink-0 items-center gap-1">
86
+ {revisionRoleBadges(rev, true).map(({ label, tone, tip }) => (
87
+ <span key={label} title={tip} className={clsx('badge', tone)}>
88
+ {label}
89
+ </span>
90
+ ))}
91
+ </div>
92
+ <span className="shrink-0 text-xs tabular-nums text-theme-text-tertiary">{rev.replicas ?? 0} replicas</span>
93
+ <span className="shrink-0 text-xs text-theme-text-tertiary">{formatAge(rev.createdAt)}</span>
94
+ </button>
95
+ <Collapse open={isOpen} mountLazily id={disclosurePanelId(panelBase, rev.number)}>
96
+ <div className="space-y-0.5 border-t border-theme-border/50 px-2 py-1.5 pl-8">
97
+ {revPods.map((pod) => (
98
+ <div key={pod.name} className="flex items-center gap-2 py-0.5 text-xs">
99
+ <Box className="h-3 w-3 shrink-0 text-theme-text-tertiary" />
100
+ {onNavigate ? (
101
+ <button
102
+ onClick={() => onNavigate({ kind: 'Pod', namespace, name: pod.name })}
103
+ className="min-w-0 flex-1 truncate text-left font-mono text-theme-text-secondary hover:text-brand hover:underline"
104
+ >
105
+ {pod.name}
106
+ </button>
107
+ ) : (
108
+ <span className="min-w-0 flex-1 truncate font-mono text-theme-text-secondary">{pod.name}</span>
109
+ )}
110
+ <span className={clsx('badge-sm', podToneClass(pod))}>{pod.phase || (pod.ready ? 'Ready' : 'Not Ready')}</span>
111
+ {(pod.restartCount ?? 0) > 0 && (
112
+ <span className="shrink-0 text-theme-text-tertiary">{pod.restartCount} restarts</span>
113
+ )}
114
+ </div>
115
+ ))}
116
+ </div>
117
+ </Collapse>
118
+ </div>
119
+ )
120
+ })}
121
+ </div>
122
+ )
123
+ }
@@ -1,5 +1,6 @@
1
1
  // Utility functions for resource display in tables
2
2
 
3
+ import { effectivePolicyTypeNames } from '../../utils/network-policy'
3
4
  import { formatCPUString, formatMemoryString, formatBytes } from '../../utils/format'
4
5
  import { pluralize } from '../../utils/pluralize'
5
6
  import type { WorkloadPodInfo } from '../../types/core'
@@ -1544,24 +1545,33 @@ export function getRolloutStatus(rollout: any): StatusBadge {
1544
1545
  return rolloutActivityBadge(getWorkloadRolloutActivity(rollout, 'rollout'))
1545
1546
  }
1546
1547
 
1547
- export function getAnalysisRunStatus(run: any): StatusBadge {
1548
- const phase = run.status?.phase || 'Unknown'
1548
+ /** The AnalysisPhase vocabulary (AnalysisRun, Experiment, and a Rollout's
1549
+ * analysis slots all report it) mapped onto HealthLevel. Shared so a caller
1550
+ * holding only the phase string lands on the same tone as one holding the
1551
+ * whole run. */
1552
+ export function analysisPhaseLevel(phase?: string): HealthLevel {
1549
1553
  switch (phase) {
1550
1554
  case 'Successful':
1551
- return { text: 'Successful', color: healthColors.healthy, level: 'healthy' }
1555
+ return 'healthy'
1552
1556
  case 'Running':
1553
1557
  case 'Pending':
1554
- return { text: phase, color: healthColors.degraded, level: 'degraded' }
1558
+ return 'degraded'
1555
1559
  case 'Inconclusive':
1556
- return { text: 'Inconclusive', color: healthColors.alert, level: 'alert' }
1560
+ return 'alert'
1557
1561
  case 'Failed':
1558
1562
  case 'Error':
1559
- return { text: phase, color: healthColors.unhealthy, level: 'unhealthy' }
1563
+ return 'unhealthy'
1560
1564
  default:
1561
- return { text: phase, color: healthColors.unknown, level: 'unknown' }
1565
+ return 'unknown'
1562
1566
  }
1563
1567
  }
1564
1568
 
1569
+ export function getAnalysisRunStatus(run: any): StatusBadge {
1570
+ const phase = run.status?.phase || 'Unknown'
1571
+ const level = analysisPhaseLevel(phase)
1572
+ return { text: phase, color: healthColors[level], level }
1573
+ }
1574
+
1565
1575
  // Counts only verdict-bearing metrics: Argo excludes dryRun results from the run's
1566
1576
  // phase, and Running/Pending have not passed yet.
1567
1577
  export function summarizeAnalysisMetrics(run: any): {
@@ -2068,8 +2078,7 @@ export function getWorkflowTemplateEntrypoint(wt: any): string {
2068
2078
  // ============================================================================
2069
2079
 
2070
2080
  export function getNetworkPolicyTypes(np: any): string {
2071
- const types = np.spec?.policyTypes || []
2072
- return types.join(', ') || '-'
2081
+ return effectivePolicyTypeNames(np.spec).join(', ')
2073
2082
  }
2074
2083
 
2075
2084
  export function getNetworkPolicyRuleCount(np: any): { ingress: number; egress: number } {