@skyhook-io/k8s-ui 1.16.2 → 1.17.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 (152) hide show
  1. package/package.json +3 -3
  2. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  3. package/src/components/applications/ApplicationDetail.tsx +17 -15
  4. package/src/components/checks/types.ts +2 -13
  5. package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
  6. package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
  7. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
  8. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
  9. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
  10. package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
  11. package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
  12. package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
  13. package/src/components/cnpg/index.ts +14 -0
  14. package/src/components/cnpg/primitives.tsx +134 -0
  15. package/src/components/cnpg/relations.test.ts +304 -0
  16. package/src/components/cnpg/relations.ts +417 -0
  17. package/src/components/cnpg/workspace.test.ts +285 -0
  18. package/src/components/cnpg/workspace.ts +700 -0
  19. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  20. package/src/components/gitops/detail-helpers.ts +1 -1
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
  22. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
  23. package/src/components/issues/IssuesView.tsx +10 -1
  24. package/src/components/issues/issue-timing.ts +2 -2
  25. package/src/components/issues/issues.test.ts +1 -1
  26. package/src/components/issues/types.ts +24 -0
  27. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  28. package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
  29. package/src/components/logs/useLogBuffer.test.ts +17 -0
  30. package/src/components/logs/useLogStream.ts +11 -0
  31. package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
  32. package/src/components/resources/ResourcesSidebar.tsx +183 -11
  33. package/src/components/resources/ResourcesView.tsx +12 -9
  34. package/src/components/resources/index.ts +1 -1
  35. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  36. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  37. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
  38. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  39. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  42. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  43. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
  44. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
  45. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
  46. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
  47. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  48. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  49. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  50. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  51. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
  53. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
  54. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  55. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  56. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  57. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  58. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  59. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  60. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  61. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  62. package/src/components/resources/renderers/HPARenderer.tsx +2 -2
  63. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  64. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  65. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  66. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  67. package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
  68. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  69. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  70. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  71. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  73. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  75. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  76. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  77. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  78. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  79. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  80. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  81. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  82. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +21 -7
  84. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  85. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  86. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  87. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  88. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  89. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  90. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  91. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
  92. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  93. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  94. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  95. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  96. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  97. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  98. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  99. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  100. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  101. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
  102. package/src/components/resources/resource-utils-cnpg.ts +3 -7
  103. package/src/components/resources/resource-utils-keda.ts +1 -7
  104. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  105. package/src/components/resources/resource-utils-velero.ts +0 -12
  106. package/src/components/resources/resource-utils.ts +17 -33
  107. package/src/components/resources/status-overclaim.test.ts +1 -1
  108. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  109. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  110. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  111. package/src/components/shared/ResourceActionsBar.tsx +9 -20
  112. package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
  113. package/src/components/shared/SetImageDialog.test.ts +75 -6
  114. package/src/components/shared/SetImageDialog.tsx +92 -84
  115. package/src/components/shared/index.ts +2 -1
  116. package/src/components/timeline/TimelineList.tsx +187 -38
  117. package/src/components/timeline/TimelineListFold.test.tsx +56 -0
  118. package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
  119. package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
  120. package/src/components/timeline/index.ts +1 -0
  121. package/src/components/timeline/timeline-filters.ts +7 -0
  122. package/src/components/topology/TopologyGraph.tsx +5 -3
  123. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  124. package/src/components/ui/ScheduleValue.tsx +3 -2
  125. package/src/components/ui/drawer-components.test.tsx +66 -1
  126. package/src/components/ui/drawer-components.tsx +43 -29
  127. package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
  128. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  129. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  130. package/src/components/workload/WorkloadView.tsx +216 -48
  131. package/src/components/workload/index.ts +1 -0
  132. package/src/components/workload/workload-logs-availability.test.ts +6 -0
  133. package/src/index.ts +3 -0
  134. package/src/types/capacity.ts +10 -0
  135. package/src/types/core.ts +32 -1
  136. package/src/utils/format.test.ts +40 -2
  137. package/src/utils/format.ts +5 -1
  138. package/src/utils/gateway-references.test.ts +17 -0
  139. package/src/utils/gateway-references.ts +29 -0
  140. package/src/utils/gitops-write-guard.test.ts +401 -0
  141. package/src/utils/gitops-write-guard.ts +509 -0
  142. package/src/utils/index.ts +2 -0
  143. package/src/utils/ingress-class-references.ts +22 -0
  144. package/src/utils/log-level.ts +14 -0
  145. package/src/utils/navigation.test.ts +13 -0
  146. package/src/utils/navigation.ts +5 -0
  147. package/src/utils/node-lifecycle.test.ts +36 -0
  148. package/src/utils/node-lifecycle.ts +77 -0
  149. package/src/utils/resource-hierarchy.test.ts +32 -0
  150. package/src/utils/resource-hierarchy.ts +28 -2
  151. package/src/utils/resource-refs.test.ts +23 -0
  152. package/src/utils/resource-refs.ts +30 -0
@@ -0,0 +1,264 @@
1
+ import type { ReactNode } from 'react'
2
+ import { getCNPGDeclarativeMessage, getCNPGReclaimPolicy } from '../resources/resource-utils-cnpg'
3
+ import type { CNPGWorkspaceResponse } from './workspace'
4
+ import { FactGrid, FactRow, FactValue, RefLink, SummaryHeading, toneTextClass, type CNPGNavigate } from './primitives'
5
+ import { ClusterLink, NotReported, ObjectProblems, SummaryShell } from './CNPGSharedSummary'
6
+ import {
7
+ appliedFact,
8
+ clustersIn,
9
+ databaseForDeclaration,
10
+ gitopsSourceOf,
11
+ missingManagedRole,
12
+ observedGenerationFact,
13
+ refOf,
14
+ relationUnavailable,
15
+ replicationForDatabase,
16
+ targetCluster,
17
+ workspaceList,
18
+ } from './relations'
19
+
20
+ interface SummaryProps {
21
+ resource: any
22
+ workspace: CNPGWorkspaceResponse | null
23
+ onNavigate?: CNPGNavigate
24
+ }
25
+
26
+ function ReclaimRow({ resource }: { resource: any }) {
27
+ const reclaim = getCNPGReclaimPolicy(resource)
28
+ return (
29
+ <FactRow label="On delete">
30
+ {reclaim.destructive ? (
31
+ <span className={toneTextClass('degraded')}>{reclaim.value} · removing this resource drops it from PostgreSQL</span>
32
+ ) : (
33
+ <span>{reclaim.value} · removing this resource leaves PostgreSQL untouched</span>
34
+ )}
35
+ </FactRow>
36
+ )
37
+ }
38
+
39
+ function Reconciled({ resource, extra }: { resource: any; extra?: ReactNode }) {
40
+ const message = getCNPGDeclarativeMessage(resource)
41
+ const applied = appliedFact(resource)
42
+ return (
43
+ <>
44
+ <SummaryHeading>Reconciled</SummaryHeading>
45
+ <FactGrid>
46
+ <FactRow label="Applied">
47
+ <FactValue fact={applied} />
48
+ </FactRow>
49
+ <FactRow label="Observed spec">
50
+ <FactValue fact={observedGenerationFact(resource)} />
51
+ </FactRow>
52
+ <FactRow label="Operator message">
53
+ {message ? (
54
+ <span className={applied.tone === 'unhealthy' ? toneTextClass('unhealthy') : undefined}>{message}</span>
55
+ ) : (
56
+ <NotReported text="None" />
57
+ )}
58
+ </FactRow>
59
+ {extra}
60
+ </FactGrid>
61
+ </>
62
+ )
63
+ }
64
+
65
+ function DeclaredIn({ resource }: { resource: any }) {
66
+ const src = gitopsSourceOf(resource)
67
+ if (!src) return <NotReported text="GitOps source not recorded" />
68
+ return (
69
+ <span>
70
+ {src.tool === 'argocd' ? 'Argo CD application' : 'Flux'} <span className="font-mono">{src.namespace ? `${src.namespace}/${src.name}` : src.name}</span>
71
+ </span>
72
+ )
73
+ }
74
+
75
+ function DatabaseRef({ resource, workspace, onNavigate }: SummaryProps) {
76
+ const dbname = resource?.spec?.dbname
77
+ if (!dbname) return <NotReported text="Not set" />
78
+ const ns = resource?.metadata?.namespace ?? ''
79
+ const db = relationUnavailable(workspace, 'databases', ns, 'Databases')
80
+ ? null
81
+ : databaseForDeclaration(resource, workspaceList(workspace, 'databases'))
82
+ return (
83
+ <span>
84
+ <span className="font-mono">{dbname}</span>
85
+ {db && (
86
+ <span className="text-theme-text-secondary">
87
+ {' · declared by Database '}
88
+ <RefLink refTo={refOf(db, 'Database')} onNavigate={onNavigate} mono />
89
+ </span>
90
+ )}
91
+ </span>
92
+ )
93
+ }
94
+
95
+ function LinkList({ items, kind, onNavigate }: { items: any[]; kind: string; onNavigate?: CNPGNavigate }) {
96
+ return (
97
+ <span className="flex flex-wrap gap-x-3">
98
+ {items.map((o) => (
99
+ <RefLink key={o.metadata?.name} refTo={refOf(o, kind)} onNavigate={onNavigate} mono />
100
+ ))}
101
+ </span>
102
+ )
103
+ }
104
+
105
+ export function CNPGDatabaseSummary({ resource, workspace, onNavigate }: SummaryProps) {
106
+ const ns = resource?.metadata?.namespace ?? ''
107
+ const cluster = targetCluster(resource, clustersIn(workspace))
108
+ const missingRole = missingManagedRole(resource, cluster)
109
+ const pubsUnavailable = relationUnavailable(workspace, 'publications', ns, 'Publications')
110
+ const subsUnavailable = relationUnavailable(workspace, 'subscriptions', ns, 'Subscriptions')
111
+ const related = replicationForDatabase(resource, workspaceList(workspace, 'publications'), workspaceList(workspace, 'subscriptions'))
112
+
113
+ return (
114
+ <SummaryShell>
115
+ <ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'Database')} onNavigate={onNavigate} />
116
+
117
+ <SummaryHeading>Declared</SummaryHeading>
118
+ <FactGrid>
119
+ <FactRow label="PostgreSQL database">
120
+ {resource?.spec?.name ? <span className="font-mono">{resource.spec.name}</span> : <NotReported text="Not set" />}
121
+ </FactRow>
122
+ <FactRow label="Owner role">
123
+ {resource?.spec?.owner ? <span className="font-mono">{resource.spec.owner}</span> : <NotReported text="Not set" />}
124
+ </FactRow>
125
+ <FactRow label="Ensure">{resource?.spec?.ensure ?? 'present'}</FactRow>
126
+ <ReclaimRow resource={resource} />
127
+ </FactGrid>
128
+
129
+ <Reconciled
130
+ resource={resource}
131
+ extra={
132
+ missingRole && (
133
+ <FactRow label="Managed roles">
134
+ “{missingRole}” is not among {cluster?.metadata?.name}'s managed roles
135
+ </FactRow>
136
+ )
137
+ }
138
+ />
139
+
140
+ <SummaryHeading>Source and target</SummaryHeading>
141
+ <FactGrid>
142
+ <FactRow label="Declared in">
143
+ <DeclaredIn resource={resource} />
144
+ </FactRow>
145
+ <FactRow label="Cluster">
146
+ <ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
147
+ </FactRow>
148
+ <FactRow label="Publications">
149
+ {pubsUnavailable ? (
150
+ <NotReported text={pubsUnavailable} />
151
+ ) : related.publications.length === 0 ? (
152
+ <span className="text-theme-text-secondary">None on this database</span>
153
+ ) : (
154
+ <LinkList items={related.publications} kind="Publication" onNavigate={onNavigate} />
155
+ )}
156
+ </FactRow>
157
+ <FactRow label="Subscriptions">
158
+ {subsUnavailable ? (
159
+ <NotReported text={subsUnavailable} />
160
+ ) : related.subscriptions.length === 0 ? (
161
+ <span className="text-theme-text-secondary">None on this database</span>
162
+ ) : (
163
+ <LinkList items={related.subscriptions} kind="Subscription" onNavigate={onNavigate} />
164
+ )}
165
+ </FactRow>
166
+ </FactGrid>
167
+ </SummaryShell>
168
+ )
169
+ }
170
+
171
+ function publicationTargets(resource: any): ReactNode {
172
+ const target = resource?.spec?.target
173
+ if (target?.allTables === true) return 'All tables'
174
+ const objects = Array.isArray(target?.objects) ? target.objects : []
175
+ if (objects.length === 0) return <NotReported text="Not set" />
176
+ const labels = objects.map((o: any) => {
177
+ if (o?.tablesInSchema) return `All tables in schema ${o.tablesInSchema}`
178
+ const t = o?.table
179
+ if (t?.name) {
180
+ const name = t.schema ? `${t.schema}.${t.name}` : t.name
181
+ return Array.isArray(t.columns) && t.columns.length > 0 ? `${name} (${t.columns.join(', ')})` : name
182
+ }
183
+ return 'Unrecognized entry'
184
+ })
185
+ return (
186
+ <ul className="space-y-0.5 font-mono">
187
+ {labels.map((l: string, i: number) => (
188
+ <li key={i}>{l}</li>
189
+ ))}
190
+ </ul>
191
+ )
192
+ }
193
+
194
+ export function CNPGPublicationSummary({ resource, workspace, onNavigate }: SummaryProps) {
195
+ return (
196
+ <SummaryShell>
197
+ <ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'Publication')} onNavigate={onNavigate} />
198
+
199
+ <SummaryHeading>Declared</SummaryHeading>
200
+ <FactGrid>
201
+ <FactRow label="Publication">
202
+ {resource?.spec?.name ? <span className="font-mono">{resource.spec.name}</span> : <NotReported text="Not set" />}
203
+ </FactRow>
204
+ <FactRow label="Cluster">
205
+ <ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
206
+ </FactRow>
207
+ <FactRow label="Database">
208
+ <DatabaseRef resource={resource} workspace={workspace} onNavigate={onNavigate} />
209
+ </FactRow>
210
+ <FactRow label="Publishes">{publicationTargets(resource)}</FactRow>
211
+ <ReclaimRow resource={resource} />
212
+ <FactRow label="Declared in">
213
+ <DeclaredIn resource={resource} />
214
+ </FactRow>
215
+ </FactGrid>
216
+
217
+ <Reconciled resource={resource} />
218
+ </SummaryShell>
219
+ )
220
+ }
221
+
222
+ export function CNPGSubscriptionSummary({ resource, workspace, onNavigate }: SummaryProps) {
223
+ const pub = resource?.spec?.publicationName
224
+ const ext = resource?.spec?.externalClusterName
225
+ return (
226
+ <SummaryShell>
227
+ <ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'Subscription')} onNavigate={onNavigate} />
228
+
229
+ <SummaryHeading>Declared</SummaryHeading>
230
+ <FactGrid>
231
+ <FactRow label="Subscription">
232
+ {resource?.spec?.name ? <span className="font-mono">{resource.spec.name}</span> : <NotReported text="Not set" />}
233
+ </FactRow>
234
+ <FactRow label="Cluster">
235
+ <ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
236
+ </FactRow>
237
+ <FactRow label="Database">
238
+ <DatabaseRef resource={resource} workspace={workspace} onNavigate={onNavigate} />
239
+ </FactRow>
240
+ <FactRow label="Subscribes to">
241
+ {pub ? (
242
+ <span>
243
+ Publication <span className="font-mono">{pub}</span>
244
+ {ext ? (
245
+ <span className="text-theme-text-secondary">
246
+ {' on external cluster '}
247
+ <span className="font-mono">{ext}</span>
248
+ </span>
249
+ ) : null}
250
+ </span>
251
+ ) : (
252
+ <NotReported text="Not set" />
253
+ )}
254
+ </FactRow>
255
+ <ReclaimRow resource={resource} />
256
+ <FactRow label="Declared in">
257
+ <DeclaredIn resource={resource} />
258
+ </FactRow>
259
+ </FactGrid>
260
+
261
+ <Reconciled resource={resource} />
262
+ </SummaryShell>
263
+ )
264
+ }
@@ -0,0 +1,80 @@
1
+ import { CNPG_GROUP, getCNPGImageCatalogEntries } from '../resources/resource-utils-cnpg'
2
+ import type { CNPGWorkspaceResponse } from './workspace'
3
+ import { FactGrid, FactRow, RefLink, SummaryHeading, toneTextClass, type CNPGNavigate } from './primitives'
4
+ import { NotReported, Note, ObjectProblems, SummaryShell } from './CNPGSharedSummary'
5
+ import { clustersIn, clustersUsingCatalog, refOf, relationUnavailable } from './relations'
6
+
7
+ export function CNPGImageCatalogSummary({
8
+ resource,
9
+ workspace,
10
+ onNavigate,
11
+ }: {
12
+ resource: any
13
+ workspace: CNPGWorkspaceResponse | null
14
+ onNavigate?: CNPGNavigate
15
+ }) {
16
+ const clusterScoped = resource?.kind === 'ClusterImageCatalog'
17
+ const ns = resource?.metadata?.namespace ?? ''
18
+ const entries = getCNPGImageCatalogEntries(resource)
19
+ const majors = new Set(entries.map((e) => e.major))
20
+ // A ClusterImageCatalog's users can be in any namespace, so partial coverage
21
+ // lists what was read; an ImageCatalog's users are all in its own namespace.
22
+ const partial = clusterScoped && workspace?.coverage?.clusters?.state === 'partial'
23
+ const unavailable = partial ? null : relationUnavailable(workspace, 'clusters', clusterScoped ? undefined : ns, 'Clusters')
24
+ const users = unavailable ? [] : clustersUsingCatalog(resource, clustersIn(workspace))
25
+
26
+ return (
27
+ <SummaryShell>
28
+ <ObjectProblems issues={workspace?.issues} subject={refOf(resource, resource?.kind ?? 'ImageCatalog')} onNavigate={onNavigate} />
29
+
30
+ <SummaryHeading>Images</SummaryHeading>
31
+ {entries.length === 0 ? (
32
+ <div className="text-sm">
33
+ <NotReported text="No images listed" />
34
+ </div>
35
+ ) : (
36
+ <FactGrid>
37
+ {entries.map((e) => (
38
+ <FactRow key={e.major} label={`PostgreSQL ${e.major}`}>
39
+ <span className="font-mono">{e.image}</span>
40
+ </FactRow>
41
+ ))}
42
+ </FactGrid>
43
+ )}
44
+
45
+ <SummaryHeading>Used by</SummaryHeading>
46
+ {unavailable ? (
47
+ <div className="text-sm">
48
+ <NotReported text={unavailable} />
49
+ </div>
50
+ ) : users.length === 0 ? (
51
+ <div className="text-sm text-theme-text-secondary">No visible cluster uses this catalog</div>
52
+ ) : (
53
+ <FactGrid>
54
+ {users.map((u) => (
55
+ <FactRow
56
+ key={`${u.cluster.metadata?.namespace}/${u.cluster.metadata?.name}`}
57
+ label={
58
+ <RefLink refTo={refOf(u.cluster, 'Cluster', CNPG_GROUP)} onNavigate={onNavigate} mono>
59
+ {clusterScoped ? `${u.cluster.metadata?.namespace}/${u.cluster.metadata?.name}` : u.cluster.metadata?.name}
60
+ </RefLink>
61
+ }
62
+ >
63
+ {u.major === null ? (
64
+ <NotReported text="Major not set" />
65
+ ) : majors.has(u.major) ? (
66
+ `Requests PostgreSQL ${u.major}`
67
+ ) : (
68
+ <span className={toneTextClass('unhealthy')}>Requests PostgreSQL {u.major} · not in this catalog</span>
69
+ )}
70
+ </FactRow>
71
+ ))}
72
+ </FactGrid>
73
+ )}
74
+ {!unavailable && partial && <Note>Only clusters in namespaces you can read are listed.</Note>}
75
+ {!unavailable && !partial && clusterScoped && (
76
+ <Note>Among clusters you can see; clusters in namespaces you cannot read are not listed</Note>
77
+ )}
78
+ </SummaryShell>
79
+ )
80
+ }
@@ -0,0 +1,176 @@
1
+ import {
2
+ CNPG_BARMAN_OBJECTSTORE_GROUP,
3
+ CNPG_GROUP,
4
+ getCNPGObjectStoreCredentialSecret,
5
+ getCNPGObjectStoreDestination,
6
+ getCNPGObjectStoreProvider,
7
+ getCNPGObjectStoreRecoveryWindows,
8
+ getCNPGObjectStoreRetention,
9
+ } from '../resources/resource-utils-cnpg'
10
+ import type { CNPGWorkspaceResponse } from './workspace'
11
+ import { FactGrid, FactRow, FactValue, RefLink, SummaryHeading, toneTextClass, type CNPGNavigate } from './primitives'
12
+ import { NotReported, Note, ObjectProblems, SummaryShell, TimeAgo } from './CNPGSharedSummary'
13
+ import { clustersIn, inferredObjectStoreHealth, refOf, relationUnavailable, usersOfObjectStore } from './relations'
14
+
15
+ function utc(at: string | undefined): string {
16
+ if (!at || !Number.isFinite(Date.parse(at))) return 'unknown'
17
+ return new Date(at).toUTCString().replace(' GMT', ' UTC')
18
+ }
19
+
20
+ export function CNPGObjectStoreSummary({
21
+ resource,
22
+ workspace,
23
+ onNavigate,
24
+ }: {
25
+ resource: any
26
+ workspace: CNPGWorkspaceResponse | null
27
+ onNavigate?: CNPGNavigate
28
+ }) {
29
+ const ns = resource?.metadata?.namespace ?? ''
30
+ const clustersUnavailable = relationUnavailable(workspace, 'clusters', ns, 'Clusters')
31
+ const users = clustersUnavailable ? [] : usersOfObjectStore(resource, clustersIn(workspace))
32
+ const health = inferredObjectStoreHealth(resource, users)
33
+ const clusterForServer = new Map(users.map((u) => [u.serverName, u.cluster?.metadata?.name as string]))
34
+ const windows = getCNPGObjectStoreRecoveryWindows(resource)
35
+ const destination = getCNPGObjectStoreDestination(resource)
36
+ const provider = getCNPGObjectStoreProvider(resource)
37
+ const secret = getCNPGObjectStoreCredentialSecret(resource)
38
+ const retention = getCNPGObjectStoreRetention(resource)
39
+ const clusterWord = users.length === 1 ? "1 cluster's" : `${users.length} clusters'`
40
+
41
+ return (
42
+ <SummaryShell>
43
+ <ObjectProblems
44
+ issues={workspace?.issues}
45
+ subject={refOf(resource, 'ObjectStore', CNPG_BARMAN_OBJECTSTORE_GROUP)}
46
+ onNavigate={onNavigate}
47
+ />
48
+
49
+ <SummaryHeading hint="inferred">Upload health</SummaryHeading>
50
+ {clustersUnavailable ? (
51
+ <div className="text-sm">
52
+ <NotReported text={`Unknown · ${clustersUnavailable}, which are the only evidence`} />
53
+ </div>
54
+ ) : (
55
+ <>
56
+ <div className="text-sm">
57
+ <FactValue fact={health.summary} />
58
+ </div>
59
+ {users.length > 0 && (
60
+ <Note>Inferred from {clusterWord} WAL archiving and backup results — ObjectStore has no health status</Note>
61
+ )}
62
+ {health.evidence.length > 0 && (
63
+ <div className="mt-2">
64
+ <FactGrid>
65
+ {health.evidence.map((e) => (
66
+ <FactRow key={e.cluster.name} label={<RefLink refTo={e.cluster} onNavigate={onNavigate} mono />}>
67
+ <div>
68
+ <FactValue fact={e.archiving} />
69
+ <div className="text-xs text-theme-text-secondary">
70
+ {e.window ? (
71
+ <>
72
+ Last backup success <TimeAgo at={e.window.lastSuccessfulBackupTime} missing="not recorded" />
73
+ {e.window.lastFailedBackupTime && (
74
+ <span className={e.window.failingSinceLastSuccess ? toneTextClass('unhealthy') : undefined}>
75
+ {' · '}last failure <TimeAgo at={e.window.lastFailedBackupTime} />
76
+ </span>
77
+ )}
78
+ </>
79
+ ) : (
80
+ <NotReported text={`No recovery window for server ${e.serverName}`} />
81
+ )}
82
+ </div>
83
+ </div>
84
+ </FactRow>
85
+ ))}
86
+ </FactGrid>
87
+ </div>
88
+ )}
89
+ </>
90
+ )}
91
+
92
+ <SummaryHeading hint="ObjectStore status">Recovery window</SummaryHeading>
93
+ {windows.length === 0 ? (
94
+ <div className="text-sm">
95
+ <NotReported text="No server has reported a recovery window" />
96
+ </div>
97
+ ) : (
98
+ <FactGrid>
99
+ {windows.map((w) => {
100
+ const cluster = clusterForServer.get(w.server)
101
+ return (
102
+ <FactRow
103
+ key={w.server}
104
+ label={
105
+ cluster ? (
106
+ <RefLink refTo={{ kind: 'Cluster', group: CNPG_GROUP, namespace: ns, name: cluster }} onNavigate={onNavigate} mono>
107
+ {w.server}
108
+ </RefLink>
109
+ ) : (
110
+ <span className="font-mono">{w.server}</span>
111
+ )
112
+ }
113
+ >
114
+ <div className="space-y-0.5">
115
+ <div>
116
+ <span className="text-theme-text-secondary">First recoverability point </span>
117
+ {w.firstRecoverabilityPoint ? utc(w.firstRecoverabilityPoint) : <NotReported />}
118
+ </div>
119
+ <div>
120
+ <span className="text-theme-text-secondary">Last successful backup </span>
121
+ {w.lastSuccessfulBackupTime ? utc(w.lastSuccessfulBackupTime) : <NotReported text="None recorded" />}
122
+ </div>
123
+ {w.lastFailedBackupTime && (
124
+ <div>
125
+ <span className="text-theme-text-secondary">Last failed backup </span>
126
+ <span className={w.failingSinceLastSuccess ? toneTextClass('degraded') : undefined}>{utc(w.lastFailedBackupTime)}</span>
127
+ </div>
128
+ )}
129
+ {w.failingSinceLastSuccess && (
130
+ <Note>{w.lastSuccessfulBackupTime ? 'A backup failed after the last recorded success.' : 'No successful backup recorded.'}</Note>
131
+ )}
132
+ </div>
133
+ </FactRow>
134
+ )
135
+ })}
136
+ </FactGrid>
137
+ )}
138
+
139
+ <SummaryHeading>Destination</SummaryHeading>
140
+ <FactGrid>
141
+ <FactRow label="Path">{destination !== '-' ? <span className="font-mono">{destination}</span> : <NotReported text="Not set" />}</FactRow>
142
+ <FactRow label="Provider">{provider ?? <NotReported />}</FactRow>
143
+ <FactRow label="Credentials">
144
+ {secret ? (
145
+ <span>
146
+ Secret <RefLink refTo={{ kind: 'Secret', group: '', namespace: ns, name: secret }} onNavigate={onNavigate} mono />
147
+ </span>
148
+ ) : (
149
+ <NotReported text="No Secret referenced" />
150
+ )}
151
+ </FactRow>
152
+ <FactRow label="Retention">{retention ?? <NotReported text="Not set" />}</FactRow>
153
+ </FactGrid>
154
+
155
+ <SummaryHeading>Used by</SummaryHeading>
156
+ {clustersUnavailable ? (
157
+ <div className="text-sm">
158
+ <NotReported text={clustersUnavailable} />
159
+ </div>
160
+ ) : users.length === 0 ? (
161
+ <div className="text-sm text-theme-text-secondary">
162
+ No visible cluster uses this store
163
+ {workspace?.coverage?.clusters?.state === 'partial' && (
164
+ <Note>Clusters in namespaces you cannot read are not checked.</Note>
165
+ )}
166
+ </div>
167
+ ) : (
168
+ <div className="flex flex-wrap gap-x-3 text-sm">
169
+ {users.map((u) => (
170
+ <RefLink key={u.cluster.metadata?.name} refTo={refOf(u.cluster, 'Cluster')} onNavigate={onNavigate} mono />
171
+ ))}
172
+ </div>
173
+ )}
174
+ </SummaryShell>
175
+ )
176
+ }