@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useRef, useCallback, useId, forwardRef } from 'react'
|
|
1
|
+
import { useState, useMemo, useEffect, useRef, useCallback, useId, forwardRef, type ComponentType } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Search,
|
|
4
4
|
Eye,
|
|
@@ -29,6 +29,28 @@ export interface PinnedItem {
|
|
|
29
29
|
group: string
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** A task destination shown inside a category, above its exact kinds. */
|
|
33
|
+
export interface SidebarCategoryDestination {
|
|
34
|
+
id: string
|
|
35
|
+
label: string
|
|
36
|
+
icon?: ComponentType<{ className?: string }>
|
|
37
|
+
/** Problem count. `undefined` renders no badge; `null` renders the unknown dash. */
|
|
38
|
+
count?: number | null
|
|
39
|
+
countTitle?: string
|
|
40
|
+
active?: boolean
|
|
41
|
+
/** The object currently open under this destination, nested beneath it. */
|
|
42
|
+
child?: { label: string; title?: string }
|
|
43
|
+
onSelect: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SidebarCategoryWorkspace {
|
|
47
|
+
destinations: SidebarCategoryDestination[]
|
|
48
|
+
/** Kinds start collapsed under a "Resource kinds" disclosure until the user opens them. */
|
|
49
|
+
defaultKindsCollapsed?: boolean
|
|
50
|
+
/** Explains what the destination counts are scoped to. */
|
|
51
|
+
scopeNote?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
export interface ResourcesSidebarProps {
|
|
33
55
|
selectedKind: SelectedKindInfo | null
|
|
34
56
|
onSelectedKindChange: (kind: SelectedKindInfo) => void
|
|
@@ -48,10 +70,15 @@ export interface ResourcesSidebarProps {
|
|
|
48
70
|
/** Called when a kind is selected via keyboard (Enter in the filter). Parent uses this
|
|
49
71
|
* to move focus to the next UI level (e.g., the table search input). */
|
|
50
72
|
onKindNavigated?: () => void
|
|
73
|
+
/** Task destinations keyed by category name (e.g. "CloudNativePG"). A category
|
|
74
|
+
* with a workspace stays visible even when it has no resources. */
|
|
75
|
+
categoryWorkspaces?: Record<string, SidebarCategoryWorkspace>
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
// Persisted across remounts so collapsed categories survive tab switches
|
|
54
79
|
let persistedExpandedCategories: Set<string> | null = null
|
|
80
|
+
// Per-category "Resource kinds" disclosure, once the user has toggled it.
|
|
81
|
+
const persistedKindsOpen = new Map<string, boolean>()
|
|
55
82
|
const COUNT_UNAVAILABLE_MESSAGE = 'Count unavailable. Open to view resources.'
|
|
56
83
|
|
|
57
84
|
// Fallback resource types when API resources aren't loaded yet
|
|
@@ -102,10 +129,11 @@ interface ResourceTypeButtonProps {
|
|
|
102
129
|
isPinned?: boolean
|
|
103
130
|
onTogglePin?: () => void
|
|
104
131
|
onClick: () => void
|
|
132
|
+
indent?: boolean
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps>(
|
|
108
|
-
function ResourceTypeButton({ resource, count, isSelected, isHighlighted, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
136
|
+
function ResourceTypeButton({ resource, count, isSelected, isHighlighted, isForbidden: forbidden, isPinned, onTogglePin, onClick, indent }, ref) {
|
|
109
137
|
const Icon = getResourceIcon(resource.kind, resource.group)
|
|
110
138
|
return (
|
|
111
139
|
<button
|
|
@@ -113,6 +141,7 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
|
|
|
113
141
|
onClick={onClick}
|
|
114
142
|
className={clsx(
|
|
115
143
|
'w-full flex items-center gap-2 px-2 xl:px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
|
|
144
|
+
indent && 'pl-5 xl:pl-6',
|
|
116
145
|
isSelected
|
|
117
146
|
? 'selection-strong selection-text'
|
|
118
147
|
: isHighlighted
|
|
@@ -193,6 +222,7 @@ export function ResourcesSidebar({
|
|
|
193
222
|
onNavigate,
|
|
194
223
|
basePath,
|
|
195
224
|
onKindNavigated,
|
|
225
|
+
categoryWorkspaces,
|
|
196
226
|
}: ResourcesSidebarProps) {
|
|
197
227
|
// Wraps kind selection to also navigate when basePath/onNavigate are provided
|
|
198
228
|
const selectKind = (kind: SelectedKindInfo) => {
|
|
@@ -229,8 +259,35 @@ export function ResourcesSidebar({
|
|
|
229
259
|
}
|
|
230
260
|
}, [])
|
|
231
261
|
|
|
232
|
-
|
|
233
|
-
|
|
262
|
+
const activeDestinationCategory = useMemo(() => {
|
|
263
|
+
for (const [name, ws] of Object.entries(categoryWorkspaces ?? {})) {
|
|
264
|
+
if (ws.destinations.some(d => d.active)) return name
|
|
265
|
+
}
|
|
266
|
+
return null
|
|
267
|
+
}, [categoryWorkspaces])
|
|
268
|
+
|
|
269
|
+
// Effective selected kind — fall back to a safe default. While a workspace
|
|
270
|
+
// destination is active no kind is selected.
|
|
271
|
+
const effectiveSelectedKind = activeDestinationCategory
|
|
272
|
+
? { name: '', kind: '', group: '\u0000' }
|
|
273
|
+
: selectedKind ?? { name: 'pods', kind: 'Pod', group: '' }
|
|
274
|
+
|
|
275
|
+
const [kindsOpenOverrides, setKindsOpenOverrides] = useState<Map<string, boolean>>(() => new Map(persistedKindsOpen))
|
|
276
|
+
const kindsOpen = (categoryName: string) => {
|
|
277
|
+
const ws = categoryWorkspaces?.[categoryName]
|
|
278
|
+
if (!ws) return true
|
|
279
|
+
const override = kindsOpenOverrides.get(categoryName)
|
|
280
|
+
if (override !== undefined) return override
|
|
281
|
+
const selectedHere = !activeDestinationCategory && categories?.find(c => c.name === categoryName)?.resources.some(
|
|
282
|
+
r => r.name === effectiveSelectedKind.name && r.group === effectiveSelectedKind.group,
|
|
283
|
+
)
|
|
284
|
+
return selectedHere || !ws.defaultKindsCollapsed
|
|
285
|
+
}
|
|
286
|
+
const toggleKinds = (categoryName: string) => {
|
|
287
|
+
const next = !kindsOpen(categoryName)
|
|
288
|
+
persistedKindsOpen.set(categoryName, next)
|
|
289
|
+
setKindsOpenOverrides(prev => new Map(prev).set(categoryName, next))
|
|
290
|
+
}
|
|
234
291
|
|
|
235
292
|
// Categorize resources for sidebar
|
|
236
293
|
const categories = useMemo(() => {
|
|
@@ -254,6 +311,12 @@ export function ResourcesSidebar({
|
|
|
254
311
|
}
|
|
255
312
|
}, [categories, effectiveSelectedKind.kind, effectiveSelectedKind.name]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
256
313
|
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (activeDestinationCategory && !expandedCategories.has(activeDestinationCategory)) {
|
|
316
|
+
setExpandedCategories(prev => new Set([...prev, activeDestinationCategory]))
|
|
317
|
+
}
|
|
318
|
+
}, [activeDestinationCategory]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
319
|
+
|
|
257
320
|
// Derive counts from resourceCounts prop
|
|
258
321
|
const resourcesToCount = useMemo(() => {
|
|
259
322
|
if (categories) {
|
|
@@ -333,8 +396,11 @@ export function ResourcesSidebar({
|
|
|
333
396
|
return { ...category, total, visibleResources }
|
|
334
397
|
})
|
|
335
398
|
|
|
336
|
-
// Sort:
|
|
399
|
+
// Sort: the category whose workspace is open first, then categories with
|
|
400
|
+
// resources, empty ones at bottom
|
|
337
401
|
const sorted = withTotals.sort((a, b) => {
|
|
402
|
+
if (a.name === activeDestinationCategory) return -1
|
|
403
|
+
if (b.name === activeDestinationCategory) return 1
|
|
338
404
|
if (a.total === 0 && b.total > 0) return 1
|
|
339
405
|
if (a.total > 0 && b.total === 0) return -1
|
|
340
406
|
return 0
|
|
@@ -342,13 +408,13 @@ export function ResourcesSidebar({
|
|
|
342
408
|
|
|
343
409
|
// Filter out empty groups unless they have visible resources or showEmptyKinds is true.
|
|
344
410
|
const visibleCategories = sorted.filter(category => {
|
|
345
|
-
const shouldShow = category.total > 0 || category.visibleResources.length > 0 || showEmptyKinds
|
|
411
|
+
const shouldShow = category.total > 0 || category.visibleResources.length > 0 || showEmptyKinds || !!categoryWorkspaces?.[category.name]
|
|
346
412
|
if (!shouldShow) totalHiddenGroups++
|
|
347
413
|
return shouldShow
|
|
348
414
|
})
|
|
349
415
|
|
|
350
416
|
return { sortedCategories: visibleCategories, hiddenKindsCount: totalHiddenKinds, hiddenGroupsCount: totalHiddenGroups }
|
|
351
|
-
}, [categories, counts, showEmptyKinds, effectiveSelectedKind.name, effectiveSelectedKind.kind, effectiveSelectedKind.group])
|
|
417
|
+
}, [categories, counts, showEmptyKinds, effectiveSelectedKind.name, effectiveSelectedKind.kind, effectiveSelectedKind.group, categoryWorkspaces, activeDestinationCategory])
|
|
352
418
|
|
|
353
419
|
// Filter sidebar categories/kinds by the kind search term
|
|
354
420
|
const filteredCategories = useMemo(() => {
|
|
@@ -400,7 +466,7 @@ export function ResourcesSidebar({
|
|
|
400
466
|
}
|
|
401
467
|
if (filteredCategories) {
|
|
402
468
|
for (const cat of filteredCategories) {
|
|
403
|
-
if (effectiveExpandedCategories.has(cat.name)) {
|
|
469
|
+
if (effectiveExpandedCategories.has(cat.name) && (isKindFiltering || kindsOpen(cat.name))) {
|
|
404
470
|
for (const r of cat.visibleResources) {
|
|
405
471
|
kinds.push({ name: r.name, kind: r.kind, group: r.group })
|
|
406
472
|
}
|
|
@@ -408,7 +474,7 @@ export function ResourcesSidebar({
|
|
|
408
474
|
}
|
|
409
475
|
}
|
|
410
476
|
return kinds
|
|
411
|
-
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories])
|
|
477
|
+
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories, kindsOpenOverrides, categoryWorkspaces, activeDestinationCategory]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
412
478
|
|
|
413
479
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
414
480
|
// Reset highlight when the filter or kind list changes
|
|
@@ -550,6 +616,8 @@ export function ResourcesSidebar({
|
|
|
550
616
|
filteredCategories.map((category) => {
|
|
551
617
|
const isExpanded = effectiveExpandedCategories.has(category.name)
|
|
552
618
|
const rawGroupTitle = rawCRDGroupTitle(category.resources)
|
|
619
|
+
const workspace = categoryWorkspaces?.[category.name]
|
|
620
|
+
const showKinds = isKindFiltering || kindsOpen(category.name)
|
|
553
621
|
return (
|
|
554
622
|
<div key={category.name} className="mb-2">
|
|
555
623
|
<button
|
|
@@ -567,8 +635,17 @@ export function ResourcesSidebar({
|
|
|
567
635
|
)}
|
|
568
636
|
</button>
|
|
569
637
|
<Collapse open={isExpanded} id={disclosurePanelId(categoryPanelBase, category.name)}>
|
|
638
|
+
{workspace && !isKindFiltering && (
|
|
639
|
+
<WorkspaceDestinations
|
|
640
|
+
workspace={workspace}
|
|
641
|
+
kindsOpen={showKinds}
|
|
642
|
+
onToggleKinds={() => toggleKinds(category.name)}
|
|
643
|
+
kindsPanelId={disclosurePanelId(categoryPanelBase, `${category.name}-kinds`)}
|
|
644
|
+
/>
|
|
645
|
+
)}
|
|
646
|
+
<Collapse open={showKinds} id={disclosurePanelId(categoryPanelBase, `${category.name}-kinds`)}>
|
|
570
647
|
<div className="space-y-0.5">
|
|
571
|
-
{category.visibleResources.map((resource) => {
|
|
648
|
+
{(workspace ? sortByGroup(category.visibleResources) : category.visibleResources).map((resource, index, list) => {
|
|
572
649
|
const resourceIsPinned = isPinned(resource.name, resource.group)
|
|
573
650
|
const isResourceSelected =
|
|
574
651
|
(effectiveSelectedKind.name === resource.name && effectiveSelectedKind.group === resource.group) ||
|
|
@@ -576,9 +653,13 @@ export function ResourcesSidebar({
|
|
|
576
653
|
// If the resource is pinned, let the Favorites section own the highlight
|
|
577
654
|
const showSelected = isResourceSelected && !resourceIsPinned
|
|
578
655
|
const highlighted = isKindHighlighted(resource.name, resource.group)
|
|
656
|
+
const groupLabel = workspace ? groupHeaderBefore(list, index) : null
|
|
579
657
|
return (
|
|
658
|
+
<div key={`${resource.group}/${resource.name}`}>
|
|
659
|
+
{groupLabel && (
|
|
660
|
+
<div className="pl-6 pr-2 pt-1.5 pb-0.5 text-[10.5px] font-mono text-theme-text-tertiary truncate" title={groupLabel}>{groupLabel}</div>
|
|
661
|
+
)}
|
|
580
662
|
<ResourceTypeButton
|
|
581
|
-
key={resource.name}
|
|
582
663
|
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
583
664
|
resource={resource}
|
|
584
665
|
count={counts[resource.group ? `${resource.group}/${resource.kind}` : resource.kind] ?? null}
|
|
@@ -588,10 +669,13 @@ export function ResourcesSidebar({
|
|
|
588
669
|
isPinned={resourceIsPinned}
|
|
589
670
|
onTogglePin={() => togglePin({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
590
671
|
onClick={() => selectKind({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
672
|
+
indent={!!workspace}
|
|
591
673
|
/>
|
|
674
|
+
</div>
|
|
592
675
|
)
|
|
593
676
|
})}
|
|
594
677
|
</div>
|
|
678
|
+
</Collapse>
|
|
595
679
|
</Collapse>
|
|
596
680
|
</div>
|
|
597
681
|
)
|
|
@@ -673,3 +757,91 @@ export function ResourcesSidebar({
|
|
|
673
757
|
</div>
|
|
674
758
|
)
|
|
675
759
|
}
|
|
760
|
+
|
|
761
|
+
function sortByGroup(resources: APIResource[]): APIResource[] {
|
|
762
|
+
const size = new Map<string, number>()
|
|
763
|
+
for (const r of resources) size.set(r.group, (size.get(r.group) ?? 0) + 1)
|
|
764
|
+
return [...resources].sort((a, b) =>
|
|
765
|
+
a.group === b.group
|
|
766
|
+
? a.kind.localeCompare(b.kind)
|
|
767
|
+
: (size.get(b.group) ?? 0) - (size.get(a.group) ?? 0) || a.group.localeCompare(b.group),
|
|
768
|
+
)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function groupHeaderBefore(resources: APIResource[], index: number): string | null {
|
|
772
|
+
const groups = new Set(resources.map(r => r.group))
|
|
773
|
+
if (groups.size < 2) return null
|
|
774
|
+
const group = resources[index]?.group
|
|
775
|
+
if (index > 0 && resources[index - 1]?.group === group) return null
|
|
776
|
+
return group || 'core'
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function WorkspaceDestinations({
|
|
780
|
+
workspace,
|
|
781
|
+
kindsOpen,
|
|
782
|
+
onToggleKinds,
|
|
783
|
+
kindsPanelId,
|
|
784
|
+
}: {
|
|
785
|
+
workspace: SidebarCategoryWorkspace
|
|
786
|
+
kindsOpen: boolean
|
|
787
|
+
onToggleKinds: () => void
|
|
788
|
+
kindsPanelId: string
|
|
789
|
+
}) {
|
|
790
|
+
return (
|
|
791
|
+
<div className="space-y-0.5">
|
|
792
|
+
<div className="pl-5 pr-2 pt-0.5 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Workspace</div>
|
|
793
|
+
{workspace.destinations.map((d) => {
|
|
794
|
+
const Icon = d.icon
|
|
795
|
+
return (
|
|
796
|
+
<div key={d.id}>
|
|
797
|
+
<button
|
|
798
|
+
onClick={d.onSelect}
|
|
799
|
+
aria-current={d.active && !d.child ? 'page' : undefined}
|
|
800
|
+
className={clsx(
|
|
801
|
+
'w-full flex items-center gap-2 pl-5 xl:pl-6 pr-2 xl:pr-3 py-1.5 rounded-lg text-sm transition-colors min-w-0',
|
|
802
|
+
d.active && !d.child
|
|
803
|
+
? 'selection-strong selection-text'
|
|
804
|
+
: d.active
|
|
805
|
+
? 'text-theme-text-primary font-medium hover:bg-theme-elevated'
|
|
806
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary',
|
|
807
|
+
)}
|
|
808
|
+
>
|
|
809
|
+
{Icon && <Icon className="w-4 h-4 shrink-0" />}
|
|
810
|
+
<span className="flex-1 text-left truncate">{d.label}</span>
|
|
811
|
+
{d.count === null ? (
|
|
812
|
+
<Tooltip content={d.countTitle ?? COUNT_UNAVAILABLE_MESSAGE} position="left">
|
|
813
|
+
<span className="text-xs py-0.5 rounded text-center font-mono w-8 bg-theme-elevated text-theme-text-disabled">–</span>
|
|
814
|
+
</Tooltip>
|
|
815
|
+
) : d.count !== undefined && d.count > 0 ? (
|
|
816
|
+
<Tooltip content={d.countTitle ?? `${d.count}`} position="left">
|
|
817
|
+
<span className="text-xs py-0.5 rounded text-center font-mono w-8 status-degraded">{d.count}</span>
|
|
818
|
+
</Tooltip>
|
|
819
|
+
) : null}
|
|
820
|
+
</button>
|
|
821
|
+
{d.child && (
|
|
822
|
+
<div
|
|
823
|
+
className="ml-10 xl:ml-11 mr-1 my-0.5 pl-2 pr-2 py-1 rounded-md text-xs font-medium truncate selection-strong selection-text border-l-2 border-skyhook-500"
|
|
824
|
+
title={d.child.title ?? d.child.label}
|
|
825
|
+
aria-current="page"
|
|
826
|
+
>
|
|
827
|
+
{d.child.label}
|
|
828
|
+
</div>
|
|
829
|
+
)}
|
|
830
|
+
</div>
|
|
831
|
+
)
|
|
832
|
+
})}
|
|
833
|
+
{workspace.scopeNote && (
|
|
834
|
+
<div className="pl-6 pr-2 pt-0.5 text-[11px] text-theme-text-tertiary">{workspace.scopeNote}</div>
|
|
835
|
+
)}
|
|
836
|
+
<button
|
|
837
|
+
aria-expanded={kindsOpen}
|
|
838
|
+
aria-controls={kindsPanelId}
|
|
839
|
+
onClick={onToggleKinds}
|
|
840
|
+
className="w-full flex items-center gap-1.5 pl-5 pr-2 pt-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary hover:text-theme-text-secondary"
|
|
841
|
+
>
|
|
842
|
+
<CollapseChevron open={kindsOpen} className="w-3 h-3" />
|
|
843
|
+
<span>Resource kinds</span>
|
|
844
|
+
</button>
|
|
845
|
+
</div>
|
|
846
|
+
)
|
|
847
|
+
}
|
|
@@ -147,10 +147,10 @@ import {
|
|
|
147
147
|
type PrinterColumnDef, type PrinterTable, PRINTER_COLUMN_PREFIX, formatPrinterCell, printerCellSortValue,
|
|
148
148
|
printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
|
|
149
149
|
} from './printer-columns'
|
|
150
|
-
import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
150
|
+
import { SEVERITY_BADGE, SEVERITY_TEXT, healthToSeverity, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
151
151
|
import { pluralize } from '../../utils/pluralize'
|
|
152
152
|
import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
|
|
153
|
-
import { parseQuantityToNumber } from '../../utils/format'
|
|
153
|
+
import { formatRelativeAgeTime, parseQuantityToNumber } from '../../utils/format'
|
|
154
154
|
import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
|
|
155
155
|
import { isRolloutActivityVisible } from '../../utils/workload-rollout'
|
|
156
156
|
import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
|
|
@@ -205,7 +205,7 @@ import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
|
|
|
205
205
|
import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
|
|
206
206
|
import { useRegisterShortcut, useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
207
207
|
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
208
|
-
import type { SelectedKindInfo } from './ResourcesSidebar'
|
|
208
|
+
import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
|
|
209
209
|
import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
|
|
210
210
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
211
211
|
|
|
@@ -3411,6 +3411,8 @@ interface ResourcesViewProps {
|
|
|
3411
3411
|
onSelectedKindChange?: (kind: { name: string; kind: string; group: string }) => void
|
|
3412
3412
|
/** When true, the sidebar is not rendered. Useful when a standalone ResourcesSidebar is used externally. */
|
|
3413
3413
|
hideSidebar?: boolean
|
|
3414
|
+
/** Task destinations rendered inside sidebar categories (see ResourcesSidebar). */
|
|
3415
|
+
sidebarCategoryWorkspaces?: Record<string, SidebarCategoryWorkspace>
|
|
3414
3416
|
/** Callback when the [+] create button is clicked. Receives the currently selected kind info. */
|
|
3415
3417
|
onCreateResource?: (kind: { name: string; kind: string; group: string } | null) => void
|
|
3416
3418
|
/** Default kind when the URL does not include one. */
|
|
@@ -3653,6 +3655,7 @@ export function ResourcesView({
|
|
|
3653
3655
|
onOpenWorkloadLogs,
|
|
3654
3656
|
onSelectedKindChange,
|
|
3655
3657
|
hideSidebar = false,
|
|
3658
|
+
sidebarCategoryWorkspaces,
|
|
3656
3659
|
onCreateResource,
|
|
3657
3660
|
defaultKind = DEFAULT_KIND_INFO,
|
|
3658
3661
|
extraLeadingColumns,
|
|
@@ -5867,6 +5870,7 @@ export function ResourcesView({
|
|
|
5867
5870
|
pinned={pinned}
|
|
5868
5871
|
togglePin={togglePin}
|
|
5869
5872
|
isPinned={isPinned}
|
|
5873
|
+
categoryWorkspaces={sidebarCategoryWorkspaces}
|
|
5870
5874
|
onKindNavigated={() => {
|
|
5871
5875
|
// After selecting a kind via keyboard, move focus to the table search
|
|
5872
5876
|
// so the user can immediately filter within the selected kind.
|
|
@@ -6124,7 +6128,7 @@ export function ResourcesView({
|
|
|
6124
6128
|
)}
|
|
6125
6129
|
|
|
6126
6130
|
{/* Column picker */}
|
|
6127
|
-
<div className="relative" ref={columnPickerRef}>
|
|
6131
|
+
<div className="relative flex" ref={columnPickerRef}>
|
|
6128
6132
|
<Tooltip content="Configure columns">
|
|
6129
6133
|
<button
|
|
6130
6134
|
onClick={() => setShowColumnPicker(prev => !prev)}
|
|
@@ -8718,7 +8722,7 @@ function NodeCell({ resource, column, majorityNodeMinorVersion }: { resource: an
|
|
|
8718
8722
|
</span>
|
|
8719
8723
|
{problems.length > 0 && (
|
|
8720
8724
|
<Tooltip content={problems.join(', ')}>
|
|
8721
|
-
<span className=
|
|
8725
|
+
<span className={SEVERITY_TEXT[healthToSeverity(status.level ?? 'unknown')]}>
|
|
8722
8726
|
<AlertTriangle className="w-3.5 h-3.5" />
|
|
8723
8727
|
</span>
|
|
8724
8728
|
</Tooltip>
|
|
@@ -8731,7 +8735,8 @@ function NodeCell({ resource, column, majorityNodeMinorVersion }: { resource: an
|
|
|
8731
8735
|
return <span className="text-sm text-theme-text-secondary">{roles}</span>
|
|
8732
8736
|
}
|
|
8733
8737
|
case 'conditions': {
|
|
8734
|
-
const { problems, healthy } = getNodeConditions(resource)
|
|
8738
|
+
const { problems, healthy, readinessLabel } = getNodeConditions(resource)
|
|
8739
|
+
if (problems.length === 0 && !healthy) return <span className="text-theme-text-secondary">{readinessLabel}</span>
|
|
8735
8740
|
if (healthy) {
|
|
8736
8741
|
return <span className="text-sm text-green-400">Healthy</span>
|
|
8737
8742
|
}
|
|
@@ -9314,9 +9319,7 @@ function LeaseCell({ resource, column }: { resource: any; column: string }) {
|
|
|
9314
9319
|
const elapsed = (Date.now() - new Date(renewTime).getTime()) / 1000
|
|
9315
9320
|
const duration = resource.spec?.leaseDurationSeconds || 0
|
|
9316
9321
|
const isStale = duration > 0 && elapsed > duration
|
|
9317
|
-
const
|
|
9318
|
-
const seconds = Math.floor(diff / 1000)
|
|
9319
|
-
const label = seconds < 60 ? `${seconds}s ago` : seconds < 3600 ? `${Math.floor(seconds / 60)}m ago` : seconds < 86400 ? `${Math.floor(seconds / 3600)}h ago` : `${Math.floor(seconds / 86400)}d ago`
|
|
9322
|
+
const label = formatRelativeAgeTime(renewTime)
|
|
9320
9323
|
return <span className={clsx('text-sm', isStale ? 'text-red-400' : 'text-green-400')} title={new Date(renewTime).toLocaleString()}>{label}</span>
|
|
9321
9324
|
}
|
|
9322
9325
|
default:
|
|
@@ -54,7 +54,7 @@ export * from './resource-utils-velero'
|
|
|
54
54
|
export { ResourcesView, ResourcesViewDataContext, hasCuratedColumns } from './ResourcesView'
|
|
55
55
|
export type { ResourceQueryResult, ExtraColumn, LargeListGuardState } from './ResourcesView'
|
|
56
56
|
export { ResourcesSidebar } from './ResourcesSidebar'
|
|
57
|
-
export type { ResourcesSidebarProps, SelectedKindInfo, PinnedItem } from './ResourcesSidebar'
|
|
57
|
+
export type { ResourcesSidebarProps, SelectedKindInfo, PinnedItem, SidebarCategoryDestination, SidebarCategoryWorkspace } from './ResourcesSidebar'
|
|
58
58
|
export {
|
|
59
59
|
sanitizePrinterTable,
|
|
60
60
|
printerTableKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Activity, BarChart3, Server } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
3
3
|
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
4
|
-
import {
|
|
4
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
5
5
|
|
|
6
6
|
interface AnalysisRunRendererProps {
|
|
7
7
|
data: any
|
|
@@ -60,8 +60,8 @@ export function AnalysisRunRenderer({ data, onNavigate }: AnalysisRunRendererPro
|
|
|
60
60
|
<Property label="Phase" value={<Badge severity={phaseSeverity(phase)}>{phase}</Badge>} />
|
|
61
61
|
{labels['rollout-type'] && <Property label="Triggered as" value={labels['rollout-type']} />}
|
|
62
62
|
{labels['step-index'] && <Property label="Canary step" value={labels['step-index']} />}
|
|
63
|
-
<Property label="Started" value={status.startedAt ?
|
|
64
|
-
<Property label="Completed" value={status.completedAt ?
|
|
63
|
+
<Property label="Started" value={status.startedAt ? <TimeValue timestamp={status.startedAt} /> : undefined} />
|
|
64
|
+
<Property label="Completed" value={status.completedAt ? <TimeValue timestamp={status.completedAt} /> : undefined} />
|
|
65
65
|
<Property label="Message" value={status.message} />
|
|
66
66
|
{ownerRollout && (
|
|
67
67
|
<Property
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { GitBranch, FolderTree, Settings, Target, XCircle, History, ListChecks, ExternalLink } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { GitOpsStatusBadge, ManagedResourcesList, SyncCountdown } from '../../gitops'
|
|
6
5
|
import {
|
|
7
6
|
argoStatusToGitOpsStatus,
|
|
@@ -11,6 +10,8 @@ import {
|
|
|
11
10
|
} from '../../../types/gitops'
|
|
12
11
|
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
13
12
|
import { buildRepoBrowseUrl, buildPathBrowseUrl } from '../../../utils/git-provider-urls'
|
|
13
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
14
|
+
import { formatRelativeAgeTime } from '../../../utils/format'
|
|
14
15
|
|
|
15
16
|
const REPO_LINK_CLASS = 'text-blue-400 hover:text-blue-300 hover:underline break-all'
|
|
16
17
|
|
|
@@ -297,10 +298,10 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
297
298
|
<Property label="Message" value={operationState.message} />
|
|
298
299
|
)}
|
|
299
300
|
{operationState.startedAt && (
|
|
300
|
-
<Property label="Started" value={
|
|
301
|
+
<Property label="Started" value={<TimeValue timestamp={operationState.startedAt} />} />
|
|
301
302
|
)}
|
|
302
303
|
{operationState.finishedAt && (
|
|
303
|
-
<Property label="Finished" value={
|
|
304
|
+
<Property label="Finished" value={<TimeValue timestamp={operationState.finishedAt} />} />
|
|
304
305
|
)}
|
|
305
306
|
{operationState.retryCount != null && (
|
|
306
307
|
<Property
|
|
@@ -402,7 +403,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
402
403
|
</span>
|
|
403
404
|
{entry.deployedAt && (
|
|
404
405
|
<span className="text-xs text-theme-text-tertiary">
|
|
405
|
-
{
|
|
406
|
+
{formatRelativeAgeTime(entry.deployedAt)}
|
|
406
407
|
</span>
|
|
407
408
|
)}
|
|
408
409
|
</div>
|
|
@@ -425,7 +426,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
425
426
|
<Property label="Current Revision" value={status.sync.revision} />
|
|
426
427
|
)}
|
|
427
428
|
{status.reconciledAt && (
|
|
428
|
-
<Property label="Last Reconciled" value={
|
|
429
|
+
<Property label="Last Reconciled" value={<TimeValue timestamp={status.reconciledAt} />} />
|
|
429
430
|
)}
|
|
430
431
|
</PropertyList>
|
|
431
432
|
</Section>
|
|
@@ -2,8 +2,8 @@ import { useState } from 'react'
|
|
|
2
2
|
import { Server, Globe, Network, Layers, Download, CheckCircle, AlertCircle } from 'lucide-react'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
4
4
|
import { kindToPlural } from '../../../utils/navigation'
|
|
5
|
-
import { formatAge } from '../resource-utils'
|
|
6
5
|
import { getClusterStatus, getClusterClass, getClusterVersion, getClusterEndpoint, getProviderFromInfraKind, parseCAPIConditionMessage } from '../resource-utils-capi'
|
|
6
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
data: any
|
|
@@ -119,7 +119,7 @@ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '', canConnect
|
|
|
119
119
|
{className !== '-' && <Property label="Cluster Class" value={className} />}
|
|
120
120
|
{endpoint !== '-' && <Property label="Control Plane Endpoint" value={endpoint} />}
|
|
121
121
|
{readyCond?.lastTransitionTime && (
|
|
122
|
-
<Property label="Since" value={
|
|
122
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
123
123
|
)}
|
|
124
124
|
</PropertyList>
|
|
125
125
|
</Section>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Server, Shield, Settings } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { getKCPStatus, getKCPVersion, getKCPInitialized, getMachineClusterName } from '../resource-utils-capi'
|
|
5
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
data: any
|
|
@@ -50,7 +50,7 @@ export function CAPIKubeadmControlPlaneRenderer({ data, onNavigate }: Props) {
|
|
|
50
50
|
<Property label="Initialized" value={initialized ? 'Yes' : 'No'} />
|
|
51
51
|
{updateStrategy && <Property label="Update Strategy" value={updateStrategy} />}
|
|
52
52
|
{readyCond?.lastTransitionTime && (
|
|
53
|
-
<Property label="Since" value={
|
|
53
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
54
54
|
)}
|
|
55
55
|
</PropertyList>
|
|
56
56
|
</Section>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Server, Settings } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { getMachineDeploymentStatus, getMachineDeploymentVersion, getMachineClusterName } from '../resource-utils-capi'
|
|
5
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
data: any
|
|
@@ -56,7 +56,7 @@ export function CAPIMachineDeploymentRenderer({ data, onNavigate }: Props) {
|
|
|
56
56
|
<Property label="Cluster" value={clusterName} />
|
|
57
57
|
<Property label="Version" value={version} />
|
|
58
58
|
{readyCond?.lastTransitionTime && (
|
|
59
|
-
<Property label="Since" value={
|
|
59
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
60
60
|
)}
|
|
61
61
|
</PropertyList>
|
|
62
62
|
</Section>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Server, Settings } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { getMachinePoolStatus } from '../resource-utils-capi'
|
|
5
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
data: any
|
|
@@ -44,7 +44,7 @@ export function CAPIMachinePoolRenderer({ data, onNavigate }: Props) {
|
|
|
44
44
|
<Property label="Min Ready Seconds" value={String(spec.minReadySeconds)} />
|
|
45
45
|
)}
|
|
46
46
|
{readyCond?.lastTransitionTime && (
|
|
47
|
-
<Property label="Since" value={
|
|
47
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
48
48
|
)}
|
|
49
49
|
</PropertyList>
|
|
50
50
|
</Section>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Cpu, Server, Network, Cloud } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { getMachineStatus, getMachineRole, getMachineClusterName, getMachineNodeRef, getMachineVersion, getMachineProviderID, parseProviderID, getProviderFromInfraKind, parseCAPIConditionMessage } from '../resource-utils-capi'
|
|
5
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
data: any
|
|
@@ -59,7 +59,7 @@ export function CAPIMachineRenderer({ data, onNavigate }: Props) {
|
|
|
59
59
|
{infraProvider && <Property label="Provider" value={infraProvider} />}
|
|
60
60
|
{spec.failureDomain && <Property label="Failure Domain" value={spec.failureDomain} />}
|
|
61
61
|
{readyCond?.lastTransitionTime && (
|
|
62
|
-
<Property label="Since" value={
|
|
62
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
63
63
|
)}
|
|
64
64
|
</PropertyList>
|
|
65
65
|
</Section>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Server, Settings } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
|
-
import { formatAge } from '../resource-utils'
|
|
5
4
|
import { getMachineSetStatus, getMachineClusterName } from '../resource-utils-capi'
|
|
5
|
+
import { TimeValue } from '../../ui/ScheduleValue'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
data: any
|
|
@@ -46,7 +46,7 @@ export function CAPIMachineSetRenderer({ data, onNavigate }: Props) {
|
|
|
46
46
|
<Property label="Min Ready Seconds" value={String(spec.minReadySeconds)} />
|
|
47
47
|
)}
|
|
48
48
|
{readyCond?.lastTransitionTime && (
|
|
49
|
-
<Property label="Since" value={
|
|
49
|
+
<Property label="Since" value={<TimeValue timestamp={readyCond.lastTransitionTime} />} />
|
|
50
50
|
)}
|
|
51
51
|
</PropertyList>
|
|
52
52
|
</Section>
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../resource-utils-cnpg'
|
|
29
29
|
import { formatAge } from '../resource-utils'
|
|
30
30
|
import { RecoveryTime } from './CNPGShared'
|
|
31
|
+
import { formatRelativeAgeTime } from '../../../utils/format'
|
|
31
32
|
|
|
32
33
|
interface CNPGClusterRendererProps {
|
|
33
34
|
/** Filled by the host with the Databases, Publications and Subscriptions
|
|
@@ -195,7 +196,7 @@ export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRe
|
|
|
195
196
|
variant="warning"
|
|
196
197
|
title="Last backup failed"
|
|
197
198
|
message={
|
|
198
|
-
`The most recent backup attempt failed${lastBackupFailure.lastTransitionTime ? ` ${
|
|
199
|
+
`The most recent backup attempt failed${lastBackupFailure.lastTransitionTime ? ` ${formatRelativeAgeTime(lastBackupFailure.lastTransitionTime)}` : ''}.` +
|
|
199
200
|
(lastBackupFailure.message ? ` Operator reported: ${lastBackupFailure.message}` : '')
|
|
200
201
|
}
|
|
201
202
|
/>
|
|
@@ -97,3 +97,28 @@ describe('a window that has stopped advancing', () => {
|
|
|
97
97
|
expect(html).not.toContain('WAL archiving has stopped')
|
|
98
98
|
})
|
|
99
99
|
})
|
|
100
|
+
|
|
101
|
+
describe('ObjectStore failing backups', () => {
|
|
102
|
+
const failing = {
|
|
103
|
+
...store,
|
|
104
|
+
status: { serverRecoveryWindow: { 'pg-main': { firstRecoverabilityPoint: '2026-08-01T00:00:00Z', lastSuccessfulBackupTime: '2026-08-10T00:00:00Z', lastFailedBackupTime: '2026-08-11T00:00:00Z' } } },
|
|
105
|
+
}
|
|
106
|
+
it('does not say recovery still reaches the newest WAL when archiving has stopped too', () => {
|
|
107
|
+
const html = renderToString(<CNPGObjectStoreRenderer data={failing} archivingFailing={new Set(['pg-main'])} onNavigate={nav} />)
|
|
108
|
+
expect(html).toContain('WAL archiving has stopped for pg-main')
|
|
109
|
+
expect(html).not.toContain('can still replay archived WAL')
|
|
110
|
+
expect(html).toContain('WAL archiving has stopped on the cluster behind this server')
|
|
111
|
+
})
|
|
112
|
+
it('keeps recovery replaying archived WAL when only base backups fail', () => {
|
|
113
|
+
const html = renderToString(<CNPGObjectStoreRenderer data={failing} archivingFailing={new Set()} onNavigate={nav} />)
|
|
114
|
+
expect(html).toContain('can still replay archived WAL written since')
|
|
115
|
+
expect(html).not.toContain('retention')
|
|
116
|
+
})
|
|
117
|
+
it('claims nothing about recovery when no successful backup is recorded', () => {
|
|
118
|
+
const never = { ...store, status: { serverRecoveryWindow: { 'pg-main': { lastFailedBackupTime: '2026-08-11T00:00:00Z' } } } }
|
|
119
|
+
const html = renderToString(<CNPGObjectStoreRenderer data={never} archivingFailing={new Set()} onNavigate={nav} />)
|
|
120
|
+
expect(html).toContain('No successful base backup is recorded for pg-main')
|
|
121
|
+
expect(html).not.toContain('nothing to restore')
|
|
122
|
+
expect(html).not.toContain('replay')
|
|
123
|
+
})
|
|
124
|
+
})
|