@skyhook-io/k8s-ui 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. package/src/utils/view-transition.ts +16 -1
@@ -90,6 +90,8 @@ export interface WorkloadExtraTab {
90
90
  icon?: ReactNode
91
91
  after?: WorkloadTabType
92
92
  replaces?: WorkloadTabType
93
+ /** Trailing adornment after the label, e.g. a status mark. */
94
+ badge?: ReactNode
93
95
  render: () => ReactNode
94
96
  }
95
97
 
@@ -145,6 +147,23 @@ interface WorkloadViewProps {
145
147
  * the Escape shortcut.
146
148
  */
147
149
  breadcrumb?: ReactNode
150
+ /** Where the object lives, shown on the title line before its name (e.g. "Workspace / View /"). */
151
+ titlePrefix?: ReactNode
152
+ /**
153
+ * Kind, status and namespace on the title line, wrapping below the name
154
+ * only when there is no room; other chips (image, owners) keep a second
155
+ * row. The header actions then wrap below the title as a group rather
156
+ * than squeezing it.
157
+ */
158
+ inlineBadges?: boolean
159
+ /** A note right after the namespace, e.g. that it is outside the namespace filter. */
160
+ namespaceNote?: ReactNode
161
+ /** Replaces the derived status badge on the title line, e.g. with the controller's own reported phase. */
162
+ renderStatusBadge?: (resource: any) => ReactNode
163
+ /** A short status right after the controller status on the title line, e.g. a derived health verdict. */
164
+ statusNote?: ReactNode
165
+ /** Leave out the kind badge, for a host whose title prefix already names the kind. */
166
+ hideKindBadge?: boolean
148
167
  /** Suppress the standalone back arrow — for embeddings where "back" has no
149
168
  * meaningful target (a single-workload app has no app graph to return to). */
150
169
  hideBackButton?: boolean
@@ -319,6 +338,14 @@ interface WorkloadViewProps {
319
338
  }) => ReactNode
320
339
  /** Extra tabs for the expanded view (e.g. a domain's own sections). */
321
340
  extraTabs?: WorkloadExtraTab[]
341
+ /** Expanded view: a line under the title, above the tabs, shown on every tab. */
342
+ subheader?: ReactNode
343
+ /** Expanded view: tab ids in display order; tabs not listed follow in their usual order. */
344
+ tabOrder?: string[]
345
+ /** Expanded view: relabel, lead, or replace the whole "Spec & status" body. */
346
+ specTab?: { label?: string; icon?: ReactNode; lead?: ReactNode; render?: () => ReactNode }
347
+ /** Domain actions rendered in the header (drawer and expanded). */
348
+ renderHeaderActions?: (props: { resource: any; context: 'drawer' | 'expanded'; onNavigate?: NavigateToResource }) => ReactNode
322
349
  /** Render a full replacement for the expanded Overview tab. */
323
350
  renderExpandedOverview?: (props: {
324
351
  kind: string
@@ -409,6 +436,12 @@ export function WorkloadView({
409
436
  initialTab,
410
437
  group,
411
438
  breadcrumb,
439
+ titlePrefix,
440
+ inlineBadges = false,
441
+ namespaceNote,
442
+ renderStatusBadge,
443
+ statusNote,
444
+ hideKindBadge = false,
412
445
  hideBackButton,
413
446
  scopeControls,
414
447
  compactHeader,
@@ -469,6 +502,10 @@ export function WorkloadView({
469
502
  renderExpandedOverview,
470
503
  renderSummary,
471
504
  extraTabs,
505
+ tabOrder,
506
+ specTab,
507
+ subheader,
508
+ renderHeaderActions,
472
509
  renderRelatedYaml,
473
510
  renderMetricsTab,
474
511
  renderCostTab,
@@ -794,6 +831,9 @@ export function WorkloadView({
794
831
  const renderOverviewLeadForState = resourceGone ? undefined : renderOverviewLead
795
832
  const renderOverviewExtraForState = resourceGone ? undefined : renderOverviewExtra
796
833
  const renderDiagnoseTabForState = resourceGone ? undefined : renderDiagnoseTab
834
+ // A host's summary, tabs and subheader can hold actions on the live object.
835
+ const renderSummaryForState = resourceGone ? undefined : renderSummary
836
+ const extraTabsForState = resourceGone ? undefined : extraTabs
797
837
  // A remount closes any dialog the bar had open: once its actions are gone,
798
838
  // a confirm button left on screen would do nothing.
799
839
  const actionsBarKey = resourceGone ? 'gone' : 'live'
@@ -830,6 +870,7 @@ export function WorkloadView({
830
870
  const renderDiagnose = actionsBarProps?.renderDiagnose as
831
871
  | ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
832
872
  | undefined
873
+ const headerActions = resource && !resourceGone && renderHeaderActions?.({ resource, context: expanded ? 'expanded' : 'drawer', onNavigate: onNavigateToResource })
833
874
  const diagnoseAction = resourceGone ? null : renderDiagnose?.({
834
875
  kind: resource?.kind ?? knownKindForPluralWithGroup(apiKind, group ?? '') ?? apiKind,
835
876
  group,
@@ -849,11 +890,11 @@ export function WorkloadView({
849
890
  const logsFallbackReady = !renderLogsTab || (!logsTabVisible && !podEvidenceLoading)
850
891
  const requestedTab: TabType = activeTab
851
892
  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
893
+ const expandedSummary = expanded && resource ? renderSummaryForState?.({ ...summaryContext, context: 'expanded' }) ?? null : null
894
+ const drawerSummary = !expanded && resource ? renderSummaryForState?.({ ...summaryContext, context: 'drawer' }) ?? null : null
854
895
  const tabs: DetailShellTab<TabType>[] = [
855
896
  { 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 },
897
+ { id: 'spec', label: specTab?.label ?? 'Spec & status', icon: specTab?.icon ?? <FileText className="w-4 h-4" />, hidden: !expandedSummary },
857
898
  { id: 'topology', label: 'Topology', icon: <Network className="w-4 h-4" />, hidden: topologyTabHidden },
858
899
  {
859
900
  id: 'timeline',
@@ -872,10 +913,10 @@ export function WorkloadView({
872
913
  { id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
873
914
  { id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
874
915
  ]
875
- const allTabs = mergeExtraTabs(tabs, expanded ? extraTabs : undefined)
916
+ const allTabs = orderTabs(mergeExtraTabs(tabs, expanded ? extraTabsForState : undefined), expanded ? tabOrder : undefined)
876
917
  const requestedTabAvailable = allTabs.some((tab) => tab.id === requestedTab && !tab.hidden)
877
918
  const effectiveTab: TabType = requestedTabAvailable ? requestedTab : 'overview'
878
- const activeExtraTab = expanded ? extraTabs?.find((x) => x.id === effectiveTab) : undefined
919
+ const activeExtraTab = expanded ? extraTabsForState?.find((x) => x.id === effectiveTab) : undefined
879
920
  const shouldCommitFallback =
880
921
  requestedTab !== 'overview' &&
881
922
  !requestedTabAvailable &&
@@ -904,13 +945,13 @@ export function WorkloadView({
904
945
  <span className={clsx('badge', getKindColorOutline(apiKind))}>
905
946
  {displayKindName(apiKind, resource?.kind)}
906
947
  </span>
907
- {status && (
948
+ {renderStatusBadge && resource && !resourceGone ? renderStatusBadge(resource) : status && (
908
949
  <span className={clsx('badge', status.color)}>
909
950
  {status.text}
910
951
  </span>
911
952
  )}
912
953
  </div>
913
- <div className="flex items-center gap-1.5">
954
+ <div className="flex shrink-0 flex-nowrap items-center justify-end gap-1.5">
914
955
  {diagnoseAction}
915
956
  {onExpand && (
916
957
  <Tooltip content="Open full view" delay={150} position="bottom">
@@ -982,7 +1023,7 @@ export function WorkloadView({
982
1023
  </div>
983
1024
 
984
1025
  {/* Actions bar */}
985
- <ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} />
1026
+ <ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} leadingActions={headerActions} />
986
1027
  </div>
987
1028
 
988
1029
  {/* Success animation overlay */}
@@ -1091,6 +1132,8 @@ export function WorkloadView({
1091
1132
  <OperationalIssuesShownContext.Provider value={!!hasOperationalIssues || !!operationalIssuesPending}>
1092
1133
  <DetailShell
1093
1134
  breadcrumb={breadcrumb}
1135
+ wrapHeader={inlineBadges}
1136
+ subheader={resourceGone ? undefined : subheader}
1094
1137
  nav={
1095
1138
  breadcrumb || hideBackButton ? undefined : (
1096
1139
  <Tooltip content="Go back (Esc)" delay={150} position="bottom">
@@ -1104,68 +1147,111 @@ export function WorkloadView({
1104
1147
  </Tooltip>
1105
1148
  )
1106
1149
  }
1107
- identity={
1108
- <>
1109
- {showOwnershipHeading && ownershipContext ? (
1110
- <OwnershipHeading
1111
- podName={name}
1112
- context={ownershipContext}
1113
- copied={copied === 'name'}
1114
- onCopy={() => copyToClipboard(name, 'name')}
1115
- onNavigateToResource={onNavigateToResource}
1116
- onOpenApplication={onOpenApplication}
1117
- />
1118
- ) : (
1119
- <div className="flex items-center gap-3 mb-1">
1120
- <h1 className="text-lg font-semibold text-theme-text-primary truncate">{name}</h1>
1121
- <Tooltip content="Copy name" delay={150}>
1122
- <button
1123
- onClick={() => copyToClipboard(name, 'name')}
1124
- className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
1125
- aria-label="Copy name"
1126
- >
1127
- {copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
1128
- </button>
1129
- </Tooltip>
1130
- </div>
1131
- )}
1132
- <div className="flex items-center gap-3 text-sm text-theme-text-secondary">
1133
- <span className={clsx('badge', getKindColorOutline(apiKind))}>
1134
- {displayKindName(apiKind, resource?.kind)}
1135
- </span>
1136
- {status && (
1137
- <span className={clsx('badge', status.color)}>
1150
+ identity={(() => {
1151
+ const nameAndCopy = (
1152
+ <>
1153
+ <h1 className="text-lg font-semibold text-theme-text-primary truncate">{name}</h1>
1154
+ <Tooltip content="Copy name" delay={150}>
1155
+ <button
1156
+ onClick={() => copyToClipboard(name, 'name')}
1157
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
1158
+ aria-label="Copy name"
1159
+ >
1160
+ {copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
1161
+ </button>
1162
+ </Tooltip>
1163
+ </>
1164
+ )
1165
+ const kindAndStatus = (
1166
+ <>
1167
+ {!hideKindBadge && (
1168
+ <span className={clsx('badge whitespace-nowrap', getKindColorOutline(apiKind))}>
1169
+ {displayKindName(apiKind, resource?.kind)}
1170
+ </span>
1171
+ )}
1172
+ {renderStatusBadge && resource && !resourceGone ? renderStatusBadge(resource) : status && (
1173
+ <span className={clsx('badge whitespace-nowrap', status.color)}>
1138
1174
  {status.text}
1139
1175
  </span>
1140
1176
  )}
1177
+ {!resourceGone && statusNote}
1178
+ </>
1179
+ )
1180
+ const coreBadges = (
1181
+ <>
1182
+ {/* On the title line the kind and status wrap together, never apart. */}
1183
+ {inlineBadges ? <span className="inline-flex items-center gap-x-3">{kindAndStatus}</span> : kindAndStatus}
1141
1184
  {namespace && namespace !== '_' && (
1142
- <span>Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
1185
+ <span className="whitespace-nowrap">Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
1143
1186
  )}
1144
- {headerImage && (
1145
- <Tooltip content={headerImage} delay={300} wrapperClassName="min-w-0 max-w-md">
1146
- <span className="truncate font-mono text-xs">
1147
- {midTruncate(headerImage, 72)}
1148
- </span>
1149
- </Tooltip>
1150
- )}
1151
- {gitopsOwner && (
1152
- <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
1153
- )}
1154
- {helmOwner && (
1155
- <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
1156
- )}
1157
- {gitOpsResourcePath && onNavigateGitOpsPath && (
1158
- <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
1159
- )}
1160
- {onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
1161
- {relationships?.owner && !showOwnershipHeading && (
1162
- <span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
1187
+ {namespace && namespace !== '_' && namespaceNote}
1188
+ </>
1189
+ )
1190
+ const extraChips = [
1191
+ headerImage && (
1192
+ <Tooltip key="image" content={headerImage} delay={300} wrapperClassName="min-w-0 max-w-md">
1193
+ <span className="truncate font-mono text-xs">
1194
+ {midTruncate(headerImage, 72)}
1195
+ </span>
1196
+ </Tooltip>
1197
+ ),
1198
+ gitopsOwner && (
1199
+ <ManagedByChip key="gitops" owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
1200
+ ),
1201
+ helmOwner && (
1202
+ <HelmManagedByChip key="helm" owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
1203
+ ),
1204
+ gitOpsResourcePath && onNavigateGitOpsPath && (
1205
+ <OpenInGitOpsChip key="open-gitops" onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
1206
+ ),
1207
+ onOpenLiveTraffic && <LiveTrafficChip key="live-traffic" onClick={onOpenLiveTraffic} />,
1208
+ relationships?.owner && !showOwnershipHeading && (
1209
+ <span key="owner">Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
1210
+ ),
1211
+ ].filter(Boolean)
1212
+ if (inlineBadges && !(showOwnershipHeading && ownershipContext)) {
1213
+ // The name and its copy button stay together and truncate only when
1214
+ // the line holds nothing else; the badges wrap below before that.
1215
+ return (
1216
+ <>
1217
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">
1218
+ {titlePrefix}
1219
+ <div className="flex min-w-0 max-w-full items-center gap-2">{nameAndCopy}</div>
1220
+ {coreBadges}
1221
+ </div>
1222
+ {extraChips.length > 0 && (
1223
+ <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">{extraChips}</div>
1224
+ )}
1225
+ </>
1226
+ )
1227
+ }
1228
+ return (
1229
+ <>
1230
+ {showOwnershipHeading && ownershipContext ? (
1231
+ <OwnershipHeading
1232
+ podName={name}
1233
+ context={ownershipContext}
1234
+ copied={copied === 'name'}
1235
+ onCopy={() => copyToClipboard(name, 'name')}
1236
+ onNavigateToResource={onNavigateToResource}
1237
+ onOpenApplication={onOpenApplication}
1238
+ />
1239
+ ) : (
1240
+ <div className="flex items-center gap-3 mb-1">
1241
+ {titlePrefix}
1242
+ {nameAndCopy}
1243
+ </div>
1163
1244
  )}
1164
- </div>
1165
- </>
1166
- }
1245
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-theme-text-secondary">
1246
+ {coreBadges}
1247
+ {extraChips}
1248
+ </div>
1249
+ </>
1250
+ )
1251
+ })()}
1167
1252
  headerActions={
1168
1253
  <>
1254
+ {headerActions}
1169
1255
  {diagnoseAction}
1170
1256
  <Tooltip content="Refresh" delay={150} position="bottom">
1171
1257
  <button
@@ -1224,6 +1310,8 @@ export function WorkloadView({
1224
1310
  )}
1225
1311
  {expandedOverview}
1226
1312
  </div>
1313
+ ) : effectiveTab === 'spec' && specTab?.render && !resourceGone ? (
1314
+ <div className="h-full min-h-0 overflow-y-auto">{specTab.render()}</div>
1227
1315
  ) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
1228
1316
  <InfoTab
1229
1317
  resource={resource}
@@ -1258,7 +1346,14 @@ export function WorkloadView({
1258
1346
  updatesError={resourceFocusedUpdatesError}
1259
1347
  extraContent={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'expanded' })}
1260
1348
  introContent={overviewIntro}
1261
- leadContent={hasOperationalIssues && renderOverviewLeadForState ? renderOverviewLeadForState({ kind, namespace, name }) : undefined}
1349
+ leadContent={
1350
+ // A host's spec lead replaces the operational issues there: its Overview already leads with them.
1351
+ effectiveTab === 'spec' && specTab?.lead
1352
+ ? specTab.lead
1353
+ : hasOperationalIssues && renderOverviewLeadForState
1354
+ ? renderOverviewLeadForState({ kind, namespace, name })
1355
+ : undefined
1356
+ }
1262
1357
  onEvaluateCapacity={onEvaluateCapacity}
1263
1358
  recentImageSave={recentImageSave}
1264
1359
  />
@@ -2110,6 +2205,7 @@ function WorkloadOverviewTab({
2110
2205
  const relationshipGroups = buildRelationshipGroups(relationships)
2111
2206
  const servingRelationshipGroups = buildServingRelationshipGroups(relationships)
2112
2207
  const dependencyRelationshipGroups = buildDependencyRelationshipGroups(relationships)
2208
+ const observationGroups = dependencyRelationshipGroups.filter((group) => group.label === 'Monitored By' || group.label === 'Staged Policies')
2113
2209
  const showServingPath = servingRelationshipGroups.length > 0 && overviewShape !== 'job' && overviewShape !== 'cronjob'
2114
2210
  const secondaryRelationshipGroups = showServingPath ? dependencyRelationshipGroups : relationshipGroups
2115
2211
  const relationshipCardTitle =
@@ -2188,6 +2284,14 @@ function WorkloadOverviewTab({
2188
2284
  onNavigate={onNavigate}
2189
2285
  onSwitchToTopology={onSwitchToTopology}
2190
2286
  />
2287
+ {observationGroups.length > 0 && (
2288
+ <ServingDependenciesCard
2289
+ title="Related resources"
2290
+ groups={observationGroups}
2291
+ onNavigate={onNavigate}
2292
+ onSwitchToTopology={onSwitchToTopology}
2293
+ />
2294
+ )}
2191
2295
  {extraContent}
2192
2296
  </div>
2193
2297
  </div>
@@ -3877,6 +3981,7 @@ function buildServingRelationshipGroups(relationships: Relationships | undefined
3877
3981
  ...(relationships.ingresses ?? []),
3878
3982
  ...(relationships.gateways ?? []),
3879
3983
  ...(relationships.routes ?? []),
3984
+ ...(relationships.routedFrom ?? []),
3880
3985
  ])
3881
3986
  return [
3882
3987
  { label: 'Services', refs: dedupeResourceRefs(relationships.services ?? []) },
@@ -3891,6 +3996,8 @@ function buildDependencyRelationshipGroups(relationships: Relationships | undefi
3891
3996
  { label: 'Autoscalers', refs: dedupeResourceRefs(relationships.scalers ?? []) },
3892
3997
  { label: 'Disruption budgets', refs: dedupeResourceRefs(relationships.pdbs ?? []) },
3893
3998
  { label: 'Network policies', refs: dedupeResourceRefs(relationships.networkPolicies ?? []) },
3999
+ { label: 'Staged Policies', refs: dedupeResourceRefs(relationships.stagedPolicies ?? []) },
4000
+ { label: 'Monitored By', refs: dedupeResourceRefs(relationships.monitors ?? []) },
3894
4001
  { label: 'Storage claims', refs: dedupeResourceRefs(relationships.resourceClaims ?? []) },
3895
4002
  ].filter((group) => group.refs.length > 0)
3896
4003
  }
@@ -3990,12 +4097,21 @@ function mergeAndRankEvents(events: TimelineEvent[], updates: TimelineEvent[]):
3990
4097
  })
3991
4098
  }
3992
4099
 
4100
+ function orderTabs(tabs: DetailShellTab<TabType>[], order: string[] | undefined): DetailShellTab<TabType>[] {
4101
+ if (!order || order.length === 0) return tabs
4102
+ const rank = (id: string) => {
4103
+ const i = order.indexOf(id)
4104
+ return i < 0 ? order.length : i
4105
+ }
4106
+ return tabs.map((t, i) => ({ t, i })).sort((a, b) => rank(a.t.id) - rank(b.t.id) || a.i - b.i).map((x) => x.t)
4107
+ }
4108
+
3993
4109
  function mergeExtraTabs(tabs: DetailShellTab<TabType>[], extra: WorkloadExtraTab[] | undefined): DetailShellTab<TabType>[] {
3994
4110
  if (!extra || extra.length === 0) return tabs
3995
4111
  const replaced = new Set(extra.map((x) => x.replaces).filter(Boolean))
3996
4112
  const out = tabs.map((t) => (replaced.has(t.id) ? { ...t, hidden: true } : t))
3997
4113
  for (const x of extra) {
3998
- const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon }
4114
+ const tab: DetailShellTab<TabType> = { id: x.id as TabType, label: x.label, icon: x.icon, badge: x.badge }
3999
4115
  const anchor = x.after ?? x.replaces
4000
4116
  const idx = anchor ? out.findIndex((t) => t.id === anchor) : -1
4001
4117
  if (idx >= 0) out.splice(idx + 1, 0, tab)
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ 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)
58
+ // CloudNativePG workspace model (fleet derivation over /api/cnpg/workspace) and summaries
59
59
  export * from './components/cnpg'
60
60
 
61
61
  // Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
@@ -82,3 +82,16 @@ export * from './components/compare'
82
82
 
83
83
  // Perf instrumentation (ELK + structureKey timers, surfaced in diagnostics overlay)
84
84
  export * from './perf'
85
+
86
+ export { CertaintyGlyph, certaintyGlyph, certaintyValueLabel, type Certainty } from './components/facts/certainty'
87
+
88
+ export * from './components/facts'
89
+ export {
90
+ WorkspaceProblemCallout,
91
+ ProblemList,
92
+ ProblemMeta,
93
+ OpenIssueContext,
94
+ problemOriginLabel,
95
+ type WorkspaceProblem,
96
+ type ProblemOrigin,
97
+ } from './components/problems'
@@ -1,15 +1,22 @@
1
+ // @monaco-editor/react and @monaco-editor/loader type monaco through this path,
2
+ // which monaco-editor's exports map does not serve.
1
3
  declare module 'monaco-editor/esm/vs/editor/editor.api' {
2
- export * from 'monaco-editor'
4
+ export * from 'monaco-editor/editor/editor.api'
3
5
  }
4
6
 
5
- declare module 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution'
6
- declare module 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution'
7
- declare module 'monaco-editor/esm/vs/basic-languages/html/html.contribution'
8
- declare module 'monaco-editor/esm/vs/basic-languages/css/css.contribution'
9
- declare module 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
10
- declare module 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'
11
- declare module 'monaco-editor/esm/vs/basic-languages/python/python.contribution'
12
- declare module 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution'
13
- declare module 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution'
14
- declare module 'monaco-editor/esm/vs/basic-languages/ini/ini.contribution'
15
- declare module 'monaco-editor/esm/vs/basic-languages/dockerfile/dockerfile.contribution'
7
+ declare module 'monaco-editor/languages/definitions/yaml/register'
8
+ declare module 'monaco-editor/languages/definitions/xml/register'
9
+ declare module 'monaco-editor/languages/definitions/html/register'
10
+ declare module 'monaco-editor/languages/definitions/css/register'
11
+ declare module 'monaco-editor/languages/definitions/javascript/register'
12
+ declare module 'monaco-editor/languages/definitions/typescript/register'
13
+ declare module 'monaco-editor/languages/definitions/python/register'
14
+ declare module 'monaco-editor/languages/definitions/shell/register'
15
+ declare module 'monaco-editor/languages/definitions/markdown/register'
16
+ declare module 'monaco-editor/languages/definitions/ini/register'
17
+ declare module 'monaco-editor/languages/definitions/dockerfile/register'
18
+ declare module 'monaco-editor/editor/contrib/*'
19
+ declare module 'monaco-editor/editor/standalone/*'
20
+ declare module 'monaco-editor/editor/browser/*'
21
+ declare module 'monaco-editor/editor/common/*'
22
+ declare module 'monaco-editor/features/*'
@@ -35,6 +35,19 @@
35
35
  opacity: 0.5;
36
36
  }
37
37
 
38
+ .btn-secondary {
39
+ border: 1px solid var(--border-default);
40
+ background-color: var(--bg-surface);
41
+ color: var(--text-primary);
42
+ border-radius: 0.625rem;
43
+ }
44
+ .btn-secondary:hover {
45
+ background-color: var(--bg-hover);
46
+ }
47
+ .btn-secondary:disabled {
48
+ opacity: 0.5;
49
+ }
50
+
38
51
  .btn-brand-toggle {
39
52
  background-color: color-mix(in srgb, var(--color-brand) 50%, transparent);
40
53
  color: var(--text-primary);
@@ -1,4 +1,5 @@
1
1
  import type { Issue } from "../components/issues/types";
2
+ import type { Certainty } from "../components/facts/certainty";
2
3
  import type { NodeLifecycleObservation } from "../utils/node-lifecycle";
3
4
 
4
5
  export const CAPACITY_SCHEMA_VERSION = "v1alpha1" as const;
@@ -16,8 +17,7 @@ export interface CapacityResourceIdentity {
16
17
  uid?: string;
17
18
  }
18
19
 
19
- export type CapacityCertainty =
20
- "exact" | "lower_bound" | "upper_bound" | "unknown";
20
+ export type CapacityCertainty = Certainty;
21
21
  export type CapacityGranularity =
22
22
  "aggregate" | "aggregate_not_binpacked" | "per_node";
23
23
  export type CapacityResourceVector = Record<string, string>;
@@ -418,6 +418,14 @@ export interface CapacityScalingFact {
418
418
  summary: string;
419
419
  }
420
420
 
421
+ /** Published scale-down observations; candidates do not imply a removal ETA. */
422
+ export interface CapacityAutoscalerScaleDown {
423
+ status?: string;
424
+ candidates?: number;
425
+ lastTransitionTime?: string;
426
+ asOf?: string;
427
+ }
428
+
421
429
  export interface CapacityAutoscalerBackoff {
422
430
  errorClass?: string;
423
431
  errorCode?: string;
@@ -440,6 +448,7 @@ export interface CapacityAutoscalerChildObservation {
440
448
  readyNodes?: number;
441
449
  totalNodes?: number;
442
450
  backoff?: CapacityAutoscalerBackoff;
451
+ scaleDown?: CapacityAutoscalerScaleDown;
443
452
  asOf?: string;
444
453
  }
445
454
 
package/src/types/core.ts CHANGED
@@ -164,12 +164,15 @@ export interface FeatureCapabilities {
164
164
  yamlSchemas?: boolean
165
165
  workloadImages?: boolean
166
166
  resourceIssues?: boolean
167
+ resourceIssueCoverage?: boolean
167
168
  podEnvironment?: boolean
168
169
  policyResource?: boolean
169
170
  workloadHistory?: boolean
171
+ cnpgWorkspace?: boolean
172
+ cnpgProtectionSetup?: boolean
173
+ gitopsWriteEvidence?: boolean
170
174
  serviceEndpointSlices?: boolean
171
175
  trafficRecords?: boolean
172
- gitopsWriteEvidence?: boolean
173
176
  }
174
177
 
175
178
  // DeploymentMode is the closed set of topologies Radar can run in.
@@ -201,6 +204,7 @@ export type CoreNodeKind =
201
204
  | 'HTTPRoute'
202
205
  | 'GRPCRoute'
203
206
  | 'TCPRoute'
207
+ | 'UDPRoute'
204
208
  | 'TLSRoute'
205
209
  | 'Service'
206
210
  | 'Deployment'
@@ -639,6 +643,8 @@ export interface ResourceRef {
639
643
 
640
644
  // Computed relationships for a resource
641
645
  export interface Relationships {
646
+ /** Istio registry domain supplied by the producer; omitted when unknown. */
647
+ istioServiceDomain?: string
642
648
  reflection?: {
643
649
  source?: ResourceRef
644
650
  sourceResourceVersion?: string
@@ -658,6 +664,13 @@ export interface Relationships {
658
664
  storageRefs?: ResourceRef[]
659
665
  dependencies?: ResourceRef[]
660
666
  dependents?: ResourceRef[]
667
+ monitors?: ResourceRef[]
668
+ monitorTargets?: ResourceRef[]
669
+ backends?: ResourceRef[]
670
+ routedFrom?: ResourceRef[]
671
+ protects?: ResourceRef[]
672
+ healthChecks?: ResourceRef[]
673
+ stagedPolicies?: ResourceRef[]
661
674
  scaleTarget?: ResourceRef
662
675
  pdbs?: ResourceRef[] // PodDisruptionBudgets protecting this workload
663
676
  networkPolicies?: ResourceRef[] // NetworkPolicy / CiliumNetworkPolicy / ClusterNetworkPolicy variants selecting this workload
@@ -728,6 +741,7 @@ export interface HPADiagnosis extends HPADiagnosisView {
728
741
  name?: string
729
742
  }
730
743
  bounds: HPABounds
744
+ /** Spec order keeps source references paired with values; unmatched status entries follow. */
731
745
  metrics?: HPAMetricSummary[]
732
746
  }
733
747
 
@@ -785,6 +799,8 @@ export interface APIResource {
785
799
  name: string // Plural name (e.g., "deployments")
786
800
  namespaced: boolean
787
801
  isCrd: boolean
802
+ /** Confirmed, readable cached CRD identity; never inferred from isCrd. */
803
+ definitionName?: string
788
804
  featured?: boolean
789
805
  verbs: string[]
790
806
  observation?: DynamicResourceObservation
@@ -1073,12 +1089,23 @@ export interface UpgradeInfo {
1073
1089
  error?: string
1074
1090
  // Machine-readable source-resolution failure, used to make the Helm drawer
1075
1091
  // explain whether tracking an OCI source can help.
1076
- sourceIssue?: 'untracked' | 'repo_index_error' | 'ambiguous_repository'
1092
+ sourceIssue?: 'untracked' | 'repo_index_error' | 'ambiguous_repository' | 'ambiguous_source' | 'source_unavailable'
1077
1093
  // True only when the error is a genuinely untracked source (registering a
1078
1094
  // chart source could fix it). Kept for compatibility; prefer sourceIssue.
1079
1095
  untracked?: boolean
1080
1096
  }
1081
1097
 
1098
+ export type ChartSourceCandidate =
1099
+ | { type: 'repository'; reference: string; url: string }
1100
+ | { type: 'oci'; reference: string; url?: never }
1101
+
1102
+ export interface ChartSourceStatus {
1103
+ recorded?: ChartSourceCandidate
1104
+ configured: boolean
1105
+ available: boolean
1106
+ candidates?: ChartSourceCandidate[]
1107
+ }
1108
+
1082
1109
  // Batch upgrade info keyed by "storageNamespace/name".
1083
1110
  export interface BatchUpgradeInfo {
1084
1111
  releases: Record<string, UpgradeInfo>
@@ -29,13 +29,44 @@ describe('fluxConditionsToGitOpsStatus truth table', () => {
29
29
  })
30
30
 
31
31
  it('treats the -1 sentinel as drift', () => {
32
- const got = fluxConditionsToGitOpsStatus([c('Ready', 'False'), c('Reconciling', 'True')], false, {
32
+ // A first reconcile reports Ready=Unknown; a reported failure outranks drift.
33
+ const got = fluxConditionsToGitOpsStatus([c('Ready', 'Unknown'), c('Reconciling', 'True')], false, {
33
34
  generation: 1,
34
35
  observedGeneration: -1,
35
36
  })
36
37
  expect(got.sync).toBe('Reconciling')
37
38
  })
38
39
 
40
+ it('a failure while retrying and never reconciled is degraded', () => {
41
+ // Live shape of a GitRepository whose repository doesn't exist.
42
+ const got = fluxConditionsToGitOpsStatus(
43
+ [c('Reconciling', 'True', { reason: 'ProgressingWithRetry' }), c('Ready', 'False', { reason: 'GitOperationFailed', observedGeneration: 1 })],
44
+ false,
45
+ { generation: 1, observedGeneration: -1 },
46
+ )
47
+ expect(got.sync).toBe('OutOfSync')
48
+ expect(got.health).toBe('Degraded')
49
+ })
50
+
51
+ it('a failure about an older generation does not outrank a new spec', () => {
52
+ const got = fluxConditionsToGitOpsStatus([c('Ready', 'False', { reason: 'BuildFailed', observedGeneration: 2 })], false, {
53
+ generation: 3,
54
+ observedGeneration: 1,
55
+ })
56
+ expect(got.sync).toBe('Reconciling')
57
+ expect(got.health).toBe('Progressing')
58
+ })
59
+
60
+ it('a retry while reconciling stays progressing', () => {
61
+ const got = fluxConditionsToGitOpsStatus(
62
+ [c('Reconciling', 'True', { reason: 'Progressing' }), c('Ready', 'False', { reason: 'ProgressingWithRetry' })],
63
+ false,
64
+ { generation: 2, observedGeneration: 1 },
65
+ )
66
+ expect(got.sync).toBe('Reconciling')
67
+ expect(got.health).toBe('Progressing')
68
+ })
69
+
39
70
  it('does not treat an absent observedGeneration as drift', () => {
40
71
  // Kinds and controller versions that never set it must not all read stale.
41
72
  expect(fluxConditionsToGitOpsStatus([c('Ready', 'True')], false, { generation: 3 }).sync).toBe('Synced')