@skyhook-io/k8s-ui 1.18.1 → 1.18.2

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 (85) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/components/applications/ApplicationsView.tsx +8 -7
  4. package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
  5. package/src/components/compare/ResourceCompareView.tsx +6 -2
  6. package/src/components/gitops/GitOpsTableView.tsx +13 -8
  7. package/src/components/issues/IssuesView.tsx +2 -2
  8. package/src/components/issues/diagnostic.ts +2 -0
  9. package/src/components/issues/severity.ts +1 -0
  10. package/src/components/logs/LogCore.tsx +13 -11
  11. package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
  12. package/src/components/namespace-switcher/NamespacePicker.tsx +2 -0
  13. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
  14. package/src/components/resources/ResourcesSidebar.tsx +26 -5
  15. package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
  16. package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
  17. package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
  18. package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
  19. package/src/components/resources/ResourcesView.tsx +238 -66
  20. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  21. package/src/components/resources/generic-status.ts +15 -0
  22. package/src/components/resources/printer-columns.test.ts +36 -0
  23. package/src/components/resources/printer-columns.ts +12 -1
  24. package/src/components/resources/readiness-gates.test.ts +95 -0
  25. package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
  26. package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
  27. package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
  28. package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
  29. package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
  30. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
  31. package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
  32. package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
  33. package/src/components/resources/renderers/index.ts +3 -0
  34. package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
  35. package/src/components/resources/resource-utils-crossplane.ts +44 -8
  36. package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
  37. package/src/components/resources/resource-utils-flagger.ts +76 -0
  38. package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
  39. package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
  40. package/src/components/resources/resource-utils.ts +80 -2
  41. package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
  42. package/src/components/shared/CreateResourceDialog.tsx +5 -0
  43. package/src/components/shared/DetailShell.phone.test.tsx +54 -0
  44. package/src/components/shared/DetailShell.tsx +14 -3
  45. package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
  46. package/src/components/shared/EditableYamlView.test.tsx +58 -0
  47. package/src/components/shared/EditableYamlView.tsx +18 -3
  48. package/src/components/shared/FilterRailPanel.test.tsx +39 -0
  49. package/src/components/shared/FilterRailPanel.tsx +62 -0
  50. package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
  51. package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
  52. package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
  53. package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
  54. package/src/components/shared/ManagedByChip.tsx +135 -2
  55. package/src/components/shared/ResourceActionsBar.tsx +22 -6
  56. package/src/components/shared/ResourceRendererDispatch.tsx +23 -0
  57. package/src/components/shared/SetImageDialog.tsx +3 -0
  58. package/src/components/topology/TopologyControls.tsx +1 -1
  59. package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
  60. package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
  61. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
  62. package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
  63. package/src/components/ui/PageHeader.tsx +3 -2
  64. package/src/components/ui/SummaryTile.tsx +13 -3
  65. package/src/components/ui/Tooltip.touch.test.tsx +55 -0
  66. package/src/components/ui/Tooltip.tsx +11 -2
  67. package/src/components/ui/YamlReview.test.tsx +52 -0
  68. package/src/components/ui/YamlReview.tsx +7 -0
  69. package/src/components/ui/drawer-components.test.tsx +27 -0
  70. package/src/components/ui/drawer-components.tsx +22 -42
  71. package/src/components/workload/WorkloadView.relationships.test.tsx +40 -0
  72. package/src/components/workload/WorkloadView.tsx +57 -34
  73. package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
  74. package/src/hooks/useKeepInViewport.test.ts +27 -0
  75. package/src/hooks/useKeepInViewport.ts +87 -0
  76. package/src/types/core.ts +53 -1
  77. package/src/utils/api-resources.ts +1 -0
  78. package/src/utils/gitops-owner.ts +136 -8
  79. package/src/utils/gitops-write-guard.test.ts +332 -0
  80. package/src/utils/gitops-write-guard.ts +175 -11
  81. package/src/utils/index.ts +1 -0
  82. package/src/utils/read-actions.ts +34 -0
  83. package/src/utils/touch-intent.test.ts +39 -0
  84. package/src/utils/touch-intent.ts +35 -0
  85. package/vitest.config.ts +26 -0
@@ -1,10 +1,15 @@
1
+ import { getOpenTelemetryColumnValue } from './resource-utils-opentelemetry'
2
+ import { getFlaggerColumnValue } from './resource-utils-flagger'
1
3
  import type { ResourceCoverage } from '../../types/resource-coverage'
2
4
  import { resourceCoverageNotice, resourceEmptyMessage, resourceNarrowedNotice } from '../../utils/resource-coverage'
3
5
  import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
4
6
  import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
5
7
  import { useDebouncedValue } from '../../hooks/useDebouncedValue'
8
+ import { usePhoneLayout } from '../../hooks/usePhoneLayout'
9
+ import { CrossplaneDeleteNotice } from '../ui/CrossplaneOperationNotice'
6
10
  import { Badge } from '../ui/Badge'
7
11
  import { PaneLoader } from '../ui/PaneLoader'
12
+ import { DialogPortal } from '../ui/DialogPortal'
8
13
  import { RestrictedState } from '../ui/RestrictedState'
9
14
  import { FetchResult } from '../ui/FetchResult'
10
15
  import { Input } from '../ui/Input'
@@ -34,6 +39,7 @@ import {
34
39
  ListChecks,
35
40
  Minus,
36
41
  Scale,
42
+ PanelLeft,
37
43
  } from 'lucide-react'
38
44
  import { clsx } from 'clsx'
39
45
  import { ResourceBar } from '../ui/ResourceBar'
@@ -204,8 +210,9 @@ import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell, Volum
204
210
  import { isVeleroResource } from './resource-utils-velero'
205
211
  import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell, CNPGObjectStoreCell, CNPGDeclarativeCell, CNPGImageCatalogCell } from './renderers/cnpg-cells'
206
212
  import { isApiGroup, CNPG_GROUP } from './resource-utils-cnpg'
207
- import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
208
- import { isManagedResource, isComposite } from './resource-utils-crossplane'
213
+ import { CrossplaneStatusCell, ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
214
+ import { getCrossplaneStatus, isManagedResource, isComposite, getCrossplanePauseState } from './resource-utils-crossplane'
215
+ import { CrossplanePauseChip } from './renderers/CrossplanePauseContext'
209
216
  import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
210
217
  import { KnativeServiceCell, ConfigurationCell as KnativeConfigurationCell, RevisionCell as KnativeRevisionCell, RouteCell as KnativeRouteCell, BrokerCell, TriggerCell, EventTypeCell, PingSourceCell, ApiServerSourceCell, ContainerSourceCell, SinkBindingCell, ChannelCell, InMemoryChannelCell, SubscriptionCell, SequenceCell, ParallelCell, DomainMappingCell, ServerlessServiceCell, KnativeIngressCell, KnativeCertificateCell } from './renderers/knative-cells'
211
218
  import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
@@ -218,7 +225,7 @@ import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
218
225
  import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
219
226
  import { useRegisterShortcut, useRegisterShortcuts, type KeyboardShortcut } from '../../hooks/useKeyboardShortcuts'
220
227
  import { ResourcesSidebar } from './ResourcesSidebar'
221
- import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
228
+ import type { SelectedKindInfo, SidebarCategoryWorkspace, SidebarDisplayState } from './ResourcesSidebar'
222
229
  import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
223
230
  import { ConfirmDialog } from '../ui/ConfirmDialog'
224
231
 
@@ -341,6 +348,24 @@ const COMPARE_COLUMN_STYLE: React.CSSProperties = {
341
348
  maxWidth: COMPARE_COLUMN_WIDTH,
342
349
  }
343
350
 
351
+ // Phones: the leading columns (compare marker, selection checkbox, then Name
352
+ // when it is the first column) stay in place while the rest of the table
353
+ // scrolls sideways, so every row stays identifiable. The opaque background
354
+ // keeps scrolled cells from showing through; on phones it hides the selection
355
+ // tint on these cells, where a tap opens the full-screen detail anyway.
356
+ // The offset rides a CSS variable that only the phone classes read, so the
357
+ // desktop header (position: relative) never moves.
358
+ const CHECKBOX_COLUMN_WIDTH = 40
359
+ const PHONE_PINNED_CELL = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[1] phone:bg-theme-surface'
360
+ const PHONE_PINNED_HEAD = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[3]'
361
+ const PHONE_NAME_MAX_VW = 45
362
+ function phonePinnedLeft(compareMode: boolean | undefined, showCheckbox: boolean | undefined, column: 'compare' | 'checkbox' | 'name'): React.CSSProperties {
363
+ const compare = compareMode ? COMPARE_COLUMN_WIDTH : 0
364
+ const checkbox = showCheckbox ? CHECKBOX_COLUMN_WIDTH : 0
365
+ const left = column === 'compare' ? 0 : column === 'checkbox' ? compare : compare + checkbox
366
+ return { '--pin-left': `${left}px` } as React.CSSProperties
367
+ }
368
+
344
369
  // Built-in sortable keys. Hoisted so getColumnMinWidth can reserve room for the
345
370
  // sort icon without the header render and the width math drifting apart.
346
371
  const BUILTIN_SORTABLE_COLUMN_KEYS = new Set([
@@ -554,6 +579,44 @@ const DEFAULT_COLUMNS: Column[] = [
554
579
  ]
555
580
 
556
581
  const KNOWN_COLUMNS: Record<string, Column[]> = {
582
+ opentelemetrycollectors: [
583
+ { key: 'name', label: 'Name', width: 'min-w-40' },
584
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
585
+ { key: 'status', label: 'Reconciliation', width: 'w-40 shrink-0', tooltip: 'Operator reconciliation only, not Pod health or telemetry delivery.' },
586
+ { key: 'mode', label: 'Mode', width: 'w-28 shrink-0' },
587
+ { key: 'reportedPods', label: 'Reported Pods', width: 'w-32 shrink-0', tooltip: 'Last reported ready/total snapshot. Total is observed Pods for Deployment/StatefulSet, desired scheduled Pods for DaemonSet. Inspect the owned workload for current readiness.' },
588
+ { key: 'version', label: 'Version', width: 'w-24 shrink-0' },
589
+ { key: 'image', label: 'Image', width: 'min-w-48' },
590
+ { key: 'management', label: 'Management', width: 'w-28 shrink-0' },
591
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
592
+ ],
593
+ instrumentations: [
594
+ { key: 'name', label: 'Name', width: 'min-w-40' },
595
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
596
+ { key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
597
+ { key: 'endpoint', label: 'OTLP Endpoint', width: 'min-w-48' },
598
+ { key: 'sampler', label: 'Sampler', width: 'w-40 shrink-0' },
599
+ { key: 'samplerArgument', label: 'Sampler Argument', width: 'w-32 shrink-0' },
600
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
601
+ ],
602
+ canaries: [
603
+ { key: 'name', label: 'Name', width: 'min-w-40' },
604
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
605
+ { key: 'status', label: 'Rollout State', width: 'w-40 shrink-0', tooltip: 'Last recorded rollout outcome, with requested suspension separate from a failed result. Not a latest-generation guarantee.' },
606
+ { key: 'strategy', label: 'Strategy', width: 'w-36 shrink-0' },
607
+ { key: 'target', label: 'Canary Target', width: 'min-w-40' },
608
+ { key: 'canaryWeight', label: 'Reported Weight', width: 'w-36 shrink-0', tooltip: 'Last reported canary routing weight, not measured traffic. Blue/green and A/B strategies are not weighted.' },
609
+ { key: 'suspended', label: 'Suspend Requested', width: 'w-36 shrink-0' },
610
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
611
+ ],
612
+ metrictemplates: [
613
+ { key: 'name', label: 'Name', width: 'min-w-40' },
614
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
615
+ { key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
616
+ { key: 'provider', label: 'Provider', width: 'w-32 shrink-0' },
617
+ { key: 'address', label: 'Provider Address', width: 'min-w-48' },
618
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
619
+ ],
557
620
  pods: [
558
621
  { key: 'name', label: 'Name' },
559
622
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
@@ -2851,6 +2914,10 @@ function isCuratedUnboundedGroup(key: string, group: string): boolean {
2851
2914
  * cannot occupy one. See CORE_API_GROUPS.
2852
2915
  */
2853
2916
  const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
2917
+ opentelemetrycollectors: ['opentelemetry.io'],
2918
+ instrumentations: ['opentelemetry.io'],
2919
+ canaries: ['flagger.app'],
2920
+ metrictemplates: ['flagger.app'],
2854
2921
  challenges: ['acme.cert-manager.io'],
2855
2922
  orders: ['acme.cert-manager.io'],
2856
2923
  compositeresourcedefinitions: ['apiextensions.crossplane.io'],
@@ -3132,12 +3199,12 @@ function buildPrinterColumn(def: PrinterColumnDef, index: number, table: Printer
3132
3199
  // A column the vendor named Ready/Healthy/Established carries the only
3133
3200
  // scannable signal these kinds have — they have no curated Status column.
3134
3201
  // Rendering it as grey text would leave a screen of uniform "True".
3135
- const tone = printerCellTone(def, value)
3202
+ const tone = printerCellTone(def, value, resource)
3136
3203
  if (tone) {
3137
3204
  return (
3138
3205
  // The value, not the column's description — the header tooltip
3139
3206
  // carries that. A cell tooltip exists to reveal a truncated value.
3140
- <Tooltip content={text}>
3207
+ <Tooltip content={tone === 'neutral' ? `${text} · ${String(value).trim().toLowerCase() === 'false' ? 'Controller recorded reconciliation paused' : 'Last reported readiness while reconciliation is paused'}` : text}>
3141
3208
  <span className={clsx('badge', healthColors[tone])}>{text}</span>
3142
3209
  </Tooltip>
3143
3210
  )
@@ -3765,6 +3832,15 @@ export function ResourcesView({
3765
3832
  }: ResourcesViewProps) {
3766
3833
  const initialFilters = getInitialFiltersFromURL()
3767
3834
  const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
3835
+ // Phones: the 224px kind list would leave the table a sliver, so it lives in
3836
+ // a drawer over the table, opened from the toolbar and closed by a pick.
3837
+ const isPhone = usePhoneLayout()
3838
+ const [kindsDrawerOpen, setKindsDrawerOpen] = useState(false)
3839
+ // The drawer unmounts its sidebar on close; keep its display settings here.
3840
+ const [drawerSidebarState, setDrawerSidebarState] = useState<SidebarDisplayState>(() => ({ showEmptyKinds: false, favoritesExpanded: pinned.length > 0 }))
3841
+ useEffect(() => {
3842
+ if (!isPhone) setKindsDrawerOpen(false)
3843
+ }, [isPhone])
3768
3844
  // Sync selectedKind from URL when the URL changes (browser back, external sidebar navigation).
3769
3845
  // Deps are URL-derived only — including selectedKind.name/group would race against pending
3770
3846
  // navigation: a sidebar click flips state before navigate() lands, this effect re-reads the
@@ -4340,7 +4416,7 @@ export function ResourcesView({
4340
4416
  category: 'Search',
4341
4417
  scope: 'resources',
4342
4418
  handler: () => searchInputRef.current?.focus(),
4343
- enabled: tableActive,
4419
+ enabled: tableActive && !kindsDrawerOpen,
4344
4420
  })
4345
4421
 
4346
4422
  // Keyboard navigation: highlighted row state
@@ -4449,6 +4525,8 @@ export function ResourcesView({
4449
4525
  onResourceClick?.(isSelected ? null : stripped)
4450
4526
  }, [onRowSelect, onResourceClick, selectedKind.name, selectedKind.group])
4451
4527
 
4528
+ // Shortcuts here (and the search shortcut above) pause while the phone
4529
+ // kind drawer is open: it's modal, so the table behind it takes no keys.
4452
4530
  useRegisterShortcuts(([
4453
4531
  {
4454
4532
  id: 'resources-nav-down',
@@ -4622,7 +4700,7 @@ export function ResourcesView({
4622
4700
  else searchInputRef.current?.blur()
4623
4701
  },
4624
4702
  },
4625
- ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && (sc.enabled ?? true) })))
4703
+ ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && !kindsDrawerOpen && (sc.enabled ?? true) })))
4626
4704
 
4627
4705
  // Refs for accessing filteredResources inside shortcuts (computed later in component)
4628
4706
  const filteredResourceCountRef = useRef(0)
@@ -4632,7 +4710,7 @@ export function ResourcesView({
4632
4710
  const flatKindListRef = useRef<SelectedKindInfo[]>([])
4633
4711
 
4634
4712
  // Sidebar kind navigation: [ = previous kind, ] = next kind
4635
- useRegisterShortcuts([
4713
+ useRegisterShortcuts(([
4636
4714
  {
4637
4715
  id: 'resources-prev-kind',
4638
4716
  keys: '[',
@@ -4665,7 +4743,7 @@ export function ResourcesView({
4665
4743
  onKindChange?.()
4666
4744
  },
4667
4745
  },
4668
- ])
4746
+ ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: !kindsDrawerOpen && (sc.enabled ?? true) })))
4669
4747
 
4670
4748
  // Close dropdowns on outside click
4671
4749
  useEffect(() => {
@@ -5347,14 +5425,26 @@ export function ResourcesView({
5347
5425
  if (issue) return issueTitle(issue)
5348
5426
  if (operationalIssuesIncomplete) return 'Unknown'
5349
5427
  }
5428
+ if (column === 'status') {
5429
+ const managedColumns = isLikelyCrossplaneMRGroup(selectedKind.name, selectedKind.group ?? '')
5430
+ if ((managedColumns && isManagedResource(resource))
5431
+ || ((!hasCuratedColumns(selectedKind.name, selectedKind.group) || managedColumns)
5432
+ && getCrossplanePauseState(resource) === 'acknowledged')) {
5433
+ return getCrossplaneStatus(resource).text
5434
+ }
5435
+ }
5350
5436
  return getCellFilterValue(resource, column, kind)
5351
- }, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
5437
+ }, [operationalIssues, operationalIssuesIncomplete, selectedKind.name, selectedKind.group])
5352
5438
 
5353
5439
  // Get sortable value from a resource for a given column
5354
5440
  const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
5355
5441
  const meta = resource.metadata || {}
5356
5442
  const status = resource.status || {}
5357
5443
  const kindLower = kind?.toLowerCase() || ''
5444
+ const otelValue = getOpenTelemetryColumnValue(resource, column)
5445
+ if (otelValue !== undefined) return otelValue
5446
+ const flaggerValue = getFlaggerColumnValue(resource, column)
5447
+ if (flaggerValue !== undefined) return flaggerValue
5358
5448
 
5359
5449
  switch (column) {
5360
5450
  case 'name':
@@ -5801,7 +5891,9 @@ export function ResourcesView({
5801
5891
  const selectedKindName = selectedKind.name.toLowerCase()
5802
5892
  const canBulkRestartSelectedKind = onBulkRestart != null && BULK_RESTART_WORKLOAD_KINDS.has(selectedKindName)
5803
5893
  const canBulkScaleSelectedKind = onBulkScale != null && BULK_SCALE_WORKLOAD_KINDS.has(selectedKindName)
5804
- const canBulkSelect = onBulkDelete != null || canBulkRestartSelectedKind || canBulkScaleSelectedKind
5894
+ // Phones skip bulk selection and create (see the toolbar): phone triage
5895
+ // is one resource at a time.
5896
+ const canBulkSelect = !isPhone && (onBulkDelete != null || canBulkRestartSelectedKind || canBulkScaleSelectedKind)
5805
5897
  const isBulkMutating = isBulkDeleting || isBulkRestarting || isBulkScaling
5806
5898
 
5807
5899
  const openBulkScaleDialog = useCallback(() => {
@@ -5852,14 +5944,30 @@ export function ResourcesView({
5852
5944
  // Fixed-width columns can consume the table's flexible space and collapse
5853
5945
  // the required name column. Keep a real table minimum and let the container
5854
5946
  // scroll horizontally when the viewport is too narrow.
5947
+ // Phones pin a leading Name column (see PHONE_PINNED_CELL). Cap it at
5948
+ // PHONE_NAME_MAX_VW percent of the screen so a width saved on desktop can't
5949
+ // make the pinned column cover it; the saved width itself is left alone.
5950
+ // Tracked through resizes and rotation, which can stay within phone width.
5951
+ const [phoneViewportWidth, setPhoneViewportWidth] = useState(() => (typeof window === 'undefined' ? 0 : window.innerWidth))
5952
+ useEffect(() => {
5953
+ if (!isPhone) return
5954
+ const onResize = () => setPhoneViewportWidth(window.innerWidth)
5955
+ onResize()
5956
+ window.addEventListener('resize', onResize)
5957
+ return () => window.removeEventListener('resize', onResize)
5958
+ }, [isPhone])
5959
+ const phoneNameCap = isPhone && columns[0]?.key === 'name' ? Math.round(phoneViewportWidth * PHONE_NAME_MAX_VW / 100) : Infinity
5855
5960
  const tableMinWidth = useMemo(() => {
5856
5961
  const compareColumnWidth = compareMode ? COMPARE_COLUMN_WIDTH : 0
5857
- const baseMinWidth = columns.reduce((sum, col) => sum + (columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)), compareColumnWidth)
5962
+ const baseMinWidth = columns.reduce((sum, col, i) => {
5963
+ const width = columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)
5964
+ return sum + (i === 0 && col.key === 'name' ? Math.min(width, phoneNameCap) : width)
5965
+ }, compareColumnWidth)
5858
5966
  const flexibleNameColumn = columns.find(col => col.key === 'name' && !columnWidths[col.key])
5859
5967
 
5860
5968
  if (!hasResizedColumns || !flexibleNameColumn) return baseMinWidth
5861
5969
  return baseMinWidth + getColumnMinWidth(flexibleNameColumn, extraColumnsByKey)
5862
- }, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch])
5970
+ }, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch, phoneNameCap])
5863
5971
 
5864
5972
  // Stable virtuoso components — memoized to avoid remounting the table on every render
5865
5973
  const virtuosoComponents = useMemo(() => ({
@@ -5881,13 +5989,15 @@ export function ResourcesView({
5881
5989
  */}
5882
5990
  {compareMode && <col style={{ width: COMPARE_COLUMN_WIDTH }} />}
5883
5991
  {isCheckboxMode && <col style={{ width: '40px' }} />}
5884
- {columns.map(col => (
5992
+ {columns.map((col, i) => (
5885
5993
  <col
5886
5994
  key={col.key}
5887
5995
  style={{
5888
- width: columnWidths[col.key]
5889
- ? `${columnWidths[col.key]}px`
5890
- : col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
5996
+ width: i === 0 && col.key === 'name' && phoneNameCap !== Infinity
5997
+ ? `min(${columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)}px, ${PHONE_NAME_MAX_VW}vw)`
5998
+ : columnWidths[col.key]
5999
+ ? `${columnWidths[col.key]}px`
6000
+ : col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
5891
6001
  }}
5892
6002
  />
5893
6003
  ))}
@@ -5898,7 +6008,7 @@ export function ResourcesView({
5898
6008
  )
5899
6009
  }),
5900
6010
  TableRow: VirtuosoTableRow,
5901
- }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch])
6011
+ }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch, phoneNameCap])
5902
6012
 
5903
6013
  // Calculate filter options with counts based on current resources (before filtering)
5904
6014
  const filterOptions = useMemo(() => {
@@ -6096,42 +6206,81 @@ export function ResourcesView({
6096
6206
  onOpenWorkloadLogs,
6097
6207
  }), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
6098
6208
 
6209
+ const renderSidebar = (className?: string, inDrawer = false) => (
6210
+ <ResourcesSidebar
6211
+ className={className}
6212
+ displayState={inDrawer ? drawerSidebarState : undefined}
6213
+ onDisplayStateChange={inDrawer ? setDrawerSidebarState : undefined}
6214
+ containEscape={inDrawer}
6215
+ selectedKind={selectedKind}
6216
+ onSelectedKindChange={(kind) => {
6217
+ shouldPushHistory.current = true
6218
+ setSelectedKind(kind)
6219
+ setKindsDrawerOpen(false)
6220
+ }}
6221
+ onKindChange={onKindChange}
6222
+ apiResources={apiResourcesProp}
6223
+ resourceCounts={counts}
6224
+ resourceCoverage={sidebarResourceCoverage}
6225
+ resourceForbidden={Array.from(forbiddenKinds)}
6226
+ resourceUnavailable={sidebarResourceUnavailable}
6227
+ pinned={pinned}
6228
+ togglePin={togglePin}
6229
+ isPinned={isPinned}
6230
+ categoryWorkspaces={sidebarCategoryWorkspaces}
6231
+ onKindNavigated={() => {
6232
+ // After selecting a kind via keyboard, move focus to the table search
6233
+ // so the user can immediately filter within the selected kind.
6234
+ setTimeout(() => searchInputRef.current?.focus(), 50)
6235
+ }}
6236
+ />
6237
+ )
6238
+
6239
+ const kindsButton = (
6240
+ <button
6241
+ type="button"
6242
+ onClick={() => setKindsDrawerOpen(true)}
6243
+ aria-expanded={kindsDrawerOpen}
6244
+ aria-label={`Resource kinds (showing ${selectedKind.kind})`}
6245
+ className="flex shrink-0 items-center gap-1.5 rounded-lg border border-theme-border bg-theme-elevated px-2.5 py-1.5 text-sm font-medium text-theme-text-primary hover:bg-theme-hover"
6246
+ >
6247
+ <PanelLeft className="h-4 w-4 text-theme-text-tertiary" />
6248
+ <span className="max-w-[7rem] truncate">{selectedKind.kind}</span>
6249
+ </button>
6250
+ )
6251
+
6099
6252
  return (
6100
6253
  <ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
6101
6254
  <div className="flex h-full w-full">
6102
- {/* Sidebar - Resource Types */}
6103
- {!hideSidebar && (
6104
- <ResourcesSidebar
6105
- selectedKind={selectedKind}
6106
- onSelectedKindChange={(kind) => {
6107
- shouldPushHistory.current = true
6108
- setSelectedKind(kind)
6109
- }}
6110
- onKindChange={onKindChange}
6111
- apiResources={apiResourcesProp}
6112
- resourceCounts={counts}
6113
- resourceCoverage={sidebarResourceCoverage}
6114
- resourceForbidden={Array.from(forbiddenKinds)}
6115
- resourceUnavailable={sidebarResourceUnavailable}
6116
- pinned={pinned}
6117
- togglePin={togglePin}
6118
- isPinned={isPinned}
6119
- categoryWorkspaces={sidebarCategoryWorkspaces}
6120
- onKindNavigated={() => {
6121
- // After selecting a kind via keyboard, move focus to the table search
6122
- // so the user can immediately filter within the selected kind.
6123
- setTimeout(() => searchInputRef.current?.focus(), 50)
6124
- }}
6125
- />
6255
+ {/* Sidebar - Resource Types. On phones it is a modal drawer: the
6256
+ dialog primitive traps focus, captures Escape before the table's
6257
+ own shortcuts, and returns focus to the kinds button. */}
6258
+ {!hideSidebar && !isPhone && renderSidebar()}
6259
+ {!hideSidebar && isPhone && (
6260
+ <DialogPortal
6261
+ open={kindsDrawerOpen}
6262
+ onClose={() => setKindsDrawerOpen(false)}
6263
+ ariaLabel="Resource kinds"
6264
+ className="!ml-0 !mr-auto h-full w-72 max-w-[85vw] self-stretch overflow-hidden rounded-none border-y-0 border-l-0"
6265
+ >
6266
+ {renderSidebar('!w-full h-full border-r-0', true)}
6267
+ </DialogPortal>
6126
6268
  )}
6127
6269
 
6128
6270
  {kindView !== null ? (
6129
- <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">{kindView}</div>
6271
+ <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
6272
+ {!hideSidebar && isPhone && (
6273
+ <div className="flex items-center px-3 py-2 border-b border-theme-border bg-theme-base shrink-0">{kindsButton}</div>
6274
+ )}
6275
+ {kindView}
6276
+ </div>
6130
6277
  ) : (
6131
6278
  <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
6132
6279
  {/* Toolbar */}
6133
- <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0">
6134
- <div className="flex-1 min-w-0">
6280
+ {/* Phones: the toolbar wraps and search takes its own full row. */}
6281
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0 phone:relative phone:flex-wrap phone:gap-2 phone:px-3">
6282
+ {!hideSidebar && isPhone && kindsButton}
6283
+ <div className="flex-1 min-w-0 phone:order-last phone:basis-full">
6135
6284
  <div className="relative max-w-md">
6136
6285
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
6137
6286
  <Input
@@ -6214,9 +6363,11 @@ export function ResourcesView({
6214
6363
  </div>
6215
6364
  </div>
6216
6365
 
6217
- {/* Problems dropdown (pods only) */}
6366
+ {/* Problems dropdown (pods only). On phones the toolbar wraps, so this
6367
+ menu and Labels' anchor to the toolbar (full width below it)
6368
+ rather than to a trigger that may sit near either edge. */}
6218
6369
  {filterOptions?.problems && filterOptions.problems.length > 0 && (
6219
- <div className="relative" ref={problemsDropdownRef}>
6370
+ <div className="relative phone:static" ref={problemsDropdownRef}>
6220
6371
  <button
6221
6372
  onClick={() => { setShowProblemsDropdown(!showProblemsDropdown); setShowLabelsDropdown(false) }}
6222
6373
  className={clsx(
@@ -6235,7 +6386,7 @@ export function ResourcesView({
6235
6386
  )}
6236
6387
  </button>
6237
6388
  {showProblemsDropdown && (
6238
- <div className="absolute right-0 top-full mt-1 min-w-48 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6389
+ <div className="absolute right-0 top-full mt-1 min-w-48 phone:left-3 phone:right-3 phone:min-w-0 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6239
6390
  {problemFilters.length > 0 && (
6240
6391
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-theme-border">
6241
6392
  <span className="text-xs font-medium text-theme-text-secondary">Problems</span>
@@ -6266,7 +6417,7 @@ export function ResourcesView({
6266
6417
 
6267
6418
  {/* Labels dropdown */}
6268
6419
  {filterOptions?.labels && filterOptions.labels.length > 0 && (
6269
- <div className="relative" ref={labelsDropdownRef}>
6420
+ <div className="relative phone:static" ref={labelsDropdownRef}>
6270
6421
  <button
6271
6422
  onClick={() => { setShowLabelsDropdown(!showLabelsDropdown); setShowProblemsDropdown(false); setLabelSearch('') }}
6272
6423
  className={clsx(
@@ -6290,7 +6441,7 @@ export function ResourcesView({
6290
6441
  ? labels.filter(l => l.value.toLowerCase().includes(labelSearch.toLowerCase()))
6291
6442
  : labels
6292
6443
  return (
6293
- <div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6444
+ <div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 phone:left-3 phone:right-3 phone:min-w-0 phone:max-w-none bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6294
6445
  <div className="flex items-center gap-2 p-2 border-b border-theme-border">
6295
6446
  <div className="relative flex-1">
6296
6447
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
@@ -6470,7 +6621,7 @@ export function ResourcesView({
6470
6621
  </div>
6471
6622
  )}
6472
6623
  </div>
6473
- {onCreateResource && (
6624
+ {onCreateResource && !isPhone && (
6474
6625
  <Tooltip content={`Create ${selectedKind.kind || 'resource'}`}>
6475
6626
  <button
6476
6627
  aria-label={`Create ${selectedKind.kind || 'resource'}`}
@@ -6530,7 +6681,7 @@ export function ResourcesView({
6530
6681
 
6531
6682
  {/* Bulk actions bar */}
6532
6683
  {showBulkBar && (
6533
- <div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
6684
+ <div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0 phone:flex-wrap phone:gap-2 phone:px-3">
6534
6685
  <span className="text-sm font-medium text-theme-text-primary">
6535
6686
  {checkedItems.length} selected
6536
6687
  </span>
@@ -6727,15 +6878,15 @@ export function ResourcesView({
6727
6878
  // Inline px width — under `table-layout:fixed`,
6728
6879
  // `w-9` is a hint the browser absorbs into leftover
6729
6880
  // row width on an icon-only column.
6730
- style={COMPARE_COLUMN_STYLE}
6731
- className="px-2 py-3 text-xs font-medium uppercase tracking-wide bg-theme-base border-b border-r-subtle border-theme-border text-center text-skyhook-400"
6881
+ style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, isCheckboxMode, 'compare') }}
6882
+ className={clsx('px-2 py-3 text-xs font-medium uppercase tracking-wide bg-theme-base border-b border-r-subtle border-theme-border text-center text-skyhook-400', PHONE_PINNED_HEAD)}
6732
6883
  title="Compare mode"
6733
6884
  >
6734
6885
  <GitCompare className="w-3.5 h-3.5 inline-block opacity-70" />
6735
6886
  </th>
6736
6887
  )}
6737
6888
  {isCheckboxMode && (
6738
- <th className="bg-theme-surface border-b border-theme-border w-10 text-center px-0 py-3">
6889
+ <th style={phonePinnedLeft(compareMode, isCheckboxMode, 'checkbox')} className={clsx('bg-theme-surface border-b border-theme-border w-10 text-center px-0 py-3', PHONE_PINNED_HEAD)}>
6739
6890
  <input
6740
6891
  type="checkbox"
6741
6892
  checked={allVisibleChecked}
@@ -6758,9 +6909,11 @@ export function ResourcesView({
6758
6909
  return (
6759
6910
  <th
6760
6911
  key={col.key}
6912
+ style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, isCheckboxMode, 'name') : undefined}
6761
6913
  className={clsx(
6762
6914
  'text-left px-4 py-3 text-xs font-medium uppercase tracking-wide relative group/th',
6763
6915
  'bg-theme-base border-b border-theme-border',
6916
+ colIdx === 0 && col.key === 'name' && PHONE_PINNED_HEAD,
6764
6917
  !isLastCol && 'border-r-subtle',
6765
6918
  isSortable ? 'text-theme-text-secondary hover:text-theme-text-primary cursor-pointer select-none' : 'text-theme-text-secondary'
6766
6919
  )}
@@ -6948,7 +7101,7 @@ export function ResourcesView({
6948
7101
  })()}
6949
7102
  {/* Resize handle with visible divider */}
6950
7103
  <div
6951
- className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center"
7104
+ className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center phone:hidden"
6952
7105
  style={{ transform: 'translateX(50%)' , zIndex: 10 }}
6953
7106
  onMouseDown={(e) => {
6954
7107
  const th = e.currentTarget.parentElement!
@@ -7030,13 +7183,12 @@ export function ResourcesView({
7030
7183
  open={tableActive && showBulkDeleteConfirm}
7031
7184
  onClose={() => { setShowBulkDeleteConfirm(false); setBulkForceDelete(false) }}
7032
7185
  onConfirm={() => {
7186
+ const submittedKeys = new Set(checkedItems.map(getResourceKey))
7187
+ setShowBulkDeleteConfirm(false)
7188
+ setBulkForceDelete(false)
7033
7189
  onBulkDelete?.(checkedBulkItems, {
7034
7190
  force: bulkForceDelete,
7035
- onSuccess: () => {
7036
- clearBulkSelection()
7037
- setShowBulkDeleteConfirm(false)
7038
- setBulkForceDelete(false)
7039
- },
7191
+ onSuccess: () => setCheckedResources(current => new Set([...current].filter(key => !submittedKeys.has(key)))),
7040
7192
  })
7041
7193
  }}
7042
7194
  title={`Delete ${checkedItems.length} ${selectedKind.kind}${checkedItems.length > 1 ? 's' : ''}?`}
@@ -7047,6 +7199,7 @@ export function ResourcesView({
7047
7199
  isLoading={isBulkDeleting}
7048
7200
  isClosable
7049
7201
  >
7202
+ <CrossplaneDeleteNotice resources={checkedItems} force={bulkForceDelete} />
7050
7203
  <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
7051
7204
  <input
7052
7205
  type="checkbox"
@@ -7203,8 +7356,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7203
7356
  <td
7204
7357
  onClick={onClick}
7205
7358
  onMouseEnter={onMouseEnter}
7206
- style={COMPARE_COLUMN_STYLE}
7207
- className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight)}
7359
+ style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, showCheckbox, 'compare') }}
7360
+ className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight, PHONE_PINNED_CELL)}
7208
7361
  >
7209
7362
  {pickedSide ? (
7210
7363
  <span
@@ -7226,7 +7379,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7226
7379
  )}
7227
7380
  {showCheckbox && (
7228
7381
  <td
7229
- className={clsx('border-b-subtle text-center px-0 py-3 w-10 cursor-pointer transition-colors', rowHighlight)}
7382
+ style={phonePinnedLeft(compareMode, showCheckbox, 'checkbox')}
7383
+ className={clsx('border-b-subtle text-center px-0 py-3 w-10 cursor-pointer transition-colors', rowHighlight, PHONE_PINNED_CELL)}
7230
7384
  onClick={(e) => { e.stopPropagation(); onRowCheckToggle?.(resource) }}
7231
7385
  >
7232
7386
  <input
@@ -7237,16 +7391,18 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7237
7391
  />
7238
7392
  </td>
7239
7393
  )}
7240
- {columns.map((col) => (
7394
+ {columns.map((col, colIdx) => (
7241
7395
  <td
7242
7396
  key={col.key}
7243
7397
  onClick={cellsAreClickable ? onClick : undefined}
7244
7398
  onMouseEnter={onMouseEnter}
7399
+ style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, showCheckbox, 'name') : undefined}
7245
7400
  className={clsx(
7246
7401
  'px-4 py-3 border-b-subtle transition-colors',
7247
7402
  cellsAreClickable && 'cursor-pointer',
7248
7403
  col.key !== 'status' && 'overflow-hidden truncate',
7249
7404
  rowHighlight,
7405
+ colIdx === 0 && col.key === 'name' && PHONE_PINNED_CELL,
7250
7406
  )}
7251
7407
  >
7252
7408
  <CellContent
@@ -7325,11 +7481,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7325
7481
  if (column === 'name') {
7326
7482
  const isTerminating = !!meta.deletionTimestamp
7327
7483
  const nameClass = clsx('text-sm font-medium truncate block', isTerminating ? 'text-theme-text-tertiary line-through' : 'text-theme-text-primary')
7484
+ const pauseState = isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged' ? getCrossplanePauseState(resource) : null
7328
7485
  const audit = auditBadges?.[`${meta.namespace || ''}/${meta.name}`]
7329
7486
  const auditTotal = audit ? audit.danger + audit.warning : 0
7330
7487
  return (
7331
7488
  <div className="flex items-center gap-1.5 min-w-0">
7332
- <Tooltip content={meta.name} wrapperClassName={findingMatch ? 'min-w-0' : undefined}>
7489
+ <Tooltip content={meta.name} wrapperClassName={findingMatch || pauseState ? 'min-w-0' : undefined}>
7333
7490
  {nameHref ? (
7334
7491
  <a
7335
7492
  href={nameHref}
@@ -7344,6 +7501,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7344
7501
  )}
7345
7502
  </Tooltip>
7346
7503
  <CopyNameButton name={meta.name} />
7504
+ <CrossplanePauseChip state={pauseState} compact />
7347
7505
  {findingMatch && (
7348
7506
  <Tooltip content={findingMatch.text}>
7349
7507
  <span className="shrink-0 text-xs italic text-theme-text-tertiary cursor-help">
@@ -7392,6 +7550,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7392
7550
 
7393
7551
  // Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
7394
7552
  const kindLower = normalizeKindToPlural(kind, group)
7553
+ if (column === 'status' && isLikelyCrossplaneMRGroup(kind, group ?? '') && isManagedResource(resource)) {
7554
+ return <ManagedResourceCell resource={resource} column={column} />
7555
+ }
7395
7556
  if (group && !hasCuratedColumns(kind, group)) {
7396
7557
  return <GenericCell resource={resource} column={column} />
7397
7558
  }
@@ -7418,6 +7579,14 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7418
7579
  return <KyvernoCleanupPolicyCell resource={resource} column={column} />
7419
7580
  }
7420
7581
 
7582
+ if (isApiGroup(resource.apiVersion, 'opentelemetry.io') && ['opentelemetrycollectors', 'instrumentations'].includes(kindLower)) {
7583
+ if (column === 'status') return <GenericCell resource={resource} column={column} />
7584
+ return <span className="text-sm text-theme-text-secondary break-all">{getOpenTelemetryColumnValue(resource, column) || '-'}</span>
7585
+ }
7586
+ if (isApiGroup(resource.apiVersion, 'flagger.app') && ['canaries', 'metrictemplates'].includes(kindLower)) {
7587
+ if (column === 'status') return <GenericCell resource={resource} column={column} />
7588
+ return <span className="text-sm text-theme-text-secondary break-all">{getFlaggerColumnValue(resource, column) || '-'}</span>
7589
+ }
7421
7590
  switch (kindLower) {
7422
7591
  case 'pods':
7423
7592
  return <PodCell resource={resource} column={column} />
@@ -7977,6 +8146,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7977
8146
  function GenericCell({ resource, column }: { resource: any; column: string }) {
7978
8147
  switch (column) {
7979
8148
  case 'status': {
8149
+ if (isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged') {
8150
+ return <CrossplaneStatusCell resource={resource} />
8151
+ }
7980
8152
  const derived = getGenericResourceStatus(resource)
7981
8153
  if (!derived) return <span className="text-sm text-theme-text-tertiary">-</span>
7982
8154
  return (
@@ -108,8 +108,8 @@ describe('curated column ownership', () => {
108
108
  // Brace-counting, not parsing: assert the extraction actually saw the whole
109
109
  // table, or a silently-missed key would let the guard above pass vacuously.
110
110
  it('extracted every curated key', () => {
111
- expect(CURATED.length).toBe(240)
112
- expect(OWNED.size).toBe(202)
111
+ expect(CURATED.length).toBe(244)
112
+ expect(OWNED.size).toBe(206)
113
113
  expect(CURATED).toContain('pods')
114
114
  expect(CURATED).toContain('crossplanemanagedresources')
115
115
  expect([...OWNED]).toContain('awsmanagedcontrolplanes')
@@ -166,7 +166,7 @@ describe('every declaration resolves', () => {
166
166
  .map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
167
167
 
168
168
  it('covers the whole ownership table', () => {
169
- expect(DECLARED.length).toBe(202)
169
+ expect(DECLARED.length).toBe(206)
170
170
  })
171
171
 
172
172
  it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {