@skyhook-io/k8s-ui 1.16.2 → 1.17.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 (152) hide show
  1. package/package.json +3 -3
  2. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  3. package/src/components/applications/ApplicationDetail.tsx +17 -15
  4. package/src/components/checks/types.ts +2 -13
  5. package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
  6. package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
  7. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
  8. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
  9. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
  10. package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
  11. package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
  12. package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
  13. package/src/components/cnpg/index.ts +14 -0
  14. package/src/components/cnpg/primitives.tsx +134 -0
  15. package/src/components/cnpg/relations.test.ts +304 -0
  16. package/src/components/cnpg/relations.ts +417 -0
  17. package/src/components/cnpg/workspace.test.ts +285 -0
  18. package/src/components/cnpg/workspace.ts +700 -0
  19. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  20. package/src/components/gitops/detail-helpers.ts +1 -1
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
  22. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
  23. package/src/components/issues/IssuesView.tsx +10 -1
  24. package/src/components/issues/issue-timing.ts +2 -2
  25. package/src/components/issues/issues.test.ts +1 -1
  26. package/src/components/issues/types.ts +24 -0
  27. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  28. package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
  29. package/src/components/logs/useLogBuffer.test.ts +17 -0
  30. package/src/components/logs/useLogStream.ts +11 -0
  31. package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
  32. package/src/components/resources/ResourcesSidebar.tsx +183 -11
  33. package/src/components/resources/ResourcesView.tsx +12 -9
  34. package/src/components/resources/index.ts +1 -1
  35. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  36. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  37. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
  38. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  39. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  42. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  43. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
  44. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
  45. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
  46. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
  47. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  48. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  49. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  50. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  51. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
  53. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
  54. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  55. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  56. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  57. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  58. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  59. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  60. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  61. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  62. package/src/components/resources/renderers/HPARenderer.tsx +2 -2
  63. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  64. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  65. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  66. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  67. package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
  68. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  69. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  70. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  71. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  73. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  75. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  76. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  77. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  78. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  79. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  80. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  81. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  82. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +21 -7
  84. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  85. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  86. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  87. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  88. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  89. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  90. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  91. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
  92. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  93. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  94. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  95. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  96. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  97. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  98. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  99. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  100. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  101. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
  102. package/src/components/resources/resource-utils-cnpg.ts +3 -7
  103. package/src/components/resources/resource-utils-keda.ts +1 -7
  104. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  105. package/src/components/resources/resource-utils-velero.ts +0 -12
  106. package/src/components/resources/resource-utils.ts +17 -33
  107. package/src/components/resources/status-overclaim.test.ts +1 -1
  108. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  109. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  110. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  111. package/src/components/shared/ResourceActionsBar.tsx +9 -20
  112. package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
  113. package/src/components/shared/SetImageDialog.test.ts +75 -6
  114. package/src/components/shared/SetImageDialog.tsx +92 -84
  115. package/src/components/shared/index.ts +2 -1
  116. package/src/components/timeline/TimelineList.tsx +187 -38
  117. package/src/components/timeline/TimelineListFold.test.tsx +56 -0
  118. package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
  119. package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
  120. package/src/components/timeline/index.ts +1 -0
  121. package/src/components/timeline/timeline-filters.ts +7 -0
  122. package/src/components/topology/TopologyGraph.tsx +5 -3
  123. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  124. package/src/components/ui/ScheduleValue.tsx +3 -2
  125. package/src/components/ui/drawer-components.test.tsx +66 -1
  126. package/src/components/ui/drawer-components.tsx +43 -29
  127. package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
  128. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  129. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  130. package/src/components/workload/WorkloadView.tsx +216 -48
  131. package/src/components/workload/index.ts +1 -0
  132. package/src/components/workload/workload-logs-availability.test.ts +6 -0
  133. package/src/index.ts +3 -0
  134. package/src/types/capacity.ts +10 -0
  135. package/src/types/core.ts +32 -1
  136. package/src/utils/format.test.ts +40 -2
  137. package/src/utils/format.ts +5 -1
  138. package/src/utils/gateway-references.test.ts +17 -0
  139. package/src/utils/gateway-references.ts +29 -0
  140. package/src/utils/gitops-write-guard.test.ts +401 -0
  141. package/src/utils/gitops-write-guard.ts +509 -0
  142. package/src/utils/index.ts +2 -0
  143. package/src/utils/ingress-class-references.ts +22 -0
  144. package/src/utils/log-level.ts +14 -0
  145. package/src/utils/navigation.test.ts +13 -0
  146. package/src/utils/navigation.ts +5 -0
  147. package/src/utils/node-lifecycle.test.ts +36 -0
  148. package/src/utils/node-lifecycle.ts +77 -0
  149. package/src/utils/resource-hierarchy.test.ts +32 -0
  150. package/src/utils/resource-hierarchy.ts +28 -2
  151. package/src/utils/resource-refs.test.ts +23 -0
  152. package/src/utils/resource-refs.ts +30 -0
@@ -1,3 +1,4 @@
1
+ import { dedupeResourceRefs } from '../../utils/resource-refs'
1
2
  import { PodRow, PodListFrame, PodListToggle, workloadPodDetail } from './PodList'
2
3
  export { workloadPodDetail, podStatusLabel, podStatusClass, podDotClass } from './PodList'
3
4
  import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
@@ -37,6 +38,7 @@ import {
37
38
  } from 'lucide-react'
38
39
  import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
39
40
  import type { GitOpsStatus } from '../../types/gitops'
41
+ import { isFetchError } from '../../types/fetch-error'
40
42
  import type { NavigateToResource } from '../../utils/navigation'
41
43
  import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
42
44
  import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
@@ -76,10 +78,21 @@ import { ServicePortCards, type ServicePortRenderProps } from '../resources/rend
76
78
  import { rolloutMayAdvanceAutomatically, type WorkloadRolloutActivity } from '../../utils/workload-rollout'
77
79
  import { WorkloadRolloutNotice } from './WorkloadRolloutNotice'
78
80
  import { isCoreBatchJob } from '../../utils/api-resources'
81
+ import { formatRelativeAgeTime } from '../../utils/format'
79
82
 
80
- export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
83
+ export type WorkloadTabType = 'overview' | 'spec' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
81
84
  type TabType = WorkloadTabType
82
85
 
86
+ /** A host-provided tab. `after` places it behind a built-in tab; `replaces` hides that built-in tab. */
87
+ export interface WorkloadExtraTab {
88
+ id: string
89
+ label: string
90
+ icon?: ReactNode
91
+ after?: WorkloadTabType
92
+ replaces?: WorkloadTabType
93
+ render: () => ReactNode
94
+ }
95
+
83
96
  export interface ResourceOwnershipContext {
84
97
  application?: {
85
98
  key: string
@@ -271,6 +284,8 @@ interface WorkloadViewProps {
271
284
  * resource rather than from owner labels on a managed object.
272
285
  */
273
286
  onNavigateGitOpsPath?: (path: string) => void
287
+ /** Opens the live traffic view focused on this workload. */
288
+ onOpenLiveTraffic?: () => void
274
289
 
275
290
  // ── Render props for platform-specific content ───────────────────────────
276
291
  /** Render the logs tab content */
@@ -286,6 +301,24 @@ interface WorkloadViewProps {
286
301
  initialContainer: string | null
287
302
  onConsumeInitialContainer: () => void
288
303
  }) => ReactNode
304
+ /**
305
+ * A composed summary for kinds that have one. When it returns content, the
306
+ * Overview tab shows the summary and the resource's own renderer moves to a
307
+ * "Spec & status" tab, so the same facts never render twice. Return null to
308
+ * keep the default Overview.
309
+ */
310
+ renderSummary?: (props: {
311
+ kind: string
312
+ apiKind: string
313
+ namespace: string
314
+ name: string
315
+ group?: string
316
+ resource: any
317
+ context: 'drawer' | 'expanded'
318
+ onNavigate?: NavigateToResource
319
+ }) => ReactNode
320
+ /** Extra tabs for the expanded view (e.g. a domain's own sections). */
321
+ extraTabs?: WorkloadExtraTab[]
289
322
  /** Render a full replacement for the expanded Overview tab. */
290
323
  renderExpandedOverview?: (props: {
291
324
  kind: string
@@ -434,6 +467,8 @@ export function WorkloadView({
434
467
  renderDiagnoseTab,
435
468
  reachableVia,
436
469
  renderExpandedOverview,
470
+ renderSummary,
471
+ extraTabs,
437
472
  renderRelatedYaml,
438
473
  renderMetricsTab,
439
474
  renderCostTab,
@@ -464,6 +499,7 @@ export function WorkloadView({
464
499
  helmOwnerSource,
465
500
  onOpenHelmRelease,
466
501
  onNavigateGitOpsPath,
502
+ onOpenLiveTraffic,
467
503
  }: WorkloadViewProps) {
468
504
  // The live object preserves exact CRD capitalization (RayJob, TFJob); the URL
469
505
  // plural remains the API resource name used by fetches and mutations.
@@ -480,8 +516,10 @@ export function WorkloadView({
480
516
 
481
517
  // Collapsed mode state (YAML toggle for drawer mode)
482
518
  const [showYaml, setShowYaml] = useState(initialTab === 'yaml')
519
+ const [drawerSpec, setDrawerSpec] = useState(false)
483
520
  useEffect(() => {
484
521
  setShowYaml(initialTab === 'yaml')
522
+ setDrawerSpec(false)
485
523
  }, [kindProp, namespace, name, initialTab])
486
524
 
487
525
  const switchView = useCallback((yaml: boolean) => {
@@ -656,6 +694,11 @@ export function WorkloadView({
656
694
  setTimeout(() => setCopied(null), 2000)
657
695
  }, [])
658
696
 
697
+ // The edit is applied on top of the copy on screen, so the server is asked to
698
+ // reject it if the Secret was deleted or changed since that copy loaded.
699
+ // Otherwise the save would recreate a deleted Secret, or put back the old
700
+ // value of a key someone else changed.
701
+ const resourceVersion: string | undefined = resource?.metadata?.resourceVersion
659
702
  const handleSaveSecretValue = useCallback(async (yaml: string) => {
660
703
  if (!onUpdateResource) return
661
704
  try {
@@ -665,12 +708,14 @@ export function WorkloadView({
665
708
  name,
666
709
  yaml,
667
710
  force: true,
711
+ reviewedResourceVersion: resourceVersion,
668
712
  })
669
713
  setTimeout(() => refetch(), 1000)
670
- } catch {
671
- // Error handled by mutation (toast)
714
+ } catch (error) {
715
+ refetch()
716
+ throw error
672
717
  }
673
- }, [onUpdateResource, apiKind, namespace, name, refetch])
718
+ }, [onUpdateResource, apiKind, namespace, name, resourceVersion, refetch])
674
719
 
675
720
  const handleSaved = useCallback(() => {
676
721
  setSaveSuccess(true)
@@ -733,7 +778,32 @@ export function WorkloadView({
733
778
  ? getWorkloadDisplayStatus(resource, apiKind, workloadPods)
734
779
  : null
735
780
  const rolloutActivity = rolloutDisplay?.activity ?? null
736
- const status = rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
781
+ // A 404 with a cached copy: the object was deleted while this view was
782
+ // open. Everything below that copy describes the past.
783
+ const resourceGone = Boolean(resource) && isFetchError(resourceError) && resourceError.status === 404
784
+ const status = resourceGone
785
+ ? { text: 'Deleted', color: 'status-unknown' }
786
+ : rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
787
+ // The host's actions, and the host's renderers with their per-container
788
+ // terminal, logs, scale and env controls, would act on an object that no
789
+ // longer exists. The plain renderers show the last copy without them.
790
+ const actionsBarPropsForState = resourceGone ? undefined : effectiveActionsBarProps
791
+ const rendererOverridesForState = resourceGone ? undefined : rendererOverrides
792
+ // The host's overview panels and Reachability tab diagnose the live object,
793
+ // and the reachability panel polls for it every few seconds.
794
+ const renderOverviewLeadForState = resourceGone ? undefined : renderOverviewLead
795
+ const renderOverviewExtraForState = resourceGone ? undefined : renderOverviewExtra
796
+ const renderDiagnoseTabForState = resourceGone ? undefined : renderDiagnoseTab
797
+ // A remount closes any dialog the bar had open: once its actions are gone,
798
+ // a confirm button left on screen would do nothing.
799
+ const actionsBarKey = resourceGone ? 'gone' : 'live'
800
+ const deletedNotice = resourceGone ? (
801
+ <DeletedResourceNotice
802
+ kindLabel={displayKindName(apiKind, resource?.kind)}
803
+ owner={relationships?.deployment ?? relationships?.owner}
804
+ onNavigateToResource={onNavigateToResource}
805
+ />
806
+ ) : null
737
807
  const rolloutMayAutoAdvance = rolloutActivity ? rolloutMayAdvanceAutomatically(rolloutActivity) : false
738
808
  useEffect(() => {
739
809
  if (!recentImageSave) return
@@ -746,7 +816,9 @@ export function WorkloadView({
746
816
  const timeout = window.setTimeout(() => setRecentImageSave(false), 15000)
747
817
  return () => window.clearTimeout(timeout)
748
818
  }, [recentImageSave, rolloutActivity?.phase])
749
- useProgressiveRefresh(rolloutMayAutoAdvance || recentImageSave, refetchProp)
819
+ // The rollout state above is the last copy fetched before a 404; a workload
820
+ // the server says is gone has nothing left to roll out.
821
+ useProgressiveRefresh((rolloutMayAutoAdvance || recentImageSave) && !resourceGone, refetchProp)
750
822
  const showOwnershipHeading = kind === 'Pod' && Boolean(ownershipContext)
751
823
  const headerImage = metadata.find(m => m.label === 'Image')?.value
752
824
 
@@ -758,7 +830,7 @@ export function WorkloadView({
758
830
  const renderDiagnose = actionsBarProps?.renderDiagnose as
759
831
  | ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
760
832
  | undefined
761
- const diagnoseAction = renderDiagnose?.({
833
+ const diagnoseAction = resourceGone ? null : renderDiagnose?.({
762
834
  kind: resource?.kind ?? knownKindForPluralWithGroup(apiKind, group ?? '') ?? apiKind,
763
835
  group,
764
836
  namespace,
@@ -776,8 +848,12 @@ export function WorkloadView({
776
848
  const podEvidenceLoading = resourceLoading || workloadPodsLoading || eventsLoading
777
849
  const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
778
850
  const requestedTab: TabType = activeTab
851
+ const summaryContext = { kind, apiKind, namespace, name, group, resource, onNavigate: onNavigateToResource }
852
+ const expandedSummary = expanded && resource ? renderSummary?.({ ...summaryContext, context: 'expanded' }) ?? null : null
853
+ const drawerSummary = !expanded && resource ? renderSummary?.({ ...summaryContext, context: 'drawer' }) ?? null : null
779
854
  const tabs: DetailShellTab<TabType>[] = [
780
855
  { id: 'overview', label: 'Overview', icon: <Layers className="w-4 h-4" /> },
856
+ { id: 'spec', label: 'Spec & status', icon: <FileText className="w-4 h-4" />, hidden: !expandedSummary },
781
857
  { id: 'topology', label: 'Topology', icon: <Network className="w-4 h-4" />, hidden: topologyTabHidden },
782
858
  {
783
859
  id: 'timeline',
@@ -791,17 +867,20 @@ export function WorkloadView({
791
867
  id: 'reachability',
792
868
  label: 'Reachability',
793
869
  icon: <Stethoscope className="w-4 h-4" />,
794
- hidden: !(renderDiagnoseTab && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
870
+ hidden: !(renderDiagnoseTabForState && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
795
871
  },
796
872
  { id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
797
873
  { id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
798
874
  ]
799
- const requestedTabAvailable = tabs.some((tab) => tab.id === requestedTab && !tab.hidden)
875
+ const allTabs = mergeExtraTabs(tabs, expanded ? extraTabs : undefined)
876
+ const requestedTabAvailable = allTabs.some((tab) => tab.id === requestedTab && !tab.hidden)
800
877
  const effectiveTab: TabType = requestedTabAvailable ? requestedTab : 'overview'
878
+ const activeExtraTab = expanded ? extraTabs?.find((x) => x.id === effectiveTab) : undefined
801
879
  const shouldCommitFallback =
802
880
  requestedTab !== 'overview' &&
803
881
  !requestedTabAvailable &&
804
882
  (
883
+ (requestedTab === 'spec' && !!resource && !resourceLoading && !expandedSummary) ||
805
884
  (requestedTab === 'topology' && topologyTabHidden) ||
806
885
  (requestedTab === 'metrics' && (!renderMetricsTab || (!!resource && !resourceLoading && !showMetricsTab))) ||
807
886
  (requestedTab === 'cost' && (!renderCostTab || (!!resource && !resourceLoading && !showCostTab))) ||
@@ -890,19 +969,20 @@ export function WorkloadView({
890
969
  </Tooltip>
891
970
  </div>
892
971
  <p className="text-sm text-theme-text-tertiary">{namespace}</p>
893
- {(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
972
+ {(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath) || onOpenLiveTraffic) && (
894
973
  <div className="mt-1 flex flex-wrap items-center gap-1.5">
895
974
  {gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
896
975
  {helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
897
976
  {gitOpsResourcePath && onNavigateGitOpsPath && (
898
977
  <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
899
978
  )}
979
+ {onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
900
980
  </div>
901
981
  )}
902
982
  </div>
903
983
 
904
984
  {/* Actions bar */}
905
- <ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...effectiveActionsBarProps} />
985
+ <ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} />
906
986
  </div>
907
987
 
908
988
  {/* Success animation overlay */}
@@ -917,6 +997,7 @@ export function WorkloadView({
917
997
  damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
918
998
  place. */}
919
999
  <div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
1000
+ {deletedNotice && <div className="px-4 pt-4">{deletedNotice}</div>}
920
1001
  {!resource ? (
921
1002
  // Fill the drawer body so the loading logo centers in it, not in a
922
1003
  // 128px box pinned to the top (matches the splash/PaneLoader centering).
@@ -941,9 +1022,36 @@ export function WorkloadView({
941
1022
  />
942
1023
  ) : (
943
1024
  <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
944
- {renderOverviewLead && hasOperationalIssues && (
1025
+ {drawerSummary && (
1026
+ <div className="px-4 pt-3" role="tablist" aria-label="Detail view">
1027
+ <div className="inline-flex rounded-lg bg-theme-elevated p-0.5 text-xs">
1028
+ {([['overview', 'Overview'], ['spec', 'Spec & status']] as const).map(([id, label]) => {
1029
+ const on = id === 'spec' ? drawerSpec : !drawerSpec
1030
+ return (
1031
+ <button
1032
+ key={id}
1033
+ role="tab"
1034
+ aria-selected={on}
1035
+ onClick={() => setDrawerSpec(id === 'spec')}
1036
+ className={clsx(
1037
+ 'rounded-md px-2.5 py-1 font-medium transition-colors',
1038
+ on ? 'bg-theme-surface text-theme-text-primary shadow-theme-sm' : 'text-theme-text-secondary hover:text-theme-text-primary',
1039
+ )}
1040
+ >
1041
+ {label}
1042
+ </button>
1043
+ )
1044
+ })}
1045
+ </div>
1046
+ </div>
1047
+ )}
1048
+ {drawerSummary && !drawerSpec ? (
1049
+ drawerSummary
1050
+ ) : (
1051
+ <>
1052
+ {renderOverviewLeadForState && hasOperationalIssues && (
945
1053
  <div className="px-4 pt-4">
946
- {renderOverviewLead({ kind, namespace, name })}
1054
+ {renderOverviewLeadForState({ kind, namespace, name })}
947
1055
  </div>
948
1056
  )}
949
1057
  <ResourceRendererDispatch
@@ -957,9 +1065,9 @@ export function WorkloadView({
957
1065
  onCopy={copyToClipboard}
958
1066
  copied={copied}
959
1067
  onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
960
- onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
1068
+ onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
961
1069
  isSavingSecret={isUpdatingResource}
962
- rendererOverrides={rendererOverrides}
1070
+ rendererOverrides={rendererOverridesForState}
963
1071
  resolvedEnvFrom={resolvedEnvFrom}
964
1072
  renderMetrics={renderMetricsTab}
965
1073
  events={resourceFocusedK8sEvents}
@@ -967,8 +1075,10 @@ export function WorkloadView({
967
1075
  updates={resourceFocusedUpdates}
968
1076
  eventsError={resourceFocusedK8sError}
969
1077
  updatesError={resourceFocusedUpdatesError}
970
- mainFooter={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name, group, context: 'drawer' })}
1078
+ mainFooter={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'drawer' })}
971
1079
  />
1080
+ </>
1081
+ )}
972
1082
  </OperationalIssuesShownContext.Provider>
973
1083
  )}
974
1084
  </div>
@@ -1047,6 +1157,7 @@ export function WorkloadView({
1047
1157
  {gitOpsResourcePath && onNavigateGitOpsPath && (
1048
1158
  <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
1049
1159
  )}
1160
+ {onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
1050
1161
  {relationships?.owner && !showOwnershipHeading && (
1051
1162
  <span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
1052
1163
  )}
@@ -1085,31 +1196,35 @@ export function WorkloadView({
1085
1196
  )}
1086
1197
  </>
1087
1198
  }
1088
- tabs={tabs}
1199
+ tabs={allTabs}
1089
1200
  activeTab={effectiveTab}
1090
1201
  onTabChange={handleSetTab}
1091
1202
  scopeControls={scopeControls}
1092
- tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...effectiveActionsBarProps} />}
1203
+ tabStripEnd={<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} hideLogs {...actionsBarPropsForState} />}
1093
1204
  overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
1094
1205
  compactHeader={compactHeader}
1095
1206
  >
1096
1207
  <div className="flex h-full min-h-0 flex-col">
1097
- {effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
1208
+ {deletedNotice && <div className="shrink-0 px-4 pt-4">{deletedNotice}</div>}
1209
+ {!resourceGone && effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
1098
1210
  <div className="shrink-0 px-4 pt-4">
1099
1211
  <WorkloadRolloutNotice activity={rolloutActivity} recentImageSave={recentImageSave} />
1100
1212
  </div>
1101
1213
  )}
1102
1214
  <div className="min-h-0 flex-1">
1103
- {effectiveTab === 'overview' && expandedOverview ? (
1215
+ {activeExtraTab && <div className="h-full min-h-0 overflow-y-auto">{activeExtraTab.render()}</div>}
1216
+ {effectiveTab === 'overview' && expandedSummary ? (
1217
+ <div className="h-full min-h-0 overflow-y-auto">{expandedSummary}</div>
1218
+ ) : effectiveTab === 'overview' && expandedOverview ? (
1104
1219
  <div className="h-full min-h-0">
1105
- {hasOperationalIssues && renderOverviewLead && (
1220
+ {hasOperationalIssues && renderOverviewLeadForState && (
1106
1221
  <div className="px-4 pt-4">
1107
- {renderOverviewLead({ kind, namespace, name })}
1222
+ {renderOverviewLeadForState({ kind, namespace, name })}
1108
1223
  </div>
1109
1224
  )}
1110
1225
  {expandedOverview}
1111
1226
  </div>
1112
- ) : effectiveTab === 'overview' && (
1227
+ ) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
1113
1228
  <InfoTab
1114
1229
  resource={resource}
1115
1230
  selectedResource={selectedResource}
@@ -1128,22 +1243,22 @@ export function WorkloadView({
1128
1243
  onNavigate={onNavigateToResource}
1129
1244
  onCopy={copyToClipboard}
1130
1245
  copied={copied}
1131
- onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
1246
+ onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
1132
1247
  isSavingSecret={isUpdatingResource}
1133
1248
  onOpenLogs={handleOpenLogs}
1134
1249
  onSwitchToTimeline={() => handleSetTab('timeline')}
1135
1250
  onSwitchToLogs={logsTabVisible ? () => handleSetTab('logs') : undefined}
1136
1251
  onSwitchToTopology={!topologyTabHidden ? () => handleSetTab('topology') : undefined}
1137
- rendererOverrides={rendererOverrides}
1252
+ rendererOverrides={rendererOverridesForState}
1138
1253
  resolvedEnvFrom={resolvedEnvFrom}
1139
1254
  events={overviewEvents}
1140
1255
  eventsLoading={overviewEventsLoading}
1141
1256
  updates={resourceFocusedUpdates}
1142
1257
  eventsError={overviewEventsError}
1143
1258
  updatesError={resourceFocusedUpdatesError}
1144
- extraContent={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name, group, context: 'expanded' })}
1259
+ extraContent={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'expanded' })}
1145
1260
  introContent={overviewIntro}
1146
- leadContent={hasOperationalIssues && renderOverviewLead ? renderOverviewLead({ kind, namespace, name }) : undefined}
1261
+ leadContent={hasOperationalIssues && renderOverviewLeadForState ? renderOverviewLeadForState({ kind, namespace, name }) : undefined}
1147
1262
  onEvaluateCapacity={onEvaluateCapacity}
1148
1263
  recentImageSave={recentImageSave}
1149
1264
  />
@@ -1202,9 +1317,9 @@ export function WorkloadView({
1202
1317
  {renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
1203
1318
  </div>
1204
1319
  )}
1205
- {effectiveTab === 'reachability' && renderDiagnoseTab && (
1320
+ {effectiveTab === 'reachability' && renderDiagnoseTabForState && (
1206
1321
  <div className="flex h-full min-h-0 flex-col p-3">
1207
- {renderDiagnoseTab({ kind: resource?.kind || kind, namespace, name })}
1322
+ {renderDiagnoseTabForState({ kind: resource?.kind || kind, namespace, name })}
1208
1323
  </div>
1209
1324
  )}
1210
1325
  {effectiveTab === 'cost' && renderCostTab && (
@@ -1335,6 +1450,34 @@ function extractMetadata(kind: string, resource: any): { label: string; value: s
1335
1450
  // SUB-COMPONENTS
1336
1451
  // ============================================================================
1337
1452
 
1453
+ function DeletedResourceNotice({
1454
+ kindLabel,
1455
+ owner,
1456
+ onNavigateToResource,
1457
+ }: {
1458
+ kindLabel: string
1459
+ owner?: ResourceRef
1460
+ onNavigateToResource?: NavigateToResource
1461
+ }) {
1462
+ return (
1463
+ <AlertBanner
1464
+ variant="warning"
1465
+ title={`This ${kindLabel} no longer exists in the cluster`}
1466
+ message="You're looking at the last version Radar loaded before it was deleted."
1467
+ >
1468
+ {owner && onNavigateToResource && (
1469
+ <button
1470
+ type="button"
1471
+ onClick={() => onNavigateToResource(refToSelectedResource(owner))}
1472
+ className="mt-2 text-xs text-accent-text hover:underline"
1473
+ >
1474
+ Go to its {displayKindName(owner.kind, owner.kind)} {owner.name}
1475
+ </button>
1476
+ )}
1477
+ </AlertBanner>
1478
+ )
1479
+ }
1480
+
1338
1481
  function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
1339
1482
  return (
1340
1483
  <Tooltip content="Open this resource in the GitOps tab (tree + insights + ops)" delay={150}>
@@ -1350,6 +1493,21 @@ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
1350
1493
  )
1351
1494
  }
1352
1495
 
1496
+ function LiveTrafficChip({ onClick }: { onClick: () => void }) {
1497
+ return (
1498
+ <Tooltip content="What this workload talks to, from the cluster's traffic source" delay={150}>
1499
+ <button
1500
+ type="button"
1501
+ onClick={onClick}
1502
+ className="inline-flex items-center gap-1 rounded border border-theme-border bg-theme-elevated px-1.5 py-0.5 text-[11px] font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
1503
+ >
1504
+ Live traffic
1505
+ <ArrowRight className="h-3 w-3 shrink-0" />
1506
+ </button>
1507
+ </Tooltip>
1508
+ )
1509
+ }
1510
+
1353
1511
  function OwnershipHeading({
1354
1512
  podName,
1355
1513
  context,
@@ -1846,6 +2004,7 @@ const LOGS_TAB_WITHOUT_PODS_KINDS = new Set([
1846
2004
  'clusterworkflowtemplates',
1847
2005
  'scaledjobs',
1848
2006
  'jobsets',
2007
+ 'clusters',
1849
2008
  ])
1850
2009
  const RUNTIME_WORKLOAD_OVERVIEW_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs'])
1851
2010
  const ROLLOUT_STATUS_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'rollouts'])
@@ -1862,6 +2021,11 @@ export function supportsLogsWithoutPods(
1862
2021
  if (normalizedKind === 'jobsets') {
1863
2022
  return group === 'jobset.x-k8s.io' && apiVersion === 'jobset.x-k8s.io/v1alpha2'
1864
2023
  }
2024
+ // A CloudNativePG Cluster's instance Pods are its children, not related
2025
+ // Pods, and its logs are resolved server-side from the Cluster itself.
2026
+ if (normalizedKind === 'clusters') {
2027
+ return group === 'postgresql.cnpg.io' || !!apiVersion?.startsWith('postgresql.cnpg.io/')
2028
+ }
1865
2029
  return true
1866
2030
  }
1867
2031
 
@@ -2362,8 +2526,8 @@ function buildSummaryMetrics(
2362
2526
  },
2363
2527
  {
2364
2528
  label: 'Latest run',
2365
- value: status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never',
2366
- detail: status.lastSuccessfulTime ? `last success ${formatAge(status.lastSuccessfulTime)}` : 'no successful run recorded',
2529
+ value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never',
2530
+ detail: status.lastSuccessfulTime ? `last success ${formatRelativeAgeTime(status.lastSuccessfulTime)}` : 'no successful run recorded',
2367
2531
  icon: Activity,
2368
2532
  },
2369
2533
  {
@@ -2479,8 +2643,8 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
2479
2643
  { label: 'Schedule', value: spec.schedule || '-' },
2480
2644
  { label: 'Active jobs', value: active, tone: active > 0 ? 'warn' : 'default' },
2481
2645
  { label: 'Suspend', value: suspended ? 'Yes' : 'No', tone: suspended ? 'warn' : 'default' },
2482
- { label: 'Last schedule', value: status.lastScheduleTime ? formatAge(status.lastScheduleTime) : '-' },
2483
- { label: 'Last success', value: status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : '-' },
2646
+ { label: 'Last schedule', value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : '-' },
2647
+ { label: 'Last success', value: status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : '-' },
2484
2648
  ],
2485
2649
  }
2486
2650
  }
@@ -2579,8 +2743,8 @@ function getScalingSummary(
2579
2743
  label: phase,
2580
2744
  detail: '',
2581
2745
  stats: [
2582
- { label: 'Started', value: status.startTime ? formatAge(status.startTime) : '-' },
2583
- { label: 'Finished', value: status.completionTime ? formatAge(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
2746
+ { label: 'Started', value: status.startTime ? formatRelativeAgeTime(status.startTime) : '-' },
2747
+ { label: 'Finished', value: status.completionTime ? formatRelativeAgeTime(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
2584
2748
  { label: 'Parallelism', value: spec.parallelism ?? 1 },
2585
2749
  { label: 'Completions', value: spec.completions ?? 1 },
2586
2750
  { label: 'Completion mode', value: spec.completionMode ?? 'NonIndexed' },
@@ -3220,8 +3384,8 @@ function CronJobRunsCard({
3220
3384
  >
3221
3385
  <div className="mb-3 grid gap-x-8 gap-y-3 sm:grid-cols-2">
3222
3386
  <OverviewStat label="Active jobs" value={activeJobs.length} tone={activeJobs.length > 0 ? 'warn' : 'default'} />
3223
- <OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never'} />
3224
- <OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : 'Never'} />
3387
+ <OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never'} />
3388
+ <OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : 'Never'} />
3225
3389
  <OverviewStat label="Retained history" value={`${spec.successfulJobsHistoryLimit ?? 3} ok / ${spec.failedJobsHistoryLimit ?? 1} failed`} />
3226
3390
  </div>
3227
3391
 
@@ -3678,7 +3842,7 @@ function ActivityCard({
3678
3842
  <div className="min-w-0 flex-1">
3679
3843
  <div className="flex items-center justify-between gap-2">
3680
3844
  <span className="truncate text-sm font-medium text-theme-text-primary">{event.reason || event.eventType || 'update'}</span>
3681
- <span className="shrink-0 text-xs text-theme-text-tertiary">{formatAge(event.timestamp)}</span>
3845
+ <span className="shrink-0 text-xs text-theme-text-tertiary">{formatRelativeAgeTime(event.timestamp)}</span>
3682
3846
  </div>
3683
3847
  <div className="mt-0.5 line-clamp-2 text-xs text-theme-text-secondary">
3684
3848
  {event.message || event.diff?.summary || `${event.kind} ${event.name}`}
@@ -3801,16 +3965,6 @@ function summarizeServingExposure(
3801
3965
  return { label: 'No serving path detected', badge: 'None', severity: 'neutral' }
3802
3966
  }
3803
3967
 
3804
- function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
3805
- const seen = new Set<string>()
3806
- return refs.filter((ref) => {
3807
- const key = resourceRefId(ref)
3808
- if (seen.has(key)) return false
3809
- seen.add(key)
3810
- return true
3811
- })
3812
- }
3813
-
3814
3968
  function resourceRefId(ref: ResourceRef): string {
3815
3969
  return `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
3816
3970
  }
@@ -3835,3 +3989,17 @@ function mergeAndRankEvents(events: TimelineEvent[], updates: TimelineEvent[]):
3835
3989
  return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
3836
3990
  })
3837
3991
  }
3992
+
3993
+ function mergeExtraTabs(tabs: DetailShellTab<TabType>[], extra: WorkloadExtraTab[] | undefined): DetailShellTab<TabType>[] {
3994
+ if (!extra || extra.length === 0) return tabs
3995
+ const replaced = new Set(extra.map((x) => x.replaces).filter(Boolean))
3996
+ const out = tabs.map((t) => (replaced.has(t.id) ? { ...t, hidden: true } : t))
3997
+ for (const x of extra) {
3998
+ const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon }
3999
+ const anchor = x.after ?? x.replaces
4000
+ const idx = anchor ? out.findIndex((t) => t.id === anchor) : -1
4001
+ if (idx >= 0) out.splice(idx + 1, 0, tab)
4002
+ else out.push(tab)
4003
+ }
4004
+ return out
4005
+ }
@@ -8,5 +8,6 @@ export {
8
8
  type ResourceOwnershipContext,
9
9
  type ServingResourceDetail,
10
10
  type WorkloadTabType,
11
+ type WorkloadExtraTab,
11
12
  } from './WorkloadView'
12
13
  export { ResourceDetailDrawer } from './ResourceDetailDrawer'
@@ -17,4 +17,10 @@ describe('supportsLogsWithoutPods', () => {
17
17
  expect(supportsLogsWithoutPods('jobs', 'Job', 'batch', 'batch/v1')).toBe(true)
18
18
  expect(supportsLogsWithoutPods('jobs', 'Job', 'example.io', 'example.io/v1')).toBe(false)
19
19
  })
20
+
21
+ it('offers a CloudNativePG Cluster its merged instance logs, but not other Cluster kinds', () => {
22
+ expect(supportsLogsWithoutPods('clusters', 'Cluster', 'postgresql.cnpg.io', 'postgresql.cnpg.io/v1')).toBe(true)
23
+ expect(supportsLogsWithoutPods('clusters', 'Cluster', undefined, 'postgresql.cnpg.io/v1')).toBe(true)
24
+ expect(supportsLogsWithoutPods('clusters', 'Cluster', 'cluster.x-k8s.io', 'cluster.x-k8s.io/v1beta1')).toBe(false)
25
+ })
20
26
  })
package/src/index.ts CHANGED
@@ -55,6 +55,9 @@ export * from './components/checks'
55
55
  // queue)
56
56
  export * from './components/issues'
57
57
 
58
+ // CloudNativePG workspace model (fleet derivation over /api/cnpg/workspace)
59
+ export * from './components/cnpg'
60
+
58
61
  // Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
59
62
  export * from './components/cluster-switcher'
60
63
 
@@ -1,4 +1,5 @@
1
1
  import type { Issue } from "../components/issues/types";
2
+ import type { NodeLifecycleObservation } from "../utils/node-lifecycle";
2
3
 
3
4
  export const CAPACITY_SCHEMA_VERSION = "v1alpha1" as const;
4
5
 
@@ -232,6 +233,14 @@ export interface CapacityClaimLifecycleSummary {
232
233
  }
233
234
 
234
235
  export interface CapacityNodeLifecycleSummary {
236
+ operational?: {
237
+ total: number;
238
+ ready: number;
239
+ notReady: number;
240
+ cordoned: number;
241
+ removing: number;
242
+ removingUnhealthy: number;
243
+ };
235
244
  total: number;
236
245
  ready: number;
237
246
  notReady: number;
@@ -325,6 +334,7 @@ export type CapacityClaimStage =
325
334
  | "unknown";
326
335
 
327
336
  export interface CapacityNodeMember {
337
+ lifecycle?: NodeLifecycleObservation;
328
338
  ready?: boolean;
329
339
  cordoned: boolean;
330
340
  conditions: CapacityCondition[];