@skyhook-io/k8s-ui 1.14.3 → 1.14.5

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/charts/AreaChart.test.tsx +160 -0
  3. package/src/components/charts/AreaChart.tsx +191 -39
  4. package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
  5. package/src/components/charts/PrometheusChartsView.tsx +193 -76
  6. package/src/components/charts/SeriesLegend.tsx +8 -3
  7. package/src/components/charts/annotations.ts +59 -0
  8. package/src/components/charts/axis.test.ts +32 -0
  9. package/src/components/charts/axis.ts +60 -0
  10. package/src/components/charts/colors.test.ts +27 -0
  11. package/src/components/charts/colors.ts +23 -0
  12. package/src/components/charts/format.test.ts +19 -0
  13. package/src/components/charts/format.ts +17 -1
  14. package/src/components/charts/index.ts +3 -1
  15. package/src/components/charts/types.ts +12 -0
  16. package/src/components/logs/LogCore.tsx +182 -16
  17. package/src/components/logs/LogsViewer.tsx +4 -21
  18. package/src/components/logs/WorkloadLogsViewer.tsx +5 -23
  19. package/src/components/resources/HPADiagnosisSummary.test.tsx +178 -0
  20. package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
  21. package/src/components/resources/ResourcesView.tsx +17 -3
  22. package/src/components/resources/index.ts +7 -0
  23. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +2 -2
  24. package/src/components/resources/renderers/AlertRenderer.tsx +3 -3
  25. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +5 -5
  26. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -1
  27. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -3
  28. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +5 -5
  29. package/src/components/resources/renderers/ChallengeRenderer.tsx +8 -8
  30. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +3 -3
  31. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +2 -2
  32. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +3 -3
  33. package/src/components/resources/renderers/GatewayRenderer.tsx +2 -2
  34. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  35. package/src/components/resources/renderers/HPARenderer.tsx +4 -57
  36. package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -1
  37. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +3 -3
  38. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +2 -2
  39. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -3
  40. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -1
  42. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +23 -20
  43. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +2 -2
  44. package/src/components/resources/renderers/NodeRenderer.tsx +3 -3
  45. package/src/components/resources/renderers/OrderRenderer.tsx +6 -6
  46. package/src/components/resources/renderers/RoleRenderer.tsx +3 -3
  47. package/src/components/resources/renderers/RolloutRenderer.tsx +2 -2
  48. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -3
  49. package/src/components/resources/renderers/SecretStoreRenderer.tsx +8 -8
  50. package/src/components/resources/renderers/VPARenderer.tsx +1 -1
  51. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -1
  52. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -9
  53. package/src/components/resources/renderers/WorkloadRenderer.tsx +4 -20
  54. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +6 -12
  55. package/src/components/resources/renderers/cnpg-cells.tsx +1 -1
  56. package/src/components/resources/renderers/eso-cells.tsx +17 -1
  57. package/src/components/resources/renderers/knative-cells.tsx +1 -1
  58. package/src/components/resources/renderers/kyverno-cells.tsx +7 -3
  59. package/src/components/resources/resource-utils-certmanager.ts +2 -2
  60. package/src/components/resources/resource-utils-eso.test.ts +26 -1
  61. package/src/components/resources/resource-utils-eso.ts +13 -0
  62. package/src/components/resources/resource-utils-knative.ts +3 -3
  63. package/src/components/topology/K8sResourceNode.test.ts +9 -0
  64. package/src/components/topology/K8sResourceNode.tsx +56 -5
  65. package/src/components/topology/TopologyGraph.tsx +18 -1
  66. package/src/components/topology/pod-phase-fallback.test.ts +24 -0
  67. package/src/components/ui/Badge.tsx +5 -1
  68. package/src/components/ui/index.ts +5 -0
  69. package/src/components/ui/monacoRuntime.ts +14 -0
  70. package/src/monaco-deep.d.ts +10 -0
  71. package/src/theme/components.css +29 -0
  72. package/src/types/core.ts +23 -10
  73. package/src/utils/badge-colors.ts +1 -1
  74. package/src/utils/index.ts +1 -0
  75. package/src/utils/log-export.test.ts +225 -0
  76. package/src/utils/log-export.ts +149 -0
  77. package/src/utils/navigation.test.ts +10 -0
  78. package/src/utils/navigation.ts +4 -0
@@ -0,0 +1,218 @@
1
+ import type { ReactNode } from "react";
2
+ import { Badge, type BadgeSeverity } from "../ui/Badge";
3
+ import { StatusDot } from "../ui/status-tone";
4
+ import { hpaStateLabel, hpaStateLevel } from "./resource-utils-hpa";
5
+ import type {
6
+ HPADiagnosisState,
7
+ HPADiagnosisView,
8
+ HPAReasonSummary,
9
+ } from "../../types";
10
+
11
+ export function hpaBadgeSeverity(state: HPADiagnosisState): BadgeSeverity {
12
+ switch (hpaStateLevel(state)) {
13
+ case "healthy":
14
+ return "success";
15
+ case "unhealthy":
16
+ return "error";
17
+ case "degraded":
18
+ return "warning";
19
+ case "alert":
20
+ return "alert";
21
+ case "neutral":
22
+ return "info";
23
+ default:
24
+ return "neutral";
25
+ }
26
+ }
27
+
28
+ /**
29
+ * A reason that named the state, or repeats the summary verbatim, says the
30
+ * headline over again — only the controller's own sentence behind it (the
31
+ * reason's detail) adds anything a reader does not already have.
32
+ */
33
+ export function isHPAReasonRedundant(
34
+ diagnosis: HPADiagnosisView,
35
+ reason: HPAReasonSummary,
36
+ ): boolean {
37
+ return reason.id === diagnosis.state || reason.message === diagnosis.summary;
38
+ }
39
+
40
+ export interface HPAReasonGroups {
41
+ /** Reasons whose message restates the headline; render their detail, not their message. */
42
+ redundant: HPAReasonSummary[];
43
+ /** Reasons that carry something the headline does not. */
44
+ additional: HPAReasonSummary[];
45
+ }
46
+
47
+ export function hpaReasonGroups(diagnosis: HPADiagnosisView): HPAReasonGroups {
48
+ const reasons = diagnosis.reasons ?? [];
49
+ const redundant = reasons.filter((reason) =>
50
+ isHPAReasonRedundant(diagnosis, reason),
51
+ );
52
+ return {
53
+ redundant,
54
+ additional: reasons.filter((reason) => !redundant.includes(reason)),
55
+ };
56
+ }
57
+
58
+ function formatReasonID(id: string): string {
59
+ return id.replace(/_/g, " ");
60
+ }
61
+
62
+ export interface HPADiagnosisSummaryProps {
63
+ diagnosis: HPADiagnosisView;
64
+ /**
65
+ * `detail` — the HPA detail page: a boxed Evidence block per reason.
66
+ * `inline` — an evidence card on the workload the HPA scales: the headline
67
+ * once, then the controller's condition quoted and attributed.
68
+ */
69
+ variant: "detail" | "inline";
70
+ /**
71
+ * Host-rendered identity for the `inline` variant (the scaler's name, owner
72
+ * links) placed before the state indicator. Navigation stays with the host.
73
+ */
74
+ header?: ReactNode;
75
+ }
76
+
77
+ export function HPADiagnosisSummary({
78
+ diagnosis,
79
+ variant,
80
+ header,
81
+ }: HPADiagnosisSummaryProps) {
82
+ const { redundant, additional } = hpaReasonGroups(diagnosis);
83
+ const bounds = diagnosis.bounds;
84
+
85
+ if (variant === "inline") {
86
+ return (
87
+ <div className="space-y-1 text-xs">
88
+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
89
+ {header}
90
+ <span className="inline-flex items-center gap-1.5 text-theme-text-secondary">
91
+ <StatusDot
92
+ tone={hpaStateLevel(diagnosis.state)}
93
+ className="shrink-0"
94
+ />
95
+ {hpaStateLabel(diagnosis.state)}
96
+ </span>
97
+ {bounds ? (
98
+ <span className="font-mono tabular-nums text-theme-text-tertiary">
99
+ {bounds.current}/{bounds.desired} replicas · bounds {bounds.min}-
100
+ {bounds.max}
101
+ </span>
102
+ ) : null}
103
+ </div>
104
+ <p className="text-theme-text-secondary">{diagnosis.summary}</p>
105
+ {redundant.map((reason) =>
106
+ reason.detail ? (
107
+ <p
108
+ key={`${reason.id}-${reason.detail}`}
109
+ className="text-theme-text-tertiary"
110
+ >
111
+ {/* Same rule as the detail variant: only a condition-built
112
+ reason carries the controller's own sentence. */}
113
+ {reason.conditionType ? (
114
+ <>
115
+ Kubernetes {reason.conditionType}
116
+ {reason.conditionReason ? ` · ${reason.conditionReason}` : ""}
117
+ : &ldquo;{reason.detail}&rdquo;
118
+ </>
119
+ ) : (
120
+ reason.detail
121
+ )}
122
+ </p>
123
+ ) : null,
124
+ )}
125
+ {additional.length > 0 ? (
126
+ <ul className="list-disc space-y-0.5 pl-4 text-theme-text-secondary marker:text-theme-text-tertiary">
127
+ {additional.map((reason) => (
128
+ <li key={`${reason.id}-${reason.message}`}>
129
+ {reason.message}
130
+ {reason.detail ? (
131
+ <span className="text-theme-text-tertiary">
132
+ {" "}
133
+ · {reason.detail}
134
+ </span>
135
+ ) : null}
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ ) : null}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ const reasons = diagnosis.reasons ?? [];
145
+ return (
146
+ <div className="card-inner space-y-3">
147
+ <div className="flex flex-wrap items-start justify-between gap-2">
148
+ <div className="min-w-0">
149
+ <div className="text-sm font-medium text-theme-text-primary">
150
+ {diagnosis.summary}
151
+ </div>
152
+ {bounds ? (
153
+ <div className="mt-1 text-xs text-theme-text-secondary">
154
+ {bounds.current}/{bounds.desired} replicas, bounds {bounds.min}-
155
+ {bounds.max}
156
+ </div>
157
+ ) : null}
158
+ </div>
159
+ <Badge severity={hpaBadgeSeverity(diagnosis.state)}>
160
+ {hpaStateLabel(diagnosis.state)}
161
+ </Badge>
162
+ </div>
163
+ {reasons.length > 0 && (
164
+ <div className="space-y-2">
165
+ {reasons.map((reason) => (
166
+ <div
167
+ key={`${reason.id}-${reason.message}`}
168
+ className="rounded border border-theme-border bg-theme-surface p-2"
169
+ >
170
+ <div className="flex flex-wrap items-center gap-2 text-xs">
171
+ <span className="font-medium text-theme-text-primary">
172
+ Evidence
173
+ </span>
174
+ <span className="text-theme-text-tertiary">
175
+ {formatReasonID(reason.id)}
176
+ </span>
177
+ {reason.conditionType && (
178
+ <span className="text-theme-text-tertiary">
179
+ {reason.conditionType}
180
+ </span>
181
+ )}
182
+ {reason.conditionReason && (
183
+ <span className="text-theme-text-tertiary">
184
+ {reason.conditionReason}
185
+ </span>
186
+ )}
187
+ </div>
188
+ {/* Radar's reading leads, as the reason is built to; the
189
+ controller's own sentence follows it, quoted and attributed
190
+ the same way the inline variant attributes it. Unlabelled
191
+ beside the condition chips, it read as Radar's words. */}
192
+ {!isHPAReasonRedundant(diagnosis, reason) && (
193
+ <div className="mt-1 text-xs text-theme-text-secondary">
194
+ {reason.message}
195
+ </div>
196
+ )}
197
+ {reason.detail &&
198
+ // `detail` is the controller's own sentence only on a reason
199
+ // built from a condition, which is the only kind that carries
200
+ // a condition type. Elsewhere it is Radar's — a list of metric
201
+ // names, a note about an unobserved generation — and quoting
202
+ // that as Kubernetes would put words in its mouth.
203
+ (reason.conditionType ? (
204
+ <p className="mt-1 text-xs text-theme-text-tertiary">
205
+ Kubernetes: &ldquo;{reason.detail}&rdquo;
206
+ </p>
207
+ ) : (
208
+ <p className="mt-1 text-xs text-theme-text-tertiary">
209
+ {reason.detail}
210
+ </p>
211
+ ))}
212
+ </div>
213
+ ))}
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ }
@@ -1839,6 +1839,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1839
1839
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1840
1840
  { key: 'status', label: 'Status', width: 'w-24' },
1841
1841
  { key: 'store', label: 'Store', width: 'w-36' },
1842
+ { key: 'provider', label: 'Provider', width: 'w-48', tooltip: 'Backend the referenced store reads from. A dash means the store could not be read — it may not exist, or may be outside your access; an ellipsis means the stores are still loading.' },
1842
1843
  { key: 'refreshInterval', label: 'Refresh', width: 'w-24', tooltip: 'How often the value is re-pulled from the provider. A zero interval means it is fetched once and not updated afterward.' },
1843
1844
  { key: 'lastSync', label: 'Last Sync', width: 'w-28' },
1844
1845
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -3270,6 +3271,10 @@ interface ResourcesViewData {
3270
3271
  onNavigate?: (path: string, options?: { replace?: boolean }) => void
3271
3272
  certExpiry?: Record<string, { expired?: boolean; daysLeft: number }>
3272
3273
  certExpiryError?: boolean
3274
+ // Provider type per SecretStore, keyed 'ns/name' and 'name' for the
3275
+ // cluster-scoped kind. An ExternalSecret names a store; only the store
3276
+ // knows which backend it reads from.
3277
+ storeProviders?: Record<string, string>
3273
3278
  /** Cluster Audit findings keyed by "namespace/name". Raw compatibility
3274
3279
  * counts render danger as High and warning as Medium. */
3275
3280
  auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
@@ -3330,6 +3335,10 @@ interface ResourcesViewProps {
3330
3335
  topNodeMetrics?: TopNodeMetrics[]
3331
3336
  certExpiry?: Record<string, { expired?: boolean; daysLeft: number }>
3332
3337
  certExpiryError?: boolean
3338
+ // Provider type per SecretStore, keyed 'ns/name' and 'name' for the
3339
+ // cluster-scoped kind. An ExternalSecret names a store; only the store
3340
+ // knows which backend it reads from.
3341
+ storeProviders?: Record<string, string>
3333
3342
  /** Cluster Audit findings keyed by "namespace/name". Raw compatibility
3334
3343
  * counts render danger as High and warning as Medium. */
3335
3344
  auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
@@ -3579,6 +3588,7 @@ export function ResourcesView({
3579
3588
  topNodeMetrics,
3580
3589
  certExpiry,
3581
3590
  certExpiryError,
3591
+ storeProviders,
3582
3592
  auditBadges,
3583
3593
  pinned = [],
3584
3594
  togglePin = () => {},
@@ -6621,6 +6631,7 @@ export function ResourcesView({
6621
6631
  isChecked={checkedResources.has(resourceKey)}
6622
6632
  showCheckbox={isCheckboxMode}
6623
6633
  majorityNodeMinorVersion={majorityNodeMinorVersion}
6634
+ storeProviders={storeProviders}
6624
6635
  onRowClick={handleRowClick}
6625
6636
  onRowMouseEnter={handleRowMouseEnter}
6626
6637
  compareMode={compareMode}
@@ -6776,6 +6787,7 @@ interface ResourceRowCellsProps {
6776
6787
  isChecked?: boolean
6777
6788
  showCheckbox?: boolean
6778
6789
  majorityNodeMinorVersion?: string
6790
+ storeProviders?: Record<string, string>
6779
6791
  // Row callbacks receive the resource so the parent can pass referentially
6780
6792
  // stable handlers — per-row closures would defeat React.memo and re-render
6781
6793
  // every visible row on each SSE-driven refetch.
@@ -6810,7 +6822,7 @@ function rowHighlightClass(
6810
6822
  return 'group-hover/row:bg-theme-surface/50'
6811
6823
  }
6812
6824
 
6813
- const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, isChecked, showCheckbox, majorityNodeMinorVersion, onRowClick, onRowMouseEnter, compareMode, comparePickIndex = -1, rowHref, onRowCheckToggle }: ResourceRowCellsProps) {
6825
+ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, isChecked, showCheckbox, majorityNodeMinorVersion, storeProviders, onRowClick, onRowMouseEnter, compareMode, comparePickIndex = -1, rowHref, onRowCheckToggle }: ResourceRowCellsProps) {
6814
6826
  const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted, isChecked)
6815
6827
  const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
6816
6828
  const onClick = onRowClick ? () => onRowClick(resource, !!isSelected) : undefined
@@ -6877,6 +6889,7 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
6877
6889
  group={group}
6878
6890
  column={col.key}
6879
6891
  majorityNodeMinorVersion={majorityNodeMinorVersion}
6892
+ storeProviders={storeProviders}
6880
6893
  extraColumn={extraColumnsByKey?.get(col.key)}
6881
6894
  nameHref={col.key === 'name' ? rowHref : undefined}
6882
6895
  />
@@ -6920,6 +6933,7 @@ interface CellContentProps {
6920
6933
  column: string
6921
6934
  group?: string
6922
6935
  majorityNodeMinorVersion?: string
6936
+ storeProviders?: Record<string, string>
6923
6937
  /** When provided, the parent has injected an ExtraColumn for this
6924
6938
  * column key. Render via the extra's render() and short-circuit
6925
6939
  * the built-in cell logic. */
@@ -6929,7 +6943,7 @@ interface CellContentProps {
6929
6943
  nameHref?: string
6930
6944
  }
6931
6945
 
6932
- function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, extraColumn, nameHref }: CellContentProps) {
6946
+ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref }: CellContentProps) {
6933
6947
  const { auditBadges } = useContext(ResourcesViewDataContext)
6934
6948
  // Parent-injected extra columns short-circuit the built-in switch.
6935
6949
  // Used by hosts that inject leading columns (e.g. a multi-cluster Cluster column).
@@ -7317,7 +7331,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7317
7331
  return <SbomReportCell resource={resource} column={column} />
7318
7332
  // External Secrets Operator
7319
7333
  case 'externalsecrets':
7320
- return <ExternalSecretCell resource={resource} column={column} />
7334
+ return <ExternalSecretCell resource={resource} column={column} storeProviders={storeProviders} />
7321
7335
  case 'clusterexternalsecrets':
7322
7336
  return <ClusterExternalSecretCell resource={resource} column={column} />
7323
7337
  case 'secretstores':
@@ -1,5 +1,12 @@
1
1
  export * from './resource-utils'
2
2
  export * from './resource-utils-hpa'
3
+ export {
4
+ HPADiagnosisSummary,
5
+ hpaBadgeSeverity,
6
+ hpaReasonGroups,
7
+ isHPAReasonRedundant,
8
+ } from './HPADiagnosisSummary'
9
+ export type { HPADiagnosisSummaryProps, HPAReasonGroups } from './HPADiagnosisSummary'
3
10
  export * from './resource-utils-argo'
4
11
  export * from './resource-utils-certmanager'
5
12
  export * from './resource-utils-cnpg'
@@ -145,9 +145,9 @@ export function AWSManagedControlPlaneRenderer({ data }: Props) {
145
145
  <td className="py-1 text-theme-text-secondary font-mono text-[10px]">{a.statusVersion !== '-' ? a.statusVersion : a.specVersion}</td>
146
146
  <td className="py-1">
147
147
  <span className={`badge badge-sm ${a.status === 'ACTIVE'
148
- ? 'status-healthy'
148
+ ? 'status-green'
149
149
  : a.status === 'DEGRADED'
150
- ? 'status-unhealthy'
150
+ ? 'status-red'
151
151
  : 'status-neutral'
152
152
  }`}>{a.status}</span>
153
153
  </td>
@@ -92,9 +92,9 @@ export function AlertRenderer({ data }: AlertRendererProps) {
92
92
  value={
93
93
  <span className={clsx(
94
94
  'badge',
95
- eventSeverity === 'error' ? 'bg-red-500/20 text-red-400' :
96
- eventSeverity === 'warning' ? 'bg-yellow-500/20 text-yellow-400' :
97
- 'bg-blue-500/20 text-blue-400'
95
+ eventSeverity === 'error' ? 'status-red' :
96
+ eventSeverity === 'warning' ? 'status-amber' :
97
+ 'status-blue'
98
98
  )}>
99
99
  {eventSeverity}
100
100
  </span>
@@ -178,7 +178,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
178
178
  <button
179
179
  onClick={() => onTerminate({ namespace, name })}
180
180
  disabled={isTerminating}
181
- className="flex items-center gap-1.5 px-2 py-1 rounded text-xs bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-50"
181
+ className="flex items-center gap-1.5 px-2 py-1 rounded text-xs status-red hover:opacity-80 transition-colors disabled:opacity-50"
182
182
  title="Terminate sync"
183
183
  >
184
184
  <XCircle className="w-3.5 h-3.5" />
@@ -237,7 +237,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
237
237
  <span
238
238
  className={clsx(
239
239
  'badge',
240
- syncPolicy.automated ? 'bg-green-500/20 text-green-400' : BADGE_INACTIVE
240
+ syncPolicy.automated ? 'status-green' : BADGE_INACTIVE
241
241
  )}
242
242
  >
243
243
  {syncPolicy.automated ? 'Enabled' : 'Disabled'}
@@ -281,11 +281,11 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
281
281
  className={clsx(
282
282
  'badge',
283
283
  operationState.phase === 'Succeeded'
284
- ? 'bg-green-500/20 text-green-400'
284
+ ? 'status-green'
285
285
  : operationState.phase === 'Running'
286
- ? 'bg-blue-500/20 text-blue-400'
286
+ ? 'status-blue'
287
287
  : operationState.phase === 'Failed' || operationState.phase === 'Error'
288
- ? 'bg-red-500/20 text-red-400'
288
+ ? 'status-red'
289
289
  : BADGE_INACTIVE
290
290
  )}
291
291
  >
@@ -287,7 +287,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
287
287
  )}
288
288
  {data.spec?.enableSuperuserAccess !== undefined && (
289
289
  <Property label="Superuser Access" value={
290
- <span className={`badge-sm ${data.spec.enableSuperuserAccess ? 'bg-green-500/20 text-green-400' : 'bg-theme-hover text-theme-text-secondary'}`}>
290
+ <span className={`badge-sm ${data.spec.enableSuperuserAccess ? 'status-green' : 'bg-theme-hover text-theme-text-secondary'}`}>
291
291
  {data.spec.enableSuperuserAccess ? 'Enabled' : 'Disabled'}
292
292
  </span>
293
293
  } />
@@ -120,13 +120,13 @@ export function CertificateRenderer({ data }: CertificateRendererProps) {
120
120
  <span className={clsx(
121
121
  'badge',
122
122
  isReady
123
- ? 'bg-green-500/20 text-green-400'
124
- : 'bg-red-500/20 text-red-400'
123
+ ? 'status-green'
124
+ : 'status-red'
125
125
  )}>
126
126
  {isReady ? 'Ready' : 'Not Ready'}
127
127
  </span>
128
128
  {spec.isCA && (
129
- <span className="badge bg-purple-500/20 text-purple-400">
129
+ <span className="badge status-purple">
130
130
  CA Certificate
131
131
  </span>
132
132
  )}
@@ -54,8 +54,8 @@ export function CertificateRequestRenderer({ data }: CertificateRequestRendererP
54
54
  <span className={clsx(
55
55
  'badge',
56
56
  isReady
57
- ? 'bg-green-500/20 text-green-400'
58
- : 'bg-red-500/20 text-red-400'
57
+ ? 'status-green'
58
+ : 'status-red'
59
59
  )}>
60
60
  {isReady ? 'Ready' : 'Not Ready'}
61
61
  </span>
@@ -67,10 +67,10 @@ export function CertificateRequestRenderer({ data }: CertificateRequestRendererP
67
67
  <span className={clsx(
68
68
  'badge',
69
69
  isApproved
70
- ? 'bg-green-500/20 text-green-400'
70
+ ? 'status-green'
71
71
  : isDenied
72
- ? 'bg-red-500/20 text-red-400'
73
- : 'bg-yellow-500/20 text-yellow-400'
72
+ ? 'status-red'
73
+ : 'status-amber'
74
74
  )}>
75
75
  {isApproved ? 'Yes' : isDenied ? 'No' : 'Pending'}
76
76
  </span>
@@ -7,19 +7,19 @@ import { BADGE_INACTIVE } from '../../../utils/badge-colors'
7
7
  function getChallengeStateBadge(state: string): { color: string; text: string } {
8
8
  switch (state?.toLowerCase()) {
9
9
  case 'valid':
10
- return { text: 'Valid', color: 'bg-green-500/20 text-green-400' }
10
+ return { text: 'Valid', color: 'status-green' }
11
11
  case 'ready':
12
- return { text: 'Ready', color: 'bg-blue-500/20 text-blue-400' }
12
+ return { text: 'Ready', color: 'status-blue' }
13
13
  case 'pending':
14
- return { text: 'Pending', color: 'bg-yellow-500/20 text-yellow-400' }
14
+ return { text: 'Pending', color: 'status-amber' }
15
15
  case 'processing':
16
- return { text: 'Processing', color: 'bg-yellow-500/20 text-yellow-400' }
16
+ return { text: 'Processing', color: 'status-amber' }
17
17
  case 'invalid':
18
- return { text: 'Invalid', color: 'bg-red-500/20 text-red-400' }
18
+ return { text: 'Invalid', color: 'status-red' }
19
19
  case 'expired':
20
- return { text: 'Expired', color: 'bg-red-500/20 text-red-400' }
20
+ return { text: 'Expired', color: 'status-red' }
21
21
  case 'errored':
22
- return { text: 'Errored', color: 'bg-red-500/20 text-red-400' }
22
+ return { text: 'Errored', color: 'status-red' }
23
23
  default:
24
24
  return { text: state || 'Unknown', color: BADGE_INACTIVE }
25
25
  }
@@ -78,7 +78,7 @@ export function ChallengeRenderer({ data }: { data: any }) {
78
78
  {challengeType}
79
79
  </span>
80
80
  {spec.wildcard && (
81
- <span className="badge bg-purple-500/20 text-purple-400">
81
+ <span className="badge status-purple">
82
82
  Wildcard
83
83
  </span>
84
84
  )}
@@ -144,7 +144,7 @@ function CiliumRuleCard({ rule }: { rule: any }) {
144
144
  <div className="text-xs text-theme-text-tertiary mb-1">From Entities</div>
145
145
  <div className="flex flex-wrap gap-1">
146
146
  {fromEntities.map((e, j) => (
147
- <span key={j} className="badge bg-purple-500/20 text-purple-400 border-purple-500/30">{e}</span>
147
+ <span key={j} className="badge status-purple">{e}</span>
148
148
  ))}
149
149
  </div>
150
150
  </div>
@@ -215,7 +215,7 @@ function CiliumRuleCard({ rule }: { rule: any }) {
215
215
  <div className="text-xs text-theme-text-tertiary mb-1">To Entities</div>
216
216
  <div className="flex flex-wrap gap-1">
217
217
  {toEntities.map((e, j) => (
218
- <span key={j} className="badge bg-purple-500/20 text-purple-400 border-purple-500/30">{e}</span>
218
+ <span key={j} className="badge status-purple">{e}</span>
219
219
  ))}
220
220
  </div>
221
221
  </div>
@@ -365,7 +365,7 @@ function L7Rules({ rules, ports }: { rules: any; ports?: any[] }) {
365
365
  {httpRules.map((r: any, i: number) => (
366
366
  <div key={i} className="flex flex-wrap items-center gap-1">
367
367
  {r.method && (
368
- <span className="badge badge-sm bg-blue-500/20 text-blue-400 border-blue-500/30">
368
+ <span className="badge badge-sm status-blue">
369
369
  {r.method}
370
370
  </span>
371
371
  )}
@@ -81,8 +81,8 @@ function IssuerRendererBase({ data, kind }: { data: any; kind: string }) {
81
81
  <span className={clsx(
82
82
  'badge',
83
83
  isReady
84
- ? 'bg-green-500/20 text-green-400'
85
- : 'bg-red-500/20 text-red-400'
84
+ ? 'status-green'
85
+ : 'status-red'
86
86
  )}>
87
87
  {isReady ? 'Ready' : 'Not Ready'}
88
88
  </span>
@@ -76,10 +76,10 @@ function AdminRuleCard({ rule, direction }: { rule: any; direction: 'from' | 'to
76
76
  const ports: any[] = rule.ports || []
77
77
 
78
78
  const actionColor = action === 'Deny'
79
- ? 'bg-red-500/20 text-red-400 border-red-500/30'
79
+ ? 'status-red'
80
80
  : action === 'Pass'
81
- ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
82
- : 'bg-green-500/20 text-green-400 border-green-500/30'
81
+ ? 'status-amber'
82
+ : 'status-green'
83
83
 
84
84
  return (
85
85
  <div className="card-inner-lg">
@@ -131,9 +131,9 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
131
131
  <span className={clsx(
132
132
  'w-4 h-4 rounded-full flex items-center justify-center text-xs shrink-0',
133
133
  isListenerAccepted
134
- ? 'bg-green-500/20 text-green-400'
134
+ ? 'status-green'
135
135
  : isListenerNotAccepted
136
- ? 'bg-red-500/20 text-red-400'
136
+ ? 'status-red'
137
137
  : BADGE_INACTIVE
138
138
  )}>
139
139
  {isListenerAccepted ? '\u2713' : isListenerNotAccepted ? '\u2717' : '?'}
@@ -364,7 +364,7 @@ function GenericConditionsSection({ conditions }: { conditions: any[] }) {
364
364
  <span className="text-theme-text-primary font-medium">{cond.type}</span>
365
365
  <span className={clsx(
366
366
  'badge-sm',
367
- isTrue ? 'bg-theme-elevated text-theme-text-secondary' : 'bg-red-500/20 text-red-400'
367
+ isTrue ? 'bg-theme-elevated text-theme-text-secondary' : 'status-red'
368
368
  )}>
369
369
  {cond.status}
370
370
  </span>
@@ -2,11 +2,11 @@ import type { ReactNode } from 'react'
2
2
  import { Activity, Cpu } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property, ConditionsSection, ResourceLink, type ConditionTone } from '../../ui/drawer-components'
5
- import { Badge, type BadgeSeverity } from '../../ui/Badge'
5
+ import { Badge } from '../../ui/Badge'
6
6
  import { kindToPlural } from '../../../utils/navigation'
7
7
  import { formatAge } from '../resource-utils'
8
- import { hpaStateLabel, hpaStateLevel } from '../resource-utils-hpa'
9
- import type { HPADiagnosis, HPADiagnosisState } from '../../../types'
8
+ import { HPADiagnosisSummary } from '../HPADiagnosisSummary'
9
+ import type { HPADiagnosis } from '../../../types'
10
10
 
11
11
  interface HPARendererProps {
12
12
  data: any
@@ -16,23 +16,6 @@ interface HPARendererProps {
16
16
  extraSections?: ReactNode
17
17
  }
18
18
 
19
- function hpaBadgeSeverity(state: HPADiagnosisState): BadgeSeverity {
20
- switch (hpaStateLevel(state)) {
21
- case 'healthy':
22
- return 'success'
23
- case 'unhealthy':
24
- return 'error'
25
- case 'degraded':
26
- return 'warning'
27
- case 'alert':
28
- return 'alert'
29
- case 'neutral':
30
- return 'info'
31
- default:
32
- return 'neutral'
33
- }
34
- }
35
-
36
19
  function hpaConditionTone(condition: any): ConditionTone | undefined {
37
20
  if (condition?.type === 'ScaledToZero') {
38
21
  if (condition.status === 'False') return 'ok'
@@ -50,14 +33,6 @@ function hpaConditionTone(condition: any): ConditionTone | undefined {
50
33
  return 'warning'
51
34
  }
52
35
 
53
- function formatReasonID(id: string): string {
54
- return id.replace(/_/g, ' ')
55
- }
56
-
57
- function isReasonMessageRedundant(state: HPADiagnosisState, reason: NonNullable<HPADiagnosis['reasons']>[number]): boolean {
58
- return state === 'limited_max' && reason.id === 'limited_max'
59
- }
60
-
61
36
  export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: HPARendererProps) {
62
37
  const status = data.status || {}
63
38
  const spec = data.spec || {}
@@ -67,35 +42,7 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
67
42
  <>
68
43
  {hpaDiagnosis && (
69
44
  <Section title="Diagnosis" icon={Activity}>
70
- <div className="card-inner space-y-3">
71
- <div className="flex flex-wrap items-start justify-between gap-2">
72
- <div className="min-w-0">
73
- <div className="text-sm font-medium text-theme-text-primary">{hpaDiagnosis.summary}</div>
74
- <div className="mt-1 text-xs text-theme-text-secondary">
75
- {hpaDiagnosis.bounds.current}/{hpaDiagnosis.bounds.desired} replicas, bounds {hpaDiagnosis.bounds.min}-{hpaDiagnosis.bounds.max}
76
- </div>
77
- </div>
78
- <Badge severity={hpaBadgeSeverity(hpaDiagnosis.state)}>{hpaStateLabel(hpaDiagnosis.state)}</Badge>
79
- </div>
80
- {hpaDiagnosis.reasons && hpaDiagnosis.reasons.length > 0 && (
81
- <div className="space-y-2">
82
- {hpaDiagnosis.reasons.map((reason) => (
83
- <div key={`${reason.id}-${reason.message}`} className="rounded border border-theme-border bg-theme-surface p-2">
84
- <div className="flex flex-wrap items-center gap-2 text-xs">
85
- <span className="font-medium text-theme-text-primary">Evidence</span>
86
- <span className="text-theme-text-tertiary">{formatReasonID(reason.id)}</span>
87
- {reason.conditionType && <span className="text-theme-text-tertiary">{reason.conditionType}</span>}
88
- {reason.conditionReason && <span className="text-theme-text-tertiary">{reason.conditionReason}</span>}
89
- {reason.detail && <span className="text-theme-text-tertiary">{reason.detail}</span>}
90
- </div>
91
- {!isReasonMessageRedundant(hpaDiagnosis.state, reason) && (
92
- <div className="mt-1 text-xs text-theme-text-secondary">{reason.message}</div>
93
- )}
94
- </div>
95
- ))}
96
- </div>
97
- )}
98
- </div>
45
+ <HPADiagnosisSummary diagnosis={hpaDiagnosis} variant="detail" />
99
46
  </Section>
100
47
  )}
101
48