@skyhook-io/radar-app 1.13.4 → 1.14.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 (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { podLimitRangeNames } from './quotas'
3
+
4
+ describe('Pod-relevant LimitRanges', () => {
5
+ it('excludes PVC-only rules and retains Pod, Container and mixed rules', () => {
6
+ const rules = [
7
+ { metadata: { name: 'storage-only' }, spec: { limits: [{ type: 'PersistentVolumeClaim' }] } },
8
+ { metadata: { name: 'container-defaults' }, spec: { limits: [{ type: 'Container' }] } },
9
+ { metadata: { name: 'pod-total' }, spec: { limits: [{ type: 'Pod' }] } },
10
+ { metadata: { name: 'mixed' }, spec: { limits: [{ type: 'PersistentVolumeClaim' }, { type: 'Container' }] } },
11
+ ]
12
+ expect(podLimitRangeNames(rules)).toEqual(['container-defaults', 'pod-total', 'mixed'])
13
+ })
14
+
15
+ it('ignores LimitRanges with no declared entries', () => {
16
+ expect(podLimitRangeNames([
17
+ { metadata: { name: 'null-entries' }, spec: { limits: null } },
18
+ { metadata: { name: 'empty-entries' }, spec: { limits: [] } },
19
+ ])).toEqual([])
20
+ })
21
+
22
+ it('has no contextual links before data is available or for an empty namespace', () => {
23
+ expect(podLimitRangeNames(undefined)).toEqual([])
24
+ expect(podLimitRangeNames([])).toEqual([])
25
+ })
26
+ })
package/src/api/quotas.ts CHANGED
@@ -14,3 +14,23 @@ export function useNamespaceQuotas(namespace: string, enabled = true) {
14
14
  staleTime: 15000,
15
15
  })
16
16
  }
17
+
18
+ // useNamespaceLimitRanges fetches a namespace's LimitRange objects via
19
+ // /api/resources/limitranges?namespace=<ns> (a bare array). Backs the
20
+ // NamespaceRenderer rules section and the contextual link on Pod/workload
21
+ // details — the defaults and constraints applied at admission are otherwise
22
+ // only visible by reading the object.
23
+ export function useNamespaceLimitRanges(namespace: string, enabled = true) {
24
+ return useQuery<any[]>({
25
+ queryKey: ['limitranges', namespace],
26
+ queryFn: () => fetchJSON<any[]>(`/resources/limitranges?namespace=${encodeURIComponent(namespace)}`),
27
+ enabled: enabled && !!namespace,
28
+ staleTime: 15000,
29
+ })
30
+ }
31
+
32
+ export function podLimitRangeNames(limitRanges: any[] | undefined): string[] {
33
+ return (limitRanges ?? [])
34
+ .filter(lr => lr.spec.limits?.some((entry: { type: string }) => entry.type === 'Container' || entry.type === 'Pod'))
35
+ .map(lr => lr.metadata.name)
36
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import type { WorkloadMetrics } from './workloadMetrics'
3
+
4
+ vi.mock('@tanstack/react-query', () => ({ useQuery: (options: unknown) => options }))
5
+ vi.mock('./client', () => ({
6
+ useClusterInfo: () => ({ data: { context: 'cluster-a' } }),
7
+ fetchJSON: vi.fn(),
8
+ ApiError: class extends Error {},
9
+ }))
10
+ vi.mock('./config', () => ({ getApiBase: () => '/api' }))
11
+ const { useWorkloadMetrics: workloadQueryOptions } = await import('./workloadMetrics')
12
+
13
+ describe('workload source-switch placeholders', () => {
14
+ const options = () => workloadQueryOptions('Deployment', 'shop', 'api', '1h', 'istio', true) as unknown as {
15
+ queryKey: unknown[]
16
+ placeholderData: (data: WorkloadMetrics, query: { queryKey: unknown[] }) => WorkloadMetrics | undefined
17
+ refetchInterval: (query: { state: { data: WorkloadMetrics } }) => number
18
+ }
19
+ const data: WorkloadMetrics = {
20
+ state: 'available', sources: [], pods: 1, podsTotal: 1,
21
+ history: {}, comparison: {},
22
+ start: 0, end: 60, stepSeconds: 60, rateWindowSeconds: 300, panels: {},
23
+ }
24
+ it('keeps the previous payload when only the observer changes', () => {
25
+ const query = options()
26
+ const key = [...query.queryKey]
27
+ key[key.length - 1] = 'beyla'
28
+ expect(query.placeholderData(data, { queryKey: key })).toBe(data)
29
+ })
30
+ it('polls pending current attribution even while history is available', () => {
31
+ expect(options().refetchInterval({ state: { data } })).toBe(30_000)
32
+ expect(options().refetchInterval({ state: { data: { ...data, comparison: { cpu: { state: 'detecting', unit: 'cores', series: [] } } } } })).toBe(3_000)
33
+ })
34
+ it.each([1, 2, 3, 4, 5, 6])('drops the previous payload when identity field %s changes', (index) => {
35
+ const query = options()
36
+ const key = [...query.queryKey]
37
+ key[index] = 'different'
38
+ expect(query.placeholderData(data, { queryKey: key })).toBeUndefined()
39
+ })
40
+ })
@@ -0,0 +1,68 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import type { TimeSeries } from "@skyhook-io/k8s-ui/components/charts";
3
+ import { ApiError, fetchJSON, useClusterInfo, type PrometheusTimeRange } from "./client";
4
+ import { getApiBase } from "./config";
5
+
6
+ export type WorkloadMetricState =
7
+ "available" | "partial" | "stale" | "unavailable" | "error" | "detecting";
8
+ export type WorkloadRequestSource = "beyla" | "istio";
9
+
10
+ export interface WorkloadMetricPanel {
11
+ state: WorkloadMetricState;
12
+ reason?: string;
13
+ unit: string;
14
+ series: TimeSeries[];
15
+ }
16
+
17
+ export interface WorkloadMetrics {
18
+ history: Partial<Record<"cpu" | "memory" | "throttling" | "requests", { mode: "workload-history" | "current-pods" | "unavailable"; reason?: string }>>;
19
+ comparison: Partial<Record<"cpu" | "memory" | "throttling", WorkloadMetricPanel>>;
20
+ scopeNotice?: string;
21
+ attribution?: Record<string, string>;
22
+ state: WorkloadMetricState;
23
+ reason?: string;
24
+ source?: WorkloadRequestSource;
25
+ sources: {
26
+ id: WorkloadRequestSource;
27
+ label: string;
28
+ state: WorkloadMetricState;
29
+ }[];
30
+ pods: number;
31
+ podsTotal: number;
32
+ end: number;
33
+ start: number;
34
+ stepSeconds: number;
35
+ rateWindowSeconds: number;
36
+ panels: Partial<
37
+ Record<
38
+ "requests" | "errors" | "p50" | "p95" | "throttling" | "cpu" | "memory" | "observedPods",
39
+ WorkloadMetricPanel
40
+ >
41
+ >;
42
+ }
43
+
44
+ export function useWorkloadMetrics(
45
+ kind: string,
46
+ namespace: string,
47
+ name: string,
48
+ range: PrometheusTimeRange,
49
+ source: WorkloadRequestSource | "",
50
+ enabled: boolean,
51
+ ) {
52
+ const { data: cluster } = useClusterInfo();
53
+ const identity = ["workload-metrics", getApiBase(), cluster?.context, kind, namespace, name, range];
54
+ return useQuery<WorkloadMetrics>({
55
+ queryKey: [...identity, source],
56
+ placeholderData: (previous, previousQuery) =>
57
+ identity.every((value, index) => previousQuery?.queryKey[index] === value) ? previous : undefined,
58
+ queryFn: ({ signal }) =>
59
+ fetchJSON<WorkloadMetrics>(
60
+ `/prometheus/workload/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}?${new URLSearchParams({ range, source })}`,
61
+ signal,
62
+ ),
63
+ enabled,
64
+ staleTime: 30_000,
65
+ refetchInterval: (query) => query.state.data?.state === "detecting" || Object.values(query.state.data?.comparison ?? {}).some((panel) => panel.state === "detecting") ? 3_000 : 30_000,
66
+ retry: (failureCount, error) => failureCount < 1 && error instanceof ApiError && error.status === 409,
67
+ });
68
+ }
@@ -1,6 +1,7 @@
1
- import { useRef, useState } from 'react'
1
+ import { type ReactNode, useRef, useState } from 'react'
2
2
  import { useMutation } from '@tanstack/react-query'
3
3
  import { AlertTriangle, ArrowUpRight, Check, ExternalLink, GitBranch, Info, Loader2, ShieldAlert, X } from 'lucide-react'
4
+ import type { BlockedExit } from './cloudConnectHandoff'
4
5
  import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
5
6
  import {
6
7
  ApiError,
@@ -19,20 +20,20 @@ import { showApiError } from './ui/Toast'
19
20
  export function CloudConnectFlow({
20
21
  status,
21
22
  blocked,
22
- signupUrl,
23
+ exit,
23
24
  onStatus,
24
25
  onExit,
25
26
  }: {
26
27
  status: CloudInstallStatus
27
28
  blocked: CloudInstallBlocked | null
28
- signupUrl: string
29
+ exit: BlockedExit
29
30
  // Push a mutation's status response into the shared query state.
30
31
  onStatus: (st: CloudInstallStatus) => void
31
32
  // Leave the flow view (back to the pitch, or close after dismiss).
32
33
  onExit: () => void
33
34
  }) {
34
35
  if (blocked) {
35
- return <BlockedView blocked={blocked} signupUrl={signupUrl} onExit={onExit} />
36
+ return <BlockedView blocked={blocked} exit={exit} onExit={onExit} />
36
37
  }
37
38
 
38
39
  switch (status.state) {
@@ -66,13 +67,14 @@ export function CloudConnectFlow({
66
67
 
67
68
  function BlockedView({
68
69
  blocked,
69
- signupUrl,
70
+ exit,
70
71
  onExit,
71
72
  }: {
72
73
  blocked: CloudInstallBlocked
73
- signupUrl: string
74
+ exit: BlockedExit
74
75
  onExit: () => void
75
76
  }) {
77
+ const copy = blockedCopy(blocked, exit)
76
78
  const icon =
77
79
  blocked.reason === 'gitops' ? (
78
80
  <GitBranch className="w-4 h-4 shrink-0 mt-0.5 text-emerald-600 dark:text-emerald-400" />
@@ -81,48 +83,33 @@ function BlockedView({
81
83
  ) : (
82
84
  <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-amber-500" />
83
85
  )
84
- const title =
85
- blocked.reason === 'gitops'
86
- ? 'This install is managed by GitOps'
87
- : blocked.reason === 'preflight'
88
- ? 'Your Kubernetes identity can’t install this'
89
- : 'This cluster can’t be connected from here'
90
86
  return (
91
87
  <div className="px-8 pt-6 pb-5">
92
88
  <div className="card-inner-lg flex gap-2.5">
93
89
  {icon}
94
- <div className="min-w-0">
95
- <div className="text-[13px] font-semibold text-theme-text-primary">{title}</div>
96
- <p className="mt-1 text-[12px] leading-relaxed text-theme-text-secondary">{blocked.message}</p>
97
- {blocked.blocking && blocked.blocking.length > 0 && (
98
- <ul className="mt-2 space-y-1 text-[11.5px] text-theme-text-tertiary">
99
- {blocked.blocking.map((line) => (
100
- <li key={line} className="flex items-start gap-1.5">
101
- <span className="mt-[6px] w-1 h-1 rounded-full bg-amber-500 shrink-0" />
102
- {line}
103
- </li>
104
- ))}
105
- </ul>
106
- )}
90
+ <div className="min-w-0 space-y-3">
91
+ <div className="text-[13px] font-semibold text-theme-text-primary">{copy.title}</div>
92
+ <BlockedSection label="What Radar tried">{copy.tried}</BlockedSection>
93
+ <BlockedSection label="Why it stopped">
94
+ {copy.why}
95
+ {blocked.blocking && blocked.blocking.length > 0 && <BlockingLines lines={blocked.blocking} />}
96
+ </BlockedSection>
97
+ <BlockedSection label="What to do">{copy.next}</BlockedSection>
107
98
  </div>
108
99
  </div>
109
100
  <div className="mt-4 flex items-center gap-4">
110
- {/* Only a preflight denial has a legitimate browser alternative —
111
- someone with more Kubernetes permission can run the wizard. GitOps
112
- and unsupported refusals named a specific reason and target that a
113
- generic signup link cannot carry, so offering it would contradict
114
- the message directly above. */}
115
- {blocked.reason === 'preflight' && (
116
- <a
117
- href={signupUrl}
118
- target="_blank"
119
- rel="noopener noreferrer"
120
- className="text-[12.5px] font-semibold text-emerald-600 dark:text-emerald-400 hover:underline underline-offset-2"
121
- >
122
- Connect through the browser wizard instead →
123
- </a>
124
- )}
125
- <button onClick={onExit} className="text-[12.5px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors">
101
+ <a
102
+ href={exit.href}
103
+ target="_blank"
104
+ rel="noopener noreferrer"
105
+ className="px-5 py-2 rounded-[10px] bg-emerald-500 hover:bg-emerald-400 text-emerald-950 text-[13.5px] font-bold shadow-[0_0_22px_rgba(16,185,129,0.35)] hover:shadow-[0_0_30px_rgba(16,185,129,0.5)] hover:-translate-y-px transition-all"
106
+ >
107
+ {exit.label}
108
+ </a>
109
+ <button
110
+ onClick={onExit}
111
+ className="ml-auto text-[12.5px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
112
+ >
126
113
  Back
127
114
  </button>
128
115
  </div>
@@ -130,6 +117,156 @@ function BlockedView({
130
117
  )
131
118
  }
132
119
 
120
+ function BlockedSection({ label, children }: { label: string; children: ReactNode }) {
121
+ return (
122
+ <div>
123
+ <div className="text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">{label}</div>
124
+ <div className="mt-0.5 text-[12px] leading-relaxed text-theme-text-secondary">{children}</div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ function BlockingLines({ lines }: { lines: string[] }) {
130
+ return (
131
+ <ul className="mt-1.5 space-y-1 text-[11.5px] text-theme-text-tertiary">
132
+ {lines.map((line) => (
133
+ <li key={line} className="flex items-start gap-1.5">
134
+ <span className="mt-[6px] w-1 h-1 rounded-full bg-amber-500 shrink-0" />
135
+ {line}
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ )
140
+ }
141
+
142
+ // The blocked card stands in for the plan card the person never saw, so it
143
+ // carries what Radar did first, then why it stopped, then the way forward.
144
+ // Three short labeled parts, not one paragraph. Every card ends the same way
145
+ // — an admin with cluster access, in Radar Cloud — and the exit says whether
146
+ // that is a deep-linked install command or Radar Cloud itself.
147
+ function blockedCopy(blocked: CloudInstallBlocked, exit: BlockedExit): { title: string; tried: ReactNode; why: ReactNode; next: ReactNode } {
148
+ const a = blocked.attempted
149
+ const release = a && (
150
+ <>
151
+ release <code className="font-mono text-[11px] text-theme-text-primary">{a.release}</code> in namespace{' '}
152
+ <code className="font-mono text-[11px] text-theme-text-primary">{a.namespace}</code>
153
+ </>
154
+ )
155
+ const operation = a?.mode === 'adopt' ? 'a Helm upgrade of your existing' : 'a fresh Helm install of'
156
+ // What Radar did is told by the stage it reached, not by the plan it had:
157
+ // a target can be known from discovery before any chart was rendered or
158
+ // dry-run, and the card must not claim work that did not happen.
159
+ const tried =
160
+ !a ? (
161
+ <>Radar was looking for an existing Radar install in this cluster, as your kubeconfig identity. Nothing was changed.</>
162
+ ) : a.mode === 'gitops' ? (
163
+ <>Radar found {release} and traced how it is managed before planning anything. Nothing was changed.</>
164
+ ) : a.stage === 'inspect' ? (
165
+ a.mode === 'adopt' ? (
166
+ <>
167
+ Radar found {release} and was reading its Helm state, as your kubeconfig identity, before planning anything.
168
+ Nothing was changed.
169
+ </>
170
+ ) : (
171
+ <>
172
+ Radar found no Radar running anywhere in this cluster and was checking Helm’s release records in namespace{' '}
173
+ <code className="font-mono text-[11px] text-theme-text-primary">{a.namespace}</code>, as your kubeconfig
174
+ identity, before planning anything. Nothing was changed.
175
+ </>
176
+ )
177
+ ) : a.stage === 'prepare' ? (
178
+ <>
179
+ Radar planned {operation} {release} with the Cloud connection enabled and was preparing the chart, as your
180
+ kubeconfig identity. Nothing was changed.
181
+ </>
182
+ ) : (
183
+ <>
184
+ Radar prepared {operation} {release} with the Cloud connection enabled, and dry-ran it against the cluster as
185
+ your kubeconfig identity. Nothing was changed.
186
+ </>
187
+ )
188
+
189
+ // Where the exit is an install command, Radar Cloud asks for a cluster name
190
+ // first, then shows the instructions — say so, rather than promising a
191
+ // command on the next screen.
192
+ const installNext = (
193
+ <>
194
+ Have an admin with cluster access get the install command from Radar Cloud: it asks for a cluster name, then
195
+ shows the Helm, Argo CD or Flux instructions to review before running.
196
+ </>
197
+ )
198
+ const genericNext = (
199
+ <>
200
+ Have an admin with cluster access resolve the issue above, then connect — or recover — this cluster from Radar
201
+ Cloud.
202
+ </>
203
+ )
204
+ const unknownNext = (
205
+ <>
206
+ Have an admin with cluster access connect it from Radar Cloud.{' '}
207
+ {a?.partialScan ? (
208
+ <>
209
+ Radar could only check namespace{' '}
210
+ <code className="font-mono text-[11px] text-theme-text-primary">{a.namespace}</code> for an existing install,
211
+ so they should check the rest of the cluster first.
212
+ </>
213
+ ) : (
214
+ <>Radar couldn’t tell whether it is already installed here, so they should check first.</>
215
+ )}
216
+ </>
217
+ )
218
+
219
+ if (blocked.reason === 'gitops') {
220
+ return {
221
+ title: 'This install is managed by GitOps',
222
+ tried,
223
+ why: blocked.message,
224
+ next: exit.install ? (
225
+ <>
226
+ Have an admin with repo access get the values patch from Radar Cloud: it asks for a cluster name, then shows
227
+ the patch for your controller and the one command that creates the token Secret.
228
+ </>
229
+ ) : (
230
+ genericNext
231
+ ),
232
+ }
233
+ }
234
+ if (blocked.reason === 'unsupported') {
235
+ return { title: 'Radar can’t connect this cluster from here', tried, why: blocked.message, next: genericNext }
236
+ }
237
+ // Fresh offered on "nothing running" alone: say what was not confirmed.
238
+ const unconfirmedNext = (
239
+ <>
240
+ Radar found no Radar running in this cluster but couldn’t read Helm’s release records, so an admin with cluster
241
+ access should confirm nothing is installed before running a fresh install. {installNext}
242
+ </>
243
+ )
244
+ const next = exit.install ? (a?.releaseUnread ? unconfirmedNext : installNext) : unknownNext
245
+ switch (blocked.cause) {
246
+ case 'permissions':
247
+ return {
248
+ title: 'Your Kubernetes identity can’t do this install',
249
+ tried,
250
+ why: 'Your credentials lack permissions this needs (below). Anyone with them can complete this exact install.',
251
+ next,
252
+ }
253
+ case 'verification':
254
+ return {
255
+ title: 'This version of Radar can’t install this chart version from here',
256
+ tried,
257
+ why: 'The chart renders something this Radar build can’t check before applying, so it won’t install it unseen. A limitation of this Radar, not of your cluster or your access.',
258
+ next,
259
+ }
260
+ default:
261
+ return {
262
+ title: 'The cluster blocked part of this install',
263
+ tried,
264
+ why: 'The cluster refused: something already there conflicts with the install, or a policy rejects it. More permission wouldn’t change that.',
265
+ next: exit.install ? <>After the refusals above are resolved: {installNext}</> : next,
266
+ }
267
+ }
268
+ }
269
+
133
270
  function PlanCard({
134
271
  status,
135
272
  onStatus,
@@ -348,7 +485,7 @@ function ConsentRow({
348
485
  checked: boolean
349
486
  onChange: (v: boolean) => void
350
487
  tone: 'emerald' | 'amber'
351
- children: React.ReactNode
488
+ children: ReactNode
352
489
  }) {
353
490
  return (
354
491
  <label className="mt-3 flex items-start gap-2 cursor-pointer">