@skyhook-io/k8s-ui 1.17.0 → 1.18.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/README.md +41 -0
- package/package.json +6 -6
- 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 +7 -3
- 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 +142 -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 +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- 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 +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -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 +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- 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/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -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 +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- 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/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- 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.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- 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 +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -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/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- 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/Toast.tsx +15 -4
- 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 +4 -2
- 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 +193 -71
- 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 +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -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
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
|
|
2
2
|
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
3
3
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue'
|
|
4
|
+
import { Badge } from '../ui/Badge'
|
|
4
5
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
5
6
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
6
7
|
import { FetchResult } from '../ui/FetchResult'
|
|
7
8
|
import { Input } from '../ui/Input'
|
|
9
|
+
import { ColumnPickerList } from './ColumnPickerList'
|
|
10
|
+
import { applyColumnOrder, sanitizeColumnOrder } from './column-order'
|
|
8
11
|
import type { TopPodMetrics, TopNodeMetrics, ContainerResourceMetrics } from '../../types'
|
|
9
12
|
import {
|
|
10
13
|
Search,
|
|
@@ -137,11 +140,16 @@ import {
|
|
|
137
140
|
parseColumnFilters,
|
|
138
141
|
serializeColumnFilters,
|
|
139
142
|
parseColumnFilterExcludes,
|
|
143
|
+
kindFiltersFromSearch,
|
|
144
|
+
kindFiltersToSearch,
|
|
145
|
+
columnFiltersParam,
|
|
146
|
+
RESOURCE_LIST_FILTER_PARAMS,
|
|
140
147
|
podMatchesProblemCategory,
|
|
141
148
|
SEVERITY_DOT_COLOR,
|
|
142
149
|
healthColors,
|
|
143
150
|
} from './resource-utils'
|
|
144
151
|
import { getGenericResourceStatus } from './generic-status'
|
|
152
|
+
import { ActiveFilterBar } from './ActiveFilterBar'
|
|
145
153
|
import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
|
|
146
154
|
import {
|
|
147
155
|
type PrinterColumnDef, type PrinterTable, PRINTER_COLUMN_PREFIX, formatPrinterCell, printerCellSortValue,
|
|
@@ -156,6 +164,8 @@ import { isRolloutActivityVisible } from '../../utils/workload-rollout'
|
|
|
156
164
|
import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
|
|
157
165
|
import { Tooltip } from '../ui/Tooltip'
|
|
158
166
|
import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
|
|
167
|
+
import type { Issue } from '../issues/types'
|
|
168
|
+
import { issueTitle } from '../issues/severity'
|
|
159
169
|
import { SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
160
170
|
// CRD-specific cell components (extracted)
|
|
161
171
|
import { GitRepositoryCell, OCIRepositoryCell, HelmRepositoryCell, KustomizationCell, FluxHelmReleaseCell, FluxAlertCell } from './renderers/flux-cells'
|
|
@@ -203,7 +213,7 @@ import { GCPManagedControlPlaneCell, GCPManagedMachinePoolCell, GCPMachineCell,
|
|
|
203
213
|
import { AzureManagedControlPlaneCell, AzureManagedMachinePoolCell, AzureMachineCell, AzureMachineTemplateCell, AzureManagedClusterCell } from './renderers/azure-capi-cells'
|
|
204
214
|
import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
|
|
205
215
|
import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
|
|
206
|
-
import { useRegisterShortcut, useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
216
|
+
import { useRegisterShortcut, useRegisterShortcuts, type KeyboardShortcut } from '../../hooks/useKeyboardShortcuts'
|
|
207
217
|
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
208
218
|
import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
|
|
209
219
|
import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
|
|
@@ -1460,6 +1470,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1460
1470
|
{ key: 'rules', label: 'Rules', width: 'w-20' },
|
|
1461
1471
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1462
1472
|
],
|
|
1473
|
+
udproutes: [
|
|
1474
|
+
{ key: 'name', label: 'Name' },
|
|
1475
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
1476
|
+
{ key: 'status', label: 'Status', width: 'w-28', tooltip: 'Accepted = controllers accepted this route and confirmed it can use the backends and other objects it names. Traffic has not been tested. Degraded or Not Accepted: open the route to see which Gateway or backend failed. Pending or Unknown: check the named Gateways or Services and their controllers.' },
|
|
1477
|
+
{ key: 'parents', label: 'Gateways', width: 'w-36' },
|
|
1478
|
+
{ key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
|
|
1479
|
+
{ key: 'rules', label: 'Rules', width: 'w-20' },
|
|
1480
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1481
|
+
],
|
|
1463
1482
|
tlsroutes: [
|
|
1464
1483
|
{ key: 'name', label: 'Name' },
|
|
1465
1484
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -2884,6 +2903,7 @@ const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
|
|
|
2884
2903
|
grpcroutes: ['gateway.networking.k8s.io'],
|
|
2885
2904
|
httproutes: ['gateway.networking.k8s.io'],
|
|
2886
2905
|
tcproutes: ['gateway.networking.k8s.io'],
|
|
2906
|
+
udproutes: ['gateway.networking.k8s.io'],
|
|
2887
2907
|
tlsroutes: ['gateway.networking.k8s.io'],
|
|
2888
2908
|
helmreleases: ['helm.toolkit.fluxcd.io'],
|
|
2889
2909
|
awsmachines: ['infrastructure.cluster.x-k8s.io'],
|
|
@@ -3240,6 +3260,7 @@ function filterHostExtras(
|
|
|
3240
3260
|
|
|
3241
3261
|
// localStorage helpers for column settings
|
|
3242
3262
|
const COLUMN_SETTINGS_PREFIX = 'radar-columns-'
|
|
3263
|
+
const CHECKBOX_VISIBILITY_KEY = 'radar-resource-checkboxes'
|
|
3243
3264
|
|
|
3244
3265
|
// Storage identity for a kind's column settings. normalizeKindToPlural falls
|
|
3245
3266
|
// through to the bare plural for anything outside the curated collision
|
|
@@ -3265,6 +3286,8 @@ interface ColumnSettings {
|
|
|
3265
3286
|
* load tell a column they hid from one that did not exist yet. Absent on
|
|
3266
3287
|
* blobs written before this field. */
|
|
3267
3288
|
known?: string[]
|
|
3289
|
+
/** Optional display order for movable columns, including hidden ones. */
|
|
3290
|
+
order?: string[]
|
|
3268
3291
|
}
|
|
3269
3292
|
|
|
3270
3293
|
function loadColumnSettings(kind: string, group?: string): ColumnSettings | null {
|
|
@@ -3327,6 +3350,9 @@ interface ResourcesViewData {
|
|
|
3327
3350
|
// cluster-scoped kind. An ExternalSecret names a store; only the store
|
|
3328
3351
|
// knows which backend it reads from.
|
|
3329
3352
|
storeProviders?: Record<string, string>
|
|
3353
|
+
/** Current CronJob Issues keyed by "namespace/name", supplied by the host. */
|
|
3354
|
+
operationalIssuesIncomplete?: boolean
|
|
3355
|
+
operationalIssues?: Record<string, Issue>
|
|
3330
3356
|
/** Cluster Audit findings keyed by "namespace/name". Raw compatibility
|
|
3331
3357
|
* counts render danger as High and warning as Medium. */
|
|
3332
3358
|
auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
|
|
@@ -3391,6 +3417,9 @@ interface ResourcesViewProps {
|
|
|
3391
3417
|
// cluster-scoped kind. An ExternalSecret names a store; only the store
|
|
3392
3418
|
// knows which backend it reads from.
|
|
3393
3419
|
storeProviders?: Record<string, string>
|
|
3420
|
+
/** Current CronJob Issues keyed by "namespace/name", supplied by the host. */
|
|
3421
|
+
operationalIssuesIncomplete?: boolean
|
|
3422
|
+
operationalIssues?: Record<string, Issue>
|
|
3394
3423
|
/** Cluster Audit findings keyed by "namespace/name". Raw compatibility
|
|
3395
3424
|
* counts render danger as High and warning as Medium. */
|
|
3396
3425
|
auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
|
|
@@ -3415,6 +3444,11 @@ interface ResourcesViewProps {
|
|
|
3415
3444
|
sidebarCategoryWorkspaces?: Record<string, SidebarCategoryWorkspace>
|
|
3416
3445
|
/** Callback when the [+] create button is clicked. Receives the currently selected kind info. */
|
|
3417
3446
|
onCreateResource?: (kind: { name: string; kind: string; group: string } | null) => void
|
|
3447
|
+
/** A kind's own view, rendered in place of the table: the sidebar, kind
|
|
3448
|
+
* selection and the drawer (`?resource=`) stay; the table, its toolbar and
|
|
3449
|
+
* its row/search/bulk shortcuts do not run. Sidebar kind navigation stays.
|
|
3450
|
+
* Return null or false to keep the table. */
|
|
3451
|
+
renderKindView?: (kind: SelectedKindInfo) => React.ReactNode | null
|
|
3418
3452
|
/** Default kind when the URL does not include one. */
|
|
3419
3453
|
defaultKind?: SelectedKindInfo
|
|
3420
3454
|
/** Columns prepended to KNOWN_COLUMNS for every kind. For example, a
|
|
@@ -3480,6 +3514,29 @@ interface ResourcesViewProps {
|
|
|
3480
3514
|
* the switcher lives outside this component and may persist server-side.
|
|
3481
3515
|
*/
|
|
3482
3516
|
onClearNamespaces?: () => void
|
|
3517
|
+
/**
|
|
3518
|
+
* A kind's saved filters, as last reported through `onKindFiltersChange`
|
|
3519
|
+
* (host-owned, because this view unmounts on section switches). A user kind
|
|
3520
|
+
* switch, or a URL with no list state of its own (see `hasFilterIntent`),
|
|
3521
|
+
* restores that kind's column and problem filters from it. Pass undefined
|
|
3522
|
+
* until the host can answer: the first defined value is still applied to a
|
|
3523
|
+
* bare URL.
|
|
3524
|
+
*/
|
|
3525
|
+
rememberedKindSearch?: (kind: { name: string; group: string }) => string | undefined
|
|
3526
|
+
/**
|
|
3527
|
+
* Called with a kind's serialized `filters` + `problems` query ('' when
|
|
3528
|
+
* none) whenever the filters the kind shows change, including the ones a
|
|
3529
|
+
* link arrives with. A list that arrives with no filters does not report
|
|
3530
|
+
* '', so a link to one resource keeps what was saved.
|
|
3531
|
+
*/
|
|
3532
|
+
onKindFiltersChange?: (kind: { name: string; group: string }, search: string) => void
|
|
3533
|
+
/**
|
|
3534
|
+
* Promotes the Namespace column's included values to the global namespace
|
|
3535
|
+
* selection (replacing it). When wired, the column's filter dropdown offers
|
|
3536
|
+
* "Apply to all sections" and clears the column filter once the returned
|
|
3537
|
+
* promise resolves (a rejection keeps it).
|
|
3538
|
+
*/
|
|
3539
|
+
onApplyNamespacesToAllSections?: (namespaces: string[]) => Promise<unknown> | void
|
|
3483
3540
|
// Bulk operations
|
|
3484
3541
|
onBulkDelete?: (items: BulkResourceItem[], options?: { force?: boolean; onSuccess?: () => void }) => void
|
|
3485
3542
|
isBulkDeleting?: boolean
|
|
@@ -3603,19 +3660,24 @@ export function deriveSidebarResourceCounts(
|
|
|
3603
3660
|
return results
|
|
3604
3661
|
}
|
|
3605
3662
|
|
|
3663
|
+
const FILTER_INTENT_PARAMS = [...RESOURCE_LIST_FILTER_PARAMS, 'resource']
|
|
3664
|
+
|
|
3665
|
+
/**
|
|
3666
|
+
* Whether a URL states its own list state. Only a URL without any of these
|
|
3667
|
+
* (a bare `/resources/<kind>`) gets the kind's remembered filters.
|
|
3668
|
+
*/
|
|
3669
|
+
export function hasFilterIntent(search: string): boolean {
|
|
3670
|
+
const params = new URLSearchParams(search)
|
|
3671
|
+
return FILTER_INTENT_PARAMS.some((key) => params.has(key))
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3606
3674
|
// Get initial filters from URL
|
|
3607
3675
|
function getInitialFiltersFromURL() {
|
|
3608
3676
|
const params = new URLSearchParams(window.location.search)
|
|
3609
|
-
// Parse generic column filters
|
|
3610
|
-
const filtersParam = params.get('filters')
|
|
3611
|
-
const columnFilters = parseColumnFilters(filtersParam)
|
|
3612
|
-
const columnFilterExcludes = parseColumnFilterExcludes(filtersParam)
|
|
3613
3677
|
const result = {
|
|
3614
3678
|
search: params.get('search') || '',
|
|
3615
3679
|
regex: params.get('regex') === 'true',
|
|
3616
|
-
|
|
3617
|
-
columnFilterExcludes,
|
|
3618
|
-
problemFilters: params.get('problems')?.split(',').filter(Boolean) || [],
|
|
3680
|
+
...kindFiltersFromSearch(window.location.search),
|
|
3619
3681
|
showInactive: params.get('showInactive') === 'true',
|
|
3620
3682
|
labelSelector: params.get('labels') || '', // e.g., "app=caretta,version=v1"
|
|
3621
3683
|
ownerKind: params.get('ownerKind') || '', // e.g., "DaemonSet"
|
|
@@ -3644,6 +3706,8 @@ export function ResourcesView({
|
|
|
3644
3706
|
certExpiryError,
|
|
3645
3707
|
storeProviders,
|
|
3646
3708
|
auditBadges,
|
|
3709
|
+
operationalIssues,
|
|
3710
|
+
operationalIssuesIncomplete,
|
|
3647
3711
|
pinned = [],
|
|
3648
3712
|
togglePin = () => {},
|
|
3649
3713
|
isPinned = () => false,
|
|
@@ -3657,6 +3721,7 @@ export function ResourcesView({
|
|
|
3657
3721
|
hideSidebar = false,
|
|
3658
3722
|
sidebarCategoryWorkspaces,
|
|
3659
3723
|
onCreateResource,
|
|
3724
|
+
renderKindView,
|
|
3660
3725
|
defaultKind = DEFAULT_KIND_INFO,
|
|
3661
3726
|
extraLeadingColumns,
|
|
3662
3727
|
printerTable,
|
|
@@ -3665,6 +3730,9 @@ export function ResourcesView({
|
|
|
3665
3730
|
onCompareSubmit,
|
|
3666
3731
|
resolveRowCluster,
|
|
3667
3732
|
onClearNamespaces,
|
|
3733
|
+
rememberedKindSearch,
|
|
3734
|
+
onKindFiltersChange,
|
|
3735
|
+
onApplyNamespacesToAllSections,
|
|
3668
3736
|
onBulkDelete,
|
|
3669
3737
|
isBulkDeleting = false,
|
|
3670
3738
|
onBulkRestart,
|
|
@@ -3685,16 +3753,22 @@ export function ResourcesView({
|
|
|
3685
3753
|
kindFromURL.name !== prev.name || kindFromURL.group !== prev.group ? kindFromURL : prev,
|
|
3686
3754
|
)
|
|
3687
3755
|
}, [basePath, defaultKind, locationPathname, locationSearch])
|
|
3756
|
+
const contributedKindView = renderKindView?.(selectedKind)
|
|
3757
|
+
const kindView = contributedKindView === false ? null : contributedKindView ?? null
|
|
3758
|
+
const tableActive = kindView === null
|
|
3759
|
+
|
|
3688
3760
|
// Notify parent of selected kind changes (including initial mount)
|
|
3689
3761
|
useEffect(() => {
|
|
3690
3762
|
onSelectedKindChange?.(selectedKind)
|
|
3691
|
-
|
|
3763
|
+
}, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
3764
|
+
|
|
3765
|
+
useEffect(() => {
|
|
3692
3766
|
setCheckedResources(new Set())
|
|
3693
3767
|
setShowBulkDeleteConfirm(false)
|
|
3694
3768
|
setShowBulkRestartConfirm(false)
|
|
3695
3769
|
setShowBulkScaleDialog(false)
|
|
3696
3770
|
setBulkForceDelete(false)
|
|
3697
|
-
}, [selectedKind.name, selectedKind.group])
|
|
3771
|
+
}, [selectedKind.name, selectedKind.group, tableActive])
|
|
3698
3772
|
const [searchTerm, setSearchTerm] = useState(initialFilters.search)
|
|
3699
3773
|
// Typing must never wait on the list or the router. The input renders raw
|
|
3700
3774
|
// searchTerm; filtering consumes the deferred copy (React yields to keep
|
|
@@ -3712,6 +3786,30 @@ export function ResourcesView({
|
|
|
3712
3786
|
const [columnFilters, setColumnFilters] = useState<Record<string, string[]>>(initialFilters.columnFilters)
|
|
3713
3787
|
const [columnFilterExcludes, setColumnFilterExcludes] = useState<Record<string, boolean>>(initialFilters.columnFilterExcludes)
|
|
3714
3788
|
const [problemFilters, setProblemFilters] = useState<string[]>(initialFilters.problemFilters)
|
|
3789
|
+
// The kind the filter state belongs to. During a kind switch one render
|
|
3790
|
+
// pairs the new kind with the old kind's filters; saving then would file
|
|
3791
|
+
// them under the wrong kind.
|
|
3792
|
+
const [filtersKind, setFiltersKind] = useState(() => selectedKindIdentityOf(selectedKind))
|
|
3793
|
+
// Where the current filter state came from when it wasn't a user edit.
|
|
3794
|
+
const filtersArrivedRef = useRef<'url' | 'restore' | null>('url')
|
|
3795
|
+
// The serialized filters last restored from rememberedKindSearch ('' when none).
|
|
3796
|
+
const [restoredKindSearch, setRestoredKindSearch] = useState('')
|
|
3797
|
+
// A bare URL at mount takes the kind's saved filters. Adjusted during render
|
|
3798
|
+
// (not in an effect) so the unfiltered list never paints, and so it still
|
|
3799
|
+
// applies when the host's lookup arrives after mount.
|
|
3800
|
+
const [mountRestorePending, setMountRestorePending] = useState(() => !hasFilterIntent(locationSearch ?? window.location.search))
|
|
3801
|
+
if (mountRestorePending && rememberedKindSearch) {
|
|
3802
|
+
setMountRestorePending(false)
|
|
3803
|
+
const restored = kindFiltersToSearch(kindFiltersFromSearch(rememberedKindSearch(selectedKind)))
|
|
3804
|
+
if (restored && !hasFilterIntent(locationSearch ?? window.location.search)) {
|
|
3805
|
+
const remembered = kindFiltersFromSearch(restored)
|
|
3806
|
+
setColumnFilters(remembered.columnFilters)
|
|
3807
|
+
setColumnFilterExcludes(remembered.columnFilterExcludes)
|
|
3808
|
+
setProblemFilters(remembered.problemFilters)
|
|
3809
|
+
setRestoredKindSearch(restored)
|
|
3810
|
+
filtersArrivedRef.current = 'restore'
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3715
3813
|
const [openColumnFilter, setOpenColumnFilter] = useState<string | null>(null)
|
|
3716
3814
|
const [columnFilterSearch, setColumnFilterSearch] = useState('')
|
|
3717
3815
|
const columnFilterDropdownRef = useRef<HTMLDivElement>(null)
|
|
@@ -3725,6 +3823,7 @@ export function ResourcesView({
|
|
|
3725
3823
|
// Column visibility and resize state
|
|
3726
3824
|
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(new Set())
|
|
3727
3825
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
|
|
3826
|
+
const [columnOrder, setColumnOrder] = useState<string[] | null>(null)
|
|
3728
3827
|
const [showColumnPicker, setShowColumnPicker] = useState(false)
|
|
3729
3828
|
const [customColumns, setCustomColumns] = useState<CustomColumnDef[]>([])
|
|
3730
3829
|
const [customColumnDraft, setCustomColumnDraft] = useState<CustomColumnDef>({ source: 'label', path: '' })
|
|
@@ -3737,10 +3836,10 @@ export function ResourcesView({
|
|
|
3737
3836
|
const [ownerKind, setOwnerKind] = useState<string>(initialFilters.ownerKind)
|
|
3738
3837
|
const [ownerName, setOwnerName] = useState<string>(initialFilters.ownerName)
|
|
3739
3838
|
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3839
|
+
const [showCheckboxes, setShowCheckboxes] = useState(() => {
|
|
3840
|
+
try { return localStorage.getItem(CHECKBOX_VISIBILITY_KEY) === 'true' }
|
|
3841
|
+
catch { return false }
|
|
3842
|
+
})
|
|
3744
3843
|
const [checkedResources, setCheckedResources] = useState<Set<string>>(new Set())
|
|
3745
3844
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
|
3746
3845
|
const [showBulkRestartConfirm, setShowBulkRestartConfirm] = useState(false)
|
|
@@ -3748,8 +3847,7 @@ export function ResourcesView({
|
|
|
3748
3847
|
const [bulkForceDelete, setBulkForceDelete] = useState(false)
|
|
3749
3848
|
const [bulkScaleReplicas, setBulkScaleReplicas] = useState(0)
|
|
3750
3849
|
|
|
3751
|
-
const
|
|
3752
|
-
setBulkMode(false)
|
|
3850
|
+
const clearBulkSelection = useCallback(() => {
|
|
3753
3851
|
setCheckedResources(new Set())
|
|
3754
3852
|
}, [])
|
|
3755
3853
|
|
|
@@ -3866,13 +3964,16 @@ export function ResourcesView({
|
|
|
3866
3964
|
[printerTable, builtPrinterColumns],
|
|
3867
3965
|
)
|
|
3868
3966
|
|
|
3869
|
-
const
|
|
3967
|
+
const definedColumns = useMemo(() => {
|
|
3870
3968
|
const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
|
|
3871
3969
|
if (!extraLeadingColumns?.length && !builtCustomColumns.length) return kindColumns
|
|
3872
3970
|
const builtinKeys = new Set(kindColumns.map(c => c.key))
|
|
3873
3971
|
const filteredExtras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
|
|
3874
3972
|
return [...filteredExtras, ...kindColumns, ...builtCustomColumns]
|
|
3875
3973
|
}, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
|
|
3974
|
+
// Display order. Everything downstream reads this, so the picker, the header
|
|
3975
|
+
// and the resize width snapshot (which maps <th> index to key) agree.
|
|
3976
|
+
const allColumns = useMemo(() => applyColumnOrder(definedColumns, columnOrder), [definedColumns, columnOrder])
|
|
3876
3977
|
|
|
3877
3978
|
// The offered key set, and a stable signature for it. allColumns is a new
|
|
3878
3979
|
// identity on every printer-column refetch; depending on the array itself
|
|
@@ -3935,6 +4036,8 @@ export function ResourcesView({
|
|
|
3935
4036
|
// depends on visibleColumns) and instantly reverts the user's hide.
|
|
3936
4037
|
const gpuAutoShownKinds = useRef<Set<string>>(new Set())
|
|
3937
4038
|
useEffect(() => {
|
|
4039
|
+
// The table's own settings: nothing to load (or clear) while a kind view replaces it.
|
|
4040
|
+
if (!tableActive) return
|
|
3938
4041
|
// Re-arm the skip-initial-save guard per kind. This effect repopulates
|
|
3939
4042
|
// visible/widths/custom for the new kind via async setState, so the save
|
|
3940
4043
|
// effect (also keyed to selectedKind) that runs in this same commit still
|
|
@@ -4005,24 +4108,28 @@ export function ResourcesView({
|
|
|
4005
4108
|
hadSavedColumnSettings.current = false
|
|
4006
4109
|
setVisibleColumns(getDefaultVisibleColumns(effective))
|
|
4007
4110
|
setColumnWidths({})
|
|
4111
|
+
setColumnOrder(null)
|
|
4008
4112
|
} else {
|
|
4009
4113
|
setVisibleColumns(mergeSavedVisibleColumns(
|
|
4010
4114
|
saved.visible, extraKeys, builtPrinterColumns, effective, savedKnown,
|
|
4011
4115
|
))
|
|
4012
4116
|
setColumnWidths(saved.widths || {})
|
|
4117
|
+
setColumnOrder(sanitizeColumnOrder(saved.order))
|
|
4013
4118
|
}
|
|
4014
4119
|
} else {
|
|
4015
4120
|
setVisibleColumns(getDefaultVisibleColumns(effective))
|
|
4016
4121
|
setColumnWidths({})
|
|
4122
|
+
setColumnOrder(null)
|
|
4017
4123
|
}
|
|
4018
4124
|
// printerColumnsKey, not builtPrinterColumns: the built array is a new
|
|
4019
4125
|
// identity on every refetch, and re-running this effect would reset the
|
|
4020
4126
|
// user's in-session column choices each time the list polls. The key only
|
|
4021
4127
|
// changes when the kind changes or an operator upgrade changes the CRD.
|
|
4022
|
-
}, [selectedKind.name, selectedKind.group, extraLeadingColumns, printerColumnsKey])
|
|
4128
|
+
}, [selectedKind.name, selectedKind.group, extraLeadingColumns, printerColumnsKey, tableActive])
|
|
4023
4129
|
|
|
4024
4130
|
// Save column settings when they change (skip the initial load of each kind)
|
|
4025
4131
|
useEffect(() => {
|
|
4132
|
+
if (!tableActive) return
|
|
4026
4133
|
if (visibleColumns.size === 0) return // not loaded yet
|
|
4027
4134
|
for (const k of allColumnKeys) knownColumnKeys.current.add(k)
|
|
4028
4135
|
if (!isColumnSettingsLoaded.current) {
|
|
@@ -4034,12 +4141,13 @@ export function ResourcesView({
|
|
|
4034
4141
|
widths: columnWidths,
|
|
4035
4142
|
custom: customColumns,
|
|
4036
4143
|
known: Array.from(knownColumnKeys.current),
|
|
4144
|
+
order: columnOrder ?? undefined,
|
|
4037
4145
|
})
|
|
4038
4146
|
// A persisted blob blocks GPU auto-show from here on — same rule in-session
|
|
4039
4147
|
// as across sessions, so a later data refresh can't override user choices.
|
|
4040
4148
|
hadSavedColumnSettings.current = true
|
|
4041
4149
|
// allColumnKeysSig, not allColumnKeys: see the memo above.
|
|
4042
|
-
}, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group, allColumnKeysSig])
|
|
4150
|
+
}, [visibleColumns, columnWidths, customColumns, columnOrder, selectedKind.name, selectedKind.group, allColumnKeysSig, tableActive])
|
|
4043
4151
|
|
|
4044
4152
|
// Close column picker on outside click or Escape
|
|
4045
4153
|
useEffect(() => {
|
|
@@ -4147,6 +4255,7 @@ export function ResourcesView({
|
|
|
4147
4255
|
const customKeys = new Set(builtCustomColumns.map(c => c.key))
|
|
4148
4256
|
setVisibleColumns(getDefaultVisibleColumns(allColumns.filter(c => !customKeys.has(c.key))))
|
|
4149
4257
|
setColumnWidths({})
|
|
4258
|
+
setColumnOrder(null)
|
|
4150
4259
|
setSortColumn(null)
|
|
4151
4260
|
setSortDirection(null)
|
|
4152
4261
|
saveSavedSort(selectedKind.name, selectedKind.group, null)
|
|
@@ -4208,6 +4317,7 @@ export function ResourcesView({
|
|
|
4208
4317
|
category: 'Search',
|
|
4209
4318
|
scope: 'resources',
|
|
4210
4319
|
handler: () => searchInputRef.current?.focus(),
|
|
4320
|
+
enabled: tableActive,
|
|
4211
4321
|
})
|
|
4212
4322
|
|
|
4213
4323
|
// Keyboard navigation: highlighted row state
|
|
@@ -4227,7 +4337,7 @@ export function ResourcesView({
|
|
|
4227
4337
|
useEffect(() => {
|
|
4228
4338
|
setComparePicks([])
|
|
4229
4339
|
setCompareMode(false)
|
|
4230
|
-
}, [selectedKind.name, selectedKind.group])
|
|
4340
|
+
}, [selectedKind.name, selectedKind.group, tableActive])
|
|
4231
4341
|
|
|
4232
4342
|
const exitCompareMode = useCallback(() => {
|
|
4233
4343
|
setCompareMode(false)
|
|
@@ -4275,6 +4385,15 @@ export function ResourcesView({
|
|
|
4275
4385
|
}
|
|
4276
4386
|
}, [highlightedIndex])
|
|
4277
4387
|
|
|
4388
|
+
useEffect(() => {
|
|
4389
|
+
if (tableActive) return
|
|
4390
|
+
setHighlightedIndex(-1)
|
|
4391
|
+
setShowColumnPicker(false)
|
|
4392
|
+
setOpenColumnFilter(null)
|
|
4393
|
+
setShowProblemsDropdown(false)
|
|
4394
|
+
setShowLabelsDropdown(false)
|
|
4395
|
+
}, [tableActive])
|
|
4396
|
+
|
|
4278
4397
|
// Open logs for pod / workload resources — provided via props
|
|
4279
4398
|
const openLogs = onOpenLogs
|
|
4280
4399
|
const openWorkloadLogs = onOpenWorkloadLogs
|
|
@@ -4307,8 +4426,7 @@ export function ResourcesView({
|
|
|
4307
4426
|
onResourceClick?.(isSelected ? null : stripped)
|
|
4308
4427
|
}, [onRowSelect, onResourceClick, selectedKind.name, selectedKind.group])
|
|
4309
4428
|
|
|
4310
|
-
|
|
4311
|
-
useRegisterShortcuts([
|
|
4429
|
+
useRegisterShortcuts(([
|
|
4312
4430
|
{
|
|
4313
4431
|
id: 'resources-nav-down',
|
|
4314
4432
|
keys: 'j',
|
|
@@ -4457,14 +4575,11 @@ export function ResourcesView({
|
|
|
4457
4575
|
{
|
|
4458
4576
|
id: 'resources-bulk-select',
|
|
4459
4577
|
keys: 'b',
|
|
4460
|
-
description: '
|
|
4578
|
+
description: 'Show or hide selection checkboxes',
|
|
4461
4579
|
category: 'Table',
|
|
4462
4580
|
scope: 'resources',
|
|
4463
4581
|
handler: () => {
|
|
4464
|
-
if (
|
|
4465
|
-
if (!canBulkSelect) return
|
|
4466
|
-
exitCompareMode()
|
|
4467
|
-
setBulkMode(true)
|
|
4582
|
+
if (canBulkSelect) toggleCheckboxVisibility()
|
|
4468
4583
|
},
|
|
4469
4584
|
},
|
|
4470
4585
|
{
|
|
@@ -4479,12 +4594,12 @@ export function ResourcesView({
|
|
|
4479
4594
|
if (showProblemsDropdown) { setShowProblemsDropdown(false); return }
|
|
4480
4595
|
if (showLabelsDropdown) { setShowLabelsDropdown(false); return }
|
|
4481
4596
|
if (compareMode) { exitCompareMode(); return }
|
|
4482
|
-
if (
|
|
4597
|
+
if (checkedResources.size > 0) { clearBulkSelection(); return }
|
|
4483
4598
|
if (highlightedIndex >= 0) setHighlightedIndex(-1)
|
|
4484
4599
|
else searchInputRef.current?.blur()
|
|
4485
4600
|
},
|
|
4486
4601
|
},
|
|
4487
|
-
])
|
|
4602
|
+
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && (sc.enabled ?? true) })))
|
|
4488
4603
|
|
|
4489
4604
|
// Refs for accessing filteredResources inside shortcuts (computed later in component)
|
|
4490
4605
|
const filteredResourceCountRef = useRef(0)
|
|
@@ -4564,8 +4679,21 @@ export function ResourcesView({
|
|
|
4564
4679
|
const newFilters = getInitialFiltersFromURL()
|
|
4565
4680
|
|
|
4566
4681
|
// Update kind if it changed
|
|
4567
|
-
|
|
4682
|
+
const kindChanged = newKind.name !== selectedKind.name || newKind.group !== selectedKind.group
|
|
4683
|
+
let restored = ''
|
|
4684
|
+
if (kindChanged) {
|
|
4568
4685
|
setSelectedKind(newKind)
|
|
4686
|
+
// A bare URL for another kind links to the list, not to a state of it:
|
|
4687
|
+
// take that kind's saved filters and put them in the address bar. Never
|
|
4688
|
+
// for the same kind, where a bare URL is the echo of Clear filters.
|
|
4689
|
+
restored = hasFilterIntent(locationSearch ?? window.location.search)
|
|
4690
|
+
? ''
|
|
4691
|
+
: kindFiltersToSearch(kindFiltersFromSearch(rememberedKindSearch?.(newKind)))
|
|
4692
|
+
setRestoredKindSearch(restored)
|
|
4693
|
+
if (restored) {
|
|
4694
|
+
Object.assign(newFilters, kindFiltersFromSearch(restored))
|
|
4695
|
+
updateURL(newKind, newFilters.search, newFilters.regex, newFilters.columnFilters, newFilters.columnFilterExcludes, newFilters.problemFilters, newFilters.showInactive)
|
|
4696
|
+
}
|
|
4569
4697
|
}
|
|
4570
4698
|
|
|
4571
4699
|
// Update owner filter if it changed
|
|
@@ -4606,6 +4734,13 @@ export function ResourcesView({
|
|
|
4606
4734
|
setColumnFilterExcludes(newFilters.columnFilterExcludes)
|
|
4607
4735
|
}
|
|
4608
4736
|
|
|
4737
|
+
if (newFilters.problemFilters.join(',') !== problemFilters.join(',')) {
|
|
4738
|
+
setProblemFilters(newFilters.problemFilters)
|
|
4739
|
+
}
|
|
4740
|
+
if (kindChanged || kindFiltersToSearch(newFilters) !== kindFiltersSearch) {
|
|
4741
|
+
filtersArrivedRef.current = restored ? 'restore' : 'url'
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4609
4744
|
// Reset the flag after a tick to allow normal URL updates
|
|
4610
4745
|
requestAnimationFrame(() => {
|
|
4611
4746
|
isSyncingFromURL.current = false
|
|
@@ -4660,11 +4795,7 @@ export function ResourcesView({
|
|
|
4660
4795
|
}
|
|
4661
4796
|
// Write column filters as `filters` param; the exclude operator is folded
|
|
4662
4797
|
// into each column entry, so it can never drift from the values it negates.
|
|
4663
|
-
|
|
4664
|
-
const activeExcludes = Object.fromEntries(
|
|
4665
|
-
Object.entries(colExcludes).filter(([k, on]) => on && (colFilters[k]?.length ?? 0) > 0)
|
|
4666
|
-
)
|
|
4667
|
-
const filtersStr = serializeColumnFilters(colFilters, activeExcludes)
|
|
4798
|
+
const filtersStr = columnFiltersParam(colFilters, colExcludes)
|
|
4668
4799
|
if (filtersStr) {
|
|
4669
4800
|
params.set('filters', filtersStr)
|
|
4670
4801
|
} else {
|
|
@@ -4736,13 +4867,19 @@ export function ResourcesView({
|
|
|
4736
4867
|
// params are out of scope here; the host's onClearNamespaces (and its
|
|
4737
4868
|
// own state→URL sync) owns namespace cleanup.
|
|
4738
4869
|
const params = new URLSearchParams(window.location.search)
|
|
4739
|
-
for (const key of
|
|
4870
|
+
for (const key of RESOURCE_LIST_FILTER_PARAMS) {
|
|
4740
4871
|
params.delete(key)
|
|
4741
4872
|
}
|
|
4742
4873
|
navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
|
|
4743
4874
|
onClearNamespaces?.()
|
|
4744
4875
|
}, [navigate, onClearNamespaces])
|
|
4745
4876
|
|
|
4877
|
+
const clearKindFilters = useCallback(() => {
|
|
4878
|
+
setColumnFilters({})
|
|
4879
|
+
setColumnFilterExcludes({})
|
|
4880
|
+
setProblemFilters([])
|
|
4881
|
+
}, [])
|
|
4882
|
+
|
|
4746
4883
|
// Update URL when any filter changes
|
|
4747
4884
|
useEffect(() => {
|
|
4748
4885
|
// Skip URL update if we're syncing FROM the URL (e.g., browser back button)
|
|
@@ -4755,6 +4892,8 @@ export function ResourcesView({
|
|
|
4755
4892
|
prevSelectedResourceRef.current = selectedResource ?? null
|
|
4756
4893
|
return
|
|
4757
4894
|
}
|
|
4895
|
+
// Right after a kind switch the filters are still the previous kind's.
|
|
4896
|
+
if (filtersKind !== selectedKindIdentityOf(selectedKind)) return
|
|
4758
4897
|
// Skip URL update if selectedResource's kind doesn't match selectedKind (still syncing)
|
|
4759
4898
|
if (selectedResource) {
|
|
4760
4899
|
const resourceKindLower = selectedResource.kind.toLowerCase()
|
|
@@ -4780,7 +4919,12 @@ export function ResourcesView({
|
|
|
4780
4919
|
: typeof window !== 'undefined'
|
|
4781
4920
|
? window.location.pathname
|
|
4782
4921
|
: ''
|
|
4783
|
-
|
|
4922
|
+
// The router commits a navigation as a transition, so the injected path can
|
|
4923
|
+
// still be the previous kind's right after a push to this one (e.g. the
|
|
4924
|
+
// write of filters restored on a kind switch); the address bar is already
|
|
4925
|
+
// there, and pushing again would add a second entry for the same visit.
|
|
4926
|
+
const pathChanged = currentPath !== targetPath &&
|
|
4927
|
+
!(typeof window !== 'undefined' && window.location.pathname.endsWith(targetPath))
|
|
4784
4928
|
const prev = prevSelectedResourceRef.current
|
|
4785
4929
|
const current = selectedResource ?? null
|
|
4786
4930
|
const drawerSwitched =
|
|
@@ -4794,7 +4938,7 @@ export function ResourcesView({
|
|
|
4794
4938
|
prevSelectedResourceRef.current = current
|
|
4795
4939
|
|
|
4796
4940
|
updateURL(selectedKind, debouncedSearchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, selectedResource?.namespace, selectedResource?.name, pushHistory)
|
|
4797
|
-
}, [selectedKind, debouncedSearchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
|
|
4941
|
+
}, [selectedKind, filtersKind, debouncedSearchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
|
|
4798
4942
|
|
|
4799
4943
|
// Handle resource click from URL on mount
|
|
4800
4944
|
useEffect(() => {
|
|
@@ -4815,6 +4959,12 @@ export function ResourcesView({
|
|
|
4815
4959
|
// Signal that initial resource param has been processed — URL update effect can now run
|
|
4816
4960
|
hasProcessedInitialResource.current = true
|
|
4817
4961
|
|
|
4962
|
+
// Filters restored at mount go into the address bar (with the kind segment).
|
|
4963
|
+
if (restoredKindSearch) {
|
|
4964
|
+
updateURL(selectedKind, searchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets)
|
|
4965
|
+
return
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4818
4968
|
// If the URL has no kind segment (e.g., /resources), update to include the default kind.
|
|
4819
4969
|
// Route through `navigate` so an onNavigate-suppressing host can opt out.
|
|
4820
4970
|
const base = basePath.replace(/\/$/, '')
|
|
@@ -5083,7 +5233,7 @@ export function ResourcesView({
|
|
|
5083
5233
|
isForbiddenError(selectedQueryError) ||
|
|
5084
5234
|
(!selectedHasRows && !selectedQueryError && forbiddenKinds.has(selectedKindCountKey))
|
|
5085
5235
|
|
|
5086
|
-
//
|
|
5236
|
+
// On a kind change, restore that kind's remembered filters, or start empty (but not when syncing from URL navigation)
|
|
5087
5237
|
// Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
|
|
5088
5238
|
// Keyed on group as well as plural: two CRDs can share a plural across API
|
|
5089
5239
|
// groups, and their printer columns are unrelated. Resetting on the plural
|
|
@@ -5097,12 +5247,39 @@ export function ResourcesView({
|
|
|
5097
5247
|
}
|
|
5098
5248
|
prevKindRef.current = selectedKindIdentity
|
|
5099
5249
|
setOpenColumnFilter(null)
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5250
|
+
setFiltersKind(selectedKindIdentity)
|
|
5251
|
+
// The URL sync already set this kind's filters (and restoredKindSearch).
|
|
5252
|
+
if (isSyncingFromURL.current) return
|
|
5253
|
+
filtersArrivedRef.current = 'restore'
|
|
5254
|
+
const restored = kindFiltersToSearch(kindFiltersFromSearch(rememberedKindSearch?.(selectedKind)))
|
|
5255
|
+
const remembered = kindFiltersFromSearch(restored)
|
|
5256
|
+
setColumnFilters(remembered.columnFilters)
|
|
5257
|
+
setColumnFilterExcludes(remembered.columnFilterExcludes)
|
|
5258
|
+
setProblemFilters(remembered.problemFilters)
|
|
5259
|
+
setRestoredKindSearch(restored)
|
|
5260
|
+
}, [selectedKindIdentity, selectedKind, rememberedKindSearch])
|
|
5261
|
+
|
|
5262
|
+
const kindFiltersSearch = useMemo(
|
|
5263
|
+
() => kindFiltersToSearch({ columnFilters, columnFilterExcludes, problemFilters }),
|
|
5264
|
+
[columnFilters, columnFilterExcludes, problemFilters],
|
|
5265
|
+
)
|
|
5266
|
+
// Saved filters are the last ones chosen for a kind: by the user, or by a
|
|
5267
|
+
// link that brought its own. A restore only shows what was saved (saving it
|
|
5268
|
+
// again would overwrite another tab's newer choice); a list that arrives
|
|
5269
|
+
// with no filters (a link to one resource, an owner's pods) keeps what was
|
|
5270
|
+
// saved; Clear filters saves none.
|
|
5271
|
+
const lastSeenKindFiltersRef = useRef<{ kind: string; search: string } | null>(null)
|
|
5272
|
+
useEffect(() => {
|
|
5273
|
+
// The host may wire saving after mount; until then nothing counts as seen.
|
|
5274
|
+
if (!onKindFiltersChange || filtersKind !== selectedKindIdentity) return
|
|
5275
|
+
const last = lastSeenKindFiltersRef.current
|
|
5276
|
+
lastSeenKindFiltersRef.current = { kind: filtersKind, search: kindFiltersSearch }
|
|
5277
|
+
const arrived = filtersArrivedRef.current
|
|
5278
|
+
filtersArrivedRef.current = null
|
|
5279
|
+
if (last?.kind === filtersKind && last.search === kindFiltersSearch) return
|
|
5280
|
+
if (arrived === 'restore' || (arrived === 'url' && !kindFiltersSearch)) return
|
|
5281
|
+
onKindFiltersChange(selectedKind, kindFiltersSearch)
|
|
5282
|
+
}, [filtersKind, selectedKindIdentity, kindFiltersSearch, selectedKind, onKindFiltersChange])
|
|
5106
5283
|
|
|
5107
5284
|
// Toggle sort for a column
|
|
5108
5285
|
const handleSort = useCallback((column: string) => {
|
|
@@ -5135,6 +5312,15 @@ export function ResourcesView({
|
|
|
5135
5312
|
saveSavedSort(selectedKind.name, selectedKind.group, newColumn && newDirection ? { column: newColumn, direction: newDirection } : null)
|
|
5136
5313
|
}, [sortColumn, sortDirection, canSortBy, selectedKind.name, selectedKind.group])
|
|
5137
5314
|
|
|
5315
|
+
const getDisplayFilterValue = useCallback((resource: { metadata?: { namespace?: string; name?: string } }, column: string, kind: string): string => {
|
|
5316
|
+
if (column === 'status' && selectedKind.group === 'batch' && kind === 'cronjobs') {
|
|
5317
|
+
const issue = operationalIssues?.[`${resource.metadata?.namespace ?? ''}/${resource.metadata?.name ?? ''}`]
|
|
5318
|
+
if (issue) return issueTitle(issue)
|
|
5319
|
+
if (operationalIssuesIncomplete) return 'Unknown'
|
|
5320
|
+
}
|
|
5321
|
+
return getCellFilterValue(resource, column, kind)
|
|
5322
|
+
}, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
|
|
5323
|
+
|
|
5138
5324
|
// Get sortable value from a resource for a given column
|
|
5139
5325
|
const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
|
|
5140
5326
|
const meta = resource.metadata || {}
|
|
@@ -5163,7 +5349,7 @@ export function ResourcesView({
|
|
|
5163
5349
|
if (hasCuratedColumns(selectedKind.name, selectedKind.group) && status.phase) {
|
|
5164
5350
|
return status.phase
|
|
5165
5351
|
}
|
|
5166
|
-
return
|
|
5352
|
+
return getDisplayFilterValue(resource, 'status', kindLower)
|
|
5167
5353
|
case 'servers':
|
|
5168
5354
|
// Numeric so 10 does not sort before 9.
|
|
5169
5355
|
if (kindLower === 'istiogateways') return getIstioGatewayServerCount(resource)
|
|
@@ -5253,7 +5439,7 @@ export function ResourcesView({
|
|
|
5253
5439
|
default:
|
|
5254
5440
|
return ''
|
|
5255
5441
|
}
|
|
5256
|
-
}, [metricsLookup, selectedKind.name, selectedKind.group])
|
|
5442
|
+
}, [metricsLookup, selectedKind.name, selectedKind.group, getDisplayFilterValue])
|
|
5257
5443
|
|
|
5258
5444
|
// Helper to check if a pod matches problem filters
|
|
5259
5445
|
const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
|
|
@@ -5278,7 +5464,7 @@ export function ResourcesView({
|
|
|
5278
5464
|
|
|
5279
5465
|
// Filter resources by search term, status, problems, and sort
|
|
5280
5466
|
const filteredResources = useMemo(() => {
|
|
5281
|
-
if (!resources) return []
|
|
5467
|
+
if (!tableActive || !resources) return []
|
|
5282
5468
|
|
|
5283
5469
|
let result = resources
|
|
5284
5470
|
|
|
@@ -5310,7 +5496,7 @@ export function ResourcesView({
|
|
|
5310
5496
|
result = result.filter((r: any) =>
|
|
5311
5497
|
activeColFilters.every(([col, vals]) => {
|
|
5312
5498
|
const extra = extraColumnsByKey.get(col)
|
|
5313
|
-
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) :
|
|
5499
|
+
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getDisplayFilterValue(r, col, cellFilterKind)
|
|
5314
5500
|
const match = vals.includes(cellVal)
|
|
5315
5501
|
return columnFilterExcludes[col] ? !match : match
|
|
5316
5502
|
})
|
|
@@ -5464,7 +5650,7 @@ export function ResourcesView({
|
|
|
5464
5650
|
}
|
|
5465
5651
|
|
|
5466
5652
|
return result
|
|
5467
|
-
}, [resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
5653
|
+
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
5468
5654
|
|
|
5469
5655
|
// For nodes table: compute the majority minor version so outliers can be highlighted
|
|
5470
5656
|
const majorityNodeMinorVersion = useMemo(() => {
|
|
@@ -5589,16 +5775,25 @@ export function ResourcesView({
|
|
|
5589
5775
|
setCheckedResources(allVisibleChecked ? new Set() : new Set(filteredResources.map(getResourceKey)))
|
|
5590
5776
|
}, [allVisibleChecked, filteredResources, getResourceKey])
|
|
5591
5777
|
|
|
5592
|
-
const
|
|
5778
|
+
const toggleCheckboxVisibility = useCallback(() => {
|
|
5779
|
+
const next = compareMode || !showCheckboxes
|
|
5780
|
+
exitCompareMode()
|
|
5781
|
+
clearBulkSelection()
|
|
5782
|
+
setShowCheckboxes(next)
|
|
5783
|
+
try { localStorage.setItem(CHECKBOX_VISIBILITY_KEY, String(next)) }
|
|
5784
|
+
catch { /* Keep the choice for this session when storage is unavailable. */ }
|
|
5785
|
+
}, [compareMode, showCheckboxes, exitCompareMode, clearBulkSelection])
|
|
5786
|
+
|
|
5787
|
+
const isCheckboxMode = canBulkSelect && !compareMode && showCheckboxes
|
|
5788
|
+
const showBulkBar = isCheckboxMode && checkedItems.length > 0
|
|
5593
5789
|
|
|
5594
5790
|
// Stable row handlers so the memoized ResourceRowCells skips re-rendering
|
|
5595
5791
|
// rows whose data didn't change on a refetch (React Query's structural
|
|
5596
5792
|
// sharing keeps unchanged resources referentially identical).
|
|
5597
5793
|
const handleRowClick = useCallback((resource: any, isSelected: boolean) => {
|
|
5598
5794
|
if (compareMode) toggleComparePick(resource)
|
|
5599
|
-
else if (isCheckboxMode) toggleChecked(resource)
|
|
5600
5795
|
else selectResource(resource, isSelected)
|
|
5601
|
-
}, [compareMode,
|
|
5796
|
+
}, [compareMode, toggleComparePick, selectResource])
|
|
5602
5797
|
const handleRowMouseEnter = useCallback(() => setHighlightedIndex(-1), [])
|
|
5603
5798
|
|
|
5604
5799
|
// Filter columns by visibility
|
|
@@ -5660,7 +5855,7 @@ export function ResourcesView({
|
|
|
5660
5855
|
|
|
5661
5856
|
// Calculate filter options with counts based on current resources (before filtering)
|
|
5662
5857
|
const filterOptions = useMemo(() => {
|
|
5663
|
-
if (!resources || resources.length === 0) return null
|
|
5858
|
+
if (!tableActive || !resources || resources.length === 0) return null
|
|
5664
5859
|
|
|
5665
5860
|
const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
|
|
5666
5861
|
const cellFilterKind = getCellFilterKind(selectedKind.name, selectedKind.group)
|
|
@@ -5690,7 +5885,7 @@ export function ResourcesView({
|
|
|
5690
5885
|
// Count distinct values for this column
|
|
5691
5886
|
const valueCounts: Record<string, number> = {}
|
|
5692
5887
|
for (const r of resources) {
|
|
5693
|
-
const val = extra?.getFilterValue ? extra.getFilterValue(r) :
|
|
5888
|
+
const val = extra?.getFilterValue ? extra.getFilterValue(r) : getDisplayFilterValue(r, col.key, cellFilterKind)
|
|
5694
5889
|
if (val) {
|
|
5695
5890
|
valueCounts[val] = (valueCounts[val] || 0) + 1
|
|
5696
5891
|
}
|
|
@@ -5773,7 +5968,7 @@ export function ResourcesView({
|
|
|
5773
5968
|
|
|
5774
5969
|
if (filterableColumns.length === 0 && !problems && labelValues.length === 0) return null
|
|
5775
5970
|
return { columns: filterableColumns, problems, labels: labelValues }
|
|
5776
|
-
}, [resources, selectedKind.name, selectedKind.group, allColumns])
|
|
5971
|
+
}, [tableActive, resources, selectedKind.name, selectedKind.group, allColumns, getDisplayFilterValue])
|
|
5777
5972
|
|
|
5778
5973
|
// Map filterable columns by key for O(1) lookup in header rendering
|
|
5779
5974
|
const filterableColumnMap = useMemo(() => {
|
|
@@ -5795,15 +5990,16 @@ export function ResourcesView({
|
|
|
5795
5990
|
// Namespace contribution gated on a host-wired clearer: without it the
|
|
5796
5991
|
// Clear filters button can't drop the namespace, so showing it would be
|
|
5797
5992
|
// a no-op for that case.
|
|
5798
|
-
const
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5993
|
+
const activeFilterCount =
|
|
5994
|
+
Object.values(columnFilters).filter((vals) => vals.length > 0).length +
|
|
5995
|
+
(problemFilters.length > 0 ? 1 : 0) +
|
|
5996
|
+
(labelSelector ? 1 : 0) +
|
|
5997
|
+
(searchTerm || regexMode ? 1 : 0) +
|
|
5998
|
+
(hasOwnerFilter ? 1 : 0) +
|
|
5999
|
+
(showInactiveReplicaSets ? 1 : 0) +
|
|
6000
|
+
(!!onClearNamespaces && namespaces.length > 0 ? 1 : 0)
|
|
6001
|
+
const hasAnyFilter = activeFilterCount > 0
|
|
6002
|
+
const filtersRestored = !!kindFiltersSearch && kindFiltersSearch === restoredKindSearch
|
|
5807
6003
|
|
|
5808
6004
|
|
|
5809
6005
|
// Toggle problem filter
|
|
@@ -5847,9 +6043,11 @@ export function ResourcesView({
|
|
|
5847
6043
|
certExpiry,
|
|
5848
6044
|
certExpiryError,
|
|
5849
6045
|
auditBadges,
|
|
6046
|
+
operationalIssues,
|
|
6047
|
+
operationalIssuesIncomplete,
|
|
5850
6048
|
onOpenLogs,
|
|
5851
6049
|
onOpenWorkloadLogs,
|
|
5852
|
-
}), [onNavigate, certExpiry, certExpiryError, auditBadges, onOpenLogs, onOpenWorkloadLogs])
|
|
6050
|
+
}), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
|
|
5853
6051
|
|
|
5854
6052
|
return (
|
|
5855
6053
|
<ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
|
|
@@ -5879,7 +6077,9 @@ export function ResourcesView({
|
|
|
5879
6077
|
/>
|
|
5880
6078
|
)}
|
|
5881
6079
|
|
|
5882
|
-
{
|
|
6080
|
+
{kindView !== null ? (
|
|
6081
|
+
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">{kindView}</div>
|
|
6082
|
+
) : (
|
|
5883
6083
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
5884
6084
|
{/* Toolbar */}
|
|
5885
6085
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0">
|
|
@@ -6119,10 +6319,10 @@ export function ResourcesView({
|
|
|
6119
6319
|
<button
|
|
6120
6320
|
type="button"
|
|
6121
6321
|
onClick={clearAllFilters}
|
|
6122
|
-
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs
|
|
6322
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs selection selection-text hover:selection-strong transition-colors"
|
|
6123
6323
|
>
|
|
6124
6324
|
<RotateCcw className="w-3.5 h-3.5" />
|
|
6125
|
-
<span>Clear filters</span>
|
|
6325
|
+
<span>Clear filters ({activeFilterCount})</span>
|
|
6126
6326
|
</button>
|
|
6127
6327
|
</Tooltip>
|
|
6128
6328
|
)}
|
|
@@ -6156,41 +6356,14 @@ export function ResourcesView({
|
|
|
6156
6356
|
{/* Column list scrolls; the add-column footer stays pinned so
|
|
6157
6357
|
its input isn't pushed below the fold on kinds with many columns. */}
|
|
6158
6358
|
<div className="overflow-y-auto flex-1 min-h-0">
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
<input
|
|
6168
|
-
type="checkbox"
|
|
6169
|
-
checked={visibleColumns.has(col.key)}
|
|
6170
|
-
onChange={() => toggleColumnVisibility(col.key)}
|
|
6171
|
-
disabled={col.key === 'name'}
|
|
6172
|
-
className="rounded border-theme-border"
|
|
6173
|
-
/>
|
|
6174
|
-
<span className={clsx(
|
|
6175
|
-
'text-sm truncate',
|
|
6176
|
-
col.key === 'name' ? 'text-theme-text-tertiary' : 'text-theme-text-primary'
|
|
6177
|
-
)} title={col.tooltip || col.label}>
|
|
6178
|
-
{col.label}
|
|
6179
|
-
</span>
|
|
6180
|
-
</label>
|
|
6181
|
-
{isCustom && (
|
|
6182
|
-
<button
|
|
6183
|
-
type="button"
|
|
6184
|
-
onClick={() => removeCustomColumn(col.key)}
|
|
6185
|
-
className="shrink-0 p-0.5 text-theme-text-tertiary hover:text-red-500 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
|
6186
|
-
title="Remove column"
|
|
6187
|
-
>
|
|
6188
|
-
<X className="w-3.5 h-3.5" />
|
|
6189
|
-
</button>
|
|
6190
|
-
)}
|
|
6191
|
-
</div>
|
|
6192
|
-
)
|
|
6193
|
-
})}
|
|
6359
|
+
<ColumnPickerList
|
|
6360
|
+
columns={allColumns}
|
|
6361
|
+
visibleColumns={visibleColumns}
|
|
6362
|
+
customColumnKeys={customColumnKeySet}
|
|
6363
|
+
onToggle={toggleColumnVisibility}
|
|
6364
|
+
onRemoveCustom={removeCustomColumn}
|
|
6365
|
+
onReorder={setColumnOrder}
|
|
6366
|
+
/>
|
|
6194
6367
|
</div>
|
|
6195
6368
|
{/* Add a label/annotation column */}
|
|
6196
6369
|
<div className="shrink-0 border-t border-theme-border pt-2 px-3 pb-1">
|
|
@@ -6260,7 +6433,7 @@ export function ResourcesView({
|
|
|
6260
6433
|
<button
|
|
6261
6434
|
onClick={() => {
|
|
6262
6435
|
if (compareMode) { exitCompareMode(); return }
|
|
6263
|
-
|
|
6436
|
+
clearBulkSelection()
|
|
6264
6437
|
setCompareMode(true)
|
|
6265
6438
|
}}
|
|
6266
6439
|
aria-pressed={compareMode}
|
|
@@ -6276,17 +6449,14 @@ export function ResourcesView({
|
|
|
6276
6449
|
</Tooltip>
|
|
6277
6450
|
)}
|
|
6278
6451
|
{canBulkSelect && (
|
|
6279
|
-
<Tooltip content={
|
|
6452
|
+
<Tooltip content={isCheckboxMode ? 'Hide selection checkboxes' : 'Show selection checkboxes'}>
|
|
6280
6453
|
<button
|
|
6281
|
-
onClick={
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
setBulkMode(true)
|
|
6285
|
-
}}
|
|
6286
|
-
aria-pressed={bulkMode}
|
|
6454
|
+
onClick={toggleCheckboxVisibility}
|
|
6455
|
+
aria-label={isCheckboxMode ? 'Hide selection checkboxes' : 'Show selection checkboxes'}
|
|
6456
|
+
aria-pressed={isCheckboxMode}
|
|
6287
6457
|
className={clsx(
|
|
6288
6458
|
'p-2 rounded-lg transition-colors',
|
|
6289
|
-
|
|
6459
|
+
isCheckboxMode
|
|
6290
6460
|
? 'bg-skyhook-500/15 text-skyhook-300 border border-skyhook-400/50'
|
|
6291
6461
|
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated',
|
|
6292
6462
|
)}
|
|
@@ -6306,7 +6476,7 @@ export function ResourcesView({
|
|
|
6306
6476
|
</div>
|
|
6307
6477
|
|
|
6308
6478
|
{/* Bulk actions bar */}
|
|
6309
|
-
{
|
|
6479
|
+
{showBulkBar && (
|
|
6310
6480
|
<div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
|
|
6311
6481
|
<span className="text-sm font-medium text-theme-text-primary">
|
|
6312
6482
|
{checkedItems.length} selected
|
|
@@ -6346,14 +6516,29 @@ export function ResourcesView({
|
|
|
6346
6516
|
)}
|
|
6347
6517
|
<button
|
|
6348
6518
|
type="button"
|
|
6349
|
-
onClick={
|
|
6519
|
+
onClick={clearBulkSelection}
|
|
6350
6520
|
className="px-3 py-1.5 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors"
|
|
6351
6521
|
>
|
|
6352
|
-
|
|
6522
|
+
Clear selection
|
|
6353
6523
|
</button>
|
|
6354
6524
|
</div>
|
|
6355
6525
|
)}
|
|
6356
6526
|
|
|
6527
|
+
{!isLoading && !isSelectedForbidden && !largeListGuard && filteredResources.length > 0 && (
|
|
6528
|
+
<ActiveFilterBar
|
|
6529
|
+
columnFilters={columnFilters}
|
|
6530
|
+
columnFilterExcludes={columnFilterExcludes}
|
|
6531
|
+
problemFilters={problemFilters}
|
|
6532
|
+
labelSelector={labelSelector}
|
|
6533
|
+
onClearColumn={clearColumnFilter}
|
|
6534
|
+
onClearProblems={() => setProblemFilters([])}
|
|
6535
|
+
onClearLabels={() => setLabelSelector('')}
|
|
6536
|
+
restored={filtersRestored}
|
|
6537
|
+
onClearAll={clearKindFilters}
|
|
6538
|
+
className="shrink-0 px-4 py-1.5 border-b border-theme-border"
|
|
6539
|
+
/>
|
|
6540
|
+
)}
|
|
6541
|
+
|
|
6357
6542
|
{/* Table */}
|
|
6358
6543
|
<div
|
|
6359
6544
|
className="flex-1 overflow-auto relative"
|
|
@@ -6430,46 +6615,17 @@ export function ResourcesView({
|
|
|
6430
6615
|
</button>
|
|
6431
6616
|
)}
|
|
6432
6617
|
{namespaces.length > 0 && <p className="text-sm mt-1 text-theme-text-disabled">Searching in {namespaces.length === 1 ? `namespace: ${namespaces[0]}` : `${namespaces.length} namespaces`}</p>}
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
>
|
|
6445
|
-
<ListFilter className="w-3 h-3" />
|
|
6446
|
-
<span>{key}: {columnFilterExcludes[key] ? 'not ' : ''}{vals.join(', ')}</span>
|
|
6447
|
-
<X className="w-3 h-3" />
|
|
6448
|
-
</button>
|
|
6449
|
-
))}
|
|
6450
|
-
{problemFilters.length > 0 && (
|
|
6451
|
-
<button
|
|
6452
|
-
onClick={() => setProblemFilters([])}
|
|
6453
|
-
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-500/15 text-red-700 dark:text-red-300 rounded-md hover:bg-red-500/25 transition-colors"
|
|
6454
|
-
>
|
|
6455
|
-
<AlertTriangle className="w-3 h-3" />
|
|
6456
|
-
<span>Problems: {problemFilters.join(', ')}</span>
|
|
6457
|
-
<X className="w-3 h-3" />
|
|
6458
|
-
</button>
|
|
6459
|
-
)}
|
|
6460
|
-
{labelSelector && (
|
|
6461
|
-
<button
|
|
6462
|
-
onClick={() => setLabelSelector('')}
|
|
6463
|
-
className="flex items-center gap-1 px-2 py-1 text-xs bg-green-500/15 text-green-700 dark:text-green-300 rounded-md hover:bg-green-500/25 transition-colors"
|
|
6464
|
-
>
|
|
6465
|
-
<Tag className="w-3 h-3" />
|
|
6466
|
-
<span>{labelSelector}</span>
|
|
6467
|
-
<X className="w-3 h-3" />
|
|
6468
|
-
</button>
|
|
6469
|
-
)}
|
|
6470
|
-
</div>
|
|
6471
|
-
)
|
|
6472
|
-
})()}
|
|
6618
|
+
<ActiveFilterBar
|
|
6619
|
+
columnFilters={columnFilters}
|
|
6620
|
+
columnFilterExcludes={columnFilterExcludes}
|
|
6621
|
+
problemFilters={problemFilters}
|
|
6622
|
+
labelSelector={labelSelector}
|
|
6623
|
+
onClearColumn={clearColumnFilter}
|
|
6624
|
+
onClearProblems={() => setProblemFilters([])}
|
|
6625
|
+
onClearLabels={() => setLabelSelector('')}
|
|
6626
|
+
restored={filtersRestored}
|
|
6627
|
+
className="mt-3"
|
|
6628
|
+
/>
|
|
6473
6629
|
{hasAnyFilter && (
|
|
6474
6630
|
<button
|
|
6475
6631
|
type="button"
|
|
@@ -6697,6 +6853,22 @@ export function ResourcesView({
|
|
|
6697
6853
|
<div className="px-3 py-2 text-xs text-theme-text-disabled">No matches</div>
|
|
6698
6854
|
)}
|
|
6699
6855
|
</div>
|
|
6856
|
+
{col.key === 'namespace' && onApplyNamespacesToAllSections && activeFilterValues.length > 0 && !columnFilterExcludes[col.key] && (
|
|
6857
|
+
<div className="p-1 border-t border-theme-border">
|
|
6858
|
+
<button
|
|
6859
|
+
onClick={() => {
|
|
6860
|
+
setOpenColumnFilter(null)
|
|
6861
|
+
// Keep the column filter until the scope change lands; a failed one keeps the user's filter.
|
|
6862
|
+
Promise.resolve(onApplyNamespacesToAllSections(activeFilterValues))
|
|
6863
|
+
.then(() => clearColumnFilter(col.key), () => {})
|
|
6864
|
+
}}
|
|
6865
|
+
className="w-full text-left px-2 py-1.5 text-xs rounded text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary transition-colors"
|
|
6866
|
+
title="Scope every section to these namespaces in the header namespace picker"
|
|
6867
|
+
>
|
|
6868
|
+
Apply to all sections
|
|
6869
|
+
</button>
|
|
6870
|
+
</div>
|
|
6871
|
+
)}
|
|
6700
6872
|
</div>
|
|
6701
6873
|
)
|
|
6702
6874
|
})()}
|
|
@@ -6775,17 +6947,18 @@ export function ResourcesView({
|
|
|
6775
6947
|
/>
|
|
6776
6948
|
)}
|
|
6777
6949
|
</div>
|
|
6950
|
+
)}
|
|
6778
6951
|
</div>
|
|
6779
6952
|
|
|
6780
6953
|
{/* Bulk delete confirmation */}
|
|
6781
6954
|
<ConfirmDialog
|
|
6782
|
-
open={showBulkDeleteConfirm}
|
|
6955
|
+
open={tableActive && showBulkDeleteConfirm}
|
|
6783
6956
|
onClose={() => { setShowBulkDeleteConfirm(false); setBulkForceDelete(false) }}
|
|
6784
6957
|
onConfirm={() => {
|
|
6785
6958
|
onBulkDelete?.(checkedBulkItems, {
|
|
6786
6959
|
force: bulkForceDelete,
|
|
6787
6960
|
onSuccess: () => {
|
|
6788
|
-
|
|
6961
|
+
clearBulkSelection()
|
|
6789
6962
|
setShowBulkDeleteConfirm(false)
|
|
6790
6963
|
setBulkForceDelete(false)
|
|
6791
6964
|
},
|
|
@@ -6810,12 +6983,12 @@ export function ResourcesView({
|
|
|
6810
6983
|
</label>
|
|
6811
6984
|
</ConfirmDialog>
|
|
6812
6985
|
<ConfirmDialog
|
|
6813
|
-
open={showBulkRestartConfirm}
|
|
6986
|
+
open={tableActive && showBulkRestartConfirm}
|
|
6814
6987
|
onClose={() => setShowBulkRestartConfirm(false)}
|
|
6815
6988
|
onConfirm={() => {
|
|
6816
6989
|
onBulkRestart?.(checkedBulkItems, {
|
|
6817
6990
|
onSuccess: () => {
|
|
6818
|
-
|
|
6991
|
+
clearBulkSelection()
|
|
6819
6992
|
setShowBulkRestartConfirm(false)
|
|
6820
6993
|
},
|
|
6821
6994
|
})
|
|
@@ -6829,12 +7002,12 @@ export function ResourcesView({
|
|
|
6829
7002
|
isClosable
|
|
6830
7003
|
/>
|
|
6831
7004
|
<ConfirmDialog
|
|
6832
|
-
open={showBulkScaleDialog}
|
|
7005
|
+
open={tableActive && showBulkScaleDialog}
|
|
6833
7006
|
onClose={() => setShowBulkScaleDialog(false)}
|
|
6834
7007
|
onConfirm={() => {
|
|
6835
7008
|
onBulkScale?.(checkedBulkItems, bulkScaleReplicas, {
|
|
6836
7009
|
onSuccess: () => {
|
|
6837
|
-
|
|
7010
|
+
clearBulkSelection()
|
|
6838
7011
|
setShowBulkScaleDialog(false)
|
|
6839
7012
|
},
|
|
6840
7013
|
})
|
|
@@ -7235,6 +7408,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7235
7408
|
case 'grpcroutes':
|
|
7236
7409
|
case 'tcproutes':
|
|
7237
7410
|
case 'tlsroutes':
|
|
7411
|
+
case 'udproutes':
|
|
7238
7412
|
return <RouteCell resource={resource} column={column} />
|
|
7239
7413
|
case 'gatewayclasses':
|
|
7240
7414
|
return <GatewayClassCell resource={resource} column={column} />
|
|
@@ -7925,9 +8099,9 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
7925
8099
|
const label = isCPU ? 'CPU' : 'Memory'
|
|
7926
8100
|
const format = isCPU ? formatCPU : formatMemoryShort
|
|
7927
8101
|
|
|
7928
|
-
// Multi-container pods
|
|
7929
|
-
// pods fall back to the
|
|
7930
|
-
//
|
|
8102
|
+
// Multi-container pods (and any pod with a pod-level budget) carry a
|
|
8103
|
+
// per-container breakdown; other single-container pods fall back to the
|
|
8104
|
+
// pod-level fields as one synthetic container so the logic is uniform.
|
|
7931
8105
|
const list: ContainerResourceMetrics[] = (m.containers && m.containers.length > 0)
|
|
7932
8106
|
? m.containers
|
|
7933
8107
|
: [{
|
|
@@ -7936,7 +8110,15 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
7936
8110
|
memory: m.memory, memoryRequest: m.memoryRequest, memoryLimit: m.memoryLimit,
|
|
7937
8111
|
}]
|
|
7938
8112
|
|
|
7939
|
-
|
|
8113
|
+
// The pod totals already carry any pod-level value (overlaid server-side
|
|
8114
|
+
// with exact quantity parsing); spec.resources only says whether one is set.
|
|
8115
|
+
const podLevel = resource.spec?.resources
|
|
8116
|
+
const podRequest = podLevel?.requests?.[kind] ? (isCPU ? m.cpuRequest : m.memoryRequest) : undefined
|
|
8117
|
+
const podLimit = podLevel?.limits?.[kind] ? (isCPU ? m.cpuLimit : m.memoryLimit) : undefined
|
|
8118
|
+
const { mode, totalUsage, denom, markerPct, unlimitedCount } = podAggregate(list, kind, {
|
|
8119
|
+
request: podRequest,
|
|
8120
|
+
limit: podLimit,
|
|
8121
|
+
})
|
|
7940
8122
|
if (totalUsage === 0) return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
7941
8123
|
|
|
7942
8124
|
// Pod-vs-node context: how full the pod's node is (the risk signal) and
|
|
@@ -7956,7 +8138,12 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
7956
8138
|
}
|
|
7957
8139
|
: undefined
|
|
7958
8140
|
|
|
7959
|
-
|
|
8141
|
+
// The synthetic single-container row already carries the pod totals, so
|
|
8142
|
+
// only a real per-container breakdown needs the shared budget spelled out.
|
|
8143
|
+
const podBudget = podLimit && m.containers?.length
|
|
8144
|
+
? { usage: totalUsage, limit: podLimit, request: podRequest }
|
|
8145
|
+
: undefined
|
|
8146
|
+
const tip = buildContainerResourceTooltip(label, list, kind, format, nodeCtx, podBudget)
|
|
7960
8147
|
|
|
7961
8148
|
// Every container is limited → aggregate bar against the summed limit; a
|
|
7962
8149
|
// real pod-level ceiling, so it may go red.
|
|
@@ -8452,6 +8639,7 @@ function JobCell({ resource, column }: { resource: any; column: string }) {
|
|
|
8452
8639
|
}
|
|
8453
8640
|
|
|
8454
8641
|
function CronJobCell({ resource, column }: { resource: any; column: string }) {
|
|
8642
|
+
const { operationalIssues, operationalIssuesIncomplete } = useContext(ResourcesViewDataContext)
|
|
8455
8643
|
switch (column) {
|
|
8456
8644
|
case 'schedule': {
|
|
8457
8645
|
const { cron, readable } = getCronJobSchedule(resource)
|
|
@@ -8463,6 +8651,13 @@ function CronJobCell({ resource, column }: { resource: any; column: string }) {
|
|
|
8463
8651
|
)
|
|
8464
8652
|
}
|
|
8465
8653
|
case 'status': {
|
|
8654
|
+
const issue = operationalIssues?.[`${resource.metadata?.namespace ?? ''}/${resource.metadata?.name ?? ''}`]
|
|
8655
|
+
if (issue) {
|
|
8656
|
+
return <Tooltip content={issue.message}><Badge severity={issue.severity === 'critical' ? 'error' : 'warning'}>{issueTitle(issue)}</Badge></Tooltip>
|
|
8657
|
+
}
|
|
8658
|
+
if (operationalIssuesIncomplete) {
|
|
8659
|
+
return <Tooltip content="The issue list is incomplete. This CronJob's status could not be checked."><Badge severity="neutral">Unknown</Badge></Tooltip>
|
|
8660
|
+
}
|
|
8466
8661
|
const status = getCronJobStatus(resource)
|
|
8467
8662
|
return (
|
|
8468
8663
|
<span className={clsx('badge', status.color)}>
|
|
@@ -8581,12 +8776,16 @@ interface PodAggregate {
|
|
|
8581
8776
|
// headline renders — total usage measured against a pod-level yardstick. It
|
|
8582
8777
|
// keeps the dominant consumer visible instead of demoting it behind a small
|
|
8583
8778
|
// limited container, and stays honest about partial limits (which don't form a
|
|
8584
|
-
// real ceiling).
|
|
8585
|
-
|
|
8779
|
+
// real ceiling). A pod-level budget (spec.resources) bounds every container, so
|
|
8780
|
+
// its limit is a real ceiling regardless of what the containers set.
|
|
8781
|
+
export function podAggregate(
|
|
8782
|
+
list: ContainerResourceMetrics[],
|
|
8783
|
+
kind: 'cpu' | 'memory',
|
|
8784
|
+
podLevel?: { request?: number; limit?: number },
|
|
8785
|
+
): PodAggregate {
|
|
8586
8786
|
const isCPU = kind === 'cpu'
|
|
8587
8787
|
let totalUsage = 0
|
|
8588
8788
|
let limitedCount = 0
|
|
8589
|
-
let requestedCount = 0
|
|
8590
8789
|
let unlimitedCount = 0
|
|
8591
8790
|
let summedLimit = 0
|
|
8592
8791
|
let summedRequest = 0
|
|
@@ -8601,25 +8800,28 @@ export function podAggregate(list: ContainerResourceMetrics[], kind: 'cpu' | 'me
|
|
|
8601
8800
|
} else {
|
|
8602
8801
|
unlimitedCount++
|
|
8603
8802
|
}
|
|
8604
|
-
if (request > 0)
|
|
8605
|
-
requestedCount++
|
|
8606
|
-
summedRequest += request
|
|
8607
|
-
}
|
|
8803
|
+
if (request > 0) summedRequest += request
|
|
8608
8804
|
}
|
|
8805
|
+
if (podLevel?.request) summedRequest = podLevel.request
|
|
8609
8806
|
const allLimited = list.length > 0 && limitedCount === list.length
|
|
8610
|
-
|
|
8807
|
+
// Container limits still apply inside a pod-level budget, so when every
|
|
8808
|
+
// container has one the tighter of the two totals is the real ceiling.
|
|
8809
|
+
const ceiling = podLevel?.limit && allLimited
|
|
8810
|
+
? Math.min(podLevel.limit, summedLimit)
|
|
8811
|
+
: podLevel?.limit || (allLimited ? summedLimit : 0)
|
|
8812
|
+
if (ceiling) {
|
|
8611
8813
|
return {
|
|
8612
8814
|
mode: 'limit',
|
|
8613
8815
|
totalUsage,
|
|
8614
|
-
denom:
|
|
8615
|
-
markerPct: summedRequest > 0 ? (summedRequest /
|
|
8816
|
+
denom: ceiling,
|
|
8817
|
+
markerPct: summedRequest > 0 ? (summedRequest / ceiling) * 100 : undefined,
|
|
8616
8818
|
unlimitedCount,
|
|
8617
8819
|
}
|
|
8618
8820
|
}
|
|
8619
8821
|
if (limitedCount > 0) {
|
|
8620
8822
|
return { mode: 'partial', totalUsage, denom: 0, unlimitedCount }
|
|
8621
8823
|
}
|
|
8622
|
-
if (
|
|
8824
|
+
if (summedRequest > 0) {
|
|
8623
8825
|
return { mode: 'request', totalUsage, denom: summedRequest, unlimitedCount }
|
|
8624
8826
|
}
|
|
8625
8827
|
return { mode: 'none', totalUsage, denom: 0, unlimitedCount }
|
|
@@ -8647,12 +8849,13 @@ function formatSharePct(pct: number): string {
|
|
|
8647
8849
|
return `${Math.round(pct)}%`
|
|
8648
8850
|
}
|
|
8649
8851
|
|
|
8650
|
-
function buildContainerResourceTooltip(
|
|
8852
|
+
export function buildContainerResourceTooltip(
|
|
8651
8853
|
label: 'CPU' | 'Memory',
|
|
8652
8854
|
containers: ContainerResourceMetrics[],
|
|
8653
8855
|
kind: 'cpu' | 'memory',
|
|
8654
8856
|
formatFn: (n: number) => string,
|
|
8655
8857
|
nodeCtx?: NodeContext,
|
|
8858
|
+
podBudget?: { usage: number; limit: number; request?: number },
|
|
8656
8859
|
) {
|
|
8657
8860
|
const rows = [...containers].sort((a, b) => {
|
|
8658
8861
|
const diff = readContainer(b, kind).pct - readContainer(a, kind).pct
|
|
@@ -8675,7 +8878,7 @@ function buildContainerResourceTooltip(
|
|
|
8675
8878
|
? `${formatFn(r.usage)} · ${Math.round(r.pct)}% · ${formatFn(r.denom)} limit${r.request > 0 ? ` · req ${formatFn(r.request)}` : ''}`
|
|
8676
8879
|
: r.yardstick === 'request'
|
|
8677
8880
|
? `${formatFn(r.usage)} · ${Math.round(r.pct)}% · ${formatFn(r.denom)} request`
|
|
8678
|
-
: `${formatFn(r.usage)} · no limit`
|
|
8881
|
+
: `${formatFn(r.usage)} · ${podBudget ? 'pod limit' : 'no limit'}`
|
|
8679
8882
|
return (
|
|
8680
8883
|
<div key={c.name} className="flex flex-col leading-snug">
|
|
8681
8884
|
<span className="text-[11px] text-theme-text-tertiary truncate">{c.name}</span>
|
|
@@ -8689,6 +8892,14 @@ function buildContainerResourceTooltip(
|
|
|
8689
8892
|
+{remaining} more → open pod
|
|
8690
8893
|
</div>
|
|
8691
8894
|
)}
|
|
8895
|
+
{podBudget && (
|
|
8896
|
+
<div className="flex flex-col leading-snug border-t border-theme-border/50 pt-1">
|
|
8897
|
+
<span className="text-[11px] text-theme-text-tertiary truncate">Pod (shared)</span>
|
|
8898
|
+
<span className="text-xs font-mono text-theme-text-primary">
|
|
8899
|
+
{`${formatFn(podBudget.usage)} · ${Math.round((podBudget.usage / podBudget.limit) * 100)}% · ${formatFn(podBudget.limit)} limit${podBudget.request ? ` · req ${formatFn(podBudget.request)}` : ''}`}
|
|
8900
|
+
</span>
|
|
8901
|
+
</div>
|
|
8902
|
+
)}
|
|
8692
8903
|
{nodeCtx && (
|
|
8693
8904
|
<div className="flex flex-col leading-snug border-t border-theme-border/50 pt-1">
|
|
8694
8905
|
<span className="text-[11px] text-theme-text-tertiary truncate">Node {nodeCtx.name}</span>
|
|
@@ -9130,7 +9341,7 @@ function GatewayClassCell({ resource, column }: { resource: any; column: string
|
|
|
9130
9341
|
}
|
|
9131
9342
|
}
|
|
9132
9343
|
|
|
9133
|
-
// Shared cell renderer for all Gateway API route types (HTTPRoute, GRPCRoute, TCPRoute, TLSRoute)
|
|
9344
|
+
// Shared cell renderer for all Gateway API route types (HTTPRoute, GRPCRoute, TCPRoute, TLSRoute, UDPRoute)
|
|
9134
9345
|
function RouteCell({ resource, column }: { resource: any; column: string }) {
|
|
9135
9346
|
switch (column) {
|
|
9136
9347
|
case 'status': {
|