@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.
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- 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/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/IssuesView.tsx +10 -1
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/issues/types.ts +24 -0
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
- package/src/components/logs/useLogBuffer.test.ts +17 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
- package/src/components/resources/ResourcesSidebar.tsx +183 -11
- package/src/components/resources/ResourcesView.tsx +12 -9
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.ts +3 -7
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/resources/status-overclaim.test.ts +1 -1
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +187 -38
- package/src/components/timeline/TimelineListFold.test.tsx +56 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- 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/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +216 -48
- 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/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/log-level.ts +14 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
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
|
}
|
|
@@ -377,6 +377,21 @@ describe("ApplicationDetail shell", () => {
|
|
|
377
377
|
expect(local).toContain('ring is full, so the oldest activity is not loaded')
|
|
378
378
|
})
|
|
379
379
|
|
|
380
|
+
it('shows the host history-limit note beside the history, and nothing without one', () => {
|
|
381
|
+
const base = {
|
|
382
|
+
selectedView: 'history' as const,
|
|
383
|
+
onSelectView: () => {},
|
|
384
|
+
selectedWorkloadKey: null,
|
|
385
|
+
onSelectWorkload: () => {},
|
|
386
|
+
historyMode: 'local' as const,
|
|
387
|
+
historyRange: 'all' as const,
|
|
388
|
+
historyRangeOptions: [{ value: 'all' as const, label: 'All observed' }],
|
|
389
|
+
historyItems: [],
|
|
390
|
+
}
|
|
391
|
+
expect(renderDetail({ ...base, historyRetentionNote: <span>Host limit note</span> })).toContain('<span>Host limit note</span>')
|
|
392
|
+
expect(renderDetail(base)).not.toContain('Host limit note')
|
|
393
|
+
})
|
|
394
|
+
|
|
380
395
|
it('shows a single honest error state when runtime history fails without source history', () => {
|
|
381
396
|
const html = renderDetail({
|
|
382
397
|
selectedView: 'history',
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
overlayProvenance,
|
|
90
90
|
} from "../../utils/applications";
|
|
91
91
|
import { PaneLoader } from "../ui/PaneLoader";
|
|
92
|
-
import { midTruncate } from "../../utils/format";
|
|
92
|
+
import { formatRelativeAgeTime, midTruncate } from "../../utils/format";
|
|
93
93
|
import { VersionTooltip, AppIdentityTooltip, RolloutSummaryTooltip } from "./AppTooltips";
|
|
94
94
|
import {
|
|
95
95
|
ProvenanceBadge,
|
|
@@ -253,6 +253,9 @@ export type ApplicationDetailProps = {
|
|
|
253
253
|
// The retained ring itself was row-capped: the OLDEST part of the retention
|
|
254
254
|
// window is not loaded, independent of the per-query event limit.
|
|
255
255
|
historyRingTruncated?: boolean;
|
|
256
|
+
// The host's note about how far back history goes, or the stretch before
|
|
257
|
+
// the limit reads as "no history".
|
|
258
|
+
historyRetentionNote?: ReactNode;
|
|
256
259
|
onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
257
260
|
onOpenTimeline?: (timestamp?: string) => void;
|
|
258
261
|
onOpenSource?: (source: AppSourceRef) => void;
|
|
@@ -346,6 +349,7 @@ export function ApplicationDetail({
|
|
|
346
349
|
historyCoverageRecordCount,
|
|
347
350
|
historyRuntimeLimited,
|
|
348
351
|
historyRingTruncated,
|
|
352
|
+
historyRetentionNote,
|
|
349
353
|
onHistoryRangeChange,
|
|
350
354
|
onOpenTimeline,
|
|
351
355
|
onOpenSource,
|
|
@@ -784,6 +788,7 @@ export function ApplicationDetail({
|
|
|
784
788
|
historyCoverageRecordCount={historyCoverageRecordCount}
|
|
785
789
|
historyRuntimeLimited={historyRuntimeLimited}
|
|
786
790
|
historyRingTruncated={historyRingTruncated}
|
|
791
|
+
historyRetentionNote={historyRetentionNote}
|
|
787
792
|
onHistoryRangeChange={onHistoryRangeChange}
|
|
788
793
|
onOpenTimeline={onOpenTimeline}
|
|
789
794
|
onOpenSource={onOpenSource}
|
|
@@ -832,6 +837,7 @@ function ApplicationWorkspace({
|
|
|
832
837
|
historyCoverageRecordCount,
|
|
833
838
|
historyRuntimeLimited,
|
|
834
839
|
historyRingTruncated,
|
|
840
|
+
historyRetentionNote,
|
|
835
841
|
onHistoryRangeChange,
|
|
836
842
|
onOpenTimeline,
|
|
837
843
|
onOpenSource,
|
|
@@ -884,6 +890,9 @@ function ApplicationWorkspace({
|
|
|
884
890
|
historyCoverageRecordCount?: number;
|
|
885
891
|
historyRuntimeLimited?: boolean;
|
|
886
892
|
historyRingTruncated?: boolean;
|
|
893
|
+
// The host's note about how far back history goes, or the stretch before
|
|
894
|
+
// the limit reads as "no history".
|
|
895
|
+
historyRetentionNote?: ReactNode;
|
|
887
896
|
onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
888
897
|
onOpenTimeline?: (timestamp?: string) => void;
|
|
889
898
|
onOpenSource?: (source: AppSourceRef) => void;
|
|
@@ -953,6 +962,7 @@ function ApplicationWorkspace({
|
|
|
953
962
|
coverageRecordCount={historyCoverageRecordCount}
|
|
954
963
|
runtimeLimited={historyRuntimeLimited}
|
|
955
964
|
ringTruncated={historyRingTruncated}
|
|
965
|
+
retentionNote={historyRetentionNote}
|
|
956
966
|
workloads={workloads}
|
|
957
967
|
onSelectWorkload={onSelectWorkload}
|
|
958
968
|
onNavigateToResource={onNavigateToResource}
|
|
@@ -1827,7 +1837,7 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
|
|
|
1827
1837
|
return {
|
|
1828
1838
|
latestValue: latest?.latestRunPhase || latest?.label || "None",
|
|
1829
1839
|
latestDetail: latest
|
|
1830
|
-
? `${latest.latestRunName || latest.workload.name}${latestRunTimestamp(latest) ? ` · ${
|
|
1840
|
+
? `${latest.latestRunName || latest.workload.name}${latestRunTimestamp(latest) ? ` · ${formatRelativeAgeTime(latestRunTimestamp(latest)!)} ` : ""}`.trim()
|
|
1831
1841
|
: "No retained runs",
|
|
1832
1842
|
retainedValue:
|
|
1833
1843
|
retained > 0 ? `${succeeded} succeeded / ${failed} failed` : "0 retained",
|
|
@@ -1842,7 +1852,7 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
|
|
|
1842
1852
|
: `${schedules.length} schedules`,
|
|
1843
1853
|
scheduleDetail:
|
|
1844
1854
|
schedules.length === 1 && latest?.lastSuccessfulAt
|
|
1845
|
-
? `last success ${
|
|
1855
|
+
? `last success ${formatRelativeAgeTime(latest.lastSuccessfulAt)}`
|
|
1846
1856
|
: undefined,
|
|
1847
1857
|
scheduleMono: schedules.length === 1,
|
|
1848
1858
|
};
|
|
@@ -1856,17 +1866,6 @@ function activeWorkloadNames(activity: AppBatchActivity[]): string {
|
|
|
1856
1866
|
return `${active.slice(0, 2).join(", ")} +${active.length - 2} more`;
|
|
1857
1867
|
}
|
|
1858
1868
|
|
|
1859
|
-
function relativeTime(value: string): string {
|
|
1860
|
-
const t = Date.parse(value);
|
|
1861
|
-
if (Number.isNaN(t)) return value;
|
|
1862
|
-
const diff = Date.now() - t;
|
|
1863
|
-
if (diff < 60_000) return "just now";
|
|
1864
|
-
const minutes = Math.floor(diff / 60_000);
|
|
1865
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
1866
|
-
const hours = Math.floor(minutes / 60);
|
|
1867
|
-
if (hours < 48) return `${hours}h ago`;
|
|
1868
|
-
return `${Math.floor(hours / 24)}d ago`;
|
|
1869
|
-
}
|
|
1870
1869
|
|
|
1871
1870
|
function ApplicationLatestHistory({
|
|
1872
1871
|
history,
|
|
@@ -2093,7 +2092,7 @@ function workloadRuntimeDetail(workload: AppWorkload): string {
|
|
|
2093
2092
|
const at =
|
|
2094
2093
|
batch.latestStartedAt || batch.latestFinishedAt || batch.lastScheduledAt;
|
|
2095
2094
|
if (batch.latestRunName)
|
|
2096
|
-
return `${midTruncate(batch.latestRunName, 28)}${at ? ` · ${
|
|
2095
|
+
return `${midTruncate(batch.latestRunName, 28)}${at ? ` · ${formatRelativeAgeTime(at)}` : ""}`;
|
|
2097
2096
|
if (batch.schedule) return `Schedule ${batch.schedule}`;
|
|
2098
2097
|
return "No retained runs";
|
|
2099
2098
|
}
|
|
@@ -2201,6 +2200,7 @@ function ApplicationHistoryView({
|
|
|
2201
2200
|
coverageRecordCount,
|
|
2202
2201
|
runtimeLimited,
|
|
2203
2202
|
ringTruncated,
|
|
2203
|
+
retentionNote,
|
|
2204
2204
|
workloads,
|
|
2205
2205
|
onSelectWorkload,
|
|
2206
2206
|
onNavigateToResource,
|
|
@@ -2219,6 +2219,7 @@ function ApplicationHistoryView({
|
|
|
2219
2219
|
coverageRecordCount?: number;
|
|
2220
2220
|
runtimeLimited?: boolean;
|
|
2221
2221
|
ringTruncated?: boolean;
|
|
2222
|
+
retentionNote?: ReactNode;
|
|
2222
2223
|
workloads: AppWorkload[];
|
|
2223
2224
|
onSelectWorkload: (workload: AppWorkload) => void;
|
|
2224
2225
|
onNavigateToResource?: (resource: ResourceRef) => void;
|
|
@@ -2428,6 +2429,7 @@ function ApplicationHistoryView({
|
|
|
2428
2429
|
application activity in this range may not be included.
|
|
2429
2430
|
</div>
|
|
2430
2431
|
)}
|
|
2432
|
+
{retentionNote && <div>{retentionNote}</div>}
|
|
2431
2433
|
{ringTruncated && (
|
|
2432
2434
|
<div>
|
|
2433
2435
|
{mode === "retained"
|
|
@@ -61,19 +61,8 @@ export interface CheckResourceRef {
|
|
|
61
61
|
/** Built-in (Radar-detected) finding source. The only V1 source. */
|
|
62
62
|
export const SOURCE_RADAR_BUILTIN = 'radar_builtin';
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* `group|Kind|namespace|name`. Group first because group and namespace can each
|
|
67
|
-
* independently be empty; `|` is delimiter-safe (K8s API groups follow
|
|
68
|
-
* DNS-subdomain rules and can't contain it).
|
|
69
|
-
*/
|
|
70
|
-
export function resourceKey(group: string, kind: string, namespace: string, name: string): string {
|
|
71
|
-
return `${group}|${kind}|${namespace}|${name}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function resourceRefKey(ref: CheckResourceRef): string {
|
|
75
|
-
return resourceKey(ref.group, ref.kind, ref.namespace, ref.name);
|
|
76
|
-
}
|
|
64
|
+
// Preserve the Checks import path while sharing exact resource identity.
|
|
65
|
+
export { resourceKey, resourceRefKey } from '../../utils/resource-refs';
|
|
77
66
|
|
|
78
67
|
/**
|
|
79
68
|
* checkFindingKey is the canonical built-in finding key:
|
|
@@ -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
|
+
}
|