@skyhook-io/k8s-ui 1.11.0 → 1.13.0

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 (123) hide show
  1. package/package.json +4 -4
  2. package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
  5. package/src/components/gitops/detail-helpers.ts +4 -1
  6. package/src/components/issues/severity.ts +1 -0
  7. package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
  8. package/src/components/resources/ResourcesView.tsx +412 -5
  9. package/src/components/resources/index.ts +27 -0
  10. package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
  11. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  12. package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
  13. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
  14. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
  15. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
  16. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
  17. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
  18. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
  19. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
  20. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
  21. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
  22. package/src/components/resources/renderers/CNPGShared.tsx +27 -0
  23. package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
  24. package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
  25. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
  26. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
  27. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
  28. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
  29. package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
  30. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  31. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  32. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  33. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  34. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  35. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  36. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
  37. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
  38. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
  39. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
  40. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
  41. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
  42. package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
  43. package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
  44. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
  46. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  47. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  48. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
  49. package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
  50. package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
  51. package/src/components/resources/renderers/PolicySection.tsx +197 -0
  52. package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
  53. package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
  54. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
  55. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
  56. package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
  57. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
  58. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
  59. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
  60. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
  61. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
  62. package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
  63. package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
  64. package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
  65. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
  66. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  67. package/src/components/resources/renderers/calico-cells.tsx +177 -0
  68. package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
  69. package/src/components/resources/renderers/index.ts +16 -0
  70. package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
  71. package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
  72. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  73. package/src/components/resources/renderers/velero-cells.tsx +29 -7
  74. package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
  75. package/src/components/resources/resource-utils-calico.test.ts +215 -0
  76. package/src/components/resources/resource-utils-calico.ts +261 -0
  77. package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
  78. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
  79. package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
  80. package/src/components/resources/resource-utils-cnpg.ts +324 -3
  81. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  82. package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
  83. package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
  84. package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
  85. package/src/components/resources/resource-utils-kyverno.ts +43 -7
  86. package/src/components/resources/resource-utils-velero.test.ts +26 -0
  87. package/src/components/resources/resource-utils-velero.ts +68 -0
  88. package/src/components/resources/resource-utils.ts +98 -0
  89. package/src/components/shared/ResourceActionsBar.tsx +101 -16
  90. package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
  91. package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
  92. package/src/components/shared/index.ts +1 -1
  93. package/src/components/shared/revision-role-badges.test.ts +120 -0
  94. package/src/components/topology/K8sResourceNode.test.ts +10 -0
  95. package/src/components/topology/K8sResourceNode.tsx +22 -0
  96. package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
  97. package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
  98. package/src/components/topology/TopologyGraph.tsx +6 -6
  99. package/src/components/topology/TopologySearch.tsx +7 -0
  100. package/src/components/topology/layout.ts +3 -0
  101. package/src/components/trace/ReachabilityView.tsx +1 -0
  102. package/src/components/trace/TracePanel.tsx +12 -2
  103. package/src/components/trace/reachFixtures.ts +26 -1
  104. package/src/components/trace/reachGraphModel.test.ts +20 -0
  105. package/src/components/trace/reachGraphModel.ts +8 -1
  106. package/src/components/trace/reachOrigins.test.ts +31 -0
  107. package/src/components/trace/reachOrigins.ts +11 -1
  108. package/src/components/ui/Badge.tsx +10 -0
  109. package/src/components/ui/ScheduleValue.test.tsx +80 -0
  110. package/src/components/ui/ScheduleValue.tsx +85 -0
  111. package/src/components/ui/drawer-components.test.tsx +17 -1
  112. package/src/components/ui/drawer-components.tsx +9 -2
  113. package/src/components/ui/index.ts +2 -0
  114. package/src/components/workload/WorkloadView.tsx +3 -0
  115. package/src/types/core.ts +29 -0
  116. package/src/types/gitops-flux-status.test.ts +163 -0
  117. package/src/types/gitops.ts +80 -24
  118. package/src/types/index.ts +1 -0
  119. package/src/types/policy.ts +193 -0
  120. package/src/utils/navigation.test.ts +5 -0
  121. package/src/utils/navigation.ts +5 -0
  122. package/src/utils/resource-icons.test.ts +49 -2
  123. package/src/utils/resource-icons.ts +29 -2
@@ -0,0 +1,235 @@
1
+ import { Archive, Clock, HardDrive } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { HEALTH_BADGE_COLORS } from '../../../utils/badge-colors'
5
+ import { RecoveryTime } from './CNPGShared'
6
+ import {
7
+ getCNPGObjectStoreProvider,
8
+ getCNPGObjectStoreCredentialSecret,
9
+ getCNPGObjectStoreRecoveryWindows,
10
+ getCNPGObjectStoreDestination,
11
+ getCNPGObjectStoreRetention,
12
+ type CNPGObjectStoreRecoveryWindow,
13
+ } from '../resource-utils-cnpg'
14
+
15
+ /**
16
+ * ObjectStore (barmancloud.cnpg.io/v1) — where a plugin-backed CNPG Cluster's
17
+ * backups actually live.
18
+ *
19
+ * The Cluster page already tells operators the recovery window is tracked here,
20
+ * because a plugin-backed Cluster stops publishing recovery-point fields of its
21
+ * own. Until this renderer existed that sentence pointed at a page showing raw
22
+ * fields, so the restorable range was effectively unreachable in the product.
23
+ */
24
+
25
+ export function CNPGObjectStoreRenderer({
26
+ data,
27
+ clusterForServer,
28
+ archivingFailing,
29
+ onNavigate,
30
+ }: {
31
+ data: any
32
+ /** Server key -> the Cluster archiving under it, so a key is only linked when
33
+ * something is behind it. See RecoveryWindowRow. */
34
+ clusterForServer?: Map<string, string>
35
+ /** Servers whose Cluster reports WAL archiving stopped. The window keeps its
36
+ * last successful backup and reads as recoverable, while the recovery point
37
+ * has stopped advancing — a claim this status alone cannot correct. */
38
+ archivingFailing?: Set<string>
39
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
40
+ }) {
41
+ const windows = getCNPGObjectStoreRecoveryWindows(data)
42
+ const provider = getCNPGObjectStoreProvider(data)
43
+ const retention = getCNPGObjectStoreRetention(data)
44
+ const credentialSecret = getCNPGObjectStoreCredentialSecret(data)
45
+ const cfg = data?.spec?.configuration ?? {}
46
+ const failing = windows.filter((w) => w.failingSinceLastSuccess)
47
+
48
+ return (
49
+ <>
50
+ {failing.length > 0 && (
51
+ <AlertBanner
52
+ variant="error"
53
+ title={`Backups failing for ${failing.length === 1 ? failing[0].server : `${failing.length} servers`}`}
54
+ message="The most recent backup attempt failed after the last success, so the recoverable range has stopped moving forward."
55
+ />
56
+ )}
57
+
58
+ <Section title="Recovery Window" icon={Clock} defaultExpanded>
59
+ {windows.length === 0 ? (
60
+ // Configured but empty is NOT the same as healthy. Saying nothing here
61
+ // would read as "backups are fine" on a store holding nothing.
62
+ <div className="text-sm text-theme-text-secondary">
63
+ No server has reported a backup yet, so there is nothing to restore from this store.
64
+ </div>
65
+ ) : (
66
+ <div className="space-y-2">
67
+ {windows.map((w) => (
68
+ <RecoveryWindowRow
69
+ archivingFailing={archivingFailing}
70
+ clusterForServer={clusterForServer}
71
+ key={w.server}
72
+ window={w}
73
+ retention={retention}
74
+ namespace={data?.metadata?.namespace ?? ''}
75
+ onNavigate={onNavigate}
76
+ />
77
+ ))}
78
+ </div>
79
+ )}
80
+ </Section>
81
+
82
+ <Section title="Destination" icon={Archive} defaultExpanded>
83
+ <PropertyList>
84
+ <Property label="Path" value={getCNPGObjectStoreDestination(data)} />
85
+ {cfg.endpointURL && <Property label="Endpoint" value={cfg.endpointURL} />}
86
+ {/* Only when set — a custom CA is the difference between "cannot reach
87
+ the bucket" and "does not trust it", and it is otherwise invisible. */}
88
+ {(cfg.endpointCA?.name || cfg.endpointCA?.key) && (
89
+ <Property
90
+ label="Endpoint CA"
91
+ value={cfg.endpointCA?.name ? `${cfg.endpointCA.name}${cfg.endpointCA.key ? ` · ${cfg.endpointCA.key}` : ''}` : cfg.endpointCA.key}
92
+ />
93
+ )}
94
+ {provider && <Property label="Provider" value={provider} />}
95
+ {credentialSecret && (
96
+ <Property
97
+ label="Credentials"
98
+ value={
99
+ <ResourceLink
100
+ name={credentialSecret}
101
+ kind="secrets"
102
+ namespace={data?.metadata?.namespace ?? ''}
103
+ onNavigate={onNavigate}
104
+ />
105
+ }
106
+ />
107
+ )}
108
+ {cfg.serverName && <Property label="Server Name" value={cfg.serverName} />}
109
+ {retention && <Property label="Retention" value={retention} />}
110
+ </PropertyList>
111
+ </Section>
112
+
113
+ {(cfg.wal || cfg.data) && (
114
+ <Section title="Backup Settings" icon={HardDrive} defaultExpanded={false}>
115
+ <PropertyList>
116
+ {cfg.wal?.compression && <Property label="WAL Compression" value={cfg.wal.compression} />}
117
+ {cfg.wal?.encryption && <Property label="WAL Encryption" value={cfg.wal.encryption} />}
118
+ {cfg.wal?.maxParallel != null && (
119
+ <Property label="WAL Max Parallel" value={String(cfg.wal.maxParallel)} />
120
+ )}
121
+ {cfg.data?.compression && <Property label="Data Compression" value={cfg.data.compression} />}
122
+ {cfg.data?.encryption && <Property label="Data Encryption" value={cfg.data.encryption} />}
123
+ {cfg.data?.jobs != null && <Property label="Data Jobs" value={String(cfg.data.jobs)} />}
124
+ {cfg.data?.immediateCheckpoint != null && (
125
+ <Property label="Immediate Checkpoint" value={cfg.data.immediateCheckpoint ? 'Yes' : 'No'} />
126
+ )}
127
+ </PropertyList>
128
+ </Section>
129
+ )}
130
+ </>
131
+ )
132
+ }
133
+
134
+ /**
135
+ * One server's restorable range. The two timestamps are the answer to "how far
136
+ * back can I go, and how current is it" — the question the whole plugin path
137
+ * moved out of the Cluster.
138
+ */
139
+ function RecoveryWindowRow({
140
+ window: w,
141
+ retention,
142
+ namespace,
143
+ clusterForServer,
144
+ archivingFailing,
145
+ onNavigate,
146
+ }: {
147
+ window: CNPGObjectStoreRecoveryWindow
148
+ retention?: string
149
+ namespace: string
150
+ /** Server key -> the Cluster archiving under it. The key defaults to the
151
+ * cluster's name but the plugin's `serverName` parameter overrides it, so the
152
+ * two are not interchangeable and the mapping has to come from the plugin
153
+ * parameters rather than from string equality.
154
+ *
155
+ * Absent means unresolved, not "assume it resolves": the lookup may still be
156
+ * loading, it may have failed, or the host may not be able to ask. All three
157
+ * render the key as text, which is the part that is always true. */
158
+ clusterForServer?: Map<string, string>
159
+ /** Servers whose Cluster reports WAL archiving stopped. */
160
+ archivingFailing?: Set<string>
161
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
162
+ }) {
163
+ const stalled = archivingFailing?.has(w.server)
164
+ const tone = w.failingSinceLastSuccess || stalled
165
+ ? 'degraded'
166
+ : w.lastSuccessfulBackupTime
167
+ ? 'healthy'
168
+ : 'unknown'
169
+ return (
170
+ <div className="card-inner">
171
+ <div className="flex items-center gap-2 mb-1">
172
+ {/* The server key is usually the Cluster this window belongs to, and
173
+ naming it without a route leaves the reader knowing a cluster is in
174
+ trouble and unable to open it. Only usually, though — see
175
+ `clusterNames`. */}
176
+ <span className="text-sm font-medium text-theme-text-primary">
177
+ {clusterForServer?.get(w.server) ? (
178
+ <ResourceLink
179
+ name={clusterForServer.get(w.server) as string}
180
+ label={w.server}
181
+ kind="clusters"
182
+ namespace={namespace}
183
+ group="postgresql.cnpg.io"
184
+ onNavigate={onNavigate}
185
+ />
186
+ ) : (
187
+ w.server
188
+ )}
189
+ </span>
190
+ <span className={clsx('badge', HEALTH_BADGE_COLORS[tone as keyof typeof HEALTH_BADGE_COLORS])}>
191
+ {w.failingSinceLastSuccess
192
+ ? 'Backups failing'
193
+ : stalled
194
+ ? 'Not advancing'
195
+ : w.lastSuccessfulBackupTime
196
+ ? 'Recoverable'
197
+ : 'No backups yet'}
198
+ </span>
199
+ </div>
200
+ {stalled && !w.failingSinceLastSuccess && (
201
+ <div className="text-xs text-warning-text mb-1">
202
+ {/* The timestamps below are real and still describe the last backup that
203
+ worked. What they no longer describe is a window still growing, and
204
+ "Recoverable" alone invites exactly that reading on the screen
205
+ someone checks before a restore. */}
206
+ WAL archiving has stopped on the cluster behind this server, so the times below are the
207
+ last ones recorded rather than a window still advancing.
208
+ </div>
209
+ )}
210
+ <PropertyList>
211
+ {w.firstRecoverabilityPoint && (
212
+ <Property
213
+ label="Restorable From"
214
+ value={<RecoveryTime at={w.firstRecoverabilityPoint} />}
215
+ />
216
+ )}
217
+ {w.lastSuccessfulBackupTime ? (
218
+ <Property label="Last Successful Backup" value={<RecoveryTime at={w.lastSuccessfulBackupTime} />} />
219
+ ) : (
220
+ <Property label="Last Successful Backup" value="never" />
221
+ )}
222
+ {w.lastFailedBackupTime && (
223
+ <Property label="Last Failed Attempt" value={<RecoveryTime at={w.lastFailedBackupTime} />} />
224
+ )}
225
+ {retention && <Property label="Retention" value={retention} />}
226
+ </PropertyList>
227
+ {w.failingSinceLastSuccess && (
228
+ <div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">
229
+ Every backup since the last success has failed. The oldest restorable point will still age
230
+ out under the retention policy, so the window is shrinking from both ends.
231
+ </div>
232
+ )}
233
+ </div>
234
+ )
235
+ }
@@ -8,6 +8,7 @@ import {
8
8
  getCNPGPoolerParameters,
9
9
  getCNPGPoolerAuthQuery,
10
10
  getCNPGPoolerAuthQuerySecret,
11
+ isCNPGPoolerPaused,
11
12
  } from '../resource-utils-cnpg'
12
13
 
13
14
  interface CNPGPoolerRendererProps {
@@ -23,6 +24,7 @@ export function CNPGPoolerRenderer({ data, onNavigate }: CNPGPoolerRendererProps
23
24
  // CR never establishes.
24
25
  const scheduled = data.status?.instances ?? 0
25
26
  const isDegraded = desired > 0 && scheduled < desired
27
+ const isPaused = isCNPGPoolerPaused(data)
26
28
  const clusterName = getCNPGPoolerCluster(data)
27
29
  const parameters = getCNPGPoolerParameters(data)
28
30
  const authQuery = getCNPGPoolerAuthQuery(data)
@@ -39,12 +41,21 @@ export function CNPGPoolerRenderer({ data, onNavigate }: CNPGPoolerRendererProps
39
41
  />
40
42
  )}
41
43
 
44
+ {isPaused && (
45
+ <AlertBanner
46
+ variant="warning"
47
+ title="Pooler Paused"
48
+ message="PgBouncer is paused, so client connections are held rather than served. The pods stay scheduled and Ready throughout, so nothing else about this Pooler will look wrong."
49
+ />
50
+ )}
51
+
42
52
  {/* Pooler Configuration */}
43
53
  <Section title="Pooler Configuration" icon={Users} defaultExpanded>
44
54
  <PropertyList>
45
55
  <Property label="Type" value={getCNPGPoolerType(data)} />
46
56
  <Property label="Pool Mode" value={getCNPGPoolerMode(data)} />
47
57
  <Property label="Instances Scheduled" value={getCNPGPoolerInstances(data)} />
58
+ {isPaused && <Property label="PgBouncer" value="Paused" />}
48
59
  </PropertyList>
49
60
  </Section>
50
61
 
@@ -1,5 +1,6 @@
1
1
  import { Clock, Database } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { CronValue } from '../../ui/ScheduleValue'
3
4
  import {
4
5
  getCNPGScheduledBackupCluster,
5
6
  getCNPGScheduleCron,
@@ -8,7 +9,9 @@ import {
8
9
  getCNPGScheduledBackupNextSchedule,
9
10
  getCNPGScheduledBackupIsSuspended,
10
11
  getCNPGScheduledBackupIsImmediate,
12
+ getCNPGBackupPlugin,
11
13
  getCNPGScheduledBackupOwnerRef,
14
+ CNPG_BARMAN_OBJECTSTORE_GROUP,
12
15
  } from '../resource-utils-cnpg'
13
16
 
14
17
  interface CNPGScheduledBackupRendererProps {
@@ -19,6 +22,7 @@ interface CNPGScheduledBackupRendererProps {
19
22
  export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledBackupRendererProps) {
20
23
  const isSuspended = getCNPGScheduledBackupIsSuspended(data)
21
24
  const clusterName = getCNPGScheduledBackupCluster(data)
25
+ const schedulePlugin = getCNPGBackupPlugin(data)
22
26
 
23
27
  return (
24
28
  <>
@@ -34,7 +38,7 @@ export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledB
34
38
  {/* Schedule */}
35
39
  <Section title="Schedule" icon={Clock} defaultExpanded>
36
40
  <PropertyList>
37
- <Property label="Cron Expression" value={getCNPGScheduleCron(data)} />
41
+ <Property label="Cron Expression" value={<CronValue cron={getCNPGScheduleCron(data)} dialect="seconds" />} />
38
42
  <Property label="Last Schedule" value={getCNPGScheduledBackupLastSchedule(data)} />
39
43
  <Property label="Next Schedule" value={getCNPGScheduledBackupNextSchedule(data)} />
40
44
  <Property label="Suspended" value={isSuspended ? 'Yes' : 'No'} />
@@ -60,6 +64,24 @@ export function CNPGScheduledBackupRenderer({ data, onNavigate }: CNPGScheduledB
60
64
  return clusterName
61
65
  })()} />
62
66
  <Property label="Method" value={getCNPGScheduledBackupMethod(data)} />
67
+ {/* Under the plugin method the destination lives on an ObjectStore in
68
+ another API group, so naming it is the only route from here to
69
+ where these backups will actually land. */}
70
+ {schedulePlugin && <Property label="Plugin" value={schedulePlugin.name} />}
71
+ {schedulePlugin?.parameters?.barmanObjectName && (
72
+ <Property
73
+ label="Object Store"
74
+ value={
75
+ <ResourceLink
76
+ name={schedulePlugin.parameters.barmanObjectName}
77
+ kind="objectstores"
78
+ group={CNPG_BARMAN_OBJECTSTORE_GROUP}
79
+ namespace={data.metadata?.namespace || ''}
80
+ onNavigate={onNavigate}
81
+ />
82
+ }
83
+ />
84
+ )}
63
85
  <Property label="Owner Reference" value={getCNPGScheduledBackupOwnerRef(data)} />
64
86
  </PropertyList>
65
87
  </Section>
@@ -0,0 +1,27 @@
1
+ import { formatAge } from '../resource-utils'
2
+
3
+ /**
4
+ * A recovery point, stated absolutely with the interval beside it.
5
+ *
6
+ * "1d ago" is the wrong unit for this question on its own. An RPO conversation
7
+ * needs a timestamp — and "Last Successful Backup 1d ago" next to "Last Failed
8
+ * Attempt 1d ago" is the same string for events a week apart in the failure
9
+ * history, which is precisely when you are reading this screen. Same treatment
10
+ * the certificate rows already use.
11
+ *
12
+ * Shared by the Cluster and ObjectStore pages: the same recovery point is
13
+ * reachable from both depending on whether backups run through the plugin, and
14
+ * the two must not describe it differently.
15
+ */
16
+ export function RecoveryTime({ at }: { at: string }) {
17
+ const d = new Date(at)
18
+ if (Number.isNaN(d.getTime())) return <>{at}</>
19
+ return (
20
+ <span className="inline-flex items-baseline gap-2 min-w-0">
21
+ <span>{d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC')}</span>
22
+ {/* One interpolation, so the age and its unit stay a single text node
23
+ rather than being split by a renderer comment marker. */}
24
+ <span className="text-xs text-theme-text-tertiary shrink-0">{`${formatAge(at)} ago`}</span>
25
+ </span>
26
+ )
27
+ }
@@ -0,0 +1,71 @@
1
+ import { Network } from 'lucide-react'
2
+ import { Badge } from '../../ui/Badge'
3
+ import { ResourceLink, Section } from '../../ui/drawer-components'
4
+
5
+ interface CalicoHostEndpointRendererProps {
6
+ data: any
7
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
8
+ }
9
+
10
+ export function CalicoHostEndpointRenderer({ data, onNavigate }: CalicoHostEndpointRendererProps) {
11
+ const spec = data?.spec ?? {}
12
+ const expectedIPs: string[] = spec.expectedIPs ?? []
13
+ const profiles: string[] = spec.profiles ?? []
14
+ const ports: Array<{ name?: string; port?: number; protocol?: string }> = spec.ports ?? []
15
+
16
+ return (
17
+ <Section title="Host Endpoint" icon={Network}>
18
+ <div className="card-inner text-sm space-y-3">
19
+ <div className="flex items-start justify-between gap-3">
20
+ <div className="min-w-0">
21
+ <div className="text-xs text-theme-text-tertiary mb-0.5">Node</div>
22
+ <div className="font-medium text-theme-text-primary break-all">
23
+ {spec.node ? <ResourceLink name={spec.node} kind="nodes" namespace="" onNavigate={onNavigate} /> : 'Not specified'}
24
+ </div>
25
+ </div>
26
+ {spec.interfaceName && (
27
+ <div className="shrink-0 text-right">
28
+ <div className="text-xs text-theme-text-tertiary mb-0.5">Interface Name</div>
29
+ <Badge tone="structural" size="sm" className="font-mono">{spec.interfaceName}</Badge>
30
+ </div>
31
+ )}
32
+ </div>
33
+
34
+ {expectedIPs.length > 0 && (
35
+ <div className="border-t border-theme-border-subtle pt-3">
36
+ <div className="text-xs text-theme-text-tertiary mb-1.5">Expected IPs</div>
37
+ <div className="flex flex-wrap gap-1">
38
+ {expectedIPs.map((ip) => (
39
+ <Badge key={ip} tone="structural" size="sm" className="font-mono">{ip}</Badge>
40
+ ))}
41
+ </div>
42
+ </div>
43
+ )}
44
+
45
+ {profiles.length > 0 && (
46
+ <div className="border-t border-theme-border-subtle pt-3">
47
+ <div className="text-xs text-theme-text-tertiary mb-1.5">Profiles</div>
48
+ <div className="flex flex-wrap gap-1">
49
+ {profiles.map((profile) => (
50
+ <Badge key={profile} tone="structural" size="sm">{profile}</Badge>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ )}
55
+
56
+ {ports.length > 0 && (
57
+ <div className="border-t border-theme-border-subtle pt-3">
58
+ <div className="text-xs text-theme-text-tertiary mb-1.5">Named Ports</div>
59
+ <div className="flex flex-wrap gap-1">
60
+ {ports.map((port, index) => (
61
+ <Badge key={`${port.name}-${port.protocol}-${port.port}-${index}`} tone="structural" size="sm" className="font-mono">
62
+ {`${port.name ? `${port.name}: ` : ''}${port.protocol}/${port.port}`}
63
+ </Badge>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ )}
68
+ </div>
69
+ </Section>
70
+ )
71
+ }
@@ -0,0 +1,32 @@
1
+ import { Network } from 'lucide-react'
2
+ import { Property, PropertyList, Section } from '../../ui/drawer-components'
3
+ import {
4
+ getCalicoIPPoolAllowedUses,
5
+ getCalicoIPPoolBlockSize,
6
+ } from '../resource-utils-calico'
7
+
8
+ interface CalicoIPPoolRendererProps {
9
+ data: any
10
+ }
11
+
12
+ export function CalicoIPPoolRenderer({ data }: CalicoIPPoolRendererProps) {
13
+ const spec = data?.spec ?? {}
14
+
15
+ return (
16
+ <Section title="IP Pool" icon={Network}>
17
+ <PropertyList>
18
+ <Property label="Allowed Uses" value={getCalicoIPPoolAllowedUses(data)} />
19
+ <Property label="Assignment Mode" value={spec.assignmentMode ?? 'Automatic'} />
20
+ <Property label="Block Size" value={getCalicoIPPoolBlockSize(data)} />
21
+ <Property label="CIDR" value={spec.cidr} />
22
+ <Property label="IP-in-IP Mode" value={spec.ipipMode ?? 'Never'} />
23
+ <Property label="VXLAN Mode" value={spec.vxlanMode ?? 'Never'} />
24
+ <Property label="NAT Outgoing" value={spec.natOutgoing ? 'Yes' : 'No'} />
25
+ <Property label="Disabled" value={spec.disabled ? 'Yes' : 'No'} />
26
+ <Property label="BGP Export Disabled" value={spec.disableBGPExport ? 'Yes' : 'No'} />
27
+ <Property label="Node Selector" value={spec.nodeSelector ?? 'all()'} />
28
+ <Property label="Namespace Selector" value={spec.namespaceSelector} />
29
+ </PropertyList>
30
+ </Section>
31
+ )
32
+ }
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { CalicoNetworkPolicyDiagram } from './CalicoNetworkPolicyDiagram'
4
+
5
+ describe('CalicoNetworkPolicyDiagram', () => {
6
+ it('shows ordered actions and Calico match fields in both directions', () => {
7
+ const html = renderToString(
8
+ <CalicoNetworkPolicyDiagram
9
+ spec={{
10
+ selector: "app == 'api'",
11
+ namespaceSelector: "project == 'prod'",
12
+ serviceAccountSelector: "role == 'api'",
13
+ types: ['Ingress', 'Egress'],
14
+ ingress: [
15
+ {
16
+ action: 'Allow',
17
+ protocol: 'TCP',
18
+ source: {
19
+ selector: "role == 'frontend'",
20
+ nets: ['10.0.0.0/8'],
21
+ serviceAccounts: { names: ['frontend'] },
22
+ },
23
+ destination: { ports: [80] },
24
+ },
25
+ { action: 'Deny', source: { notSelector: "app == 'debug'" } },
26
+ ],
27
+ egress: [
28
+ {
29
+ action: 'Log',
30
+ protocol: 'UDP',
31
+ source: { serviceAccounts: { selector: "role == 'api'" } },
32
+ destination: {
33
+ selector: "app == 'db'",
34
+ nets: ['192.168.0.0/16'],
35
+ ports: [{ port: 5432, endPort: 5433 }],
36
+ },
37
+ },
38
+ { action: 'Pass', destination: { notNets: ['0.0.0.0/0'] } },
39
+ ],
40
+ }}
41
+ />,
42
+ )
43
+
44
+ for (const value of [
45
+ 'app == &#x27;api&#x27;',
46
+ 'role == &#x27;api&#x27;',
47
+ 'Ingress',
48
+ 'Egress',
49
+ 'Deny',
50
+ 'Log',
51
+ 'Pass',
52
+ 'TCP',
53
+ 'UDP',
54
+ 'role == &#x27;frontend&#x27;',
55
+ '10.0.0.0/8',
56
+ 'frontend',
57
+ 'TCP/80',
58
+ 'app == &#x27;db&#x27;',
59
+ '192.168.0.0/16',
60
+ 'UDP/5432-5433',
61
+ 'Not Nets',
62
+ '0.0.0.0/0',
63
+ ]) {
64
+ expect(html).toContain(value)
65
+ }
66
+
67
+ expect(html).not.toContain('Policy target')
68
+ expect(html).not.toContain('Source')
69
+ expect(html).not.toContain('Destination')
70
+ expect(html).not.toContain('Rule 1')
71
+ expect(html).toContain('Allow')
72
+ expect(html).not.toMatch(/badge-sm[^>]*>TCP<\/span>/)
73
+ expect(html).not.toMatch(/badge-sm[^>]*>UDP<\/span>/)
74
+ expect(html).toContain('text-warning-text')
75
+ expect(html).toContain('line-through')
76
+ })
77
+
78
+ it('keeps port constraints readable and distinguishes source exclusions', () => {
79
+ const html = renderToString(
80
+ <CalicoNetworkPolicyDiagram
81
+ spec={{
82
+ types: ['Egress'],
83
+ egress: [
84
+ {
85
+ protocol: 'UDP',
86
+ source: {
87
+ ports: [8080],
88
+ notPorts: [{ protocol: 'UDP', port: 5353 }],
89
+ },
90
+ destination: {
91
+ ports: [
92
+ { protocol: 'TCP', port: 8080 },
93
+ { protocol: 'TCP', port: 8443 },
94
+ ],
95
+ notPorts: [443],
96
+ },
97
+ },
98
+ ],
99
+ }}
100
+ />,
101
+ )
102
+
103
+ for (const value of [
104
+ 'src:UDP/8080',
105
+ 'src:!UDP/5353',
106
+ 'TCP/8080',
107
+ 'TCP/8443',
108
+ '!UDP/443',
109
+ ]) {
110
+ expect(html).toContain(value)
111
+ }
112
+ expect(html).not.toContain('dst:')
113
+ expect(html).not.toContain('max-w-14')
114
+ expect(html).not.toContain('truncate font-mono text-[8px]')
115
+ expect(html).not.toMatch(/badge-sm[^>]*>UDP<\/span>/)
116
+ })
117
+
118
+ it('renders protocol-only rules as wildcard arrow labels', () => {
119
+ const html = renderToString(
120
+ <CalicoNetworkPolicyDiagram
121
+ spec={{
122
+ types: ['Ingress'],
123
+ ingress: [
124
+ {
125
+ action: 'Deny',
126
+ protocol: 'TCP',
127
+ notProtocol: 'UDP',
128
+ source: { selector: "app == 'client'" },
129
+ },
130
+ ],
131
+ }}
132
+ />,
133
+ )
134
+
135
+ expect(html).toContain('TCP/*')
136
+ expect(html).not.toMatch(/badge-sm[^>]*>TCP<\/span>/)
137
+ expect(html).toContain('not')
138
+ expect(html).toContain('UDP')
139
+ })
140
+
141
+ it('uses native peer colors with stable match priority', () => {
142
+ const renderDestination = (destination: any) =>
143
+ renderToString(
144
+ <CalicoNetworkPolicyDiagram
145
+ spec={{
146
+ selector: "app == 'target'",
147
+ types: ['Egress'],
148
+ egress: [{ destination }],
149
+ }}
150
+ />,
151
+ )
152
+
153
+ const combined = renderDestination({
154
+ selector: "app == 'workload'",
155
+ namespaceSelector: "team == 'prod'",
156
+ nets: ['10.0.0.0/8'],
157
+ })
158
+ const namespace = renderDestination({
159
+ namespaceSelector: "team == 'prod'",
160
+ })
161
+ const network = renderDestination({ nets: ['10.0.0.0/8'] })
162
+ const serviceAccount = renderDestination({
163
+ serviceAccountSelector: "role == 'api'",
164
+ })
165
+ const unknown = renderDestination({ unsupportedMatch: 'value' })
166
+
167
+ expect(combined).toContain('border-emerald-500/30 bg-emerald-500/8')
168
+ expect(combined).not.toContain('border-sky-500/30 bg-sky-500/8')
169
+ expect(combined).not.toContain('border-amber-500/30 bg-amber-500/8')
170
+ expect(namespace).toContain('border-sky-500/30 bg-sky-500/8')
171
+ expect(network).toContain('border-amber-500/30 bg-amber-500/8')
172
+ expect(serviceAccount).toContain('border-emerald-500/30 bg-emerald-500/8')
173
+ expect(unknown).toContain('border-theme-border bg-theme-elevated/50')
174
+ expect(combined).toContain('border-indigo-500/30 bg-indigo-500/8')
175
+ })
176
+
177
+ it('does not imply deny-all when a policy has no explicit rules', () => {
178
+ const html = renderToString(
179
+ <CalicoNetworkPolicyDiagram spec={{ types: ['Ingress'], ingress: [] }} />,
180
+ )
181
+
182
+ expect(html).toContain('No explicit')
183
+ expect(html).toContain('ingress')
184
+ expect(html).not.toContain('Deny all')
185
+ })
186
+
187
+ it('renders staged rules as non-enforced dashed paths', () => {
188
+ const html = renderToString(
189
+ <CalicoNetworkPolicyDiagram
190
+ staged
191
+ spec={{
192
+ selector: "app == 'api'",
193
+ types: ['Ingress'],
194
+ ingress: [{ action: 'Allow', source: {} }],
195
+ }}
196
+ />,
197
+ )
198
+
199
+ expect(html).toContain('Staged preview')
200
+ expect(html).toContain('not enforced')
201
+ expect(html).toContain('stroke-dasharray="4 3"')
202
+ expect(html).toContain('Allow')
203
+ expect(html).not.toContain('Rule 1')
204
+ })
205
+ })