@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.
Files changed (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. package/src/utils/view-transition.ts +16 -1
@@ -137,11 +137,16 @@ import {
137
137
  parseColumnFilters,
138
138
  serializeColumnFilters,
139
139
  parseColumnFilterExcludes,
140
+ kindFiltersFromSearch,
141
+ kindFiltersToSearch,
142
+ columnFiltersParam,
143
+ RESOURCE_LIST_FILTER_PARAMS,
140
144
  podMatchesProblemCategory,
141
145
  SEVERITY_DOT_COLOR,
142
146
  healthColors,
143
147
  } from './resource-utils'
144
148
  import { getGenericResourceStatus } from './generic-status'
149
+ import { ActiveFilterBar } from './ActiveFilterBar'
145
150
  import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
146
151
  import {
147
152
  type PrinterColumnDef, type PrinterTable, PRINTER_COLUMN_PREFIX, formatPrinterCell, printerCellSortValue,
@@ -203,7 +208,7 @@ import { GCPManagedControlPlaneCell, GCPManagedMachinePoolCell, GCPMachineCell,
203
208
  import { AzureManagedControlPlaneCell, AzureManagedMachinePoolCell, AzureMachineCell, AzureMachineTemplateCell, AzureManagedClusterCell } from './renderers/azure-capi-cells'
204
209
  import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
205
210
  import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
206
- import { useRegisterShortcut, useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
211
+ import { useRegisterShortcut, useRegisterShortcuts, type KeyboardShortcut } from '../../hooks/useKeyboardShortcuts'
207
212
  import { ResourcesSidebar } from './ResourcesSidebar'
208
213
  import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
209
214
  import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
@@ -1460,6 +1465,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1460
1465
  { key: 'rules', label: 'Rules', width: 'w-20' },
1461
1466
  { key: 'age', label: 'Age', width: 'w-24' },
1462
1467
  ],
1468
+ udproutes: [
1469
+ { key: 'name', label: 'Name' },
1470
+ { key: 'namespace', label: 'Namespace', width: 'w-48' },
1471
+ { 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.' },
1472
+ { key: 'parents', label: 'Gateways', width: 'w-36' },
1473
+ { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1474
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1475
+ { key: 'age', label: 'Age', width: 'w-24' },
1476
+ ],
1463
1477
  tlsroutes: [
1464
1478
  { key: 'name', label: 'Name' },
1465
1479
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
@@ -2884,6 +2898,7 @@ const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
2884
2898
  grpcroutes: ['gateway.networking.k8s.io'],
2885
2899
  httproutes: ['gateway.networking.k8s.io'],
2886
2900
  tcproutes: ['gateway.networking.k8s.io'],
2901
+ udproutes: ['gateway.networking.k8s.io'],
2887
2902
  tlsroutes: ['gateway.networking.k8s.io'],
2888
2903
  helmreleases: ['helm.toolkit.fluxcd.io'],
2889
2904
  awsmachines: ['infrastructure.cluster.x-k8s.io'],
@@ -3415,6 +3430,11 @@ interface ResourcesViewProps {
3415
3430
  sidebarCategoryWorkspaces?: Record<string, SidebarCategoryWorkspace>
3416
3431
  /** Callback when the [+] create button is clicked. Receives the currently selected kind info. */
3417
3432
  onCreateResource?: (kind: { name: string; kind: string; group: string } | null) => void
3433
+ /** A kind's own view, rendered in place of the table: the sidebar, kind
3434
+ * selection and the drawer (`?resource=`) stay; the table, its toolbar and
3435
+ * its row/search/bulk shortcuts do not run. Sidebar kind navigation stays.
3436
+ * Return null or false to keep the table. */
3437
+ renderKindView?: (kind: SelectedKindInfo) => React.ReactNode | null
3418
3438
  /** Default kind when the URL does not include one. */
3419
3439
  defaultKind?: SelectedKindInfo
3420
3440
  /** Columns prepended to KNOWN_COLUMNS for every kind. For example, a
@@ -3480,6 +3500,29 @@ interface ResourcesViewProps {
3480
3500
  * the switcher lives outside this component and may persist server-side.
3481
3501
  */
3482
3502
  onClearNamespaces?: () => void
3503
+ /**
3504
+ * A kind's saved filters, as last reported through `onKindFiltersChange`
3505
+ * (host-owned, because this view unmounts on section switches). A user kind
3506
+ * switch, or a URL with no list state of its own (see `hasFilterIntent`),
3507
+ * restores that kind's column and problem filters from it. Pass undefined
3508
+ * until the host can answer: the first defined value is still applied to a
3509
+ * bare URL.
3510
+ */
3511
+ rememberedKindSearch?: (kind: { name: string; group: string }) => string | undefined
3512
+ /**
3513
+ * Called with a kind's serialized `filters` + `problems` query ('' when
3514
+ * none) whenever the filters the kind shows change, including the ones a
3515
+ * link arrives with. A list that arrives with no filters does not report
3516
+ * '', so a link to one resource keeps what was saved.
3517
+ */
3518
+ onKindFiltersChange?: (kind: { name: string; group: string }, search: string) => void
3519
+ /**
3520
+ * Promotes the Namespace column's included values to the global namespace
3521
+ * selection (replacing it). When wired, the column's filter dropdown offers
3522
+ * "Apply to all sections" and clears the column filter once the returned
3523
+ * promise resolves (a rejection keeps it).
3524
+ */
3525
+ onApplyNamespacesToAllSections?: (namespaces: string[]) => Promise<unknown> | void
3483
3526
  // Bulk operations
3484
3527
  onBulkDelete?: (items: BulkResourceItem[], options?: { force?: boolean; onSuccess?: () => void }) => void
3485
3528
  isBulkDeleting?: boolean
@@ -3603,19 +3646,24 @@ export function deriveSidebarResourceCounts(
3603
3646
  return results
3604
3647
  }
3605
3648
 
3649
+ const FILTER_INTENT_PARAMS = [...RESOURCE_LIST_FILTER_PARAMS, 'resource']
3650
+
3651
+ /**
3652
+ * Whether a URL states its own list state. Only a URL without any of these
3653
+ * (a bare `/resources/<kind>`) gets the kind's remembered filters.
3654
+ */
3655
+ export function hasFilterIntent(search: string): boolean {
3656
+ const params = new URLSearchParams(search)
3657
+ return FILTER_INTENT_PARAMS.some((key) => params.has(key))
3658
+ }
3659
+
3606
3660
  // Get initial filters from URL
3607
3661
  function getInitialFiltersFromURL() {
3608
3662
  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
3663
  const result = {
3614
3664
  search: params.get('search') || '',
3615
3665
  regex: params.get('regex') === 'true',
3616
- columnFilters,
3617
- columnFilterExcludes,
3618
- problemFilters: params.get('problems')?.split(',').filter(Boolean) || [],
3666
+ ...kindFiltersFromSearch(window.location.search),
3619
3667
  showInactive: params.get('showInactive') === 'true',
3620
3668
  labelSelector: params.get('labels') || '', // e.g., "app=caretta,version=v1"
3621
3669
  ownerKind: params.get('ownerKind') || '', // e.g., "DaemonSet"
@@ -3657,6 +3705,7 @@ export function ResourcesView({
3657
3705
  hideSidebar = false,
3658
3706
  sidebarCategoryWorkspaces,
3659
3707
  onCreateResource,
3708
+ renderKindView,
3660
3709
  defaultKind = DEFAULT_KIND_INFO,
3661
3710
  extraLeadingColumns,
3662
3711
  printerTable,
@@ -3665,6 +3714,9 @@ export function ResourcesView({
3665
3714
  onCompareSubmit,
3666
3715
  resolveRowCluster,
3667
3716
  onClearNamespaces,
3717
+ rememberedKindSearch,
3718
+ onKindFiltersChange,
3719
+ onApplyNamespacesToAllSections,
3668
3720
  onBulkDelete,
3669
3721
  isBulkDeleting = false,
3670
3722
  onBulkRestart,
@@ -3685,16 +3737,23 @@ export function ResourcesView({
3685
3737
  kindFromURL.name !== prev.name || kindFromURL.group !== prev.group ? kindFromURL : prev,
3686
3738
  )
3687
3739
  }, [basePath, defaultKind, locationPathname, locationSearch])
3740
+ const contributedKindView = renderKindView?.(selectedKind)
3741
+ const kindView = contributedKindView === false ? null : contributedKindView ?? null
3742
+ const tableActive = kindView === null
3743
+
3688
3744
  // Notify parent of selected kind changes (including initial mount)
3689
3745
  useEffect(() => {
3690
3746
  onSelectedKindChange?.(selectedKind)
3747
+ }, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
3748
+
3749
+ useEffect(() => {
3691
3750
  setBulkMode(false)
3692
3751
  setCheckedResources(new Set())
3693
3752
  setShowBulkDeleteConfirm(false)
3694
3753
  setShowBulkRestartConfirm(false)
3695
3754
  setShowBulkScaleDialog(false)
3696
3755
  setBulkForceDelete(false)
3697
- }, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
3756
+ }, [selectedKind.name, selectedKind.group, tableActive])
3698
3757
  const [searchTerm, setSearchTerm] = useState(initialFilters.search)
3699
3758
  // Typing must never wait on the list or the router. The input renders raw
3700
3759
  // searchTerm; filtering consumes the deferred copy (React yields to keep
@@ -3712,6 +3771,30 @@ export function ResourcesView({
3712
3771
  const [columnFilters, setColumnFilters] = useState<Record<string, string[]>>(initialFilters.columnFilters)
3713
3772
  const [columnFilterExcludes, setColumnFilterExcludes] = useState<Record<string, boolean>>(initialFilters.columnFilterExcludes)
3714
3773
  const [problemFilters, setProblemFilters] = useState<string[]>(initialFilters.problemFilters)
3774
+ // The kind the filter state belongs to. During a kind switch one render
3775
+ // pairs the new kind with the old kind's filters; saving then would file
3776
+ // them under the wrong kind.
3777
+ const [filtersKind, setFiltersKind] = useState(() => selectedKindIdentityOf(selectedKind))
3778
+ // Where the current filter state came from when it wasn't a user edit.
3779
+ const filtersArrivedRef = useRef<'url' | 'restore' | null>('url')
3780
+ // The serialized filters last restored from rememberedKindSearch ('' when none).
3781
+ const [restoredKindSearch, setRestoredKindSearch] = useState('')
3782
+ // A bare URL at mount takes the kind's saved filters. Adjusted during render
3783
+ // (not in an effect) so the unfiltered list never paints, and so it still
3784
+ // applies when the host's lookup arrives after mount.
3785
+ const [mountRestorePending, setMountRestorePending] = useState(() => !hasFilterIntent(locationSearch ?? window.location.search))
3786
+ if (mountRestorePending && rememberedKindSearch) {
3787
+ setMountRestorePending(false)
3788
+ const restored = kindFiltersToSearch(kindFiltersFromSearch(rememberedKindSearch(selectedKind)))
3789
+ if (restored && !hasFilterIntent(locationSearch ?? window.location.search)) {
3790
+ const remembered = kindFiltersFromSearch(restored)
3791
+ setColumnFilters(remembered.columnFilters)
3792
+ setColumnFilterExcludes(remembered.columnFilterExcludes)
3793
+ setProblemFilters(remembered.problemFilters)
3794
+ setRestoredKindSearch(restored)
3795
+ filtersArrivedRef.current = 'restore'
3796
+ }
3797
+ }
3715
3798
  const [openColumnFilter, setOpenColumnFilter] = useState<string | null>(null)
3716
3799
  const [columnFilterSearch, setColumnFilterSearch] = useState('')
3717
3800
  const columnFilterDropdownRef = useRef<HTMLDivElement>(null)
@@ -3935,6 +4018,8 @@ export function ResourcesView({
3935
4018
  // depends on visibleColumns) and instantly reverts the user's hide.
3936
4019
  const gpuAutoShownKinds = useRef<Set<string>>(new Set())
3937
4020
  useEffect(() => {
4021
+ // The table's own settings: nothing to load (or clear) while a kind view replaces it.
4022
+ if (!tableActive) return
3938
4023
  // Re-arm the skip-initial-save guard per kind. This effect repopulates
3939
4024
  // visible/widths/custom for the new kind via async setState, so the save
3940
4025
  // effect (also keyed to selectedKind) that runs in this same commit still
@@ -4019,10 +4104,11 @@ export function ResourcesView({
4019
4104
  // identity on every refetch, and re-running this effect would reset the
4020
4105
  // user's in-session column choices each time the list polls. The key only
4021
4106
  // changes when the kind changes or an operator upgrade changes the CRD.
4022
- }, [selectedKind.name, selectedKind.group, extraLeadingColumns, printerColumnsKey])
4107
+ }, [selectedKind.name, selectedKind.group, extraLeadingColumns, printerColumnsKey, tableActive])
4023
4108
 
4024
4109
  // Save column settings when they change (skip the initial load of each kind)
4025
4110
  useEffect(() => {
4111
+ if (!tableActive) return
4026
4112
  if (visibleColumns.size === 0) return // not loaded yet
4027
4113
  for (const k of allColumnKeys) knownColumnKeys.current.add(k)
4028
4114
  if (!isColumnSettingsLoaded.current) {
@@ -4039,7 +4125,7 @@ export function ResourcesView({
4039
4125
  // as across sessions, so a later data refresh can't override user choices.
4040
4126
  hadSavedColumnSettings.current = true
4041
4127
  // allColumnKeysSig, not allColumnKeys: see the memo above.
4042
- }, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group, allColumnKeysSig])
4128
+ }, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group, allColumnKeysSig, tableActive])
4043
4129
 
4044
4130
  // Close column picker on outside click or Escape
4045
4131
  useEffect(() => {
@@ -4208,6 +4294,7 @@ export function ResourcesView({
4208
4294
  category: 'Search',
4209
4295
  scope: 'resources',
4210
4296
  handler: () => searchInputRef.current?.focus(),
4297
+ enabled: tableActive,
4211
4298
  })
4212
4299
 
4213
4300
  // Keyboard navigation: highlighted row state
@@ -4227,7 +4314,7 @@ export function ResourcesView({
4227
4314
  useEffect(() => {
4228
4315
  setComparePicks([])
4229
4316
  setCompareMode(false)
4230
- }, [selectedKind.name, selectedKind.group])
4317
+ }, [selectedKind.name, selectedKind.group, tableActive])
4231
4318
 
4232
4319
  const exitCompareMode = useCallback(() => {
4233
4320
  setCompareMode(false)
@@ -4275,6 +4362,15 @@ export function ResourcesView({
4275
4362
  }
4276
4363
  }, [highlightedIndex])
4277
4364
 
4365
+ useEffect(() => {
4366
+ if (tableActive) return
4367
+ setHighlightedIndex(-1)
4368
+ setShowColumnPicker(false)
4369
+ setOpenColumnFilter(null)
4370
+ setShowProblemsDropdown(false)
4371
+ setShowLabelsDropdown(false)
4372
+ }, [tableActive])
4373
+
4278
4374
  // Open logs for pod / workload resources — provided via props
4279
4375
  const openLogs = onOpenLogs
4280
4376
  const openWorkloadLogs = onOpenWorkloadLogs
@@ -4307,8 +4403,7 @@ export function ResourcesView({
4307
4403
  onResourceClick?.(isSelected ? null : stripped)
4308
4404
  }, [onRowSelect, onResourceClick, selectedKind.name, selectedKind.group])
4309
4405
 
4310
- // Register navigation shortcuts
4311
- useRegisterShortcuts([
4406
+ useRegisterShortcuts(([
4312
4407
  {
4313
4408
  id: 'resources-nav-down',
4314
4409
  keys: 'j',
@@ -4484,7 +4579,7 @@ export function ResourcesView({
4484
4579
  else searchInputRef.current?.blur()
4485
4580
  },
4486
4581
  },
4487
- ])
4582
+ ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && (sc.enabled ?? true) })))
4488
4583
 
4489
4584
  // Refs for accessing filteredResources inside shortcuts (computed later in component)
4490
4585
  const filteredResourceCountRef = useRef(0)
@@ -4564,8 +4659,21 @@ export function ResourcesView({
4564
4659
  const newFilters = getInitialFiltersFromURL()
4565
4660
 
4566
4661
  // Update kind if it changed
4567
- if (newKind.name !== selectedKind.name || newKind.group !== selectedKind.group) {
4662
+ const kindChanged = newKind.name !== selectedKind.name || newKind.group !== selectedKind.group
4663
+ let restored = ''
4664
+ if (kindChanged) {
4568
4665
  setSelectedKind(newKind)
4666
+ // A bare URL for another kind links to the list, not to a state of it:
4667
+ // take that kind's saved filters and put them in the address bar. Never
4668
+ // for the same kind, where a bare URL is the echo of Clear filters.
4669
+ restored = hasFilterIntent(locationSearch ?? window.location.search)
4670
+ ? ''
4671
+ : kindFiltersToSearch(kindFiltersFromSearch(rememberedKindSearch?.(newKind)))
4672
+ setRestoredKindSearch(restored)
4673
+ if (restored) {
4674
+ Object.assign(newFilters, kindFiltersFromSearch(restored))
4675
+ updateURL(newKind, newFilters.search, newFilters.regex, newFilters.columnFilters, newFilters.columnFilterExcludes, newFilters.problemFilters, newFilters.showInactive)
4676
+ }
4569
4677
  }
4570
4678
 
4571
4679
  // Update owner filter if it changed
@@ -4606,6 +4714,13 @@ export function ResourcesView({
4606
4714
  setColumnFilterExcludes(newFilters.columnFilterExcludes)
4607
4715
  }
4608
4716
 
4717
+ if (newFilters.problemFilters.join(',') !== problemFilters.join(',')) {
4718
+ setProblemFilters(newFilters.problemFilters)
4719
+ }
4720
+ if (kindChanged || kindFiltersToSearch(newFilters) !== kindFiltersSearch) {
4721
+ filtersArrivedRef.current = restored ? 'restore' : 'url'
4722
+ }
4723
+
4609
4724
  // Reset the flag after a tick to allow normal URL updates
4610
4725
  requestAnimationFrame(() => {
4611
4726
  isSyncingFromURL.current = false
@@ -4660,11 +4775,7 @@ export function ResourcesView({
4660
4775
  }
4661
4776
  // Write column filters as `filters` param; the exclude operator is folded
4662
4777
  // into each column entry, so it can never drift from the values it negates.
4663
- // Guard against a stale exclude flag on a column with no active values.
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)
4778
+ const filtersStr = columnFiltersParam(colFilters, colExcludes)
4668
4779
  if (filtersStr) {
4669
4780
  params.set('filters', filtersStr)
4670
4781
  } else {
@@ -4736,13 +4847,19 @@ export function ResourcesView({
4736
4847
  // params are out of scope here; the host's onClearNamespaces (and its
4737
4848
  // own state→URL sync) owns namespace cleanup.
4738
4849
  const params = new URLSearchParams(window.location.search)
4739
- for (const key of ['search', 'regex', 'filters', 'problems', 'labels', 'ownerKind', 'ownerName', 'showInactive']) {
4850
+ for (const key of RESOURCE_LIST_FILTER_PARAMS) {
4740
4851
  params.delete(key)
4741
4852
  }
4742
4853
  navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
4743
4854
  onClearNamespaces?.()
4744
4855
  }, [navigate, onClearNamespaces])
4745
4856
 
4857
+ const clearKindFilters = useCallback(() => {
4858
+ setColumnFilters({})
4859
+ setColumnFilterExcludes({})
4860
+ setProblemFilters([])
4861
+ }, [])
4862
+
4746
4863
  // Update URL when any filter changes
4747
4864
  useEffect(() => {
4748
4865
  // Skip URL update if we're syncing FROM the URL (e.g., browser back button)
@@ -4755,6 +4872,8 @@ export function ResourcesView({
4755
4872
  prevSelectedResourceRef.current = selectedResource ?? null
4756
4873
  return
4757
4874
  }
4875
+ // Right after a kind switch the filters are still the previous kind's.
4876
+ if (filtersKind !== selectedKindIdentityOf(selectedKind)) return
4758
4877
  // Skip URL update if selectedResource's kind doesn't match selectedKind (still syncing)
4759
4878
  if (selectedResource) {
4760
4879
  const resourceKindLower = selectedResource.kind.toLowerCase()
@@ -4780,7 +4899,12 @@ export function ResourcesView({
4780
4899
  : typeof window !== 'undefined'
4781
4900
  ? window.location.pathname
4782
4901
  : ''
4783
- const pathChanged = currentPath !== targetPath
4902
+ // The router commits a navigation as a transition, so the injected path can
4903
+ // still be the previous kind's right after a push to this one (e.g. the
4904
+ // write of filters restored on a kind switch); the address bar is already
4905
+ // there, and pushing again would add a second entry for the same visit.
4906
+ const pathChanged = currentPath !== targetPath &&
4907
+ !(typeof window !== 'undefined' && window.location.pathname.endsWith(targetPath))
4784
4908
  const prev = prevSelectedResourceRef.current
4785
4909
  const current = selectedResource ?? null
4786
4910
  const drawerSwitched =
@@ -4794,7 +4918,7 @@ export function ResourcesView({
4794
4918
  prevSelectedResourceRef.current = current
4795
4919
 
4796
4920
  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])
4921
+ }, [selectedKind, filtersKind, debouncedSearchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
4798
4922
 
4799
4923
  // Handle resource click from URL on mount
4800
4924
  useEffect(() => {
@@ -4815,6 +4939,12 @@ export function ResourcesView({
4815
4939
  // Signal that initial resource param has been processed — URL update effect can now run
4816
4940
  hasProcessedInitialResource.current = true
4817
4941
 
4942
+ // Filters restored at mount go into the address bar (with the kind segment).
4943
+ if (restoredKindSearch) {
4944
+ updateURL(selectedKind, searchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets)
4945
+ return
4946
+ }
4947
+
4818
4948
  // If the URL has no kind segment (e.g., /resources), update to include the default kind.
4819
4949
  // Route through `navigate` so an onNavigate-suppressing host can opt out.
4820
4950
  const base = basePath.replace(/\/$/, '')
@@ -5083,7 +5213,7 @@ export function ResourcesView({
5083
5213
  isForbiddenError(selectedQueryError) ||
5084
5214
  (!selectedHasRows && !selectedQueryError && forbiddenKinds.has(selectedKindCountKey))
5085
5215
 
5086
- // Reset filters when kind changes (but not when syncing from URL navigation)
5216
+ // On a kind change, restore that kind's remembered filters, or start empty (but not when syncing from URL navigation)
5087
5217
  // Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
5088
5218
  // Keyed on group as well as plural: two CRDs can share a plural across API
5089
5219
  // groups, and their printer columns are unrelated. Resetting on the plural
@@ -5097,12 +5227,39 @@ export function ResourcesView({
5097
5227
  }
5098
5228
  prevKindRef.current = selectedKindIdentity
5099
5229
  setOpenColumnFilter(null)
5100
- if (!isSyncingFromURL.current) {
5101
- setColumnFilters({})
5102
- setColumnFilterExcludes({})
5103
- }
5104
- setProblemFilters([])
5105
- }, [selectedKindIdentity])
5230
+ setFiltersKind(selectedKindIdentity)
5231
+ // The URL sync already set this kind's filters (and restoredKindSearch).
5232
+ if (isSyncingFromURL.current) return
5233
+ filtersArrivedRef.current = 'restore'
5234
+ const restored = kindFiltersToSearch(kindFiltersFromSearch(rememberedKindSearch?.(selectedKind)))
5235
+ const remembered = kindFiltersFromSearch(restored)
5236
+ setColumnFilters(remembered.columnFilters)
5237
+ setColumnFilterExcludes(remembered.columnFilterExcludes)
5238
+ setProblemFilters(remembered.problemFilters)
5239
+ setRestoredKindSearch(restored)
5240
+ }, [selectedKindIdentity, selectedKind, rememberedKindSearch])
5241
+
5242
+ const kindFiltersSearch = useMemo(
5243
+ () => kindFiltersToSearch({ columnFilters, columnFilterExcludes, problemFilters }),
5244
+ [columnFilters, columnFilterExcludes, problemFilters],
5245
+ )
5246
+ // Saved filters are the last ones chosen for a kind: by the user, or by a
5247
+ // link that brought its own. A restore only shows what was saved (saving it
5248
+ // again would overwrite another tab's newer choice); a list that arrives
5249
+ // with no filters (a link to one resource, an owner's pods) keeps what was
5250
+ // saved; Clear filters saves none.
5251
+ const lastSeenKindFiltersRef = useRef<{ kind: string; search: string } | null>(null)
5252
+ useEffect(() => {
5253
+ // The host may wire saving after mount; until then nothing counts as seen.
5254
+ if (!onKindFiltersChange || filtersKind !== selectedKindIdentity) return
5255
+ const last = lastSeenKindFiltersRef.current
5256
+ lastSeenKindFiltersRef.current = { kind: filtersKind, search: kindFiltersSearch }
5257
+ const arrived = filtersArrivedRef.current
5258
+ filtersArrivedRef.current = null
5259
+ if (last?.kind === filtersKind && last.search === kindFiltersSearch) return
5260
+ if (arrived === 'restore' || (arrived === 'url' && !kindFiltersSearch)) return
5261
+ onKindFiltersChange(selectedKind, kindFiltersSearch)
5262
+ }, [filtersKind, selectedKindIdentity, kindFiltersSearch, selectedKind, onKindFiltersChange])
5106
5263
 
5107
5264
  // Toggle sort for a column
5108
5265
  const handleSort = useCallback((column: string) => {
@@ -5278,7 +5435,7 @@ export function ResourcesView({
5278
5435
 
5279
5436
  // Filter resources by search term, status, problems, and sort
5280
5437
  const filteredResources = useMemo(() => {
5281
- if (!resources) return []
5438
+ if (!tableActive || !resources) return []
5282
5439
 
5283
5440
  let result = resources
5284
5441
 
@@ -5464,7 +5621,7 @@ export function ResourcesView({
5464
5621
  }
5465
5622
 
5466
5623
  return result
5467
- }, [resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
5624
+ }, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
5468
5625
 
5469
5626
  // For nodes table: compute the majority minor version so outliers can be highlighted
5470
5627
  const majorityNodeMinorVersion = useMemo(() => {
@@ -5660,7 +5817,7 @@ export function ResourcesView({
5660
5817
 
5661
5818
  // Calculate filter options with counts based on current resources (before filtering)
5662
5819
  const filterOptions = useMemo(() => {
5663
- if (!resources || resources.length === 0) return null
5820
+ if (!tableActive || !resources || resources.length === 0) return null
5664
5821
 
5665
5822
  const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
5666
5823
  const cellFilterKind = getCellFilterKind(selectedKind.name, selectedKind.group)
@@ -5773,7 +5930,7 @@ export function ResourcesView({
5773
5930
 
5774
5931
  if (filterableColumns.length === 0 && !problems && labelValues.length === 0) return null
5775
5932
  return { columns: filterableColumns, problems, labels: labelValues }
5776
- }, [resources, selectedKind.name, selectedKind.group, allColumns])
5933
+ }, [tableActive, resources, selectedKind.name, selectedKind.group, allColumns])
5777
5934
 
5778
5935
  // Map filterable columns by key for O(1) lookup in header rendering
5779
5936
  const filterableColumnMap = useMemo(() => {
@@ -5795,15 +5952,16 @@ export function ResourcesView({
5795
5952
  // Namespace contribution gated on a host-wired clearer: without it the
5796
5953
  // Clear filters button can't drop the namespace, so showing it would be
5797
5954
  // a no-op for that case.
5798
- const hasAnyFilter =
5799
- !!searchTerm ||
5800
- regexMode ||
5801
- !!labelSelector ||
5802
- hasOwnerFilter ||
5803
- problemFilters.length > 0 ||
5804
- Object.values(columnFilters).some((vals) => vals.length > 0) ||
5805
- showInactiveReplicaSets ||
5806
- (!!onClearNamespaces && namespaces.length > 0)
5955
+ const activeFilterCount =
5956
+ Object.values(columnFilters).filter((vals) => vals.length > 0).length +
5957
+ (problemFilters.length > 0 ? 1 : 0) +
5958
+ (labelSelector ? 1 : 0) +
5959
+ (searchTerm || regexMode ? 1 : 0) +
5960
+ (hasOwnerFilter ? 1 : 0) +
5961
+ (showInactiveReplicaSets ? 1 : 0) +
5962
+ (!!onClearNamespaces && namespaces.length > 0 ? 1 : 0)
5963
+ const hasAnyFilter = activeFilterCount > 0
5964
+ const filtersRestored = !!kindFiltersSearch && kindFiltersSearch === restoredKindSearch
5807
5965
 
5808
5966
 
5809
5967
  // Toggle problem filter
@@ -5879,7 +6037,9 @@ export function ResourcesView({
5879
6037
  />
5880
6038
  )}
5881
6039
 
5882
- {/* Main Content - Resource Table */}
6040
+ {kindView !== null ? (
6041
+ <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">{kindView}</div>
6042
+ ) : (
5883
6043
  <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
5884
6044
  {/* Toolbar */}
5885
6045
  <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0">
@@ -6119,10 +6279,10 @@ export function ResourcesView({
6119
6279
  <button
6120
6280
  type="button"
6121
6281
  onClick={clearAllFilters}
6122
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated transition-colors"
6282
+ 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
6283
  >
6124
6284
  <RotateCcw className="w-3.5 h-3.5" />
6125
- <span>Clear filters</span>
6285
+ <span>Clear filters ({activeFilterCount})</span>
6126
6286
  </button>
6127
6287
  </Tooltip>
6128
6288
  )}
@@ -6354,6 +6514,21 @@ export function ResourcesView({
6354
6514
  </div>
6355
6515
  )}
6356
6516
 
6517
+ {!isLoading && !isSelectedForbidden && !largeListGuard && filteredResources.length > 0 && (
6518
+ <ActiveFilterBar
6519
+ columnFilters={columnFilters}
6520
+ columnFilterExcludes={columnFilterExcludes}
6521
+ problemFilters={problemFilters}
6522
+ labelSelector={labelSelector}
6523
+ onClearColumn={clearColumnFilter}
6524
+ onClearProblems={() => setProblemFilters([])}
6525
+ onClearLabels={() => setLabelSelector('')}
6526
+ restored={filtersRestored}
6527
+ onClearAll={clearKindFilters}
6528
+ className="shrink-0 px-4 py-1.5 border-b border-theme-border"
6529
+ />
6530
+ )}
6531
+
6357
6532
  {/* Table */}
6358
6533
  <div
6359
6534
  className="flex-1 overflow-auto relative"
@@ -6430,46 +6605,17 @@ export function ResourcesView({
6430
6605
  </button>
6431
6606
  )}
6432
6607
  {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
- {/* Show active filters as dismissible badges so user can clear them */}
6434
- {(() => {
6435
- const activeColEntries = Object.entries(columnFilters).filter(([, vals]) => vals.length > 0)
6436
- if (activeColEntries.length === 0 && problemFilters.length === 0 && !labelSelector) return null
6437
- return (
6438
- <div className="flex flex-wrap items-center gap-1.5 mt-3">
6439
- {activeColEntries.map(([key, vals]) => (
6440
- <button
6441
- key={key}
6442
- onClick={() => clearColumnFilter(key)}
6443
- className="flex items-center gap-1 px-2 py-1 text-xs selection selection-text rounded-md hover:selection-strong transition-colors"
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
- })()}
6608
+ <ActiveFilterBar
6609
+ columnFilters={columnFilters}
6610
+ columnFilterExcludes={columnFilterExcludes}
6611
+ problemFilters={problemFilters}
6612
+ labelSelector={labelSelector}
6613
+ onClearColumn={clearColumnFilter}
6614
+ onClearProblems={() => setProblemFilters([])}
6615
+ onClearLabels={() => setLabelSelector('')}
6616
+ restored={filtersRestored}
6617
+ className="mt-3"
6618
+ />
6473
6619
  {hasAnyFilter && (
6474
6620
  <button
6475
6621
  type="button"
@@ -6697,6 +6843,22 @@ export function ResourcesView({
6697
6843
  <div className="px-3 py-2 text-xs text-theme-text-disabled">No matches</div>
6698
6844
  )}
6699
6845
  </div>
6846
+ {col.key === 'namespace' && onApplyNamespacesToAllSections && activeFilterValues.length > 0 && !columnFilterExcludes[col.key] && (
6847
+ <div className="p-1 border-t border-theme-border">
6848
+ <button
6849
+ onClick={() => {
6850
+ setOpenColumnFilter(null)
6851
+ // Keep the column filter until the scope change lands; a failed one keeps the user's filter.
6852
+ Promise.resolve(onApplyNamespacesToAllSections(activeFilterValues))
6853
+ .then(() => clearColumnFilter(col.key), () => {})
6854
+ }}
6855
+ 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"
6856
+ title="Scope every section to these namespaces in the header namespace picker"
6857
+ >
6858
+ Apply to all sections
6859
+ </button>
6860
+ </div>
6861
+ )}
6700
6862
  </div>
6701
6863
  )
6702
6864
  })()}
@@ -6775,11 +6937,12 @@ export function ResourcesView({
6775
6937
  />
6776
6938
  )}
6777
6939
  </div>
6940
+ )}
6778
6941
  </div>
6779
6942
 
6780
6943
  {/* Bulk delete confirmation */}
6781
6944
  <ConfirmDialog
6782
- open={showBulkDeleteConfirm}
6945
+ open={tableActive && showBulkDeleteConfirm}
6783
6946
  onClose={() => { setShowBulkDeleteConfirm(false); setBulkForceDelete(false) }}
6784
6947
  onConfirm={() => {
6785
6948
  onBulkDelete?.(checkedBulkItems, {
@@ -6810,7 +6973,7 @@ export function ResourcesView({
6810
6973
  </label>
6811
6974
  </ConfirmDialog>
6812
6975
  <ConfirmDialog
6813
- open={showBulkRestartConfirm}
6976
+ open={tableActive && showBulkRestartConfirm}
6814
6977
  onClose={() => setShowBulkRestartConfirm(false)}
6815
6978
  onConfirm={() => {
6816
6979
  onBulkRestart?.(checkedBulkItems, {
@@ -6829,7 +6992,7 @@ export function ResourcesView({
6829
6992
  isClosable
6830
6993
  />
6831
6994
  <ConfirmDialog
6832
- open={showBulkScaleDialog}
6995
+ open={tableActive && showBulkScaleDialog}
6833
6996
  onClose={() => setShowBulkScaleDialog(false)}
6834
6997
  onConfirm={() => {
6835
6998
  onBulkScale?.(checkedBulkItems, bulkScaleReplicas, {
@@ -7235,6 +7398,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7235
7398
  case 'grpcroutes':
7236
7399
  case 'tcproutes':
7237
7400
  case 'tlsroutes':
7401
+ case 'udproutes':
7238
7402
  return <RouteCell resource={resource} column={column} />
7239
7403
  case 'gatewayclasses':
7240
7404
  return <GatewayClassCell resource={resource} column={column} />
@@ -7925,9 +8089,9 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
7925
8089
  const label = isCPU ? 'CPU' : 'Memory'
7926
8090
  const format = isCPU ? formatCPU : formatMemoryShort
7927
8091
 
7928
- // Multi-container pods carry a per-container breakdown; single-container
7929
- // pods fall back to the (union-summed) pod-level fields as one synthetic
7930
- // container so the rest of the logic is uniform.
8092
+ // Multi-container pods (and any pod with a pod-level budget) carry a
8093
+ // per-container breakdown; other single-container pods fall back to the
8094
+ // pod-level fields as one synthetic container so the logic is uniform.
7931
8095
  const list: ContainerResourceMetrics[] = (m.containers && m.containers.length > 0)
7932
8096
  ? m.containers
7933
8097
  : [{
@@ -7936,7 +8100,15 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
7936
8100
  memory: m.memory, memoryRequest: m.memoryRequest, memoryLimit: m.memoryLimit,
7937
8101
  }]
7938
8102
 
7939
- const { mode, totalUsage, denom, markerPct, unlimitedCount } = podAggregate(list, kind)
8103
+ // The pod totals already carry any pod-level value (overlaid server-side
8104
+ // with exact quantity parsing); spec.resources only says whether one is set.
8105
+ const podLevel = resource.spec?.resources
8106
+ const podRequest = podLevel?.requests?.[kind] ? (isCPU ? m.cpuRequest : m.memoryRequest) : undefined
8107
+ const podLimit = podLevel?.limits?.[kind] ? (isCPU ? m.cpuLimit : m.memoryLimit) : undefined
8108
+ const { mode, totalUsage, denom, markerPct, unlimitedCount } = podAggregate(list, kind, {
8109
+ request: podRequest,
8110
+ limit: podLimit,
8111
+ })
7940
8112
  if (totalUsage === 0) return <span className="text-sm text-theme-text-tertiary">-</span>
7941
8113
 
7942
8114
  // Pod-vs-node context: how full the pod's node is (the risk signal) and
@@ -7956,7 +8128,12 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
7956
8128
  }
7957
8129
  : undefined
7958
8130
 
7959
- const tip = buildContainerResourceTooltip(label, list, kind, format, nodeCtx)
8131
+ // The synthetic single-container row already carries the pod totals, so
8132
+ // only a real per-container breakdown needs the shared budget spelled out.
8133
+ const podBudget = podLimit && m.containers?.length
8134
+ ? { usage: totalUsage, limit: podLimit, request: podRequest }
8135
+ : undefined
8136
+ const tip = buildContainerResourceTooltip(label, list, kind, format, nodeCtx, podBudget)
7960
8137
 
7961
8138
  // Every container is limited → aggregate bar against the summed limit; a
7962
8139
  // real pod-level ceiling, so it may go red.
@@ -8581,12 +8758,16 @@ interface PodAggregate {
8581
8758
  // headline renders — total usage measured against a pod-level yardstick. It
8582
8759
  // keeps the dominant consumer visible instead of demoting it behind a small
8583
8760
  // limited container, and stays honest about partial limits (which don't form a
8584
- // real ceiling).
8585
- export function podAggregate(list: ContainerResourceMetrics[], kind: 'cpu' | 'memory'): PodAggregate {
8761
+ // real ceiling). A pod-level budget (spec.resources) bounds every container, so
8762
+ // its limit is a real ceiling regardless of what the containers set.
8763
+ export function podAggregate(
8764
+ list: ContainerResourceMetrics[],
8765
+ kind: 'cpu' | 'memory',
8766
+ podLevel?: { request?: number; limit?: number },
8767
+ ): PodAggregate {
8586
8768
  const isCPU = kind === 'cpu'
8587
8769
  let totalUsage = 0
8588
8770
  let limitedCount = 0
8589
- let requestedCount = 0
8590
8771
  let unlimitedCount = 0
8591
8772
  let summedLimit = 0
8592
8773
  let summedRequest = 0
@@ -8601,25 +8782,28 @@ export function podAggregate(list: ContainerResourceMetrics[], kind: 'cpu' | 'me
8601
8782
  } else {
8602
8783
  unlimitedCount++
8603
8784
  }
8604
- if (request > 0) {
8605
- requestedCount++
8606
- summedRequest += request
8607
- }
8785
+ if (request > 0) summedRequest += request
8608
8786
  }
8787
+ if (podLevel?.request) summedRequest = podLevel.request
8609
8788
  const allLimited = list.length > 0 && limitedCount === list.length
8610
- if (allLimited) {
8789
+ // Container limits still apply inside a pod-level budget, so when every
8790
+ // container has one the tighter of the two totals is the real ceiling.
8791
+ const ceiling = podLevel?.limit && allLimited
8792
+ ? Math.min(podLevel.limit, summedLimit)
8793
+ : podLevel?.limit || (allLimited ? summedLimit : 0)
8794
+ if (ceiling) {
8611
8795
  return {
8612
8796
  mode: 'limit',
8613
8797
  totalUsage,
8614
- denom: summedLimit,
8615
- markerPct: summedRequest > 0 ? (summedRequest / summedLimit) * 100 : undefined,
8798
+ denom: ceiling,
8799
+ markerPct: summedRequest > 0 ? (summedRequest / ceiling) * 100 : undefined,
8616
8800
  unlimitedCount,
8617
8801
  }
8618
8802
  }
8619
8803
  if (limitedCount > 0) {
8620
8804
  return { mode: 'partial', totalUsage, denom: 0, unlimitedCount }
8621
8805
  }
8622
- if (requestedCount > 0) {
8806
+ if (summedRequest > 0) {
8623
8807
  return { mode: 'request', totalUsage, denom: summedRequest, unlimitedCount }
8624
8808
  }
8625
8809
  return { mode: 'none', totalUsage, denom: 0, unlimitedCount }
@@ -8647,12 +8831,13 @@ function formatSharePct(pct: number): string {
8647
8831
  return `${Math.round(pct)}%`
8648
8832
  }
8649
8833
 
8650
- function buildContainerResourceTooltip(
8834
+ export function buildContainerResourceTooltip(
8651
8835
  label: 'CPU' | 'Memory',
8652
8836
  containers: ContainerResourceMetrics[],
8653
8837
  kind: 'cpu' | 'memory',
8654
8838
  formatFn: (n: number) => string,
8655
8839
  nodeCtx?: NodeContext,
8840
+ podBudget?: { usage: number; limit: number; request?: number },
8656
8841
  ) {
8657
8842
  const rows = [...containers].sort((a, b) => {
8658
8843
  const diff = readContainer(b, kind).pct - readContainer(a, kind).pct
@@ -8675,7 +8860,7 @@ function buildContainerResourceTooltip(
8675
8860
  ? `${formatFn(r.usage)} · ${Math.round(r.pct)}% · ${formatFn(r.denom)} limit${r.request > 0 ? ` · req ${formatFn(r.request)}` : ''}`
8676
8861
  : r.yardstick === 'request'
8677
8862
  ? `${formatFn(r.usage)} · ${Math.round(r.pct)}% · ${formatFn(r.denom)} request`
8678
- : `${formatFn(r.usage)} · no limit`
8863
+ : `${formatFn(r.usage)} · ${podBudget ? 'pod limit' : 'no limit'}`
8679
8864
  return (
8680
8865
  <div key={c.name} className="flex flex-col leading-snug">
8681
8866
  <span className="text-[11px] text-theme-text-tertiary truncate">{c.name}</span>
@@ -8689,6 +8874,14 @@ function buildContainerResourceTooltip(
8689
8874
  +{remaining} more → open pod
8690
8875
  </div>
8691
8876
  )}
8877
+ {podBudget && (
8878
+ <div className="flex flex-col leading-snug border-t border-theme-border/50 pt-1">
8879
+ <span className="text-[11px] text-theme-text-tertiary truncate">Pod (shared)</span>
8880
+ <span className="text-xs font-mono text-theme-text-primary">
8881
+ {`${formatFn(podBudget.usage)} · ${Math.round((podBudget.usage / podBudget.limit) * 100)}% · ${formatFn(podBudget.limit)} limit${podBudget.request ? ` · req ${formatFn(podBudget.request)}` : ''}`}
8882
+ </span>
8883
+ </div>
8884
+ )}
8692
8885
  {nodeCtx && (
8693
8886
  <div className="flex flex-col leading-snug border-t border-theme-border/50 pt-1">
8694
8887
  <span className="text-[11px] text-theme-text-tertiary truncate">Node {nodeCtx.name}</span>
@@ -9130,7 +9323,7 @@ function GatewayClassCell({ resource, column }: { resource: any; column: string
9130
9323
  }
9131
9324
  }
9132
9325
 
9133
- // Shared cell renderer for all Gateway API route types (HTTPRoute, GRPCRoute, TCPRoute, TLSRoute)
9326
+ // Shared cell renderer for all Gateway API route types (HTTPRoute, GRPCRoute, TCPRoute, TLSRoute, UDPRoute)
9134
9327
  function RouteCell({ resource, column }: { resource: any; column: string }) {
9135
9328
  switch (column) {
9136
9329
  case 'status': {