@skyhook-io/k8s-ui 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/package.json +9 -2
  2. package/src/components/applications/ApplicationDetail.tsx +8 -2
  3. package/src/components/cluster-switcher/ClusterSwitcher.test.tsx +54 -0
  4. package/src/components/cluster-switcher/ClusterSwitcher.tsx +7 -3
  5. package/src/components/compare/ResourceCompareView.test.tsx +54 -0
  6. package/src/components/compare/ResourceCompareView.tsx +35 -12
  7. package/src/components/gitops/GitOpsDetailLayout.test.tsx +16 -0
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +54 -7
  9. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +1 -1
  10. package/src/components/gitops/GitOpsTableView.tsx +26 -7
  11. package/src/components/gitops/RollbackDialog.test.tsx +80 -0
  12. package/src/components/gitops/RollbackDialog.tsx +25 -6
  13. package/src/components/gitops/SyncOptionsDialog.render.test.tsx +374 -0
  14. package/src/components/gitops/SyncOptionsDialog.test.ts +54 -1
  15. package/src/components/gitops/SyncOptionsDialog.tsx +179 -34
  16. package/src/components/gitops/applicationset-row.test.ts +12 -0
  17. package/src/components/gitops/argo-radar-upgrade.test.tsx +369 -0
  18. package/src/components/gitops/detail-helpers.test.ts +8 -2
  19. package/src/components/gitops/detail-helpers.ts +9 -11
  20. package/src/components/gitops/index.ts +1 -1
  21. package/src/components/gitops/insights/GitOpsActivityInsightView.test.tsx +55 -0
  22. package/src/components/gitops/insights/GitOpsInsightViews.tsx +30 -12
  23. package/src/components/gitops/insights/GitOpsRollbackInsightView.test.tsx +73 -0
  24. package/src/components/gitops/insights/GitOpsSyncWindowInsightView.test.tsx +29 -0
  25. package/src/components/gitops/insights/insights-helpers.test.ts +37 -0
  26. package/src/components/gitops/insights/insights-helpers.ts +12 -0
  27. package/src/components/issues/ResourceIssuesSection.tsx +3 -1
  28. package/src/components/issues/diagnostic.ts +14 -1
  29. package/src/components/issues/severity.ts +14 -0
  30. package/src/components/kueue/AdmissionPath.tsx +223 -0
  31. package/src/components/kueue/QueueEvidence.tsx +148 -0
  32. package/src/components/kueue/constants.ts +1 -0
  33. package/src/components/kueue/eligibility.ts +43 -0
  34. package/src/components/kueue/flavors-tried.ts +79 -0
  35. package/src/components/kueue/helpers.test.ts +105 -0
  36. package/src/components/kueue/policy.ts +100 -0
  37. package/src/components/kueue/queue-detail.test.tsx +317 -0
  38. package/src/components/kueue/queue-status.ts +42 -0
  39. package/src/components/kueue/related-read.ts +48 -0
  40. package/src/components/kueue/testdata/kueue-v0.18-policy-enums.json +89 -0
  41. package/src/components/logs/StructuredLogLine.test.tsx +22 -0
  42. package/src/components/logs/StructuredLogLine.tsx +8 -3
  43. package/src/components/resources/KueueAdmissionSection.test.tsx +117 -0
  44. package/src/components/resources/KueueAdmissionSection.tsx +51 -12
  45. package/src/components/resources/KueuePendingPosition.test.tsx +97 -0
  46. package/src/components/resources/KueuePendingPosition.tsx +91 -0
  47. package/src/components/resources/ResourcesSidebar.test.tsx +8 -0
  48. package/src/components/resources/ResourcesSidebar.tsx +3 -2
  49. package/src/components/resources/ResourcesView.crd.test.tsx +28 -0
  50. package/src/components/resources/ResourcesView.node-pods.test.tsx +25 -0
  51. package/src/components/resources/ResourcesView.strimzi.test.tsx +37 -0
  52. package/src/components/resources/ResourcesView.tsx +50 -3
  53. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  54. package/src/components/resources/generic-status.ts +7 -0
  55. package/src/components/resources/get-pod-phase-display.test.ts +70 -0
  56. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +54 -1
  57. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +7 -8
  58. package/src/components/resources/renderers/CRDSchemaBrowser.test.tsx +105 -0
  59. package/src/components/resources/renderers/CRDSchemaBrowser.tsx +134 -0
  60. package/src/components/resources/renderers/CustomResourceDefinitionRenderer.test.tsx +106 -0
  61. package/src/components/resources/renderers/CustomResourceDefinitionRenderer.tsx +152 -0
  62. package/src/components/resources/renderers/JobRenderer.test.tsx +35 -0
  63. package/src/components/resources/renderers/JobRenderer.tsx +7 -2
  64. package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +5 -5
  65. package/src/components/resources/renderers/KueueQueueRenderers.tsx +250 -170
  66. package/src/components/resources/renderers/KueueQueueWorkloads.test.tsx +189 -0
  67. package/src/components/resources/renderers/KueueQueueWorkloads.tsx +460 -0
  68. package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +1 -1
  69. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +23 -52
  70. package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +11 -6
  71. package/src/components/resources/renderers/RayClusterRenderer.test.tsx +39 -0
  72. package/src/components/resources/renderers/RayClusterRenderer.tsx +23 -5
  73. package/src/components/resources/renderers/ServiceAccountRenderer.test.tsx +83 -0
  74. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +15 -10
  75. package/src/components/resources/renderers/StrimziConnectorRenderer.tsx +61 -0
  76. package/src/components/resources/renderers/WebhookConfigRenderer.test.tsx +32 -0
  77. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -3
  78. package/src/components/resources/renderers/crd-cells.test.tsx +30 -0
  79. package/src/components/resources/renderers/crd-cells.tsx +55 -0
  80. package/src/components/resources/renderers/crd-schema-parts.test.tsx +59 -0
  81. package/src/components/resources/renderers/crd-schema-parts.tsx +121 -0
  82. package/src/components/resources/renderers/index.ts +6 -0
  83. package/src/components/resources/renderers/kueue-cells.tsx +5 -0
  84. package/src/components/resources/resource-utils-argo.test.ts +94 -1
  85. package/src/components/resources/resource-utils-argo.ts +84 -5
  86. package/src/components/resources/resource-utils-crd.test.ts +32 -0
  87. package/src/components/resources/resource-utils-crd.ts +26 -0
  88. package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +12 -0
  89. package/src/components/resources/resource-utils-kueue-admission.test.ts +26 -0
  90. package/src/components/resources/resource-utils-kueue.test.ts +15 -0
  91. package/src/components/resources/resource-utils-kueue.ts +137 -4
  92. package/src/components/resources/resource-utils-opentelemetry.test.tsx +9 -1
  93. package/src/components/resources/resource-utils-opentelemetry.ts +1 -1
  94. package/src/components/resources/resource-utils-ray.test.ts +42 -1
  95. package/src/components/resources/resource-utils-ray.ts +38 -0
  96. package/src/components/resources/resource-utils-strimzi.test.tsx +118 -0
  97. package/src/components/resources/resource-utils-strimzi.ts +35 -0
  98. package/src/components/resources/resource-utils.ts +45 -1
  99. package/src/components/shared/ResourceActionsBar.tsx +83 -25
  100. package/src/components/shared/ResourceRendererDispatch.test.tsx +37 -3
  101. package/src/components/shared/ResourceRendererDispatch.tsx +40 -7
  102. package/src/components/topology/K8sResourceNode.test.ts +14 -0
  103. package/src/components/topology/K8sResourceNode.tsx +18 -1
  104. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  105. package/src/components/topology/TopologyGraph.tsx +96 -94
  106. package/src/components/topology/layout.ts +3 -1
  107. package/src/components/topology/pod-group-expansion.test.ts +38 -0
  108. package/src/components/ui/Badge.tsx +1 -0
  109. package/src/components/ui/RadarUpgradeNote.tsx +8 -0
  110. package/src/components/ui/drawer-components.tsx +1 -1
  111. package/src/components/ui/index.ts +1 -0
  112. package/src/components/workload/PodList.tsx +35 -9
  113. package/src/components/workload/WorkloadView.spec.test.tsx +41 -1
  114. package/src/components/workload/WorkloadView.tsx +61 -8
  115. package/src/components/workload/workload-logs-availability.test.ts +7 -0
  116. package/src/types/core.ts +40 -0
  117. package/src/types/gitops-insights.ts +11 -2
  118. package/src/types/gitops-tree.ts +2 -2
  119. package/src/types/kueue-queues.ts +84 -0
  120. package/src/types/read-coverage.ts +2 -1
  121. package/src/types/scheduling.ts +72 -0
  122. package/src/utils/animation.test.ts +38 -0
  123. package/src/utils/animation.ts +34 -0
  124. package/src/utils/api-resources-crd.test.ts +11 -0
  125. package/src/utils/api-resources.ts +5 -1
  126. package/src/utils/applications.test.ts +15 -1
  127. package/src/utils/applications.ts +26 -1
  128. package/src/utils/crd-schema.test.ts +107 -0
  129. package/src/utils/crd-schema.ts +201 -0
  130. package/src/utils/format.test.ts +37 -1
  131. package/src/utils/format.ts +28 -80
  132. package/src/utils/gitops-owner.test.ts +28 -2
  133. package/src/utils/gitops-owner.ts +19 -0
  134. package/src/utils/kueue-queue-workloads.ts +19 -0
  135. package/src/utils/resource-icons.ts +2 -0
  136. package/src/utils/topology-neighborhood.test.ts +90 -1
  137. package/src/utils/topology-neighborhood.ts +55 -3
  138. package/src/utils/workload-history-notes.test.ts +76 -0
  139. package/src/utils/workload-history-notes.ts +93 -0
@@ -1,4 +1,5 @@
1
1
  import { dedupeResourceRefs } from '../../utils/resource-refs'
2
+ import { isKubeRayClusterIdentity } from '../resources/resource-utils-ray'
2
3
  import { PodRow, PodListFrame, PodListToggle, workloadPodDetail } from './PodList'
3
4
  export { workloadPodDetail, podStatusLabel, podStatusClass, podDotClass } from './PodList'
4
5
  import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
@@ -38,10 +39,12 @@ import {
38
39
  } from 'lucide-react'
39
40
  import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
40
41
  import type { GitOpsStatus } from '../../types/gitops'
42
+ import type { ReadGap } from '../../types/read-coverage'
43
+ import { workloadHistoryNotes, type WorkloadHistoryCoverage } from '../../utils/workload-history-notes'
41
44
  import { isFetchError } from '../../types/fetch-error'
42
45
  import type { NavigateToResource } from '../../utils/navigation'
43
46
  import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
44
- import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
47
+ import { jobSetMemberRunKey, neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
45
48
  import { TopologyGraph } from '../topology/TopologyGraph'
46
49
  import { gitOpsOwnerFromRelationships, managerChipCoversGroup, managerChipPresentation, managerListing, type GitOpsOwnerRef } from '../../utils/gitops-owner'
47
50
  import { gitOpsRouteForResource } from '../../utils/gitops-route'
@@ -125,8 +128,8 @@ interface WorkloadViewProps {
125
128
  onCollapseToDrawer?: () => void
126
129
  /** false = collapsed drawer mode, true (default) = full expanded mode */
127
130
  expanded?: boolean
128
- /** false on the outgoing layer during an expand/collapse crossfade — suspend
129
- * keyboard shortcuts so the invisible layer doesn't capture them (default true) */
131
+ /** false on the outgoing layer during an expand/collapse crossfade: suspend
132
+ * keyboard shortcuts and let hosted renderers pause their reads (default true). */
130
133
  active?: boolean
131
134
  /** Close the drawer (collapsed mode) */
132
135
  onClose?: () => void
@@ -230,8 +233,12 @@ interface WorkloadViewProps {
230
233
  /** allEvents holds only this workload's own history (what it runs and the
231
234
  * resources attached to it), not its whole namespace. */
232
235
  historyScoped?: boolean
233
- /** Some related resources' history isn't included: there were more than the host follows. */
236
+ /** Scope bounds or expired ownership records may leave related history out. */
234
237
  historyIncomplete?: boolean
238
+ /** Radar may not hold every change since the workload existed. */
239
+ historyCoverage?: WorkloadHistoryCoverage | null
240
+ /** Related kinds the history can't include for this viewer. */
241
+ historyGaps?: ReadGap[] | null
235
242
  historyError?: Error | null
236
243
  onRetryHistory?: () => void
237
244
  /** Persisted lifecycle events reconstructed for resources related to this workload. */
@@ -360,6 +367,12 @@ interface WorkloadViewProps {
360
367
  /** Place resource notices within the replacement layout's content area. */
361
368
  leadContent?: ReactNode
362
369
  }) => ReactNode
370
+ /** Called instead of navigating when a Topology-tab click lands on a member
371
+ * Job of the JobSet this view shows, so the host can open the member in
372
+ * place. `memberKey` is the member list's run key, `jobs/<namespace>/<name>`
373
+ * (see `jobSetMemberRunKey`), not a node ID or UID. Omit to navigate as for
374
+ * any node. */
375
+ onSelectTopologyMember?: (memberKey: string) => void
363
376
  /** Render the metrics tab content */
364
377
  renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
365
378
  /** Render the Diagnose tab content - a path-shaped trace for network
@@ -480,6 +493,8 @@ export function WorkloadView({
480
493
  olderHistoryError = null,
481
494
  historyScoped = false,
482
495
  historyIncomplete = false,
496
+ historyCoverage,
497
+ historyGaps,
483
498
  historyError = null,
484
499
  onRetryHistory,
485
500
  relatedTimelineEvents = [],
@@ -509,6 +524,7 @@ export function WorkloadView({
509
524
  renderDiagnoseTab,
510
525
  reachableVia,
511
526
  renderExpandedOverview,
527
+ onSelectTopologyMember,
512
528
  renderSummary,
513
529
  extraTabs,
514
530
  tabOrder,
@@ -579,6 +595,21 @@ export function WorkloadView({
579
595
  const [selectedEventId, setSelectedEventId] = useState<string | null>(null)
580
596
  const [selectedPod, setSelectedPod] = useState<string | null>(null)
581
597
  const [initialContainer, setInitialContainer] = useState<string | null>(null)
598
+ // Hosts reuse this view across resources, and a resource can be recreated
599
+ // under the same name: a Pod picked for one must not reach the next, not
600
+ // even for one render. A UID arriving with the first load is not a change.
601
+ const selectionIdentity = `${kind}\u0000${namespace}\u0000${name}`
602
+ // A host can briefly pass the previous resource with the new name.
603
+ const selectionUID = resource?.metadata?.name === name && (resource?.metadata?.namespace ?? '') === (namespace ?? '') ? resource?.metadata?.uid as string | undefined : undefined
604
+ const [selectionScope, setSelectionScope] = useState({ identity: selectionIdentity, uid: selectionUID })
605
+ if (selectionScope.identity !== selectionIdentity || (selectionUID && selectionScope.uid !== selectionUID)) {
606
+ const recreated = selectionScope.identity === selectionIdentity && selectionScope.uid !== undefined
607
+ setSelectionScope({ identity: selectionIdentity, uid: selectionUID })
608
+ if (selectionScope.identity !== selectionIdentity || recreated) {
609
+ setSelectedPod(null)
610
+ setInitialContainer(null)
611
+ }
612
+ }
582
613
  const [copied, setCopied] = useState<string | null>(null)
583
614
  const [saveSuccess, setSaveSuccess] = useState(false)
584
615
  const [recentImageSave, setRecentImageSave] = useState(false)
@@ -613,6 +644,7 @@ export function WorkloadView({
613
644
  groupByApp: true,
614
645
  })
615
646
  }, [allEvents, topology, kind, group, namespace, name, historyScoped])
647
+ const historyNotes = useMemo(() => workloadHistoryNotes(historyCoverage, historyGaps), [historyCoverage, historyGaps])
616
648
 
617
649
  // Topology tab — the seeded neighborhood around this one workload (its
618
650
  // ownership core + attached Services/config/policies), not the whole namespace.
@@ -667,6 +699,13 @@ export function WorkloadView({
667
699
  const yamlObject = yamlObjectId ? yamlObjects.find((o) => o.id === yamlObjectId) : undefined
668
700
  const handleTopologyNodeClick = useCallback(
669
701
  (node: TopologyNode) => {
702
+ const memberKey = onSelectTopologyMember && topology && neighborhoodFocusId
703
+ ? jobSetMemberRunKey(topology, neighborhoodFocusId, node)
704
+ : undefined
705
+ if (memberKey) {
706
+ onSelectTopologyMember?.(memberKey)
707
+ return
708
+ }
670
709
  const resourceKind = topologyNodeResourceKind(node)
671
710
  if (!onNavigateToResource || !resourceKind || !node.name) return
672
711
  const group = apiVersionToGroup(node.data?.apiVersion as string | undefined)
@@ -677,7 +716,7 @@ export function WorkloadView({
677
716
  group,
678
717
  })
679
718
  },
680
- [onNavigateToResource],
719
+ [onNavigateToResource, onSelectTopologyMember, topology, neighborhoodFocusId],
681
720
  )
682
721
 
683
722
  // Flatten events from hierarchy
@@ -896,7 +935,7 @@ export function WorkloadView({
896
935
  // prominent "Diagnose" on a problem, a quiet icon when fine. Rendered via the host
897
936
  // slot (DiagnoseCustomization); standalone Radar injects it, Hub overrides it.
898
937
  const renderDiagnose = actionsBarProps?.renderDiagnose as
899
- | ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
938
+ | ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint; createdAt?: string }) => ReactNode)
900
939
  | undefined
901
940
  const headerActions = resource && !resourceGone && renderHeaderActions?.({ resource, context: expanded ? 'expanded' : 'drawer', onNavigate: onNavigateToResource })
902
941
  const diagnoseAction = resourceGone ? null : renderDiagnose?.({
@@ -905,6 +944,7 @@ export function WorkloadView({
905
944
  namespace,
906
945
  name,
907
946
  health: diagnoseHealthHint(apiKind, resource),
947
+ createdAt: resource?.metadata?.creationTimestamp,
908
948
  })
909
949
 
910
950
  const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
@@ -1131,6 +1171,7 @@ export function WorkloadView({
1131
1171
  )}
1132
1172
  {overviewIntro && <div className="px-4 pt-4">{overviewIntro}</div>}
1133
1173
  <ResourceRendererDispatch
1174
+ active={active}
1134
1175
  resource={selectedResource}
1135
1176
  data={resource}
1136
1177
  relationships={rendererRelationships}
@@ -1357,6 +1398,7 @@ export function WorkloadView({
1357
1398
  <div className="h-full min-h-0 overflow-y-auto">{specTab.render()}</div>
1358
1399
  ) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
1359
1400
  <InfoTab
1401
+ active={active}
1360
1402
  resource={resource}
1361
1403
  selectedResource={selectedResource}
1362
1404
  relationships={rendererRelationships}
@@ -1430,6 +1472,7 @@ export function WorkloadView({
1430
1472
  olderError={olderHistoryError}
1431
1473
  scoped={historyScoped}
1432
1474
  incomplete={historyIncomplete}
1475
+ notes={historyNotes}
1433
1476
  error={historyError}
1434
1477
  onRetry={onRetryHistory}
1435
1478
  selectedEventId={selectedEventId}
@@ -1738,6 +1781,7 @@ function EventsTab({
1738
1781
  olderError,
1739
1782
  scoped,
1740
1783
  incomplete,
1784
+ notes = [],
1741
1785
  error,
1742
1786
  onRetry,
1743
1787
  selectedEventId,
@@ -1754,6 +1798,8 @@ function EventsTab({
1754
1798
  olderError?: Error | null
1755
1799
  scoped?: boolean
1756
1800
  incomplete?: boolean
1801
+ /** Coverage and access sentences from workloadHistoryNotes. */
1802
+ notes?: string[]
1757
1803
  error?: Error | null
1758
1804
  onRetry?: () => void
1759
1805
  selectedEventId: string | null
@@ -1911,14 +1957,15 @@ function EventsTab({
1911
1957
  )}
1912
1958
  </div>
1913
1959
  )}
1914
- {(scoped || truncated || incomplete) && (
1960
+ {(scoped || truncated || incomplete || notes.length > 0) && (
1915
1961
  <div className="shrink-0 flex flex-wrap items-center gap-x-2 gap-y-0.5 border-b border-theme-border bg-theme-base px-4 py-1.5 text-xs text-theme-text-secondary" role="note">
1916
1962
  {scoped && (
1917
1963
  <span className="text-theme-text-tertiary">
1918
1964
  This workload, what it runs, and the Services, Ingresses, config and scalers attached to it.
1919
1965
  </span>
1920
1966
  )}
1921
- {incomplete && <span>This workload has more past runs and related resources than Radar follows, so some are left out.</span>}
1967
+ {incomplete && <span>Some past runs or related resources may be outside the history Radar can follow.</span>}
1968
+ {notes.map((note) => <span key={note}>{note}</span>)}
1922
1969
  {truncated && <span>Showing the most recent events. Older history exists.</span>}
1923
1970
  {truncated && olderError && (
1924
1971
  <span className="text-[var(--color-error)]" title={olderError.message}>Couldn't load older events.</span>
@@ -1997,6 +2044,7 @@ function EventsTab({
1997
2044
  // ============================================================================
1998
2045
 
1999
2046
  function InfoTab({
2047
+ active,
2000
2048
  resource,
2001
2049
  selectedResource,
2002
2050
  relationships,
@@ -2034,6 +2082,7 @@ function InfoTab({
2034
2082
  onEvaluateCapacity,
2035
2083
  recentImageSave,
2036
2084
  }: {
2085
+ active: boolean
2037
2086
  resource: any
2038
2087
  selectedResource: SelectedResource
2039
2088
  relationships?: Relationships
@@ -2089,6 +2138,7 @@ function InfoTab({
2089
2138
  </div>
2090
2139
  )}
2091
2140
  <ResourceRendererDispatch
2141
+ active={active}
2092
2142
  resource={selectedResource}
2093
2143
  data={resource}
2094
2144
  relationships={relationships}
@@ -2176,6 +2226,7 @@ const LOGS_TAB_WITHOUT_PODS_KINDS = new Set([
2176
2226
  'clusterworkflowtemplates',
2177
2227
  'scaledjobs',
2178
2228
  'jobsets',
2229
+ 'rayclusters',
2179
2230
  'clusters',
2180
2231
  ])
2181
2232
  const RUNTIME_WORKLOAD_OVERVIEW_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs'])
@@ -2193,6 +2244,8 @@ export function supportsLogsWithoutPods(
2193
2244
  if (normalizedKind === 'jobsets') {
2194
2245
  return group === 'jobset.x-k8s.io' && apiVersion === 'jobset.x-k8s.io/v1alpha2'
2195
2246
  }
2247
+ // A KubeRay RayCluster's tab reads the Pods it owns now, not recorded ones.
2248
+ if (normalizedKind === 'rayclusters') return isKubeRayClusterIdentity(group, apiVersion)
2196
2249
  // A CloudNativePG Cluster's instance Pods are its children, not related
2197
2250
  // Pods, and its logs are resolved server-side from the Cluster itself.
2198
2251
  if (normalizedKind === 'clusters') {
@@ -23,4 +23,11 @@ describe('supportsLogsWithoutPods', () => {
23
23
  expect(supportsLogsWithoutPods('clusters', 'Cluster', undefined, 'postgresql.cnpg.io/v1')).toBe(true)
24
24
  expect(supportsLogsWithoutPods('clusters', 'Cluster', 'cluster.x-k8s.io', 'cluster.x-k8s.io/v1beta1')).toBe(false)
25
25
  })
26
+
27
+ it('offers a KubeRay RayCluster its owned-Pod logs without recorded Pods, but not other RayCluster kinds', () => {
28
+ expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'ray.io', 'ray.io/v1')).toBe(true)
29
+ expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'ray.io', undefined)).toBe(true)
30
+ expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'ray.io', 'ray.io/v1alpha1')).toBe(false)
31
+ expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'example.io', 'example.io/v1')).toBe(false)
32
+ })
26
33
  })
package/src/types/core.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import type { Issue, IssueResourceRef } from '../components/issues/types'
1
2
  import type { CapacityIntegrationState } from './capacity'
3
+ import type { ReadGap, ReadKindRef } from './read-coverage'
2
4
 
3
5
  // Topology types matching the Go backend
4
6
 
@@ -119,6 +121,8 @@ export interface Capabilities {
119
121
  configManagement?: 'local' | 'operator' | 'cloud'
120
122
  exec: boolean // Terminal feature (pods/exec)
121
123
  localTerminal: boolean // Local terminal available (not in-cluster, not disabled)
124
+ /** Shell family where a hand-off command runs; absent from older Radars. */
125
+ localShell?: 'powershell' | 'posix'
122
126
  logs: boolean // Log viewer (pods/log)
123
127
  portForward: boolean // Port forwarding (pods/portforward)
124
128
  secrets: boolean // List secrets
@@ -129,6 +133,7 @@ export interface Capabilities {
129
133
  mcpExecTools?: boolean // Full MCP registration; does not grant the caller exec permission
130
134
  mcpReadOnly?: boolean // Full MCP endpoint is disabled; read-only endpoints remain available
131
135
  mcpEnabled: boolean // MCP server is running
136
+ mcpApplicationEvidence?: boolean
132
137
  // Karpenter discovery and NodePool read state. Optional on the wire for the
133
138
  // same newer-frontend/older-backend reason as `deployment` below — consumers
134
139
  // must treat absence as "unknown" and fall back to discovery signals.
@@ -165,6 +170,7 @@ export interface FeatureCapabilities {
165
170
  helmSourceIdentity?: boolean
166
171
  resourceReadCoverage?: boolean
167
172
  searchCoverage?: boolean
173
+ applicationEvidence?: boolean
168
174
  yamlReview?: boolean
169
175
  yamlSchemas?: boolean
170
176
  workloadImages?: boolean
@@ -179,7 +185,17 @@ export interface FeatureCapabilities {
179
185
  agentSigninCheck?: boolean
180
186
  serviceEndpointSlices?: boolean
181
187
  trafficRecords?: boolean
188
+ argoRollbackSync?: boolean
189
+ argoSyncInheritance?: boolean
190
+ argoEnableSelfHealOff?: boolean
182
191
  clusterScopedPlaceholder?: boolean
192
+ kueuePendingPosition?: boolean
193
+ // GET /api/workloads/rayclusters/{namespace}/{name}/pods. Older Radars that
194
+ // serve the route omit the flag; radarFeatures falls back to v1.15.0.
195
+ rayClusterPods?: boolean
196
+ // Workload Pod info marks native sidecars (containerStatuses[].sidecar).
197
+ podSidecarMarks?: boolean
198
+ kueueQueueWorkloads?: boolean
183
199
  }
184
200
 
185
201
  // DeploymentMode is the closed set of topologies Radar can run in.
@@ -234,6 +250,7 @@ export type CoreNodeKind =
234
250
  | 'HorizontalPodAutoscaler'
235
251
  | 'Job'
236
252
  | 'CronJob'
253
+ | 'JobSet' // jobset.x-k8s.io JobSet
237
254
  | 'Workflow'
238
255
  | 'CronWorkflow'
239
256
  | 'WorkflowTemplate'
@@ -876,8 +893,29 @@ export interface HPAMetricSummary {
876
893
  status: string
877
894
  }
878
895
 
896
+ /** Current findings from one bounded local inventory, independent of parent health.
897
+ * No groups or no findings never establishes absence of associations or recovery. */
898
+ export interface RelatedFindingsGroup {
899
+ /** Association anchor; each finding identifies its own subject. */
900
+ associatedVia: IssueResourceRef
901
+ /** Nonempty finite set of resource kinds selected for assessment; names, when present, lists all. */
902
+ kinds: { count: number; names?: ReadKindRef[] }
903
+ /** Authorized candidate resource snapshots, not findings or API calls. */
904
+ checked: number
905
+ /** Checked snapshots the producer could not assess, not known inactive states.
906
+ * checked - unknown is not a healthy count. */
907
+ unknown: number
908
+ findings?: Issue[]
909
+ /** Inventory-read limitations; always an array, including when empty. */
910
+ gaps: ReadGap[]
911
+ /** Findings were omitted from output; inventory read failures use gaps. */
912
+ truncated?: boolean
913
+ }
914
+
879
915
  // Resource with computed relationships and optional certificate info (API response wrapper)
880
916
  export interface ResourceWithRelationships<T = unknown> {
917
+ /** Optional association bundle; absence is not an all-clear. */
918
+ relatedFindings?: RelatedFindingsGroup[]
881
919
  resource: T
882
920
  relationships?: Relationships
883
921
  certificateInfo?: SecretCertificateInfo
@@ -1738,6 +1776,8 @@ export interface ImageMetadata {
1738
1776
  export interface WorkloadPodContainerInfo {
1739
1777
  name: string
1740
1778
  init?: boolean
1779
+ /** An init container with restartPolicy Always. Absent from older Radars. */
1780
+ sidecar?: boolean
1741
1781
  ready: boolean
1742
1782
  restartCount: number
1743
1783
  }
@@ -56,6 +56,9 @@ export interface GitOpsInsightSummary {
56
56
  source?: string
57
57
  targetRevision?: string
58
58
  lastRevision?: string
59
+ // Revision(s) deployed by the latest successful marked rollback, set only while
60
+ // effective auto-sync is off. Absence does not mean no rollback happened.
61
+ rollbackRevision?: string
59
62
  lastReconcile?: string
60
63
  partialReason?: string
61
64
  // Human-readable sync mode for the chip in the status strip.
@@ -84,8 +87,8 @@ export interface GitOpsInsightSummary {
84
87
  // controller's verdicts are not in the Application object and any
85
88
  // per-resource health shown is Radar's own read.
86
89
  resourceHealthMode?: GitOpsHealthMode
87
- // The Application deploys to another cluster; Radar derives nothing about
88
- // its resources from here.
90
+ // The current destination or recorded Argo comparison is elsewhere;
91
+ // Radar derives nothing from same-named local resources.
89
92
  remoteDestination?: boolean
90
93
  // Per-resource health came from Argo CD's API server: the verdicts shown
91
94
  // are Argo's even though the Application object doesn't carry them.
@@ -242,6 +245,12 @@ export interface GitOpsHistoryItem {
242
245
  rawMessage?: string
243
246
  source?: string
244
247
  initiatedBy?: string
248
+ // Whether an Argo CD history entry has an ID and recorded source(s), independently
249
+ // of auto-sync, lifecycle, in-flight operations and RBAC. Absent where not
250
+ // applicable (current-operation and Flux rows), or not supplied by Radar.
251
+ rollbackable?: boolean
252
+ // Compatibility warning: the current Application or history entry uses plural sources.
253
+ rollbackMultiSourceWarning?: boolean
245
254
  }
246
255
 
247
256
  export interface GitOpsCapabilities {
@@ -95,7 +95,7 @@ export interface GitOpsResourceTree {
95
95
  // The host couldn't reach the controller's API server and is retrying;
96
96
  // ask again now and then.
97
97
  healthApiRetrying?: boolean
98
- // The Application (or Flux object with spec.kubeConfig) deploys to another
99
- // cluster; Radar derives nothing about its resources from here.
98
+ // The current destination or recorded Argo comparison is elsewhere;
99
+ // Radar derives nothing from same-named local resources.
100
100
  remoteDestination?: boolean
101
101
  }
@@ -0,0 +1,84 @@
1
+ // Which Workloads belong to one Kueue queue, as Radar's
2
+ // GET /api/kueue/{clusterqueues/{name},localqueues/{namespace}/{name}}/workloads
3
+ // serializes it (internal/server/kueue_queue_workloads.go).
4
+ import type { ReadGapSet } from './read-coverage'
5
+ import type { KueueAdmissionState } from './scheduling'
6
+ import type { ResourceReadCoverage, ResourceReadErrorBody } from './resource-coverage'
7
+
8
+ export type KueueQueueMembership = 'submission' | 'reservation'
9
+
10
+ export interface KueueQueueWorkload {
11
+ namespace: string
12
+ name: string
13
+ uid: string
14
+ apiVersion: string
15
+ createdAt: string | null
16
+ deleting: boolean
17
+ /** The Workload's shared admission classification (schedulinginsight.ClassifyKueueWorkload); absent when Radar doesn't classify it. */
18
+ phase?: 'pending' | 'quota_reserved' | 'admitted' | 'finished' | (string & {})
19
+ admission?: KueueAdmissionState
20
+ /** Why the Workload is in this queue. */
21
+ membership: KueueQueueMembership[]
22
+ queueName?: string
23
+ /**
24
+ * Where the Workload's LocalQueue points now, when Radar read it; not where it
25
+ * pointed at submission. Absent when there is no queueName, the LocalQueue
26
+ * doesn't exist, or it wasn't read (then `localQueueUnread` is true).
27
+ */
28
+ submission?: { localQueue: string; localQueueUID: string; clusterQueue: string }
29
+ /** Radar didn't read the LocalQueues in this Workload's namespace, so whether it was submitted here is unknown. Absent means false. */
30
+ localQueueUnread?: boolean
31
+ /** The ClusterQueue the Workload reports its admission in. */
32
+ reservation?: { clusterQueue: string }
33
+ }
34
+
35
+ /** One page of a queue's Workloads. Every count describes all members Radar read for you (not just this page), as of this page's read. */
36
+ export interface KueueQueueMembers {
37
+ items: KueueQueueWorkload[]
38
+ /** Members the filters keep, across all pages. */
39
+ matching: number
40
+ /**
41
+ * Over every member Radar read for you; submission and reservation overlap
42
+ * (ClusterQueue only: a LocalQueue's members all belong by submission, so its
43
+ * reservation count is 0). `states` counts, by admission state, the members
44
+ * the membership filter keeps.
45
+ */
46
+ counts: { members: number; submission: number; reservation: number; states: Record<string, number> }
47
+ next?: string
48
+ /**
49
+ * The Workload read alone. `complete` doesn't make the membership complete:
50
+ * see `submissionUnresolved`.
51
+ */
52
+ coverage: ResourceReadCoverage
53
+ /**
54
+ * ClusterQueue only. Namespaces whose Workloads Radar read but whose
55
+ * LocalQueues it didn't: only reservations place Workloads there, so while
56
+ * set, `counts.members`, `counts.submission` and `matching` (unless filtered
57
+ * to reservation) are lower bounds. Absent: no such namespace.
58
+ */
59
+ submissionUnresolved?: ReadGapSet<string>
60
+ }
61
+
62
+ export interface KueueQueueLocalQueue {
63
+ namespace: string
64
+ name: string
65
+ /** The LocalQueue's own counts as its controller reports them; null when not reported. */
66
+ reported: { pending: number | null; reserving: number | null; admitted: number | null }
67
+ /** Radar read Workloads in this LocalQueue's namespace for you. When false, `workloadsGap` is the pkg/readreason code, when Radar knows it. */
68
+ workloadsRead: boolean
69
+ workloadsGap?: string
70
+ }
71
+
72
+ /**
73
+ * Each part is present or its error is: `workloads` or `workloadsError`;
74
+ * `localQueues` or `localQueuesError` for a ClusterQueue's first page only
75
+ * (later pages and LocalQueue responses carry neither, except an error).
76
+ */
77
+ export interface KueueQueueWorkloadsResponse {
78
+ queue: { kind: 'ClusterQueue' | 'LocalQueue'; namespace?: string; name: string; uid: string; clusterQueue?: string }
79
+ workloads?: KueueQueueMembers
80
+ workloadsError?: ResourceReadErrorBody
81
+ /** At most 500 items; `total` counts every LocalQueue found. Not paged. */
82
+ localQueues?: { items: KueueQueueLocalQueue[]; total: number; coverage: ResourceReadCoverage }
83
+ localQueuesError?: ResourceReadErrorBody
84
+ }
@@ -38,7 +38,8 @@ export interface ReadObjectRef {
38
38
  * Part of an answer Radar did not read, and why. A gap covers every
39
39
  * combination of its axes. An axis that applies is always present, listed or
40
40
  * as an extent; an absent axis doesn't apply to the gap (namespaces on a
41
- * cluster-scoped kind). Within one answer a namespace is in at most one gap.
41
+ * cluster-scoped kind). Within a single-kind answer a namespace is in at most one gap;
42
+ * multi-kind answers may name it for different kinds and reasons.
42
43
  * `action` is what the code calls for, so a code the client doesn't know still
43
44
  * says what resolves it.
44
45
  */
@@ -45,9 +45,81 @@ export interface SchedulingObservation {
45
45
  waitingForReplacementPods?: SchedulingCondition
46
46
  requeueState?: { count?: number; requeueAt?: string }
47
47
  concurrentAdmission?: { parentName: string; parentRef?: SchedulingRef }
48
+ pendingPosition?: KueuePendingPosition
49
+ admission?: KueueAdmissionState
48
50
  }
49
51
  }
50
52
 
53
+ // Mirrors resourcecontext.KueueAdmissionFact (see its Go doc for the
54
+ // evidence contract: message is bounded display text, an absent stale means
55
+ // not known to be stale).
56
+ export interface KueueAdmissionFact {
57
+ code: string
58
+ // Open like code: a newer Radar may send a source this UI doesn't know.
59
+ source?: 'condition' | 'admission_check' | 'spec' | (string & {})
60
+ name?: string
61
+ reason?: string
62
+ message?: string
63
+ since?: string
64
+ stale?: boolean
65
+ }
66
+
67
+ // Mirrors resourcecontext.KueueAdmissionState. The server is the only place
68
+ // codes and attention are decided; a code this UI does not know still renders
69
+ // verbatim. A missing fact is not evidence on its own (see the Go doc for
70
+ // which facts each state reports), and a missing admission block means an
71
+ // older Radar, not a Workload with no blocker.
72
+ export interface KueueAdmissionState {
73
+ state: string
74
+ attention: string
75
+ stale?: boolean
76
+ inactive?: KueueAdmissionFact
77
+ blocker?: KueueAdmissionFact
78
+ disruption?: KueueAdmissionFact
79
+ }
80
+
81
+ /**
82
+ * Where Kueue's visibility API placed one pending Workload when Radar read it,
83
+ * as the viewer (pkg/resourcecontext/kueue_position.go). Never derived by Radar.
84
+ */
85
+ export type KueuePendingPositionStatus =
86
+ | 'listed'
87
+ | 'not_listed'
88
+ | 'limit_reached'
89
+ | 'queue_unknown'
90
+ | 'no_local_queue'
91
+ | 'not_served'
92
+ | 'unsupported'
93
+ | 'unavailable'
94
+
95
+ export interface KueuePendingPosition {
96
+ status: KueuePendingPositionStatus | (string & {})
97
+ /** listed only: Workloads Kueue orders before this one (Kueue's 0-based position). */
98
+ aheadInLocalQueue?: number
99
+ aheadInClusterQueue?: number
100
+ /** Workload spec.queueName; omitted when empty, even when no request was made. */
101
+ localQueue?: string
102
+ /** Rows Kueue returned; set whenever a read succeeded. */
103
+ rows?: number
104
+ /** Requested limit; present only with a successfully decoded answer. */
105
+ rowLimit?: number
106
+ /** Supported version selected for the lookup; absent when none was selected. */
107
+ apiVersion?: string
108
+ /** When Radar made the read (RFC 3339). */
109
+ readAt?: string
110
+ /** pkg/readreason code; unavailable only. */
111
+ code?: string
112
+ message?: string
113
+ }
114
+
115
+ /** GET /api/kueue/position/{namespace}/{name} */
116
+ export interface KueuePositionResponse {
117
+ uid: string
118
+ /** The supplied Workload snapshot permits a lookup; not proof of queue membership. */
119
+ eligible: boolean
120
+ position?: KueuePendingPosition
121
+ }
122
+
51
123
  export interface KueueAdmissionWorkload {
52
124
  apiVersion: string
53
125
  namespace: string
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ DURATION_HOVER_REVEAL_IN,
4
+ DURATION_HOVER_REVEAL_OUT,
5
+ HOVER_INTENT_DELAY,
6
+ HOVER_REVEAL_ACTION,
7
+ HOVER_REVEAL_HIT,
8
+ HOVER_REVEAL_REST,
9
+ } from './animation'
10
+
11
+ // The presets spell their timing out for Tailwind's scanner; this keeps the
12
+ // literals in step with the named constants.
13
+ describe('hover-reveal presets', () => {
14
+ const intent = `delay-${HOVER_INTENT_DELAY}`
15
+ const into = `duration-${DURATION_HOVER_REVEAL_IN}`
16
+ const outOf = `duration-${DURATION_HOVER_REVEAL_OUT}`
17
+
18
+ it.each([
19
+ ['action', HOVER_REVEAL_ACTION],
20
+ ['hit target', HOVER_REVEAL_HIT],
21
+ ])('the %s leaves on the out clock and comes in on the in clock after the intent delay', (_, preset) => {
22
+ const classes = preset.split(/\s+/)
23
+ expect(classes).toContain(outOf)
24
+ for (const v of ['group-hover', 'group-has-[:focus-visible]']) {
25
+ expect(classes).toContain(`${v}:${into}`)
26
+ expect(classes).toContain(`${v}:${intent}`)
27
+ }
28
+ })
29
+
30
+ it('the resting content steps out on the out clock and returns on the in clock', () => {
31
+ const classes = HOVER_REVEAL_REST.split(/\s+/)
32
+ expect(classes).toContain(into)
33
+ for (const v of ['group-hover', 'group-has-[:focus-visible]']) {
34
+ expect(classes).toContain(`${v}:${outOf}`)
35
+ expect(classes).toContain(`${v}:${intent}`)
36
+ }
37
+ })
38
+ })
@@ -45,6 +45,17 @@ export const DURATION_DISCLOSURE = 300
45
45
  * should feel immediate. */
46
46
  export const DURATION_MENU_IN = 140
47
47
  export const DURATION_MENU_OUT = 100
48
+ /** Hover-revealed row actions (HOVER_REVEAL_*). In the range of Radar's other
49
+ * hover effects (150–200ms) rather than the menu clock, which reads as a
50
+ * flicker on something the pointer merely passes over. The exit is shorter
51
+ * so leaving a row feels immediate. The preset class strings spell these
52
+ * values out for Tailwind's scanner; a test keeps them in step. */
53
+ export const DURATION_HOVER_REVEAL_IN = 200
54
+ export const DURATION_HOVER_REVEAL_OUT = 120
55
+ /** Hover intent before a row action comes in: a pointer sweeping down a long
56
+ * list passes each row in well under this, so actions don't ripple in behind
57
+ * it. Entrance only; leaving a row hides its action at once. */
58
+ export const HOVER_INTENT_DELAY = 60
48
59
  /** Modal dialogs and command palettes. */
49
60
  export const DURATION_DIALOG_IN = 220
50
61
  export const DURATION_DIALOG_OUT = 160
@@ -104,6 +115,29 @@ export const TRANSITION_PANEL =
104
115
  export const TRANSITION_MENU =
105
116
  `transition-[translate,scale,opacity] ${TW_EASE} motion-reduce:transition-none`
106
117
 
118
+ /** Hover-revealed row action, the resting half: the row content the action
119
+ * replaces (an age, a chip) lifts 3px and fades on the exit clock. Needs a
120
+ * `group` ancestor; keyboard reveal keys on :focus-visible inside the group,
121
+ * so a row left focused by a mouse click doesn't keep its action up. Timing
122
+ * is DURATION_HOVER_REVEAL_IN / _OUT and HOVER_INTENT_DELAY. */
123
+ export const HOVER_REVEAL_REST =
124
+ `transition-[opacity,translate] duration-200 ${TW_EASE} motion-reduce:transition-none group-hover:opacity-0 group-hover:-translate-y-[3px] group-hover:duration-120 group-hover:delay-60 group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:-translate-y-[3px] group-has-[:focus-visible]:duration-120 group-has-[:focus-visible]:delay-60`
125
+
126
+ /** Hover-revealed row action, the action half: rises 3px into place on the
127
+ * entrance clock after the hover-intent delay, drops out on the exit clock.
128
+ * Never a hit target itself, so its padding and any backdrop leave clicks to
129
+ * the row underneath; put HOVER_REVEAL_HIT on the control inside it. Pair
130
+ * with HOVER_REVEAL_REST in one group. */
131
+ export const HOVER_REVEAL_ACTION =
132
+ `transition-[opacity,translate] duration-120 ${TW_EASE} motion-reduce:transition-none pointer-events-none opacity-0 translate-y-[3px] group-hover:opacity-100 group-hover:translate-y-0 group-hover:duration-200 group-hover:delay-60 group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:translate-y-0 group-has-[:focus-visible]:duration-200 group-has-[:focus-visible]:delay-60`
133
+
134
+ /** Hover-revealed row action, the control inside it: takes clicks only while
135
+ * shown. pointer-events transitions as a discrete property on the same
136
+ * clock, so it flips partway through the entrance rather than at hover start
137
+ * and a click during the intent delay lands on the row. */
138
+ export const HOVER_REVEAL_HIT =
139
+ `transition-[pointer-events] transition-discrete duration-120 ${TW_EASE} motion-reduce:transition-none pointer-events-none group-hover:pointer-events-auto group-hover:duration-200 group-hover:delay-60 group-has-[:focus-visible]:pointer-events-auto group-has-[:focus-visible]:duration-200 group-has-[:focus-visible]:delay-60`
140
+
107
141
  export type OverlayKind = 'menu' | 'dialog' | 'sheet' | 'drawer'
108
142
 
109
143
  const OVERLAY_DURATIONS: Record<OverlayKind, { in: number; out: number }> = {