@skyhook-io/k8s-ui 1.4.0 → 1.4.2

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 (55) hide show
  1. package/package.json +2 -2
  2. package/src/components/resources/ResourcesSidebar.tsx +152 -57
  3. package/src/components/resources/ResourcesView.tsx +141 -49
  4. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
  5. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
  6. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
  7. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
  8. package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
  9. package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
  10. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
  11. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
  12. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
  13. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
  14. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
  15. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
  16. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
  17. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
  18. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
  19. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
  20. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
  21. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
  22. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
  23. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
  24. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
  25. package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
  26. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
  27. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
  28. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
  29. package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
  30. package/src/components/resources/renderers/PodRenderer.tsx +22 -74
  31. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
  32. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
  33. package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
  34. package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
  35. package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
  36. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
  37. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
  38. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
  39. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
  40. package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
  41. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
  42. package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
  43. package/src/components/resources/resource-utils-cnpg.ts +47 -0
  44. package/src/components/resources/resource-utils-karpenter.ts +6 -0
  45. package/src/components/resources/resource-utils-prometheus.ts +50 -6
  46. package/src/components/resources/resource-utils.ts +131 -33
  47. package/src/components/shared/EditableYamlView.tsx +38 -3
  48. package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
  49. package/src/components/timeline/TimelineList.tsx +11 -4
  50. package/src/components/ui/Tooltip.tsx +16 -0
  51. package/src/components/ui/drawer-components.tsx +48 -1
  52. package/src/theme/variables.css +1 -1
  53. package/src/types/gitops.ts +8 -0
  54. package/src/utils/navigation.test.ts +29 -2
  55. package/src/utils/navigation.ts +45 -24
@@ -1,4 +1,4 @@
1
- import { GitBranch, FolderTree, Settings, Target, XCircle } from 'lucide-react'
1
+ import { GitBranch, FolderTree, Settings, Target, XCircle, History, ListChecks } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
4
4
  import { formatAge } from '../resource-utils'
@@ -16,6 +16,62 @@ interface ArgoApplicationRendererProps {
16
16
  isTerminating?: boolean
17
17
  }
18
18
 
19
+ function SourceProperties({ source }: { source: any }) {
20
+ if (!source) return null
21
+ return (
22
+ <>
23
+ <Property label="Repository" value={source.repoURL} />
24
+ {source.path && <Property label="Path" value={source.path} />}
25
+ {source.targetRevision && (
26
+ <Property
27
+ label="Target Revision"
28
+ value={
29
+ <span className="flex items-center gap-1">
30
+ <GitBranch className="w-3.5 h-3.5" />
31
+ {source.targetRevision}
32
+ </span>
33
+ }
34
+ />
35
+ )}
36
+ {source.chart && <Property label="Helm Chart" value={source.chart} />}
37
+ {source.helm?.valueFiles && source.helm.valueFiles.length > 0 && (
38
+ <Property label="Value Files" value={source.helm.valueFiles.join(', ')} />
39
+ )}
40
+ {source.helm?.parameters && source.helm.parameters.length > 0 && (
41
+ <Property
42
+ label="Helm Parameters"
43
+ value={
44
+ <div className="flex flex-wrap gap-1">
45
+ {source.helm.parameters.filter(Boolean).map((p: { name: string; value: string }, i: number) => (
46
+ <span key={i} className="badge-sm bg-theme-elevated text-theme-text-secondary font-mono">
47
+ {p.name ?? '?'}={p.value ?? ''}
48
+ </span>
49
+ ))}
50
+ </div>
51
+ }
52
+ />
53
+ )}
54
+ {source.kustomize?.namePrefix && (
55
+ <Property label="Kustomize Prefix" value={source.kustomize.namePrefix} />
56
+ )}
57
+ </>
58
+ )
59
+ }
60
+
61
+ function isSyncResourceFailed(res: any): boolean {
62
+ return res.status === 'SyncFailed' || res.hookPhase === 'Failed' || res.hookPhase === 'Error'
63
+ }
64
+
65
+ function getSyncResourceBadgeClass(status: string, hookPhase?: string): string {
66
+ if (status === 'SyncFailed' || hookPhase === 'Failed' || hookPhase === 'Error') {
67
+ return 'status-unhealthy'
68
+ }
69
+ if (status === 'Synced') return 'status-healthy'
70
+ if (status === 'Pruned') return 'status-neutral'
71
+ if (status === 'PruneSkipped') return 'status-degraded'
72
+ return 'status-unknown'
73
+ }
74
+
19
75
  export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: ArgoApplicationRendererProps) {
20
76
  const status = (data.status || {}) as ArgoAppStatus & {
21
77
  resources?: ArgoResource[]
@@ -47,8 +103,9 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
47
103
  problems.push({ color: 'yellow', message: 'Application is out of sync with git' })
48
104
  }
49
105
 
50
- // Extract source info
51
- const source = spec.source || {}
106
+ // Extract source info — support both spec.source (single) and spec.sources (multi, ArgoCD 2.6+)
107
+ const sources: any[] = (spec.sources && spec.sources.length > 0) ? spec.sources : (spec.source ? [spec.source] : [])
108
+ const isMultiSource = Array.isArray(spec.sources) && spec.sources.length > 0
52
109
  const destination = spec.destination || {}
53
110
  const syncPolicy = spec.syncPolicy || {}
54
111
  const operationState = status.operationState
@@ -56,6 +113,12 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
56
113
  // Check if sync is in progress
57
114
  const isSyncing = operationState?.phase === 'Running'
58
115
 
116
+ // Extract sync result resources for per-resource failure details
117
+ const syncResultResources: any[] = operationState?.syncResult?.resources || []
118
+ const failedSyncResources = syncResultResources.filter(isSyncResourceFailed)
119
+ const otherSyncResources = syncResultResources.filter((r: any) => !isSyncResourceFailed(r))
120
+ const sortedSyncResources = [...failedSyncResources, ...otherSyncResources]
121
+
59
122
  return (
60
123
  <>
61
124
  <ProblemAlerts problems={problems} />
@@ -90,31 +153,27 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
90
153
  </div>
91
154
  </Section>
92
155
 
93
- {/* Source section */}
94
- <Section title="Source" icon={FolderTree}>
95
- <PropertyList>
96
- <Property label="Repository" value={source.repoURL} />
97
- {source.path && <Property label="Path" value={source.path} />}
98
- {source.targetRevision && (
99
- <Property
100
- label="Target Revision"
101
- value={
102
- <span className="flex items-center gap-1">
103
- <GitBranch className="w-3.5 h-3.5" />
104
- {source.targetRevision}
105
- </span>
106
- }
107
- />
108
- )}
109
- {source.chart && <Property label="Helm Chart" value={source.chart} />}
110
- {source.helm?.valueFiles && source.helm.valueFiles.length > 0 && (
111
- <Property label="Value Files" value={source.helm.valueFiles.join(', ')} />
112
- )}
113
- {source.kustomize?.namePrefix && (
114
- <Property label="Kustomize Prefix" value={source.kustomize.namePrefix} />
115
- )}
116
- </PropertyList>
117
- </Section>
156
+ {/* Source section — renders each source for multi-source apps */}
157
+ {isMultiSource ? (
158
+ <Section title={`Sources (${sources.length})`} icon={FolderTree}>
159
+ <div className="space-y-3">
160
+ {sources.map((src: any, idx: number) => (
161
+ <div key={idx} className="card-inner">
162
+ <div className="text-xs font-medium text-theme-text-tertiary mb-1.5">Source {idx + 1}</div>
163
+ <PropertyList>
164
+ <SourceProperties source={src} />
165
+ </PropertyList>
166
+ </div>
167
+ ))}
168
+ </div>
169
+ </Section>
170
+ ) : sources.length > 0 ? (
171
+ <Section title="Source" icon={FolderTree}>
172
+ <PropertyList>
173
+ <SourceProperties source={sources[0]} />
174
+ </PropertyList>
175
+ </Section>
176
+ ) : null}
118
177
 
119
178
  {/* Destination section */}
120
179
  <Section title="Destination" icon={Target}>
@@ -192,9 +251,22 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
192
251
  {operationState.message && (
193
252
  <Property label="Message" value={operationState.message} />
194
253
  )}
254
+ {operationState.startedAt && (
255
+ <Property label="Started" value={formatAge(operationState.startedAt)} />
256
+ )}
195
257
  {operationState.finishedAt && (
196
258
  <Property label="Finished" value={formatAge(operationState.finishedAt)} />
197
259
  )}
260
+ {operationState.retryCount != null && (
261
+ <Property
262
+ label="Retries"
263
+ value={
264
+ syncPolicy.retry?.limit
265
+ ? `${operationState.retryCount}/${syncPolicy.retry.limit}`
266
+ : String(operationState.retryCount)
267
+ }
268
+ />
269
+ )}
198
270
  {operationState.syncResult?.revision && (
199
271
  <Property label="Revision" value={operationState.syncResult.revision} />
200
272
  )}
@@ -202,6 +274,55 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
202
274
  </Section>
203
275
  )}
204
276
 
277
+ {/* Sync Result Resources — per-resource sync status with failure details */}
278
+ {sortedSyncResources.length > 0 && (
279
+ <Section
280
+ title={`Sync Result Resources (${sortedSyncResources.length})`}
281
+ icon={ListChecks}
282
+ defaultExpanded={failedSyncResources.length > 0}
283
+ >
284
+ <div className="space-y-1.5">
285
+ {sortedSyncResources.map((res: any, idx: number) => {
286
+ const isFailed = isSyncResourceFailed(res)
287
+ return (
288
+ <div
289
+ key={`${res.kind}-${res.namespace}-${res.name}-${idx}`}
290
+ className={clsx(
291
+ 'card-inner px-3 py-2',
292
+ isFailed && 'border-l-2 border-red-500'
293
+ )}
294
+ >
295
+ <div className="flex items-center gap-2 text-sm">
296
+ <span className={clsx('badge badge-sm', getSyncResourceBadgeClass(res.status, res.hookPhase))}>
297
+ {res.status || res.hookPhase || 'Unknown'}
298
+ </span>
299
+ <span className="text-theme-text-primary">
300
+ {res.kind}/{res.name}
301
+ </span>
302
+ {res.namespace && (
303
+ <span className="text-theme-text-tertiary text-xs">({res.namespace})</span>
304
+ )}
305
+ </div>
306
+ {res.message && (
307
+ <div className={clsx(
308
+ 'text-xs mt-1 break-all',
309
+ isFailed ? 'text-red-400' : 'text-theme-text-secondary'
310
+ )}>
311
+ {res.message}
312
+ </div>
313
+ )}
314
+ {res.hookPhase && res.hookPhase !== res.status && (
315
+ <div className="text-xs text-theme-text-tertiary mt-0.5">
316
+ Hook: {res.hookPhase}
317
+ </div>
318
+ )}
319
+ </div>
320
+ )
321
+ })}
322
+ </div>
323
+ </Section>
324
+ )}
325
+
205
326
  {/* Managed Resources */}
206
327
  {managedResources.length > 0 && (
207
328
  <ManagedResourcesList
@@ -210,6 +331,47 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
210
331
  />
211
332
  )}
212
333
 
334
+ {/* Revision History */}
335
+ {status.history && status.history.length > 0 && (
336
+ <Section title={`Revision History (${status.history.length})`} icon={History} defaultExpanded={false}>
337
+ <div className="space-y-2 max-h-48 overflow-y-auto">
338
+ {status.history
339
+ .slice(0, 10)
340
+ .map((entry, idx) => (
341
+ <div
342
+ key={entry.id ?? idx}
343
+ className={clsx(
344
+ 'p-2 rounded text-sm',
345
+ idx === 0
346
+ ? 'bg-green-500/10 border border-green-500/30'
347
+ : 'bg-theme-elevated/30'
348
+ )}
349
+ >
350
+ <div className="flex items-center justify-between">
351
+ <span className="font-medium text-theme-text-primary font-mono text-xs">
352
+ {entry.revision
353
+ ? entry.revision.length > 12
354
+ ? entry.revision.slice(0, 12)
355
+ : entry.revision
356
+ : '-'}
357
+ </span>
358
+ {entry.deployedAt && (
359
+ <span className="text-xs text-theme-text-tertiary">
360
+ {formatAge(entry.deployedAt)}
361
+ </span>
362
+ )}
363
+ </div>
364
+ {entry.source?.path && (
365
+ <div className="text-xs text-theme-text-secondary mt-0.5 truncate" title={entry.source.path}>
366
+ {entry.source.path}
367
+ </div>
368
+ )}
369
+ </div>
370
+ ))}
371
+ </div>
372
+ </Section>
373
+ )}
374
+
213
375
  {/* Revision Info */}
214
376
  {(status.sync?.revision || status.reconciledAt) && (
215
377
  <Section title="Revision Info" defaultExpanded={false}>
@@ -25,6 +25,13 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
25
25
  const target = getCNPGBackupTarget(data)
26
26
  const clusterName = getCNPGBackupCluster(data)
27
27
 
28
+ // WAL range fields
29
+ const beginWal = data.status?.beginWal
30
+ const endWal = data.status?.endWal
31
+ const beginLSN = data.status?.beginLSN
32
+ const endLSN = data.status?.endLSN
33
+ const hasWalRange = beginWal || endWal || beginLSN || endLSN
34
+
28
35
  return (
29
36
  <>
30
37
  {/* Problem alerts */}
@@ -43,6 +50,9 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
43
50
  <Property label="Method" value={getCNPGBackupMethod(data)} />
44
51
  <Property label="Duration" value={getCNPGBackupDuration(data)} />
45
52
  <Property label="Backup Name" value={getCNPGBackupName(data)} />
53
+ {data.status?.instanceID?.podName && (
54
+ <Property label="Instance" value={data.status.instanceID.podName} />
55
+ )}
46
56
  </PropertyList>
47
57
  {data.status?.startedAt && (
48
58
  <div className="mt-2 pt-2 border-t border-theme-border">
@@ -76,6 +86,18 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
76
86
  </PropertyList>
77
87
  </Section>
78
88
 
89
+ {/* WAL Range */}
90
+ {hasWalRange && (
91
+ <Section title="WAL Range" defaultExpanded>
92
+ <PropertyList>
93
+ {beginWal && <Property label="Begin WAL" value={beginWal} />}
94
+ {endWal && <Property label="End WAL" value={endWal} />}
95
+ {beginLSN && <Property label="Begin LSN" value={beginLSN} />}
96
+ {endLSN && <Property label="End LSN" value={endLSN} />}
97
+ </PropertyList>
98
+ </Section>
99
+ )}
100
+
79
101
  {/* Target - for PITR backups */}
80
102
  {target !== '-' && (
81
103
  <Section title="Target" defaultExpanded>
@@ -1,8 +1,7 @@
1
- import { Database, HardDrive, Activity, Clock, Shield } from 'lucide-react'
1
+ import { Database, HardDrive, Activity, Clock, Shield, KeyRound } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getCNPGClusterInstances,
5
- getCNPGClusterPrimary,
6
5
  getCNPGClusterPhase,
7
6
  getCNPGClusterImage,
8
7
  getCNPGClusterStorage,
@@ -16,6 +15,8 @@ import {
16
15
  getCNPGClusterReplicaSource,
17
16
  getCNPGClusterInstanceNames,
18
17
  getCNPGClusterPostgresParams,
18
+ getCNPGClusterInstancesReportedState,
19
+ getCNPGClusterCertificateExpirations,
19
20
  } from '../resource-utils-cnpg'
20
21
 
21
22
  interface CNPGClusterRendererProps {
@@ -35,15 +36,49 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
35
36
  const postgresParams = getCNPGClusterPostgresParams(data)
36
37
  const instanceNames = getCNPGClusterInstanceNames(data)
37
38
  const bootstrapMethod = getCNPGClusterBootstrapMethod(data)
39
+ const reportedState = getCNPGClusterInstancesReportedState(data)
40
+ const certExpirations = getCNPGClusterCertificateExpirations(data)
41
+
42
+ // Primary mismatch detection
43
+ const targetPrimary = data.status?.targetPrimary
44
+ const currentPrimary = data.status?.currentPrimary
45
+ const primaryMismatch = targetPrimary && currentPrimary && targetPrimary !== currentPrimary
46
+
47
+ // Split-brain detection: multiple instances report isPrimary
48
+ const primariesReported = reportedState.filter(i => i.isPrimary)
49
+ const hasSplitBrain = primariesReported.length > 1
50
+
51
+ // Certificate expiry warnings
52
+ const expiredCerts = certExpirations.filter(c => c.daysUntilExpiry < 0)
53
+ const criticalCerts = certExpirations.filter(c => c.daysUntilExpiry >= 0 && c.daysUntilExpiry <= 7)
54
+ const warningCerts = certExpirations.filter(c => c.daysUntilExpiry > 7 && c.daysUntilExpiry <= 30)
55
+
56
+ // Last failed backup
57
+ const lastFailedBackup = data.status?.lastFailedBackup
38
58
 
39
59
  // Problem detection
40
- const isDegraded = instances > 0 && readyInstances < instances
60
+ const isDown = instances > 0 && readyInstances === 0
61
+ const isDegraded = instances > 0 && readyInstances < instances && readyInstances > 0
41
62
  const isFailover = phase.toLowerCase().includes('failing over')
42
63
  const isSwitchover = phase.toLowerCase().includes('switchover')
43
64
 
44
65
  return (
45
66
  <>
46
67
  {/* Problem alerts */}
68
+ {hasSplitBrain && (
69
+ <AlertBanner
70
+ variant="error"
71
+ title="Potential Split-Brain Detected"
72
+ message={`Multiple instances report as primary: ${primariesReported.map(i => i.podName).join(', ')}. Immediate investigation required.`}
73
+ />
74
+ )}
75
+ {isDown && (
76
+ <AlertBanner
77
+ variant="error"
78
+ title="Cluster Down"
79
+ message={`All ${instances} instances are not ready.`}
80
+ />
81
+ )}
47
82
  {isDegraded && (
48
83
  <AlertBanner
49
84
  variant="warning"
@@ -65,15 +100,76 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
65
100
  message={`Cluster is performing a switchover. Current phase: ${phase}`}
66
101
  />
67
102
  )}
103
+ {primaryMismatch && (
104
+ <AlertBanner
105
+ variant="warning"
106
+ title="Switchover Pending"
107
+ message={`Target primary is ${targetPrimary} but current primary is ${currentPrimary}.`}
108
+ />
109
+ )}
110
+ {lastFailedBackup && (
111
+ <AlertBanner
112
+ variant="error"
113
+ title="Last Backup Failed"
114
+ message={`Last backup failed at ${lastFailedBackup}. WAL archiving may be impacted and RPO is growing.`}
115
+ />
116
+ )}
117
+ {expiredCerts.length > 0 && (
118
+ <AlertBanner
119
+ variant="error"
120
+ title="Certificate Expired"
121
+ items={expiredCerts.map(c => `${c.secretName} expired ${Math.abs(c.daysUntilExpiry)} day${Math.abs(c.daysUntilExpiry) === 1 ? '' : 's'} ago (${c.expiryDate})`)}
122
+ />
123
+ )}
124
+ {criticalCerts.length > 0 && (
125
+ <AlertBanner
126
+ variant="error"
127
+ title="Certificate Expiring Soon"
128
+ items={criticalCerts.map(c => `${c.secretName} expires in ${c.daysUntilExpiry} day${c.daysUntilExpiry === 1 ? '' : 's'} (${c.expiryDate})`)}
129
+ />
130
+ )}
131
+ {warningCerts.length > 0 && (
132
+ <AlertBanner
133
+ variant="warning"
134
+ title="Certificate Expiry Warning"
135
+ items={warningCerts.map(c => `${c.secretName} expires in ${c.daysUntilExpiry} days (${c.expiryDate})`)}
136
+ />
137
+ )}
68
138
 
69
139
  {/* Cluster Overview */}
70
140
  <Section title="Cluster Overview" icon={Database} defaultExpanded>
71
141
  <PropertyList>
72
142
  <Property label="Phase" value={phase} />
73
143
  <Property label="Instances" value={getCNPGClusterInstances(data)} />
74
- <Property label="Current Primary" value={getCNPGClusterPrimary(data)} />
144
+ <Property label="Current Primary" value={currentPrimary || '-'} />
145
+ {targetPrimary && targetPrimary !== currentPrimary && (
146
+ <Property label="Target Primary" value={targetPrimary} />
147
+ )}
75
148
  <Property label="Image" value={getCNPGClusterImage(data)} />
76
149
  <Property label="Update Strategy" value={getCNPGClusterUpdateStrategy(data)} />
150
+ {data.status?.writeService && (
151
+ <Property label="Write Service" value={
152
+ <ResourceLink name={data.status.writeService} kind="Service" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} />
153
+ } />
154
+ )}
155
+ {data.status?.readService && (
156
+ <Property label="Read Service" value={
157
+ <ResourceLink name={data.status.readService} kind="Service" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} />
158
+ } />
159
+ )}
160
+ {data.spec?.enableSuperuserAccess !== undefined && (
161
+ <Property label="Superuser Access" value={
162
+ <span className={`badge-sm ${data.spec.enableSuperuserAccess ? 'bg-green-500/20 text-green-400' : 'bg-theme-hover text-theme-text-secondary'}`}>
163
+ {data.spec.enableSuperuserAccess ? 'Enabled' : 'Disabled'}
164
+ </span>
165
+ } />
166
+ )}
167
+ {(data.spec?.minSyncReplicas !== undefined || data.spec?.maxSyncReplicas !== undefined) && (
168
+ <Property
169
+ label="Sync Replicas"
170
+ value={`Min: ${data.spec?.minSyncReplicas ?? '-'} / Max: ${data.spec?.maxSyncReplicas ?? '-'}`}
171
+ />
172
+ )}
77
173
  </PropertyList>
78
174
  {instanceNames.length > 0 && (
79
175
  <div className="mt-2 pt-2 border-t border-theme-border">
@@ -92,6 +188,29 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
92
188
  )}
93
189
  </Section>
94
190
 
191
+ {/* Replication State */}
192
+ {reportedState.length > 0 && (
193
+ <Section title="Replication" icon={Activity} defaultExpanded>
194
+ <div className="space-y-1.5">
195
+ {reportedState.map((instance) => (
196
+ <div key={instance.podName} className="flex items-center gap-2 text-sm">
197
+ <span className="text-theme-text-primary font-mono flex-1 break-all">{instance.podName}</span>
198
+ <span className={
199
+ instance.isPrimary
200
+ ? 'badge-sm bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'
201
+ : 'badge-sm bg-theme-elevated text-theme-text-secondary'
202
+ }>
203
+ {instance.isPrimary ? 'Primary' : 'Replica'}
204
+ </span>
205
+ {instance.timelineID != null && (
206
+ <span className="text-xs text-theme-text-tertiary">TL {instance.timelineID}</span>
207
+ )}
208
+ </div>
209
+ ))}
210
+ </div>
211
+ </Section>
212
+ )}
213
+
95
214
  {/* Storage */}
96
215
  <Section title="Storage" icon={HardDrive} defaultExpanded>
97
216
  <PropertyList>
@@ -131,6 +250,9 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
131
250
  {backupConfig.lastSuccessfulBackup && (
132
251
  <Property label="Last Successful" value={backupConfig.lastSuccessfulBackup} />
133
252
  )}
253
+ {lastFailedBackup && (
254
+ <Property label="Last Failed" value={lastFailedBackup} />
255
+ )}
134
256
  {backupConfig.firstRecoverabilityPoint && (
135
257
  <Property label="First Recoverability" value={backupConfig.firstRecoverabilityPoint} />
136
258
  )}
@@ -138,6 +260,35 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
138
260
  </Section>
139
261
  )}
140
262
 
263
+ {/* Certificates */}
264
+ {certExpirations.length > 0 && (
265
+ <Section title="Certificates" icon={KeyRound} defaultExpanded>
266
+ <div className="space-y-1.5">
267
+ {certExpirations.map((cert) => (
268
+ <div key={cert.secretName} className="flex items-center gap-2 text-sm">
269
+ <span className="text-theme-text-primary font-mono flex-1 break-all">{cert.secretName}</span>
270
+ <span className="text-xs text-theme-text-tertiary">{cert.expiryDate}</span>
271
+ {cert.daysUntilExpiry <= 7 && (
272
+ <span className="badge-sm bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
273
+ {cert.daysUntilExpiry}d
274
+ </span>
275
+ )}
276
+ {cert.daysUntilExpiry > 7 && cert.daysUntilExpiry <= 30 && (
277
+ <span className="badge-sm bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
278
+ {cert.daysUntilExpiry}d
279
+ </span>
280
+ )}
281
+ {cert.daysUntilExpiry > 30 && (
282
+ <span className="badge-sm bg-theme-elevated text-theme-text-secondary">
283
+ {cert.daysUntilExpiry}d
284
+ </span>
285
+ )}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ </Section>
290
+ )}
291
+
141
292
  {/* Monitoring */}
142
293
  {(monitoring.podMonitorEnabled || (monitoring.customQueriesConfigMap && monitoring.customQueriesConfigMap.length > 0)) && (
143
294
  <Section title="Monitoring" icon={Activity} defaultExpanded>
@@ -165,7 +316,7 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
165
316
 
166
317
  {/* Replication - only if this is a replica cluster */}
167
318
  {isReplica && (
168
- <Section title="Replication" icon={Shield} defaultExpanded>
319
+ <Section title="Replica Cluster" icon={Shield} defaultExpanded>
169
320
  <PropertyList>
170
321
  <Property label="Role" value="Replica" />
171
322
  <Property label="Source" value={getCNPGClusterReplicaSource(data)} />
@@ -1,4 +1,4 @@
1
- import { Users, Database } from 'lucide-react'
1
+ import { Users, Database, KeyRound } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getCNPGPoolerCluster,
@@ -6,6 +6,8 @@ import {
6
6
  getCNPGPoolerMode,
7
7
  getCNPGPoolerInstances,
8
8
  getCNPGPoolerParameters,
9
+ getCNPGPoolerAuthQuery,
10
+ getCNPGPoolerAuthQuerySecret,
9
11
  } from '../resource-utils-cnpg'
10
12
 
11
13
  interface CNPGPoolerRendererProps {
@@ -19,6 +21,8 @@ export function CNPGPoolerRenderer({ data, onNavigate }: CNPGPoolerRendererProps
19
21
  const isDegraded = desired > 0 && ready < desired
20
22
  const clusterName = getCNPGPoolerCluster(data)
21
23
  const parameters = getCNPGPoolerParameters(data)
24
+ const authQuery = getCNPGPoolerAuthQuery(data)
25
+ const authQuerySecret = getCNPGPoolerAuthQuerySecret(data)
22
26
 
23
27
  return (
24
28
  <>
@@ -40,6 +44,29 @@ export function CNPGPoolerRenderer({ data, onNavigate }: CNPGPoolerRendererProps
40
44
  </PropertyList>
41
45
  </Section>
42
46
 
47
+ {/* Authentication */}
48
+ {(authQuery || authQuerySecret) && (
49
+ <Section title="Authentication" icon={KeyRound} defaultExpanded>
50
+ <PropertyList>
51
+ {authQuery && (
52
+ <Property label="Auth Query" value={
53
+ <code className="text-xs font-mono bg-theme-elevated px-1.5 py-0.5 rounded break-all">{authQuery}</code>
54
+ } />
55
+ )}
56
+ {authQuerySecret && (
57
+ <Property label="Auth Query Secret" value={
58
+ <ResourceLink
59
+ name={authQuerySecret.name}
60
+ kind="secrets"
61
+ namespace={data.metadata?.namespace || ''}
62
+ onNavigate={onNavigate}
63
+ />
64
+ } />
65
+ )}
66
+ </PropertyList>
67
+ </Section>
68
+ )}
69
+
43
70
  {/* Cluster Reference */}
44
71
  <Section title="Cluster" icon={Database} defaultExpanded>
45
72
  <PropertyList>