@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.
- package/package.json +2 -2
- package/src/components/resources/ResourcesSidebar.tsx +152 -57
- package/src/components/resources/ResourcesView.tsx +141 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/EditableYamlView.tsx +38 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/ui/Tooltip.tsx +16 -0
- package/src/components/ui/drawer-components.tsx +48 -1
- package/src/theme/variables.css +1 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/navigation.test.ts +29 -2
- 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
|
|
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
|
-
|
|
95
|
-
<
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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={
|
|
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="
|
|
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>
|