@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
package/README.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Shared, source-distributed Kubernetes UI components used by Radar.
|
|
4
4
|
|
|
5
|
+
## Local terminal metadata
|
|
6
|
+
|
|
7
|
+
`LocalTerminalTab` accepts optional `onSessionInfo` and `toolbarExtra` props.
|
|
8
|
+
`onSessionInfo` reports the server's `session` frame (`context` and
|
|
9
|
+
`kubeconfigIsolated`), and receives `null` when a connection attempt starts.
|
|
10
|
+
This describes the supplied kubeconfig, not a shell's live command target.
|
|
11
|
+
`toolbarExtra` lets the host render that information in the terminal toolbar.
|
|
12
|
+
Hosts can update a dock tab's label and optional full-name tooltip with
|
|
13
|
+
`useDock().setTabTitle(id, title, titleTooltip)`.
|
|
14
|
+
The optional `DockTab.localTerminalContext` field lets a host retain the full
|
|
15
|
+
context selected when a local terminal was requested. An empty string records
|
|
16
|
+
no active context; an omitted field records no intent. The shared terminal and
|
|
17
|
+
open hook do not interpret it; Radar's host wrapper checks it on open/reconnect.
|
|
18
|
+
`canConnect` is a predicate evaluated during rendering to disable Reconnect/Retry,
|
|
19
|
+
and before an attempt to preserve the existing terminal when refused.
|
|
20
|
+
`onConnectionError` lets a host refresh connection state after a failed WebSocket
|
|
21
|
+
handshake.
|
|
22
|
+
|
|
23
|
+
`initialCommand` is sent once per mounted terminal. Reconnect does not repeat a
|
|
24
|
+
command that was already sent; if the connection closes before delivery, the
|
|
25
|
+
command remains pending for the next connection.
|
|
26
|
+
|
|
5
27
|
## YAML editor bundling
|
|
6
28
|
|
|
7
29
|
`YamlEditor` and `YamlDiffEditor` bundle Monaco, its editor worker, and the YAML language worker into the consuming application. They make no runtime CDN or internet requests, so they work in air-gapped environments.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@monaco-editor/react": "^4.7.0",
|
|
72
72
|
"html-to-image": "^1.11.0",
|
|
73
73
|
"monaco-yaml": "5.5.1",
|
|
74
|
-
"react-virtuoso": "^4.18.
|
|
74
|
+
"react-virtuoso": "^4.18.16",
|
|
75
75
|
"shiki": "^4.4.2"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"react": "^19.3.0",
|
|
106
106
|
"react-dom": "^19.3.0",
|
|
107
107
|
"typescript": "^6.0.2",
|
|
108
|
-
"vitest": "^5.0.
|
|
108
|
+
"vitest": "^5.0.3",
|
|
109
109
|
"yaml": "^2.9.0"
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { formatDuration } from '../resources/resource-utils'
|
|
2
|
+
import {
|
|
3
|
+
CNPG_BARMAN_OBJECTSTORE_GROUP,
|
|
4
|
+
CNPG_GROUP,
|
|
5
|
+
getCNPGBackupStatus,
|
|
6
|
+
getCNPGScheduledBackupNextSchedule,
|
|
7
|
+
getCNPGScheduledBackupStatus,
|
|
8
|
+
} from '../resources/resource-utils-cnpg'
|
|
9
|
+
import type { CNPGWorkspaceResponse } from './workspace'
|
|
10
|
+
import { FactGrid, FactRow, RefLink, SummaryHeading, toneTextClass, type CNPGNavigate } from './primitives'
|
|
11
|
+
import { ClusterLink, NotReported, Note, ObjectProblems, PhaseBadge, SummaryShell, TimeAgo } from './CNPGSharedSummary'
|
|
12
|
+
import {
|
|
13
|
+
backupDestination,
|
|
14
|
+
backupsForScheduledBackup,
|
|
15
|
+
clustersIn,
|
|
16
|
+
isBackupFromSchedule,
|
|
17
|
+
refOf,
|
|
18
|
+
relationUnavailable,
|
|
19
|
+
scheduledBackupOf,
|
|
20
|
+
workspaceList,
|
|
21
|
+
} from './relations'
|
|
22
|
+
|
|
23
|
+
interface SummaryProps {
|
|
24
|
+
resource: any
|
|
25
|
+
workspace: CNPGWorkspaceResponse | null
|
|
26
|
+
onNavigate?: CNPGNavigate
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const RECENT_RUNS = 5
|
|
30
|
+
|
|
31
|
+
function methodText(resource: any): string | null {
|
|
32
|
+
const m = resource?.status?.method || resource?.spec?.method
|
|
33
|
+
if (!m) return null
|
|
34
|
+
switch (m) {
|
|
35
|
+
case 'plugin':
|
|
36
|
+
return resource?.spec?.pluginConfiguration?.name ? `Plugin · ${resource.spec.pluginConfiguration.name}` : 'Plugin'
|
|
37
|
+
case 'barmanObjectStore':
|
|
38
|
+
return 'Barman object store (in-tree)'
|
|
39
|
+
case 'volumeSnapshot':
|
|
40
|
+
return 'Volume snapshot'
|
|
41
|
+
default:
|
|
42
|
+
return m
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Timing({ resource }: { resource: any }) {
|
|
47
|
+
const started = resource?.status?.startedAt
|
|
48
|
+
const stopped = resource?.status?.stoppedAt
|
|
49
|
+
const startMs = started ? Date.parse(started) : NaN
|
|
50
|
+
const stopMs = stopped ? Date.parse(stopped) : NaN
|
|
51
|
+
const tookMs = Number.isFinite(startMs) && Number.isFinite(stopMs) && stopMs >= startMs ? stopMs - startMs : null
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<FactRow label="Started">
|
|
55
|
+
<TimeAgo at={started} />
|
|
56
|
+
</FactRow>
|
|
57
|
+
<FactRow label="Stopped">
|
|
58
|
+
{stopped ? (
|
|
59
|
+
<span>
|
|
60
|
+
<TimeAgo at={stopped} />
|
|
61
|
+
{tookMs !== null && <span className="text-theme-text-secondary"> · took {formatDuration(tookMs, true)}</span>}
|
|
62
|
+
</span>
|
|
63
|
+
) : Number.isFinite(startMs) ? (
|
|
64
|
+
<span className="text-theme-text-secondary">Not stopped · running for {formatDuration(Date.now() - startMs, true)}</span>
|
|
65
|
+
) : (
|
|
66
|
+
<NotReported />
|
|
67
|
+
)}
|
|
68
|
+
</FactRow>
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function CNPGBackupSummary({ resource, workspace, onNavigate }: SummaryProps) {
|
|
74
|
+
const ns = resource?.metadata?.namespace ?? ''
|
|
75
|
+
const status = getCNPGBackupStatus(resource)
|
|
76
|
+
const pod = resource?.status?.instanceID?.podName
|
|
77
|
+
const error = resource?.status?.error
|
|
78
|
+
const trigger = scheduledBackupOf(resource)
|
|
79
|
+
const liveSchedule = trigger
|
|
80
|
+
? workspaceList(workspace, 'scheduledBackups').find((s) => s?.metadata?.namespace === ns && s?.metadata?.name === trigger)
|
|
81
|
+
: undefined
|
|
82
|
+
const scheduleReplaced = !!liveSchedule && !isBackupFromSchedule(resource, liveSchedule)
|
|
83
|
+
const dest = backupDestination(resource, clustersIn(workspace))
|
|
84
|
+
const method = methodText(resource)
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<SummaryShell>
|
|
88
|
+
<ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'Backup')} onNavigate={onNavigate} />
|
|
89
|
+
|
|
90
|
+
<SummaryHeading>Outcome</SummaryHeading>
|
|
91
|
+
<FactGrid>
|
|
92
|
+
<FactRow label="Phase">
|
|
93
|
+
<PhaseBadge status={status} />
|
|
94
|
+
</FactRow>
|
|
95
|
+
<Timing resource={resource} />
|
|
96
|
+
<FactRow label="Taken on">
|
|
97
|
+
{pod ? <RefLink refTo={{ kind: 'Pod', group: '', namespace: ns, name: pod }} onNavigate={onNavigate} mono /> : <NotReported />}
|
|
98
|
+
</FactRow>
|
|
99
|
+
<FactRow label="Backup ID">
|
|
100
|
+
{resource?.status?.backupId ? <span className="font-mono">{resource.status.backupId}</span> : <NotReported />}
|
|
101
|
+
</FactRow>
|
|
102
|
+
<FactRow label="Target">
|
|
103
|
+
{resource?.spec?.target ? resource.spec.target : <span className="text-theme-text-secondary">Not set · the Cluster's backup target applies</span>}
|
|
104
|
+
</FactRow>
|
|
105
|
+
{error && (
|
|
106
|
+
<FactRow label="Error">
|
|
107
|
+
<span className={toneTextClass('unhealthy')}>{error}</span>
|
|
108
|
+
</FactRow>
|
|
109
|
+
)}
|
|
110
|
+
</FactGrid>
|
|
111
|
+
|
|
112
|
+
<SummaryHeading>Relationships</SummaryHeading>
|
|
113
|
+
<FactGrid>
|
|
114
|
+
<FactRow label="Cluster">
|
|
115
|
+
<ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
|
|
116
|
+
</FactRow>
|
|
117
|
+
<FactRow label="Trigger">
|
|
118
|
+
{trigger ? (
|
|
119
|
+
<span>
|
|
120
|
+
ScheduledBackup{' '}
|
|
121
|
+
{scheduleReplaced ? (
|
|
122
|
+
<>
|
|
123
|
+
<span className="font-mono">{trigger}</span>
|
|
124
|
+
<span className="text-theme-text-tertiary"> · an earlier schedule of that name; the current one is a different object</span>
|
|
125
|
+
</>
|
|
126
|
+
) : (
|
|
127
|
+
<RefLink refTo={{ kind: 'ScheduledBackup', group: CNPG_GROUP, namespace: ns, name: trigger }} onNavigate={onNavigate} mono />
|
|
128
|
+
)}
|
|
129
|
+
</span>
|
|
130
|
+
) : (
|
|
131
|
+
'On demand'
|
|
132
|
+
)}
|
|
133
|
+
</FactRow>
|
|
134
|
+
<FactRow label="Destination">
|
|
135
|
+
{dest.type === 'objectStore' ? (
|
|
136
|
+
<span>
|
|
137
|
+
ObjectStore{' '}
|
|
138
|
+
<RefLink refTo={{ kind: 'ObjectStore', group: CNPG_BARMAN_OBJECTSTORE_GROUP, namespace: ns, name: dest.name }} onNavigate={onNavigate} mono />
|
|
139
|
+
{dest.inferred && <span className="text-theme-text-tertiary"> · from the Cluster's current configuration</span>}
|
|
140
|
+
</span>
|
|
141
|
+
) : dest.type === 'path' ? (
|
|
142
|
+
<span className="font-mono">{dest.path}</span>
|
|
143
|
+
) : dest.type === 'volumeSnapshot' ? (
|
|
144
|
+
'Volume snapshot'
|
|
145
|
+
) : (
|
|
146
|
+
<NotReported />
|
|
147
|
+
)}
|
|
148
|
+
</FactRow>
|
|
149
|
+
<FactRow label="Method">{method ?? <NotReported />}</FactRow>
|
|
150
|
+
</FactGrid>
|
|
151
|
+
</SummaryShell>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function CNPGScheduledBackupSummary({ resource, workspace, onNavigate }: SummaryProps) {
|
|
156
|
+
const ns = resource?.metadata?.namespace ?? ''
|
|
157
|
+
const cron = resource?.spec?.schedule
|
|
158
|
+
const next = getCNPGScheduledBackupNextSchedule(resource)
|
|
159
|
+
const runsUnavailable = relationUnavailable(workspace, 'backups', ns, 'Backups')
|
|
160
|
+
const runs = runsUnavailable ? [] : backupsForScheduledBackup(resource, workspaceList(workspace, 'backups'))
|
|
161
|
+
const shown = runs.slice(0, RECENT_RUNS)
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<SummaryShell>
|
|
165
|
+
<ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'ScheduledBackup')} onNavigate={onNavigate} />
|
|
166
|
+
|
|
167
|
+
<SummaryHeading>Schedule</SummaryHeading>
|
|
168
|
+
<FactGrid>
|
|
169
|
+
<FactRow label="Status">
|
|
170
|
+
<PhaseBadge status={getCNPGScheduledBackupStatus(resource)} />
|
|
171
|
+
</FactRow>
|
|
172
|
+
<FactRow label="Cluster">
|
|
173
|
+
<ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
|
|
174
|
+
</FactRow>
|
|
175
|
+
<FactRow label="Schedule">
|
|
176
|
+
{cron ? (
|
|
177
|
+
<span>
|
|
178
|
+
<span className="font-mono">{cron}</span>
|
|
179
|
+
<span className="text-theme-text-tertiary"> · six fields, seconds first</span>
|
|
180
|
+
</span>
|
|
181
|
+
) : (
|
|
182
|
+
<NotReported text="Not set" />
|
|
183
|
+
)}
|
|
184
|
+
</FactRow>
|
|
185
|
+
<FactRow label="Last scheduled">
|
|
186
|
+
<TimeAgo at={resource?.status?.lastScheduleTime} missing="Never" />
|
|
187
|
+
</FactRow>
|
|
188
|
+
<FactRow label="Next scheduled">{next === '-' ? <NotReported /> : next}</FactRow>
|
|
189
|
+
<FactRow label="Method">{methodText(resource) ?? 'Barman object store (in-tree) · default'}</FactRow>
|
|
190
|
+
</FactGrid>
|
|
191
|
+
|
|
192
|
+
<SummaryHeading hint={runs.length > shown.length ? `${shown.length} of ${runs.length}` : undefined}>Recent runs</SummaryHeading>
|
|
193
|
+
{runsUnavailable ? (
|
|
194
|
+
<div className="text-sm">
|
|
195
|
+
<NotReported text={runsUnavailable} />
|
|
196
|
+
</div>
|
|
197
|
+
) : shown.length === 0 ? (
|
|
198
|
+
<div className="text-sm text-theme-text-secondary">No Backups from this schedule are visible</div>
|
|
199
|
+
) : (
|
|
200
|
+
<FactGrid>
|
|
201
|
+
{shown.map((b) => (
|
|
202
|
+
<FactRow
|
|
203
|
+
key={b.metadata?.name}
|
|
204
|
+
label={<RefLink refTo={{ kind: 'Backup', group: CNPG_GROUP, namespace: ns, name: b.metadata?.name }} onNavigate={onNavigate} mono />}
|
|
205
|
+
>
|
|
206
|
+
<span className="inline-flex flex-wrap items-center gap-2">
|
|
207
|
+
<PhaseBadge status={getCNPGBackupStatus(b)} />
|
|
208
|
+
{b.status?.startedAt ? (
|
|
209
|
+
<span className="text-theme-text-secondary">
|
|
210
|
+
started <TimeAgo at={b.status.startedAt} />
|
|
211
|
+
</span>
|
|
212
|
+
) : (
|
|
213
|
+
<NotReported text="Not started" />
|
|
214
|
+
)}
|
|
215
|
+
</span>
|
|
216
|
+
</FactRow>
|
|
217
|
+
))}
|
|
218
|
+
</FactGrid>
|
|
219
|
+
)}
|
|
220
|
+
{!runsUnavailable && (workspace?.backupsOmitted ?? 0) > 0 && <Note>Backups older than 7 days are not listed.</Note>}
|
|
221
|
+
</SummaryShell>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Badge } from '../ui/Badge'
|
|
4
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
5
|
+
import { CNPG_BARMAN_OBJECTSTORE_GROUP, CNPG_GROUP } from '../resources/resource-utils-cnpg'
|
|
6
|
+
import type { CNPGFleetRow, CNPGInstance } from './workspace'
|
|
7
|
+
import {
|
|
8
|
+
FactGrid,
|
|
9
|
+
FactRow,
|
|
10
|
+
FactSource,
|
|
11
|
+
FactValue,
|
|
12
|
+
ProblemCallout,
|
|
13
|
+
RefLink,
|
|
14
|
+
SummaryHeading,
|
|
15
|
+
ToneDot,
|
|
16
|
+
toneTextClass,
|
|
17
|
+
CNPG_PRIMARY_BUTTON,
|
|
18
|
+
CNPG_SECONDARY_BUTTON,
|
|
19
|
+
type CNPGNavigate,
|
|
20
|
+
} from './primitives'
|
|
21
|
+
|
|
22
|
+
export interface CNPGSummaryAction {
|
|
23
|
+
label: string
|
|
24
|
+
onClick: () => void
|
|
25
|
+
primary?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function InstancePill({ pod, namespace, onNavigate }: { pod: CNPGInstance; namespace: string; onNavigate?: CNPGNavigate }) {
|
|
29
|
+
const tone = pod.ready === true ? 'healthy' : pod.ready === false ? 'unhealthy' : 'unknown'
|
|
30
|
+
const role = pod.role === 'primary' ? 'Primary' : pod.role === 'replica' ? 'Replica' : 'Role unknown'
|
|
31
|
+
const readiness = pod.ready === true ? 'Ready' : pod.ready === false ? 'Not ready' : 'Readiness unknown'
|
|
32
|
+
return (
|
|
33
|
+
<Tooltip content={`${pod.name} · ${role} · ${readiness}${pod.node ? ` · ${pod.node}` : ''}`} position="top">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => onNavigate?.({ kind: 'Pod', group: '', namespace, name: pod.name })}
|
|
37
|
+
className={clsx(
|
|
38
|
+
'inline-flex items-center gap-1.5 rounded-md border border-theme-border bg-theme-base px-2 py-0.5 text-xs',
|
|
39
|
+
onNavigate ? 'hover:border-accent' : 'cursor-default',
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<ToneDot tone={tone} />
|
|
43
|
+
<span className="font-mono">{pod.name}</span>
|
|
44
|
+
<span className="text-theme-text-tertiary">{pod.role === 'primary' ? 'P' : pod.role === 'replica' ? 'R' : '?'}</span>
|
|
45
|
+
</button>
|
|
46
|
+
</Tooltip>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CNPGClusterSummary({
|
|
51
|
+
row,
|
|
52
|
+
onNavigate,
|
|
53
|
+
actions,
|
|
54
|
+
problemsLink,
|
|
55
|
+
extra,
|
|
56
|
+
}: {
|
|
57
|
+
row: CNPGFleetRow
|
|
58
|
+
onNavigate?: CNPGNavigate
|
|
59
|
+
actions?: CNPGSummaryAction[]
|
|
60
|
+
/** Link to the complete list of this cluster's findings, shown when more than one exists. */
|
|
61
|
+
problemsLink?: (count: number) => ReactNode
|
|
62
|
+
extra?: ReactNode
|
|
63
|
+
}) {
|
|
64
|
+
const top = row.problems[0]
|
|
65
|
+
const rest = row.problems.length - 1
|
|
66
|
+
const p = row.protection
|
|
67
|
+
const ns = row.namespace
|
|
68
|
+
const radarFindings = row.problems.some((x) => x.severity !== 'posture')
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="px-4 py-4">
|
|
72
|
+
{top && (
|
|
73
|
+
<ProblemCallout
|
|
74
|
+
problem={top}
|
|
75
|
+
onNavigate={onNavigate}
|
|
76
|
+
more={rest > 0 ? problemsLink?.(row.problems.length) ?? <span>+{rest} more</span> : null}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{actions && actions.length > 0 && (
|
|
81
|
+
<div className="mb-4 flex flex-wrap gap-2">
|
|
82
|
+
{actions.map((a) => (
|
|
83
|
+
<button key={a.label} type="button" onClick={a.onClick} className={a.primary ? CNPG_PRIMARY_BUTTON : CNPG_SECONDARY_BUTTON}>
|
|
84
|
+
{a.label}
|
|
85
|
+
</button>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<SummaryHeading>State</SummaryHeading>
|
|
91
|
+
<FactGrid>
|
|
92
|
+
<FactRow label="Controller phase">
|
|
93
|
+
<span className="inline-flex flex-wrap items-center gap-2">
|
|
94
|
+
<Badge severity={row.controllerStatus.level === 'healthy' ? 'success' : row.controllerStatus.level === 'unhealthy' ? 'error' : row.controllerStatus.level === 'degraded' || row.controllerStatus.level === 'alert' ? 'warning' : 'neutral'} size="sm">
|
|
95
|
+
{row.controllerStatus.text}
|
|
96
|
+
</Badge>
|
|
97
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
98
|
+
{radarFindings ? 'reported by CNPG · Radar findings above are separate' : 'reported by CNPG'}
|
|
99
|
+
</span>
|
|
100
|
+
</span>
|
|
101
|
+
</FactRow>
|
|
102
|
+
<FactRow label="Instances">
|
|
103
|
+
<div>
|
|
104
|
+
<span>
|
|
105
|
+
{row.instances.ready ?? '–'}/{row.instances.desired ?? '–'} ready
|
|
106
|
+
{row.cluster?.status?.currentPrimary && (
|
|
107
|
+
<span className="text-theme-text-secondary"> · primary <span className="font-mono">{row.cluster.status.currentPrimary}</span></span>
|
|
108
|
+
)}
|
|
109
|
+
</span>
|
|
110
|
+
{row.pods.length > 0 && (
|
|
111
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
112
|
+
{row.pods.map((pod) => (
|
|
113
|
+
<InstancePill key={pod.name} pod={pod} namespace={ns} onNavigate={onNavigate} />
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</FactRow>
|
|
119
|
+
<FactRow label="Replication">
|
|
120
|
+
<FactValue fact={row.replication} />
|
|
121
|
+
<FactSource fact={row.replication} />
|
|
122
|
+
</FactRow>
|
|
123
|
+
{row.replicaCluster && (
|
|
124
|
+
<FactRow label="Replica cluster">
|
|
125
|
+
Follows {row.replicaCluster.source ? <span className="font-mono">{row.replicaCluster.source}</span> : 'an external primary'}
|
|
126
|
+
</FactRow>
|
|
127
|
+
)}
|
|
128
|
+
<FactRow label="PostgreSQL">
|
|
129
|
+
{row.pgVersion ?? 'Unknown'}
|
|
130
|
+
{row.catalog && (
|
|
131
|
+
<span className="text-theme-text-secondary">
|
|
132
|
+
{' · '}
|
|
133
|
+
<RefLink
|
|
134
|
+
refTo={{ kind: row.catalog.kind, group: CNPG_GROUP, namespace: row.catalog.kind === 'ClusterImageCatalog' ? '' : ns, name: row.catalog.name }}
|
|
135
|
+
onNavigate={onNavigate}
|
|
136
|
+
>
|
|
137
|
+
{row.catalog.name}
|
|
138
|
+
</RefLink>
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
</FactRow>
|
|
142
|
+
<FactRow label="Declarations">
|
|
143
|
+
<FactValue fact={row.declarations.summary} />
|
|
144
|
+
</FactRow>
|
|
145
|
+
<FactRow label="Poolers">
|
|
146
|
+
{row.poolers.length === 0 ? (
|
|
147
|
+
<span className={row.poolersKnown ? 'text-theme-text-secondary' : 'text-theme-text-tertiary'}>
|
|
148
|
+
{row.poolersKnown ? 'None' : 'No access to Poolers'}
|
|
149
|
+
</span>
|
|
150
|
+
) : (
|
|
151
|
+
<span className="flex flex-wrap gap-x-3">
|
|
152
|
+
{row.poolers.map((name) => (
|
|
153
|
+
<RefLink key={name} refTo={{ kind: 'Pooler', group: CNPG_GROUP, namespace: ns, name }} onNavigate={onNavigate} mono />
|
|
154
|
+
))}
|
|
155
|
+
</span>
|
|
156
|
+
)}
|
|
157
|
+
</FactRow>
|
|
158
|
+
{row.gitops && (
|
|
159
|
+
<FactRow label="Declared in">
|
|
160
|
+
{row.gitops.tool === 'argocd' ? 'Argo CD' : 'Flux'} <span className="font-mono">{row.gitops.name}</span>
|
|
161
|
+
</FactRow>
|
|
162
|
+
)}
|
|
163
|
+
</FactGrid>
|
|
164
|
+
|
|
165
|
+
<SummaryHeading>Protection</SummaryHeading>
|
|
166
|
+
<FactGrid>
|
|
167
|
+
<FactRow label="Schedule">
|
|
168
|
+
<FactValue fact={p.schedule} />
|
|
169
|
+
{p.schedule.names.length > 0 && (
|
|
170
|
+
<div className="mt-0.5 flex flex-wrap gap-x-3 text-xs">
|
|
171
|
+
{p.schedule.names.map((name) => (
|
|
172
|
+
<RefLink key={name} refTo={{ kind: 'ScheduledBackup', group: CNPG_GROUP, namespace: ns, name }} onNavigate={onNavigate} mono />
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</FactRow>
|
|
177
|
+
<FactRow label="Destination">
|
|
178
|
+
{p.destination.objectStore ? (
|
|
179
|
+
<RefLink refTo={{ kind: 'ObjectStore', group: CNPG_BARMAN_OBJECTSTORE_GROUP, namespace: ns, name: p.destination.objectStore }} onNavigate={onNavigate}>
|
|
180
|
+
ObjectStore {p.destination.objectStore}
|
|
181
|
+
</RefLink>
|
|
182
|
+
) : (
|
|
183
|
+
<FactValue fact={p.destination} />
|
|
184
|
+
)}
|
|
185
|
+
</FactRow>
|
|
186
|
+
<FactRow label="Last successful backup">
|
|
187
|
+
<FactValue fact={p.lastSuccessfulBackup} />
|
|
188
|
+
<FactSource fact={p.lastSuccessfulBackup} />
|
|
189
|
+
</FactRow>
|
|
190
|
+
<FactRow label="WAL archiving">
|
|
191
|
+
<FactValue fact={p.walArchiving} />
|
|
192
|
+
</FactRow>
|
|
193
|
+
<FactRow label="Recovery window">
|
|
194
|
+
{p.recoveryWindow.from ? (
|
|
195
|
+
<div>
|
|
196
|
+
<span className={p.recoveryWindow.tone === 'degraded' ? toneTextClass('degraded') : undefined}>
|
|
197
|
+
from {new Date(p.recoveryWindow.from).toUTCString().replace(' GMT', ' UTC')} {p.recoveryWindow.tone === 'degraded' ? '· not advancing' : 'to the newest archived WAL'}
|
|
198
|
+
</span>
|
|
199
|
+
<FactSource fact={p.recoveryWindow} />
|
|
200
|
+
{p.recoveryWindow.tone === 'degraded' && (
|
|
201
|
+
<div className="text-[11.5px] text-theme-text-tertiary">WAL archiving is failing, so nothing written since the last archived WAL can be recovered.</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
) : (
|
|
205
|
+
<FactValue fact={p.recoveryWindow} />
|
|
206
|
+
)}
|
|
207
|
+
</FactRow>
|
|
208
|
+
<FactRow label="Restore validation">
|
|
209
|
+
{p.restoreValidation.restoredInto ? (
|
|
210
|
+
<RefLink refTo={{ kind: 'Cluster', group: CNPG_GROUP, ...p.restoreValidation.restoredInto }} onNavigate={onNavigate}>
|
|
211
|
+
{p.restoreValidation.text}
|
|
212
|
+
</RefLink>
|
|
213
|
+
) : (
|
|
214
|
+
<FactValue fact={p.restoreValidation} />
|
|
215
|
+
)}
|
|
216
|
+
<FactSource fact={p.restoreValidation} />
|
|
217
|
+
</FactRow>
|
|
218
|
+
</FactGrid>
|
|
219
|
+
{extra}
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|