@skyhook-io/k8s-ui 1.17.0 → 1.18.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 (250) hide show
  1. package/README.md +41 -0
  2. package/package.json +6 -6
  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 +7 -3
  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 +142 -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 +40 -81
  67. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  68. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  69. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
  70. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
  71. package/src/components/gitops/tree/layout.test.ts +111 -0
  72. package/src/components/gitops/tree/layout.ts +128 -0
  73. package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
  74. package/src/components/gitops/tree/tree-helpers.ts +12 -2
  75. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  76. package/src/components/issues/IssuesView.tsx +44 -6
  77. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  78. package/src/components/issues/ResourceIssuesSection.tsx +8 -2
  79. package/src/components/issues/diagnostic.ts +2 -0
  80. package/src/components/issues/index.ts +3 -1
  81. package/src/components/issues/issue-timing.ts +12 -1
  82. package/src/components/issues/issues.test.ts +75 -1
  83. package/src/components/issues/severity.ts +30 -2
  84. package/src/components/issues/types.ts +55 -4
  85. package/src/components/logs/LogCore.theme.test.tsx +206 -3
  86. package/src/components/logs/LogCore.tsx +180 -53
  87. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  88. package/src/components/logs/LogsViewer.test.tsx +369 -0
  89. package/src/components/logs/LogsViewer.tsx +91 -13
  90. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  91. package/src/components/logs/StructuredLogLine.tsx +23 -31
  92. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  93. package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
  94. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  95. package/src/components/logs/useLogBuffer.test.ts +8 -0
  96. package/src/components/logs/useLogBuffer.ts +48 -18
  97. package/src/components/logs/useLogSearch.ts +3 -1
  98. package/src/components/logs/useLogStream.ts +17 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ColumnPickerList.test.tsx +46 -0
  105. package/src/components/resources/ColumnPickerList.tsx +174 -0
  106. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  107. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  108. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  109. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  110. package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
  111. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  112. package/src/components/resources/ResourcesView.tsx +392 -181
  113. package/src/components/resources/column-order.test.ts +104 -0
  114. package/src/components/resources/column-order.ts +64 -0
  115. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  116. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  117. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  118. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  119. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  120. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  121. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  122. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  123. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  124. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  127. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  128. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  129. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  130. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  131. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  132. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  133. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  134. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  135. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  136. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  137. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  138. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  139. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  140. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  141. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  142. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  143. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  144. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  145. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  146. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  147. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  148. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  149. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  150. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  151. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  152. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  153. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  154. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  155. package/src/components/resources/renderers/PodRenderer.tsx +81 -40
  156. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  157. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  158. package/src/components/resources/resource-utils-capi.ts +1 -1
  159. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  160. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  161. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  162. package/src/components/resources/resource-utils-contour.ts +16 -0
  163. package/src/components/resources/resource-utils-istio.ts +12 -8
  164. package/src/components/resources/resource-utils-knative.ts +18 -0
  165. package/src/components/resources/resource-utils.ts +96 -7
  166. package/src/components/resources/rules-column-width.test.ts +1 -1
  167. package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
  168. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  169. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  170. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  171. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  172. package/src/components/shared/DetailShell.test.tsx +80 -0
  173. package/src/components/shared/DetailShell.tsx +85 -10
  174. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  175. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  176. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  177. package/src/components/shared/EditableYamlView.tsx +1 -1
  178. package/src/components/shared/ResourceActionsBar.tsx +14 -7
  179. package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
  180. package/src/components/shared/index.ts +2 -0
  181. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  182. package/src/components/timeline/DiffViewer.tsx +113 -4
  183. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  184. package/src/components/timeline/shared.test.tsx +48 -0
  185. package/src/components/timeline/shared.tsx +14 -19
  186. package/src/components/timeline/timeline-filters.ts +1 -0
  187. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  188. package/src/components/topology/K8sResourceNode.tsx +44 -2
  189. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  190. package/src/components/topology/TopologyGraph.tsx +3 -71
  191. package/src/components/topology/TopologySearch.tsx +1 -0
  192. package/src/components/topology/internet-groups.test.ts +35 -0
  193. package/src/components/topology/internet-groups.ts +55 -0
  194. package/src/components/topology/layout.ts +1 -1
  195. package/src/components/ui/Badge.tsx +1 -0
  196. package/src/components/ui/FoldSection.tsx +68 -0
  197. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  198. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  199. package/src/components/ui/RefLink.tsx +20 -0
  200. package/src/components/ui/RowActionMenu.tsx +8 -3
  201. package/src/components/ui/Toast.tsx +15 -4
  202. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  203. package/src/components/ui/drawer-components.test.tsx +110 -1
  204. package/src/components/ui/drawer-components.tsx +62 -9
  205. package/src/components/ui/index.ts +4 -2
  206. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  207. package/src/components/ui/monacoRuntime.ts +14 -19
  208. package/src/components/ui/status-tone.test.ts +10 -1
  209. package/src/components/ui/status-tone.tsx +29 -0
  210. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  211. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  212. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  213. package/src/components/workload/WorkloadView.tsx +193 -71
  214. package/src/index.ts +14 -1
  215. package/src/monaco-deep.d.ts +19 -12
  216. package/src/theme/components.css +13 -0
  217. package/src/types/capacity.ts +11 -2
  218. package/src/types/core.ts +53 -2
  219. package/src/types/gitops-flux-status.test.ts +32 -1
  220. package/src/types/gitops-insights.ts +20 -2
  221. package/src/types/gitops-tree.ts +12 -0
  222. package/src/types/gitops.ts +26 -6
  223. package/src/utils/api-resources.ts +7 -0
  224. package/src/utils/application-history.ts +1 -0
  225. package/src/utils/cnpg-backup.test.ts +26 -0
  226. package/src/utils/cnpg-backup.ts +70 -0
  227. package/src/utils/format.test.ts +9 -0
  228. package/src/utils/format.ts +4 -0
  229. package/src/utils/gitops-owner.ts +3 -2
  230. package/src/utils/grant.test.ts +19 -0
  231. package/src/utils/grant.ts +35 -0
  232. package/src/utils/index.ts +4 -0
  233. package/src/utils/k8s-errors.ts +7 -7
  234. package/src/utils/log-format.test.ts +41 -0
  235. package/src/utils/log-format.ts +71 -15
  236. package/src/utils/log-level.ts +12 -8
  237. package/src/utils/navigation.test.ts +57 -0
  238. package/src/utils/navigation.ts +86 -8
  239. package/src/utils/object-reference-navigation.test.ts +43 -0
  240. package/src/utils/resource-hierarchy.test.ts +37 -0
  241. package/src/utils/resource-hierarchy.ts +15 -7
  242. package/src/utils/resource-icons.ts +2 -0
  243. package/src/utils/skeleton-yaml.test.ts +21 -0
  244. package/src/utils/skeleton-yaml.ts +16 -0
  245. package/src/utils/url-path.test.ts +35 -0
  246. package/src/utils/url-path.ts +21 -0
  247. package/src/utils/validators.test.ts +4 -13
  248. package/src/utils/validators.ts +5 -19
  249. package/src/utils/view-transition.test.ts +35 -1
  250. package/src/utils/view-transition.ts +16 -1
package/src/types/core.ts CHANGED
@@ -126,6 +126,7 @@ export interface Capabilities {
126
126
  helmWrite: boolean // Helm write operations (install, upgrade, rollback, uninstall, apply values)
127
127
  nodeWrite: boolean // Node write operations (cordon, uncordon, drain)
128
128
  workloadWrites?: WorkloadWritePermissions // Workload patch permissions (restart/scale controls)
129
+ mcpReadOnly?: boolean // Full MCP endpoint is disabled; read-only endpoints remain available
129
130
  mcpEnabled: boolean // MCP server is running
130
131
  // Karpenter discovery and NodePool read state. Optional on the wire for the
131
132
  // same newer-frontend/older-backend reason as `deployment` below — consumers
@@ -160,16 +161,20 @@ export interface CloudConnectCapability {
160
161
  }
161
162
 
162
163
  export interface FeatureCapabilities {
164
+ helmSourceIdentity?: boolean
163
165
  yamlReview?: boolean
164
166
  yamlSchemas?: boolean
165
167
  workloadImages?: boolean
166
168
  resourceIssues?: boolean
169
+ resourceIssueCoverage?: boolean
167
170
  podEnvironment?: boolean
168
171
  policyResource?: boolean
169
172
  workloadHistory?: boolean
173
+ cnpgWorkspace?: boolean
174
+ cnpgProtectionSetup?: boolean
175
+ gitopsWriteEvidence?: boolean
170
176
  serviceEndpointSlices?: boolean
171
177
  trafficRecords?: boolean
172
- gitopsWriteEvidence?: boolean
173
178
  }
174
179
 
175
180
  // DeploymentMode is the closed set of topologies Radar can run in.
@@ -201,6 +206,7 @@ export type CoreNodeKind =
201
206
  | 'HTTPRoute'
202
207
  | 'GRPCRoute'
203
208
  | 'TCPRoute'
209
+ | 'UDPRoute'
204
210
  | 'TLSRoute'
205
211
  | 'Service'
206
212
  | 'Deployment'
@@ -639,6 +645,8 @@ export interface ResourceRef {
639
645
 
640
646
  // Computed relationships for a resource
641
647
  export interface Relationships {
648
+ /** Istio registry domain supplied by the producer; omitted when unknown. */
649
+ istioServiceDomain?: string
642
650
  reflection?: {
643
651
  source?: ResourceRef
644
652
  sourceResourceVersion?: string
@@ -658,6 +666,13 @@ export interface Relationships {
658
666
  storageRefs?: ResourceRef[]
659
667
  dependencies?: ResourceRef[]
660
668
  dependents?: ResourceRef[]
669
+ monitors?: ResourceRef[]
670
+ monitorTargets?: ResourceRef[]
671
+ backends?: ResourceRef[]
672
+ routedFrom?: ResourceRef[]
673
+ protects?: ResourceRef[]
674
+ healthChecks?: ResourceRef[]
675
+ stagedPolicies?: ResourceRef[]
661
676
  scaleTarget?: ResourceRef
662
677
  pdbs?: ResourceRef[] // PodDisruptionBudgets protecting this workload
663
678
  networkPolicies?: ResourceRef[] // NetworkPolicy / CiliumNetworkPolicy / ClusterNetworkPolicy variants selecting this workload
@@ -728,6 +743,7 @@ export interface HPADiagnosis extends HPADiagnosisView {
728
743
  name?: string
729
744
  }
730
745
  bounds: HPABounds
746
+ /** Spec order keeps source references paired with values; unmatched status entries follow. */
731
747
  metrics?: HPAMetricSummary[]
732
748
  }
733
749
 
@@ -785,6 +801,8 @@ export interface APIResource {
785
801
  name: string // Plural name (e.g., "deployments")
786
802
  namespaced: boolean
787
803
  isCrd: boolean
804
+ /** Confirmed, readable cached CRD identity; never inferred from isCrd. */
805
+ definitionName?: string
788
806
  featured?: boolean
789
807
  verbs: string[]
790
808
  observation?: DynamicResourceObservation
@@ -1073,12 +1091,42 @@ export interface UpgradeInfo {
1073
1091
  error?: string
1074
1092
  // Machine-readable source-resolution failure, used to make the Helm drawer
1075
1093
  // explain whether tracking an OCI source can help.
1076
- sourceIssue?: 'untracked' | 'repo_index_error' | 'ambiguous_repository'
1094
+ sourceIssue?: 'source_confirmation_required' | 'source_change_required' | 'release_changing' | 'untracked' | 'repo_index_error' | 'ambiguous_repository' | 'ambiguous_source' | 'source_unavailable'
1077
1095
  // True only when the error is a genuinely untracked source (registering a
1078
1096
  // chart source could fix it). Kept for compatibility; prefer sourceIssue.
1079
1097
  untracked?: boolean
1080
1098
  }
1081
1099
 
1100
+ export type ChartSourceCandidate =
1101
+ | { type: 'repository'; reference: string; url: string }
1102
+ | { type: 'oci'; reference: string; url?: never }
1103
+
1104
+ export interface SourceUpgradeSelection {
1105
+ source: ChartSourceCandidate
1106
+ expectedRevision: number
1107
+ expectedSource: ChartSourceCandidate | null
1108
+ previewDigest?: string
1109
+ }
1110
+
1111
+ export interface ChartSourceSuggestion {
1112
+ source: ChartSourceCandidate
1113
+ current: boolean
1114
+ hasInstalledVersion: boolean
1115
+ state: 'available' | 'index_unavailable' | 'unchecked'
1116
+ }
1117
+
1118
+ export interface ChartSourceStatus {
1119
+ changing?: boolean
1120
+ revision?: number
1121
+ confirmed?: boolean
1122
+ sourceChangeRequired?: boolean
1123
+ suggestions?: ChartSourceSuggestion[]
1124
+ recorded?: ChartSourceCandidate
1125
+ configured: boolean
1126
+ available: boolean
1127
+ candidates?: ChartSourceCandidate[]
1128
+ }
1129
+
1082
1130
  // Batch upgrade info keyed by "storageNamespace/name".
1083
1131
  export interface BatchUpgradeInfo {
1084
1132
  releases: Record<string, UpgradeInfo>
@@ -1093,6 +1141,9 @@ export interface ApplyValuesRequest {
1093
1141
 
1094
1142
  // Response for previewing values changes
1095
1143
  export interface ValuesPreviewResponse {
1144
+ baselineSource?: ChartSourceCandidate
1145
+ previewDigest?: string
1146
+ baselineRevision?: number
1096
1147
  currentValues: Record<string, unknown>
1097
1148
  newValues: Record<string, unknown>
1098
1149
  manifestDiff: string
@@ -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')
@@ -93,6 +93,12 @@ export interface GitOpsInsightSummary {
93
93
  // The controller's API server was asked and didn't answer usefully — why,
94
94
  // in the user's words. Only set when the integration is configured.
95
95
  resourceHealthApiError?: string
96
+ // The host is still connecting to the controller's API server; ask again
97
+ // shortly.
98
+ resourceHealthApiPending?: boolean
99
+ // The host couldn't reach the controller's API server and is retrying;
100
+ // ask again now and then.
101
+ resourceHealthApiRetrying?: boolean
96
102
  }
97
103
 
98
104
  export interface GitOpsIgnoredDifferences {
@@ -164,6 +170,7 @@ export interface GitOpsChange {
164
170
  hookPhase?: string
165
171
  hasDesired: boolean
166
172
  hasLive: boolean
173
+ missing?: boolean
167
174
  // Structured per-field diff between the desired state (parsed from
168
175
  // kubectl.kubernetes.io/last-applied-configuration) and the live spec.
169
176
  // Undefined when the diff couldn't be computed (no annotation, SSA-applied
@@ -177,6 +184,8 @@ export interface GitOpsChange {
177
184
  recentEvents?: GitOpsEventSummary[]
178
185
  partial: boolean
179
186
  partialNote?: string
187
+ // A Flux source or dependsOn target the root consumes but doesn't manage.
188
+ dependency?: boolean
180
189
  }
181
190
 
182
191
  export interface GitOpsDrift {
@@ -247,6 +256,12 @@ export interface GitOpsCapabilities {
247
256
  // tell "not set up" (offer Connect) from "set up but disconnected" (offer
248
257
  // Reconnect) — instead of advertising a diff that would fail.
249
258
  argoConfigured?: boolean
259
+ // Configured, and the host is still making the first connection; ask
260
+ // again shortly instead of reporting the diff as down.
261
+ argoConnecting?: boolean
262
+ // Configured, the connection failed in a way that may pass on its own, and
263
+ // the host is retrying; ask again now and then.
264
+ argoRetrying?: boolean
250
265
  // True on an Argo CD Application detail when the integration is connected,
251
266
  // meaning the revision-metadata endpoint can resolve Git commit details
252
267
  // (author, message, signature) for this app's deployed revisions.
@@ -269,13 +284,16 @@ export interface ArgoRevisionMetadata {
269
284
  // Response of GET /api/argo/applications/{ns}/{name}/resource-diff. `desired`
270
285
  // is the Git-rendered manifest, `live` the normalized cluster state — both YAML
271
286
  // for the line-by-line view. `fieldEntries` is the same diff structured per
272
- // path for a compact summary. `redacted` is set when Secret values were masked
287
+ // path for a compact summary; Radars before the list was made non-null send
288
+ // null when it is empty. `fieldEntriesTruncated` is set when the list was cut
289
+ // at the server's entry cap. `redacted` is set when Secret values were masked
273
290
  // server-side; `hook` marks Argo sync-hook resources.
274
291
  export interface GitOpsResourceDiff {
275
292
  source: string
276
293
  desired: string
277
294
  live: string
278
- fieldEntries: GitOpsResourceDiffFieldEntry[]
295
+ fieldEntries: GitOpsResourceDiffFieldEntry[] | null
296
+ fieldEntriesTruncated?: boolean
279
297
  redacted: boolean
280
298
  hook: boolean
281
299
  }
@@ -24,6 +24,8 @@ export interface GitOpsTreeNode {
24
24
  tool: GitOpsTreeTool
25
25
  sync?: SyncStatus
26
26
  health?: GitOpsHealthStatus
27
+ // A local lookup confirmed NotFound; independent of controller health.
28
+ missing?: boolean
27
29
  // Who assessed `health`. 'controller' is the GitOps controller's own
28
30
  // verdict; 'radar' is Radar's read of the live object (topology status, or
29
31
  // the issues engine's finding when the controller's verdict isn't
@@ -43,6 +45,9 @@ export interface GitOpsTreeNode {
43
45
  // Lives on the destination cluster of a remote tree. This cluster's API
44
46
  // would answer for a different, same-named object.
45
47
  remote?: boolean
48
+ // Reached only through a Flux source, chart or dependsOn declaration: the
49
+ // root consumes it but does not manage it. Not counted as a managed resource.
50
+ dependency?: boolean
46
51
  }
47
52
 
48
53
  // 'controllerApi' is the controller's own verdict read from its API server
@@ -69,6 +74,7 @@ export interface GitOpsTreeSummary {
69
74
  grouped: number
70
75
  degraded: number
71
76
  outOfSync: number
77
+ dependencies?: number
72
78
  }
73
79
 
74
80
  export interface GitOpsResourceTree {
@@ -83,6 +89,12 @@ export interface GitOpsResourceTree {
83
89
  healthFromApi?: boolean
84
90
  // The controller's API server was asked and didn't answer usefully.
85
91
  healthApiError?: string
92
+ // The host is still connecting to the controller's API server; ask again
93
+ // shortly.
94
+ healthApiPending?: boolean
95
+ // The host couldn't reach the controller's API server and is retrying;
96
+ // ask again now and then.
97
+ healthApiRetrying?: boolean
86
98
  // The Application (or Flux object with spec.kubeConfig) deploys to another
87
99
  // cluster; Radar derives nothing about its resources from here.
88
100
  remoteDestination?: boolean
@@ -99,6 +99,7 @@ export interface FluxCondition {
99
99
  reason?: string
100
100
  message?: string
101
101
  lastTransitionTime?: string
102
+ observedGeneration?: number
102
103
  }
103
104
 
104
105
  // ============================================================================
@@ -141,8 +142,11 @@ function hasGenerationDrift(gen?: FluxGeneration): boolean {
141
142
  * - suspend → Suspended (sync from Ready, unless drifted)
142
143
  * - Stalled=True → OutOfSync / Degraded
143
144
  * - Ready=True and no generation drift → Synced, health from Healthy
145
+ * - Ready=False with a failure reason about
146
+ * the current generation → OutOfSync / Degraded
144
147
  * - drift, or Reconciling with Ready not True → Reconciling / Progressing
145
- * - Ready=False → OutOfSync
148
+ * - Ready=False → OutOfSync / Degraded, or
149
+ * Progressing for a retry/wait reason
146
150
  */
147
151
  export function fluxConditionsToGitOpsStatus(
148
152
  conditions: FluxCondition[],
@@ -203,6 +207,25 @@ export function fluxConditionsToGitOpsStatus(
203
207
  }
204
208
  }
205
209
 
210
+ // A failure the controller reported outranks activity too: a source that
211
+ // can't clone keeps Reconciling=True and never records observedGeneration
212
+ // while it retries, and reading that as "in progress" hid it.
213
+ const readyReason = readyCondition?.reason || ''
214
+ const transientReason = readyReason.includes('Progress') || readyReason.includes('Retry') || readyReason.includes('Wait')
215
+ // Only a failure about the current spec counts: after a fix, the old
216
+ // Ready=False describes an older generation until the controller looks again.
217
+ const currentFailure = readyCondition?.observedGeneration !== undefined && readyCondition.observedGeneration === generation?.generation
218
+ if (readyCondition?.status === 'False' && !transientReason && currentFailure) {
219
+ return {
220
+ sync: 'OutOfSync',
221
+ health: 'Degraded',
222
+ message: readyCondition.message,
223
+ lastSyncTime: readyCondition.lastTransitionTime,
224
+ suspended: false,
225
+ ...activity,
226
+ }
227
+ }
228
+
206
229
  // Genuinely not applied: the controller has not observed the current spec, or
207
230
  // it is working and has not reached Ready.
208
231
  if (drifted || isReconciling) {
@@ -216,14 +239,11 @@ export function fluxConditionsToGitOpsStatus(
216
239
  }
217
240
  }
218
241
 
219
- // Handle not ready state
242
+ // A retry or remediation in flight is out of sync, but not a settled failure.
220
243
  if (readyCondition?.status === 'False') {
221
- const reason = readyCondition.reason || ''
222
- const isTransient = reason.includes('Progress') || reason.includes('Retry') || reason.includes('Wait')
223
-
224
244
  return {
225
245
  sync: 'OutOfSync',
226
- health: isTransient ? 'Progressing' : 'Degraded',
246
+ health: transientReason ? 'Progressing' : 'Degraded',
227
247
  message: readyCondition.message,
228
248
  lastSyncTime: readyCondition.lastTransitionTime,
229
249
  suspended: false,
@@ -63,6 +63,13 @@ const ADDITIONAL_BUILTIN_IDENTITIES: Record<string, string> = {
63
63
  deviceclass: 'resource.k8s.io', deviceclasses: 'resource.k8s.io',
64
64
  };
65
65
 
66
+ // Kind spellings only: a resource name such as `services` is not a Kind.
67
+ const ADDITIONAL_CORE_KINDS: ReadonlySet<string> = new Set(['Endpoints'])
68
+
69
+ export function isBuiltinCoreKind(kind: string): boolean {
70
+ return ADDITIONAL_CORE_KINDS.has(kind) || CORE_RESOURCES.some(r => r.group === '' && r.kind === kind)
71
+ }
72
+
66
73
  export function builtinGroupForKind(kindOrResource: string): string | undefined {
67
74
  const value = kindOrResource.toLowerCase()
68
75
  return CORE_RESOURCES.find(resource =>
@@ -38,6 +38,7 @@ const RELATED_RUNTIME_KINDS = new Set([
38
38
  'HTTPRoute',
39
39
  'GRPCRoute',
40
40
  'TCPRoute',
41
+ 'UDPRoute',
41
42
  'TLSRoute',
42
43
  'ConfigMap',
43
44
  'Secret',
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { cnpgBackupDeclaration, cnpgBackupDestinationBlocker, cnpgBarmanPlugin, cnpgDestinationPlugin, cnpgHasBackupDestination } from './cnpg-backup'
4
+
5
+ const cases: Array<{
6
+ name: string
7
+ cluster: Parameters<typeof cnpgBackupDeclaration>[0]
8
+ method: string
9
+ plugin: string
10
+ hasDestination: boolean
11
+ destinationPlugin: string
12
+ blockerCode: string
13
+ barman: { objectStore: string; serverName: string } | null
14
+ }> = JSON.parse(readFileSync(new URL('../../../../pkg/cnpg/testdata/backup-declarations.json', import.meta.url), 'utf8'))
15
+
16
+ describe('CNPG backup declarations shared with Go', () => {
17
+ it.each(cases)('$name', (test) => {
18
+ const declaration = cnpgBackupDeclaration(test.cluster)
19
+ expect(cnpgHasBackupDestination(declaration)).toBe(test.hasDestination)
20
+ expect(cnpgDestinationPlugin(declaration)?.name ?? '').toBe(test.destinationPlugin)
21
+ expect(cnpgBackupDestinationBlocker(declaration, test.method || undefined, test.plugin)?.code ?? '').toBe(test.blockerCode)
22
+ const barman = cnpgBarmanPlugin(declaration)
23
+ if (test.barman) expect(barman).toMatchObject({ barmanObjectName: test.barman.objectStore || undefined, serverName: test.barman.serverName })
24
+ else expect(barman).toBeUndefined()
25
+ })
26
+ })
@@ -0,0 +1,70 @@
1
+ export const CNPG_BARMAN_PLUGIN_NAME = 'barman-cloud.cloudnative-pg.io'
2
+
3
+ interface BackupCluster {
4
+ metadata?: { name?: string }
5
+ spec?: {
6
+ backup?: { barmanObjectStore?: { destinationPath?: string }; volumeSnapshot?: unknown }
7
+ plugins?: { name: string; enabled?: boolean; isWALArchiver?: boolean; parameters?: { barmanObjectName?: string; serverName?: string } }[]
8
+ }
9
+ }
10
+
11
+ export interface CNPGBackupPlugin {
12
+ name: string
13
+ enabled: boolean
14
+ isWALArchiver: boolean
15
+ barmanObjectName?: string
16
+ serverName?: string
17
+ }
18
+
19
+ export interface CNPGBackupDeclaration {
20
+ plugins: CNPGBackupPlugin[]
21
+ inTreeConfigured: boolean
22
+ inTreeDestination?: string
23
+ snapshotsConfigured: boolean
24
+ }
25
+
26
+ export function cnpgBackupDeclaration(cluster: BackupCluster): CNPGBackupDeclaration {
27
+ const backup = cluster.spec?.backup
28
+ return {
29
+ inTreeConfigured: !!backup?.barmanObjectStore,
30
+ inTreeDestination: backup?.barmanObjectStore?.destinationPath,
31
+ snapshotsConfigured: backup?.volumeSnapshot != null,
32
+ plugins: (cluster.spec?.plugins ?? []).filter((p) => p?.name).map((p) => ({
33
+ name: p.name,
34
+ enabled: p.enabled !== false,
35
+ isWALArchiver: p.isWALArchiver === true,
36
+ barmanObjectName: p.parameters?.barmanObjectName,
37
+ serverName: p.parameters?.serverName || cluster.metadata?.name,
38
+ })),
39
+ }
40
+ }
41
+
42
+ export function cnpgBarmanPlugin(declaration: CNPGBackupDeclaration): CNPGBackupPlugin | undefined {
43
+ return declaration.plugins.find((p) => p.enabled && p.name === CNPG_BARMAN_PLUGIN_NAME)
44
+ }
45
+
46
+ export function cnpgDestinationPlugin(declaration: CNPGBackupDeclaration): CNPGBackupPlugin | undefined {
47
+ return declaration.plugins.find((p) => p.enabled && (p.name !== CNPG_BARMAN_PLUGIN_NAME || !!p.barmanObjectName))
48
+ }
49
+
50
+ export function cnpgHasBackupDestination(declaration: CNPGBackupDeclaration): boolean {
51
+ return !!declaration.inTreeDestination || declaration.snapshotsConfigured || !!cnpgDestinationPlugin(declaration)
52
+ }
53
+
54
+ export interface CNPGBackupBlocker {
55
+ code: 'no_destination' | 'method_destination_missing'
56
+ method: string
57
+ plugin?: string
58
+ }
59
+
60
+ export function cnpgBackupDestinationBlocker(declaration: CNPGBackupDeclaration, method = 'barmanObjectStore', plugin?: string): CNPGBackupBlocker | null {
61
+ const blocked = method === 'barmanObjectStore' ? !declaration.inTreeDestination
62
+ : method === 'volumeSnapshot' ? !declaration.snapshotsConfigured
63
+ : method === 'plugin' ? !declaration.plugins.some((p) => p.name === plugin && p.enabled && (p.name !== CNPG_BARMAN_PLUGIN_NAME || !!p.barmanObjectName))
64
+ : false
65
+ return blocked ? { code: cnpgHasBackupDestination(declaration) ? 'method_destination_missing' : 'no_destination', method, plugin: cnpgDestinationPlugin(declaration)?.name } : null
66
+ }
67
+
68
+ export function cnpgBackupBlockerText(blocker: CNPGBackupBlocker): string {
69
+ return blocker.code === 'no_destination' ? 'No backup destination' : `No ${blocker.method} destination`
70
+ }
@@ -41,6 +41,15 @@ describe('parseMemoryToBytes', () => {
41
41
  expect(parseMemoryToBytes('1P')).toBe(1000 ** 5)
42
42
  expect(parseMemoryToBytes('1E')).toBe(1000 ** 6)
43
43
  })
44
+
45
+ // The API server keeps a quoted exponent quantity as written, and defaults a
46
+ // request from it the same way ("1e9" limit → "500e6" request).
47
+ it('parses decimal-exponent quantities', () => {
48
+ expect(parseMemoryToBytes('1e9')).toBe(1e9)
49
+ expect(parseMemoryToBytes('500e6')).toBe(5e8)
50
+ expect(parseMemoryToBytes('1E3')).toBe(1000)
51
+ expect(formatMemoryString('1e9')).toBe('954 MiB')
52
+ })
44
53
  })
45
54
 
46
55
  describe('parseQuantityToNumber', () => {
@@ -120,6 +120,10 @@ export function parseMemoryToBytes(memString: string): number {
120
120
  if (!memString) return 0
121
121
 
122
122
  const str = memString.trim()
123
+ // Decimal-exponent form ("1e9") is decided before the suffix table, as in
124
+ // parseQuantityToNumber, so "1E3" reads as 1000 and not as exa.
125
+ if (/^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/.test(str)) return Number(str)
126
+
123
127
  const match = str.match(/^(-?\d+(?:\.\d+)?)\s*([A-Za-z]*)$/)
124
128
  if (!match) return 0
125
129
 
@@ -38,10 +38,11 @@ export function gitOpsOwnerFromRelationships(
38
38
  ): GitOpsOwnerRef | null {
39
39
  const refs = rel?.managedBy
40
40
  if (!refs || refs.length === 0) return null
41
- return refToGitOpsOwner(refs[0])
41
+ return gitOpsOwnerFromRef(refs[0])
42
42
  }
43
43
 
44
- function refToGitOpsOwner(ref: ResourceRef): GitOpsOwnerRef | null {
44
+ /** A manager ref (relationships.managedBy, a workspace's managedBy) as a GitOps owner, or null when the manager is not a GitOps controller. */
45
+ export function gitOpsOwnerFromRef(ref: ResourceRef): GitOpsOwnerRef | null {
45
46
  switch (true) {
46
47
  case ref.kind === 'Application' && ref.group === ARGO_APPLICATION_GROUP:
47
48
  return { tool: 'argocd', kind: 'applications', namespace: ref.namespace, name: ref.name }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { formatGrant } from './grant'
3
+
4
+ describe('formatGrant', () => {
5
+ it('words a namespaced grant like the server', () => {
6
+ expect(formatGrant({ verb: 'patch', group: 'postgresql.cnpg.io', resource: 'clusters', subresource: 'status', namespace: 'pg' })).toBe(
7
+ 'patch clusters/status (postgresql.cnpg.io) in namespace pg',
8
+ )
9
+ expect(formatGrant({ verb: 'get', resource: 'pods', subresource: 'proxy', namespace: 'pg' })).toBe('get pods/proxy in namespace pg')
10
+ })
11
+ it('words a cluster-wide grant with the group as resource.group', () => {
12
+ expect(formatGrant({ verb: 'patch', resource: 'clusters', group: 'example.io', subresource: 'status' })).toBe('patch clusters.example.io/status cluster-wide')
13
+ expect(formatGrant({ verb: 'get', resource: 'nodes' })).toBe('get nodes cluster-wide')
14
+ expect(formatGrant({ verb: 'list', group: 'postgresql.cnpg.io', resource: 'clusters' })).toBe('list clusters.postgresql.cnpg.io cluster-wide')
15
+ })
16
+ it('is undefined without a grant', () => {
17
+ expect(formatGrant(undefined)).toBeUndefined()
18
+ })
19
+ })
@@ -0,0 +1,35 @@
1
+ /**
2
+ * One RBAC permission a read or action needs, as the server sends it. An
3
+ * empty namespace means cluster-wide (a cluster-scoped resource, or a list
4
+ * across every namespace).
5
+ */
6
+ export interface Grant {
7
+ verb: string
8
+ group?: string
9
+ resource: string
10
+ subresource?: string
11
+ namespace?: string
12
+ }
13
+
14
+ function grantResource(g: Grant): string {
15
+ return g.subresource ? `${g.resource}/${g.subresource}` : g.resource
16
+ }
17
+
18
+ /**
19
+ * The grant's two halves for display: what ("patch clusters/status
20
+ * (postgresql.cnpg.io)") and where (" in namespace pg", " cluster-wide").
21
+ */
22
+ export function grantParts(g: Grant): { what: string; scope: string } {
23
+ if (g.namespace) {
24
+ return { what: `${g.verb} ${grantResource(g)}${g.group ? ` (${g.group})` : ''}`, scope: ` in namespace ${g.namespace}` }
25
+ }
26
+ return { what: `${g.verb} ${g.resource}${g.group ? `.${g.group}` : ''}${g.subresource ? `/${g.subresource}` : ''}`, scope: ' cluster-wide' }
27
+ }
28
+
29
+ export function formatGrant(g: Grant): string
30
+ export function formatGrant(g: Grant | undefined): string | undefined
31
+ export function formatGrant(g: Grant | undefined): string | undefined {
32
+ if (!g) return undefined
33
+ const { what, scope } = grantParts(g)
34
+ return what + scope
35
+ }
@@ -32,4 +32,8 @@ export * from './bulk-workload-actions'
32
32
  export * from './inClusterConsent'
33
33
  export * from './workflow-execution'
34
34
  export * from './network-policy'
35
+ export * from './cnpg-backup'
36
+ export * from './url-path'
35
37
  export * from './resource-refs'
38
+
39
+ export * from './grant'
@@ -6,7 +6,7 @@ interface FriendlyError {
6
6
  suggestion: string
7
7
  }
8
8
 
9
- const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string) => FriendlyError }> = [
9
+ const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string, allowApply: boolean) => FriendlyError }> = [
10
10
  // Admission webhook timeout
11
11
  {
12
12
  test: (msg) => /failed calling webhook.*context deadline exceeded/i.test(msg),
@@ -75,9 +75,9 @@ const patterns: Array<{ test: (msg: string) => boolean; friendly: (msg: string)
75
75
  // Already exists (create mode)
76
76
  {
77
77
  test: (msg) => /already exists/i.test(msg),
78
- friendly: () => ({
78
+ friendly: (_msg, allowApply) => ({
79
79
  summary: 'Resource already exists',
80
- suggestion: 'Switch to Apply mode to update the existing resource, or change the name to create a new one.',
80
+ suggestion: allowApply ? 'Switch to Apply mode to update the existing resource, or change the name to create a new one.' : 'Change the name to create a new resource, or remove this document if it was already created.',
81
81
  }),
82
82
  },
83
83
  // Quota exceeded
@@ -122,11 +122,11 @@ function stripPrefix(msg: string): string {
122
122
  return msg.replace(/^failed to (apply|create) resource:\s*/i, '')
123
123
  }
124
124
 
125
- export function getFriendlyError(rawError: string): { summary: string; suggestion: string; raw: string } | null {
125
+ export function getFriendlyError(rawError: string, allowApply = true): { summary: string; suggestion: string; raw: string } | null {
126
126
  const cleaned = stripPrefix(rawError)
127
127
  for (const pattern of patterns) {
128
128
  if (pattern.test(cleaned)) {
129
- const { summary, suggestion } = pattern.friendly(cleaned)
129
+ const { summary, suggestion } = pattern.friendly(cleaned, allowApply)
130
130
  return { summary, suggestion, raw: cleaned }
131
131
  }
132
132
  }
@@ -134,8 +134,8 @@ export function getFriendlyError(rawError: string): { summary: string; suggestio
134
134
  }
135
135
 
136
136
  // Always returns something displayable — friendly if matched, raw if not
137
- export function formatApplyError(rawError: string): { summary: string; suggestion?: string; raw: string } {
138
- const friendly = getFriendlyError(rawError)
137
+ export function formatApplyError(rawError: string, { allowApply = true }: { allowApply?: boolean } = {}): { summary: string; suggestion?: string; raw: string } {
138
+ const friendly = getFriendlyError(rawError, allowApply)
139
139
  if (friendly) return friendly
140
140
  const cleaned = stripPrefix(rawError)
141
141
  return { summary: cleaned, raw: rawError }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { highlightJson, tokenizeJson, unescapeJsonStrings } from './log-format'
3
+
4
+ describe('unescapeJsonStrings', () => {
5
+ it('expands whitespace inside strings while leaving escapes outside them untouched', () => {
6
+ expect(unescapeJsonStrings('{"msg":"first\\r\\nsecond\\nthird\\tend"}')).toBe('{"msg":"first\r\nsecond\nthird\tend"}')
7
+ expect(unescapeJsonStrings('outside\\n {"msg":"inside\\n"} outside\\t')).toBe('outside\\n {"msg":"inside\n"} outside\\t')
8
+ })
9
+
10
+ it('keeps escaped quotes and backslashes in their original string', () => {
11
+ expect(unescapeJsonStrings('{"msg":"say \\"hello\\"\\nnext","path":"C:\\\\docs"}')).toBe('{"msg":"say \\"hello\\"\nnext","path":"C:\\\\docs"}')
12
+ expect(unescapeJsonStrings(JSON.stringify({ empty: '', nested: ['a\nb', 'c\td'], number: 42, yes: true }))).toBe('{"empty":"","nested":["a\nb","c\td"],"number":42,"yes":true}')
13
+ })
14
+
15
+ it('stops once at an unterminated string with many escaped quotes', () => {
16
+ const text = '"' + '\\"'.repeat(100_000) + '\\n'
17
+ expect(unescapeJsonStrings(text)).toBe(text)
18
+ })
19
+ })
20
+
21
+ describe('JSON highlighting', () => {
22
+ it('keeps primitive-looking words inside strings and preserves JSON token colors', () => {
23
+ const tokens = tokenizeJson('{ "text" : "true null 123 \\"quoted\\"", "values": [-1.2e+3, true, false, null, ""] }')
24
+ expect(tokens.filter((token) => token.type === 'string').map((token) => token.value)).toEqual(['"true null 123 \\"quoted\\""', '""'])
25
+ expect(tokens.filter((token) => token.type === 'number').map((token) => token.value)).toEqual(['-1.2e+3'])
26
+ expect(highlightJson('{"a":true,"b":null,"n":12}')).toBe('{<span style="color:#7cacf8">"a"</span>:<span style="color:#c678dd">true</span>,<span style="color:#7cacf8">"b"</span>:<span style="color:#808080">null</span>,<span style="color:#7cacf8">"n"</span>:<span style="color:#e5c07b">12</span>}')
27
+ })
28
+
29
+ it('escapes HTML in log values before inserting highlighting markup', () => {
30
+ const html = highlightJson(JSON.stringify({ msg: '<script>alert("x")</script>&' }))
31
+ expect(html).toContain('&lt;script&gt;')
32
+ expect(html).toContain('&lt;/script&gt;&amp;')
33
+ expect(html).not.toContain('<script>')
34
+ })
35
+
36
+ it('leaves a long unterminated string as plain text without rescanning escaped quotes', () => {
37
+ const text = '"' + '\\"'.repeat(100_000)
38
+ expect(tokenizeJson(text)).toEqual([{ type: 'text', value: text }])
39
+ expect(highlightJson(text)).toBe(text)
40
+ })
41
+ })