@skyhook-io/k8s-ui 1.17.0 → 1.17.1
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 +41 -0
- package/package.json +3 -3
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +82 -6
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +180 -64
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +29 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -13,6 +13,7 @@ import type { APIResource } from '../../types'
|
|
|
13
13
|
import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
|
|
14
14
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
15
15
|
import { Tooltip } from '../ui/Tooltip'
|
|
16
|
+
import { certaintyGlyph } from '../facts'
|
|
16
17
|
import { Input } from '../ui/Input'
|
|
17
18
|
|
|
18
19
|
// Selected resource type info (need both name for API and kind for display)
|
|
@@ -36,6 +37,8 @@ export interface SidebarCategoryDestination {
|
|
|
36
37
|
icon?: ComponentType<{ className?: string }>
|
|
37
38
|
/** Problem count. `undefined` renders no badge; `null` renders the unknown dash. */
|
|
38
39
|
count?: number | null
|
|
40
|
+
/** The count was taken over data read only in part: rendered "≥N", and a zero is unknown rather than none. */
|
|
41
|
+
countLowerBound?: boolean
|
|
39
42
|
countTitle?: string
|
|
40
43
|
active?: boolean
|
|
41
44
|
/** The object currently open under this destination, nested beneath it. */
|
|
@@ -97,6 +100,24 @@ const CORE_RESOURCE_TYPES = [
|
|
|
97
100
|
{ kind: 'hpas', label: 'HPAs' },
|
|
98
101
|
] as const
|
|
99
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The workspace views a filter keeps: all of them when the term names the
|
|
105
|
+
* category or one of its API groups ("cloud", "cnpg"), otherwise the views
|
|
106
|
+
* whose label matches. The views are the way into a workspace, so a filter
|
|
107
|
+
* that finds the category's kinds must not hide them.
|
|
108
|
+
*/
|
|
109
|
+
export function sidebarDestinationsMatching(
|
|
110
|
+
categoryName: string,
|
|
111
|
+
resources: Pick<APIResource, 'group'>[],
|
|
112
|
+
destinations: SidebarCategoryDestination[],
|
|
113
|
+
term: string,
|
|
114
|
+
): SidebarCategoryDestination[] {
|
|
115
|
+
const normalized = term.trim().toLowerCase()
|
|
116
|
+
if (!normalized) return destinations
|
|
117
|
+
if (categoryName.toLowerCase().includes(normalized) || resources.some((r) => r.group.toLowerCase().includes(normalized))) return destinations
|
|
118
|
+
return destinations.filter((d) => d.label.toLowerCase().includes(normalized))
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
export function resourceMatchesSidebarFilter(resource: Pick<APIResource, 'kind' | 'name' | 'group'>, term: string): boolean {
|
|
101
122
|
const normalized = term.trim().toLowerCase()
|
|
102
123
|
if (!normalized) return true
|
|
@@ -427,14 +448,16 @@ export function ResourcesSidebar({
|
|
|
427
448
|
// If the group name matches, show all its resources
|
|
428
449
|
if (categoryMatches || rawGroupMatches) return category
|
|
429
450
|
const matchingResources = category.visibleResources.filter((resource: APIResource) => resourceMatchesSidebarFilter(resource, term))
|
|
430
|
-
|
|
451
|
+
const ws = categoryWorkspaces?.[category.name]
|
|
452
|
+
const matchingViews = ws ? sidebarDestinationsMatching(category.name, category.resources, ws.destinations, term) : []
|
|
453
|
+
if (matchingResources.length === 0 && matchingViews.length === 0) return null
|
|
431
454
|
return {
|
|
432
455
|
...category,
|
|
433
456
|
visibleResources: matchingResources,
|
|
434
457
|
}
|
|
435
458
|
})
|
|
436
459
|
.filter(Boolean) as typeof sortedCategories
|
|
437
|
-
}, [sortedCategories, kindFilter])
|
|
460
|
+
}, [sortedCategories, kindFilter, categoryWorkspaces])
|
|
438
461
|
|
|
439
462
|
// Auto-expand all categories when filtering
|
|
440
463
|
const isKindFiltering = kindFilter.trim().length > 0
|
|
@@ -455,26 +478,23 @@ export function ResourcesSidebar({
|
|
|
455
478
|
})
|
|
456
479
|
}
|
|
457
480
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const flatVisibleKinds = useMemo<SelectedKindInfo[]>(() => {
|
|
461
|
-
const kinds: SelectedKindInfo[] = []
|
|
481
|
+
const visibleResults = useMemo(() => {
|
|
482
|
+
const results: ({ type: 'kind'; kind: SelectedKindInfo } | { type: 'view'; category: string; destination: SidebarCategoryDestination })[] = []
|
|
462
483
|
if (favoritesExpanded) {
|
|
463
|
-
for (const
|
|
464
|
-
kinds.push({ name: p.name, kind: p.kind, group: p.group })
|
|
465
|
-
}
|
|
484
|
+
for (const kind of pinned) results.push({ type: 'kind', kind })
|
|
466
485
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
486
|
+
for (const cat of filteredCategories ?? []) {
|
|
487
|
+
if (!effectiveExpandedCategories.has(cat.name)) continue
|
|
488
|
+
const workspace = categoryWorkspaces?.[cat.name]
|
|
489
|
+
for (const destination of workspace ? sidebarDestinationsMatching(cat.name, cat.resources, workspace.destinations, kindFilter) : []) {
|
|
490
|
+
results.push({ type: 'view', category: cat.name, destination })
|
|
491
|
+
}
|
|
492
|
+
if (isKindFiltering || kindsOpen(cat.name)) {
|
|
493
|
+
for (const kind of workspace ? sortByGroup(cat.visibleResources) : cat.visibleResources) results.push({ type: 'kind', kind })
|
|
474
494
|
}
|
|
475
495
|
}
|
|
476
|
-
return
|
|
477
|
-
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories, kindsOpenOverrides, categoryWorkspaces, activeDestinationCategory]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
496
|
+
return results
|
|
497
|
+
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories, kindsOpenOverrides, categoryWorkspaces, activeDestinationCategory, isKindFiltering, kindFilter]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
478
498
|
|
|
479
499
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
480
500
|
// Reset highlight when the filter or kind list changes
|
|
@@ -482,10 +502,12 @@ export function ResourcesSidebar({
|
|
|
482
502
|
setHighlightedIndex(kindFilter ? 0 : -1)
|
|
483
503
|
}, [kindFilter]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
484
504
|
|
|
485
|
-
const
|
|
486
|
-
?
|
|
505
|
+
const highlightedResult = highlightedIndex >= 0 && highlightedIndex < visibleResults.length
|
|
506
|
+
? visibleResults[highlightedIndex]
|
|
487
507
|
: null
|
|
488
508
|
|
|
509
|
+
const highlightedKind = highlightedResult?.type === 'kind' ? highlightedResult.kind : null
|
|
510
|
+
|
|
489
511
|
// Scroll the highlighted kind button into view
|
|
490
512
|
const highlightedRef = useRef<HTMLButtonElement>(null)
|
|
491
513
|
useEffect(() => {
|
|
@@ -501,18 +523,19 @@ export function ResourcesSidebar({
|
|
|
501
523
|
;(e.target as HTMLInputElement).blur()
|
|
502
524
|
} else if (e.key === 'ArrowDown') {
|
|
503
525
|
e.preventDefault()
|
|
504
|
-
setHighlightedIndex(prev => Math.min(prev + 1,
|
|
526
|
+
setHighlightedIndex(prev => Math.min(prev + 1, visibleResults.length - 1))
|
|
505
527
|
} else if (e.key === 'ArrowUp') {
|
|
506
528
|
e.preventDefault()
|
|
507
529
|
setHighlightedIndex(prev => Math.max(prev - 1, 0))
|
|
508
|
-
} else if (e.key === 'Enter' &&
|
|
530
|
+
} else if (e.key === 'Enter' && highlightedResult) {
|
|
509
531
|
e.preventDefault()
|
|
510
|
-
|
|
532
|
+
if (highlightedResult.type === 'view') highlightedResult.destination.onSelect()
|
|
533
|
+
else selectKind(highlightedResult.kind)
|
|
511
534
|
setHighlightedIndex(-1)
|
|
512
535
|
setKindFilter('')
|
|
513
536
|
onKindNavigated?.()
|
|
514
537
|
}
|
|
515
|
-
}, [
|
|
538
|
+
}, [visibleResults.length, highlightedResult, selectKind, onKindNavigated]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
516
539
|
|
|
517
540
|
const isKindHighlighted = useCallback((name: string, group: string) => {
|
|
518
541
|
return highlightedKind?.name === name && highlightedKind?.group === group
|
|
@@ -618,6 +641,7 @@ export function ResourcesSidebar({
|
|
|
618
641
|
const rawGroupTitle = rawCRDGroupTitle(category.resources)
|
|
619
642
|
const workspace = categoryWorkspaces?.[category.name]
|
|
620
643
|
const showKinds = isKindFiltering || kindsOpen(category.name)
|
|
644
|
+
const views = workspace ? sidebarDestinationsMatching(category.name, category.resources, workspace.destinations, kindFilter) : []
|
|
621
645
|
return (
|
|
622
646
|
<div key={category.name} className="mb-2">
|
|
623
647
|
<button
|
|
@@ -635,9 +659,14 @@ export function ResourcesSidebar({
|
|
|
635
659
|
)}
|
|
636
660
|
</button>
|
|
637
661
|
<Collapse open={isExpanded} id={disclosurePanelId(categoryPanelBase, category.name)}>
|
|
638
|
-
{workspace &&
|
|
662
|
+
{workspace && views.length > 0 && (
|
|
639
663
|
<WorkspaceDestinations
|
|
640
664
|
workspace={workspace}
|
|
665
|
+
highlightedId={highlightedResult?.type === 'view' && highlightedResult.category === category.name ? highlightedResult.destination.id : undefined}
|
|
666
|
+
highlightedRef={highlightedRef}
|
|
667
|
+
destinations={views}
|
|
668
|
+
filtering={isKindFiltering}
|
|
669
|
+
hasKinds={category.visibleResources.length > 0}
|
|
641
670
|
kindsOpen={showKinds}
|
|
642
671
|
onToggleKinds={() => toggleKinds(category.name)}
|
|
643
672
|
kindsPanelId={disclosurePanelId(categoryPanelBase, `${category.name}-kinds`)}
|
|
@@ -776,45 +805,63 @@ function groupHeaderBefore(resources: APIResource[], index: number): string | nu
|
|
|
776
805
|
return group || 'core'
|
|
777
806
|
}
|
|
778
807
|
|
|
808
|
+
// While the sidebar is filtered, `destinations` are the views that match and
|
|
809
|
+
// the kinds below are forced open, so they get a label rather than a toggle.
|
|
779
810
|
function WorkspaceDestinations({
|
|
780
811
|
workspace,
|
|
812
|
+
destinations,
|
|
813
|
+
filtering,
|
|
814
|
+
hasKinds,
|
|
781
815
|
kindsOpen,
|
|
782
816
|
onToggleKinds,
|
|
783
817
|
kindsPanelId,
|
|
818
|
+
highlightedId,
|
|
819
|
+
highlightedRef,
|
|
784
820
|
}: {
|
|
785
821
|
workspace: SidebarCategoryWorkspace
|
|
822
|
+
highlightedId?: string
|
|
823
|
+
highlightedRef: React.RefObject<HTMLButtonElement | null>
|
|
824
|
+
destinations: SidebarCategoryDestination[]
|
|
825
|
+
filtering: boolean
|
|
826
|
+
hasKinds: boolean
|
|
786
827
|
kindsOpen: boolean
|
|
787
828
|
onToggleKinds: () => void
|
|
788
829
|
kindsPanelId: string
|
|
789
830
|
}) {
|
|
790
831
|
return (
|
|
791
832
|
<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">
|
|
793
|
-
{
|
|
833
|
+
<div className="pl-5 pr-2 pt-0.5 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Views</div>
|
|
834
|
+
{destinations.map((d) => {
|
|
794
835
|
const Icon = d.icon
|
|
795
836
|
return (
|
|
796
837
|
<div key={d.id}>
|
|
797
838
|
<button
|
|
839
|
+
ref={highlightedId === d.id ? highlightedRef : undefined}
|
|
798
840
|
onClick={d.onSelect}
|
|
799
841
|
aria-current={d.active && !d.child ? 'page' : undefined}
|
|
800
842
|
className={clsx(
|
|
801
843
|
'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
844
|
d.active && !d.child
|
|
803
845
|
? 'selection-strong selection-text'
|
|
804
|
-
: d.
|
|
805
|
-
? 'text-theme-text-primary
|
|
806
|
-
:
|
|
846
|
+
: highlightedId === d.id
|
|
847
|
+
? 'bg-theme-hover text-theme-text-primary'
|
|
848
|
+
: d.active
|
|
849
|
+
? 'text-theme-text-primary font-medium hover:bg-theme-elevated'
|
|
850
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary',
|
|
807
851
|
)}
|
|
808
852
|
>
|
|
809
853
|
{Icon && <Icon className="w-4 h-4 shrink-0" />}
|
|
810
854
|
<span className="flex-1 text-left truncate">{d.label}</span>
|
|
811
|
-
{d.count === null ? (
|
|
855
|
+
{d.count === null || (d.count === 0 && d.countLowerBound) ? (
|
|
812
856
|
<Tooltip content={d.countTitle ?? COUNT_UNAVAILABLE_MESSAGE} position="left">
|
|
813
857
|
<span className="text-xs py-0.5 rounded text-center font-mono w-8 bg-theme-elevated text-theme-text-disabled">–</span>
|
|
814
858
|
</Tooltip>
|
|
815
859
|
) : d.count !== undefined && d.count > 0 ? (
|
|
816
860
|
<Tooltip content={d.countTitle ?? `${d.count}`} position="left">
|
|
817
|
-
<span className=
|
|
861
|
+
<span className={clsx('text-xs py-0.5 rounded text-center font-mono status-degraded', d.countLowerBound ? 'min-w-8 px-1' : 'w-8')}>
|
|
862
|
+
{d.countLowerBound && certaintyGlyph('lower_bound')}
|
|
863
|
+
{d.count}
|
|
864
|
+
</span>
|
|
818
865
|
</Tooltip>
|
|
819
866
|
) : null}
|
|
820
867
|
</button>
|
|
@@ -833,15 +880,19 @@ function WorkspaceDestinations({
|
|
|
833
880
|
{workspace.scopeNote && (
|
|
834
881
|
<div className="pl-6 pr-2 pt-0.5 text-[11px] text-theme-text-tertiary">{workspace.scopeNote}</div>
|
|
835
882
|
)}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
883
|
+
{filtering ? (
|
|
884
|
+
hasKinds && <div className="pl-5 pr-2 pt-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Resource kinds</div>
|
|
885
|
+
) : (
|
|
886
|
+
<button
|
|
887
|
+
aria-expanded={kindsOpen}
|
|
888
|
+
aria-controls={kindsPanelId}
|
|
889
|
+
onClick={onToggleKinds}
|
|
890
|
+
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"
|
|
891
|
+
>
|
|
892
|
+
<CollapseChevron open={kindsOpen} className="w-3 h-3" />
|
|
893
|
+
<span>Resource kinds</span>
|
|
894
|
+
</button>
|
|
895
|
+
)}
|
|
845
896
|
</div>
|
|
846
897
|
)
|
|
847
898
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { hasFilterIntent } from './ResourcesView'
|
|
3
|
+
import { kindFiltersFromSearch, kindFiltersToSearch, serializeColumnFilters } from './resource-utils'
|
|
4
|
+
|
|
5
|
+
describe('kindFiltersFromSearch', () => {
|
|
6
|
+
it('restores column filters, their exclude operator and problem filters', () => {
|
|
7
|
+
const filters = serializeColumnFilters({ namespace: ['a', 'b'], status: ['Running'] }, { status: true })
|
|
8
|
+
const search = `?${new URLSearchParams({ filters, problems: 'failed,pending', search: 'web' })}`
|
|
9
|
+
|
|
10
|
+
expect(kindFiltersFromSearch(search)).toEqual({
|
|
11
|
+
columnFilters: { namespace: ['a', 'b'], status: ['Running'] },
|
|
12
|
+
columnFilterExcludes: { status: true },
|
|
13
|
+
problemFilters: ['failed', 'pending'],
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('starts empty for a kind with nothing remembered', () => {
|
|
18
|
+
expect(kindFiltersFromSearch(undefined)).toEqual({ columnFilters: {}, columnFilterExcludes: {}, problemFilters: [] })
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('kindFiltersToSearch', () => {
|
|
23
|
+
it('round-trips through kindFiltersFromSearch and keeps only the per-kind filters', () => {
|
|
24
|
+
const filters = serializeColumnFilters({ namespace: ['a'], status: ['Running'] }, { status: true })
|
|
25
|
+
const saved = kindFiltersToSearch(kindFiltersFromSearch(`?${new URLSearchParams({ filters, problems: 'failed', search: 'web', namespaces: 'x' })}`))
|
|
26
|
+
|
|
27
|
+
expect(new URLSearchParams(saved).get('search')).toBeNull()
|
|
28
|
+
expect(kindFiltersFromSearch(saved)).toEqual({
|
|
29
|
+
columnFilters: { namespace: ['a'], status: ['Running'] },
|
|
30
|
+
columnFilterExcludes: { status: true },
|
|
31
|
+
problemFilters: ['failed'],
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('is empty when there is nothing to save, ignoring an exclude flag with no values', () => {
|
|
36
|
+
expect(kindFiltersToSearch({ columnFilters: { status: [] }, columnFilterExcludes: { status: true }, problemFilters: [] })).toBe('')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('hasFilterIntent', () => {
|
|
41
|
+
it('is false for a bare list URL, even with the namespace pick or a kind group', () => {
|
|
42
|
+
expect(hasFilterIntent('')).toBe(false)
|
|
43
|
+
expect(hasFilterIntent('?namespaces=a,b&apiGroup=serving.knative.dev&ai-run=r1')).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it.each(['filters=status:Running', 'problems=failed', 'search=web', 'regex=true', 'labels=app%3Dweb', 'ownerKind=DaemonSet', 'ownerName=x', 'resource=a/web', 'showInactive=true'])(
|
|
47
|
+
'is true when the URL carries %s',
|
|
48
|
+
(param) => {
|
|
49
|
+
expect(hasFilterIntent(`?namespaces=a&${param}`)).toBe(true)
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ResourcesView } from './ResourcesView'
|
|
6
|
+
import { KeyboardShortcutProvider } from '../../hooks/useKeyboardShortcuts'
|
|
7
|
+
import type { APIResource } from '../../types'
|
|
8
|
+
|
|
9
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
10
|
+
window.matchMedia = vi.fn().mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })
|
|
11
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
12
|
+
const pods: APIResource = { group: '', version: 'v1', kind: 'Pod', name: 'pods', namespaced: true, isCrd: false, verbs: ['get', 'list'] }
|
|
13
|
+
const pod = { apiVersion: 'v1', kind: 'Pod', metadata: { name: 'example', namespace: 'test', uid: 'pod-id' }, spec: { containers: [{ name: 'app', image: 'example' }] }, status: { phase: 'Running' } }
|
|
14
|
+
let root: ReturnType<typeof createRoot>
|
|
15
|
+
let host: HTMLDivElement
|
|
16
|
+
|
|
17
|
+
afterEach(() => { act(() => root?.unmount()); host?.remove(); vi.restoreAllMocks(); localStorage.clear() })
|
|
18
|
+
|
|
19
|
+
describe('custom resource kind view', () => {
|
|
20
|
+
it('keeps sidebar navigation while disabling table keys and column settings, then restores the table on null', () => {
|
|
21
|
+
host = document.createElement('div'); document.body.append(host); root = createRoot(host)
|
|
22
|
+
const selected = vi.fn()
|
|
23
|
+
const storageRead = vi.spyOn(Storage.prototype, 'getItem')
|
|
24
|
+
const storageWrite = vi.spyOn(Storage.prototype, 'setItem')
|
|
25
|
+
const render = (custom: boolean | undefined) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
|
|
26
|
+
namespaces={['test']}
|
|
27
|
+
apiResources={[pods]}
|
|
28
|
+
resourceCounts={{ Pod: 1 }}
|
|
29
|
+
selectedKindQuery={{ isLoading: false, data: [pod], resourceName: 'pods', group: '' }}
|
|
30
|
+
locationPathname="/resources/pods"
|
|
31
|
+
locationSearch=""
|
|
32
|
+
onNavigate={() => {}}
|
|
33
|
+
onResourceClick={selected}
|
|
34
|
+
renderKindView={kind => custom === undefined ? false : custom ? <div data-testid="kind-view">Custom {kind.kind}</div> : null}
|
|
35
|
+
/></KeyboardShortcutProvider>))
|
|
36
|
+
render(true)
|
|
37
|
+
expect(host.querySelector('[data-testid="kind-view"]')?.textContent).toBe('Custom Pod')
|
|
38
|
+
expect(host.querySelectorAll('table')).toHaveLength(0)
|
|
39
|
+
expect(host.textContent).toContain('Workloads')
|
|
40
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', bubbles: true })))
|
|
41
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
|
|
42
|
+
expect(selected).not.toHaveBeenCalled()
|
|
43
|
+
expect(storageRead.mock.calls.some(([key]) => String(key).startsWith('radar-columns-'))).toBe(false)
|
|
44
|
+
expect(storageWrite.mock.calls.some(([key]) => String(key).startsWith('radar-columns-'))).toBe(false)
|
|
45
|
+
render(false)
|
|
46
|
+
expect(host.querySelector('[data-testid="kind-view"]')).toBeNull()
|
|
47
|
+
expect(host.querySelector('table')).not.toBeNull()
|
|
48
|
+
expect(storageRead.mock.calls.some(([key]) => key === 'radar-columns-pods')).toBe(true)
|
|
49
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', bubbles: true })))
|
|
50
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
|
|
51
|
+
expect(selected).toHaveBeenCalledWith(expect.objectContaining({ kind: 'pods', namespace: 'test', name: 'example' }))
|
|
52
|
+
render(undefined)
|
|
53
|
+
expect(host.querySelector('table')).not.toBeNull()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('closes bulk dialogs when the same kind switches to a custom view and keeps the inspected resource', async () => {
|
|
58
|
+
host = document.createElement('div'); document.body.append(host); root = createRoot(host)
|
|
59
|
+
const selected = vi.fn()
|
|
60
|
+
const remove = vi.fn()
|
|
61
|
+
const render = (custom: boolean) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
|
|
62
|
+
namespaces={['test']}
|
|
63
|
+
apiResources={[pods]}
|
|
64
|
+
resourceCounts={{ Pod: 1 }}
|
|
65
|
+
selectedKindQuery={{ isLoading: false, data: [pod], resourceName: 'pods', group: '' }}
|
|
66
|
+
selectedResource={{ kind: 'pods', group: '', namespace: 'test', name: 'example' }}
|
|
67
|
+
locationPathname="/resources/pods"
|
|
68
|
+
locationSearch=""
|
|
69
|
+
onNavigate={() => {}}
|
|
70
|
+
onResourceClick={selected}
|
|
71
|
+
onBulkDelete={remove}
|
|
72
|
+
renderKindView={() => custom ? <div data-testid="custom">Custom</div> : null}
|
|
73
|
+
/></KeyboardShortcutProvider>))
|
|
74
|
+
render(false)
|
|
75
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true })))
|
|
76
|
+
act(() => host.querySelector<HTMLInputElement>('input[title="Select all"]')!.click())
|
|
77
|
+
expect(host.textContent).toContain('1 selected')
|
|
78
|
+
act(() => [...host.querySelectorAll('button')].find(b => b.textContent?.trim() === 'Delete')!.click())
|
|
79
|
+
expect(document.querySelector('[role="dialog"]')?.textContent).toContain('Delete 1 Pod?')
|
|
80
|
+
render(true)
|
|
81
|
+
await act(async () => { await new Promise(resolve => setTimeout(resolve, 300)) })
|
|
82
|
+
expect(host.querySelector('[data-testid="custom"]')).not.toBeNull()
|
|
83
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull()
|
|
84
|
+
render(false)
|
|
85
|
+
expect(host.textContent).not.toContain('1 selected')
|
|
86
|
+
expect(host.querySelector('input[title="Select all"]')).toBeNull()
|
|
87
|
+
expect(document.querySelector('[role="dialog"]')).toBeNull()
|
|
88
|
+
act(() => host.querySelector<SVGElement>('button .lucide-git-compare')!.closest<HTMLButtonElement>('button')!.click())
|
|
89
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', bubbles: true })))
|
|
90
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
|
|
91
|
+
expect(host.querySelector('[aria-label="Compare resources tray"]')?.textContent).toContain('example')
|
|
92
|
+
render(true)
|
|
93
|
+
render(false)
|
|
94
|
+
expect(host.querySelector('[aria-label="Compare resources tray"]')).toBeNull()
|
|
95
|
+
expect(remove).not.toHaveBeenCalled()
|
|
96
|
+
expect(selected).not.toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('keeps sidebar kind shortcuts active while a custom view is shown', () => {
|
|
100
|
+
host = document.createElement('div'); document.body.append(host); root = createRoot(host)
|
|
101
|
+
const deployments: APIResource = { group: 'apps', version: 'v1', kind: 'Deployment', name: 'deployments', namespaced: true, isCrd: false, verbs: ['get', 'list'] }
|
|
102
|
+
act(() => root.render(<KeyboardShortcutProvider><ResourcesView
|
|
103
|
+
namespaces={['test']}
|
|
104
|
+
apiResources={[pods, deployments]}
|
|
105
|
+
resourceCounts={{ Pod: 1, Deployment: 1 }}
|
|
106
|
+
locationPathname="/resources/pods"
|
|
107
|
+
locationSearch=""
|
|
108
|
+
onNavigate={() => {}}
|
|
109
|
+
renderKindView={kind => <div data-testid="kind-view">Custom {kind.kind}</div>}
|
|
110
|
+
/></KeyboardShortcutProvider>))
|
|
111
|
+
expect(host.querySelector('[data-testid="kind-view"]')?.textContent).toBe('Custom Pod')
|
|
112
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: ']', bubbles: true })))
|
|
113
|
+
expect(host.querySelector('[data-testid="kind-view"]')?.textContent).not.toBe('Custom Pod')
|
|
114
|
+
act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: '[', bubbles: true })))
|
|
115
|
+
expect(host.querySelector('[data-testid="kind-view"]')?.textContent).toBe('Custom Pod')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('skips hidden-table sort and filter aggregation for a loaded list, then resumes them with the table', () => {
|
|
119
|
+
host = document.createElement('div'); document.body.append(host); root = createRoot(host)
|
|
120
|
+
const getSortValue = vi.fn((resource: typeof pod) => resource.metadata.name)
|
|
121
|
+
const getFilterValue = vi.fn((resource: typeof pod) => resource.metadata.name)
|
|
122
|
+
const extraColumns = [{ key: 'fixture-rank', label: 'Fixture rank', render: () => null, getSortValue, getFilterValue }]
|
|
123
|
+
const list = [pod, { ...pod, metadata: { ...pod.metadata, name: 'another', uid: 'another-id' } }]
|
|
124
|
+
localStorage.setItem('radar-sort-pods', JSON.stringify({ column: 'fixture-rank', direction: 'asc' }))
|
|
125
|
+
const render = (custom: boolean, data = list) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
|
|
126
|
+
namespaces={['test']}
|
|
127
|
+
apiResources={[pods]}
|
|
128
|
+
resourceCounts={{ Pod: data.length }}
|
|
129
|
+
selectedKindQuery={{ isLoading: false, data, resourceName: 'pods', group: '' }}
|
|
130
|
+
locationPathname="/resources/pods"
|
|
131
|
+
locationSearch=""
|
|
132
|
+
onNavigate={() => {}}
|
|
133
|
+
extraLeadingColumns={extraColumns}
|
|
134
|
+
renderKindView={() => custom ? <div data-testid="kind-view">Custom</div> : null}
|
|
135
|
+
/></KeyboardShortcutProvider>))
|
|
136
|
+
render(true)
|
|
137
|
+
expect(getSortValue).not.toHaveBeenCalled()
|
|
138
|
+
expect(getFilterValue).not.toHaveBeenCalled()
|
|
139
|
+
render(false)
|
|
140
|
+
expect(getSortValue).toHaveBeenCalled()
|
|
141
|
+
expect(getFilterValue).toHaveBeenCalled()
|
|
142
|
+
getSortValue.mockClear(); getFilterValue.mockClear()
|
|
143
|
+
render(true, [...list])
|
|
144
|
+
expect(getSortValue).not.toHaveBeenCalled()
|
|
145
|
+
expect(getFilterValue).not.toHaveBeenCalled()
|
|
146
|
+
render(false)
|
|
147
|
+
expect(getSortValue).toHaveBeenCalled()
|
|
148
|
+
expect(getFilterValue).toHaveBeenCalled()
|
|
149
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
1
2
|
import { describe, expect, it } from 'vitest'
|
|
2
3
|
import type { ContainerResourceMetrics } from '../../types'
|
|
3
|
-
import { podAggregate, readContainer } from './ResourcesView'
|
|
4
|
+
import { buildContainerResourceTooltip, podAggregate, readContainer } from './ResourcesView'
|
|
4
5
|
|
|
5
6
|
// Build a CPU-only container fixture; memory fields default to 0.
|
|
6
7
|
function cpu(name: string, usage: number, request: number, limit: number): ContainerResourceMetrics {
|
|
@@ -109,3 +110,57 @@ describe('podAggregate', () => {
|
|
|
109
110
|
expect(result.denom).toBe(384) // summed memory limit
|
|
110
111
|
})
|
|
111
112
|
})
|
|
113
|
+
|
|
114
|
+
describe('podAggregate with pod-level resources', () => {
|
|
115
|
+
it("treats a pod-level limit as the ceiling even when containers set none", () => {
|
|
116
|
+
const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 0)]
|
|
117
|
+
const result = podAggregate(containers, 'cpu', { request: 500, limit: 1000 })
|
|
118
|
+
expect(result.mode).toBe('limit')
|
|
119
|
+
expect(result.denom).toBe(1000)
|
|
120
|
+
expect(result.markerPct).toBeCloseTo(50)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("uses the pod-level limit over a partial container sum", () => {
|
|
124
|
+
const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 200)]
|
|
125
|
+
const result = podAggregate(containers, 'cpu', { limit: 1000 })
|
|
126
|
+
expect(result.mode).toBe('limit')
|
|
127
|
+
expect(result.denom).toBe(1000)
|
|
128
|
+
expect(result.markerPct).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("uses the container limits when they are tighter than the pod limit", () => {
|
|
132
|
+
const single = podAggregate([cpu('app', 450, 100, 500)], 'cpu', { request: 800, limit: 1000 })
|
|
133
|
+
expect(single.mode).toBe('limit')
|
|
134
|
+
expect(single.denom).toBe(500)
|
|
135
|
+
const both = podAggregate([cpu('app', 200, 0, 300), cpu('sidecar', 50, 0, 300)], 'cpu', { limit: 1000 })
|
|
136
|
+
expect(both.denom).toBe(600)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("uses a pod-level request as the yardstick when nothing is limited", () => {
|
|
140
|
+
const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 0)]
|
|
141
|
+
const result = podAggregate(containers, 'cpu', { request: 800 })
|
|
142
|
+
expect(result.mode).toBe('request')
|
|
143
|
+
expect(result.denom).toBe(800)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('buildContainerResourceTooltip with a pod-level budget', () => {
|
|
148
|
+
const fmt = (n: number) => `${n}m`
|
|
149
|
+
const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 0)]
|
|
150
|
+
|
|
151
|
+
it('shows the shared pod budget instead of calling covered containers unlimited', () => {
|
|
152
|
+
const html = renderToStaticMarkup(
|
|
153
|
+
buildContainerResourceTooltip('CPU', containers, 'cpu', fmt, undefined, { usage: 400, limit: 1000, request: 500 }),
|
|
154
|
+
)
|
|
155
|
+
expect(html).not.toContain('no limit')
|
|
156
|
+
expect(html).toContain('pod limit')
|
|
157
|
+
expect(html).toContain('Pod (shared)')
|
|
158
|
+
expect(html).toContain('400m · 40% · 1000m limit · req 500m')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('keeps "no limit" when there is no pod-level budget', () => {
|
|
162
|
+
const html = renderToStaticMarkup(buildContainerResourceTooltip('CPU', containers, 'cpu', fmt))
|
|
163
|
+
expect(html).toContain('no limit')
|
|
164
|
+
expect(html).not.toContain('Pod (shared)')
|
|
165
|
+
})
|
|
166
|
+
})
|