@skyhook-io/k8s-ui 1.16.1 → 1.16.3
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/README.md +22 -0
- package/package.json +3 -3
- package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
- package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
- package/src/components/cnpg/index.ts +14 -0
- package/src/components/cnpg/primitives.tsx +134 -0
- package/src/components/cnpg/relations.test.ts +304 -0
- package/src/components/cnpg/relations.ts +417 -0
- package/src/components/cnpg/workspace.test.ts +285 -0
- package/src/components/cnpg/workspace.ts +700 -0
- package/src/components/dock/BottomDock.tsx +8 -1
- package/src/components/dock/DockContext.tsx +8 -0
- package/src/components/dock/LocalTerminalTab.test.tsx +189 -0
- package/src/components/dock/LocalTerminalTab.tsx +56 -18
- package/src/components/dock/TerminalTab.tsx +2 -4
- package/src/components/issues/IssuesView.tsx +10 -1
- package/src/components/issues/types.ts +24 -0
- package/src/components/logs/LogCore.theme.test.tsx +77 -0
- package/src/components/logs/LogCore.tsx +23 -2
- package/src/components/logs/LogsViewer.tsx +5 -1
- package/src/components/logs/WorkloadLogsViewer.tsx +16 -4
- package/src/components/logs/useLogBuffer.test.ts +17 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
- package/src/components/resources/ResourcesSidebar.tsx +183 -11
- package/src/components/resources/ResourcesView.tsx +5 -1
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.ts +3 -3
- package/src/components/resources/status-overclaim.test.ts +1 -1
- package/src/components/shared/EditableYamlView.test.tsx +268 -0
- package/src/components/shared/EditableYamlView.tsx +25 -1
- package/src/components/timeline/TimelineList.tsx +185 -23
- package/src/components/timeline/TimelineListFold.test.tsx +56 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-filters.ts +7 -0
- package/src/components/ui/SelectMenu.test.tsx +25 -0
- package/src/components/ui/SelectMenu.tsx +11 -9
- package/src/components/ui/YamlReview.test.tsx +40 -0
- package/src/components/ui/YamlReview.tsx +22 -1
- package/src/components/workload/WorkloadView.tsx +96 -5
- package/src/components/workload/index.ts +1 -0
- package/src/components/workload/workload-logs-availability.test.ts +6 -0
- package/src/index.ts +3 -0
- package/src/utils/log-level.ts +14 -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
|
+
}
|