@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,486 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { resourcePath } from "../../utils/navigation";
4
+ import { Info } from "lucide-react";
5
+ import { SEVERITY_TEXT } from "@skyhook-io/k8s-ui/utils/badge-colors";
6
+ import {
7
+ AreaChart,
8
+ SeriesLegend,
9
+ formatMetricValue,
10
+ type ReferenceLine,
11
+ } from "@skyhook-io/k8s-ui/components/charts";
12
+ import { type PrometheusTimeRange } from "../../api/client";
13
+ import {
14
+ useWorkloadMetrics,
15
+ type WorkloadMetricPanel,
16
+ type WorkloadRequestSource,
17
+ type WorkloadMetrics,
18
+ } from "../../api/workloadMetrics";
19
+ import { latestWorkloadValue, workloadPodValues } from "./workloadMetricValues";
20
+ import { WorkloadMetricsHelpDialog } from './WorkloadMetricsHelpDialog';
21
+
22
+ interface Props {
23
+ kind: string;
24
+ namespace: string;
25
+ name: string;
26
+ range: PrometheusTimeRange;
27
+ cpuReferenceLines?: ReferenceLine[];
28
+ memoryReferenceLines?: ReferenceLine[];
29
+ restartLane?: ReactNode;
30
+ nameMatchedCharts?: Partial<Record<'cpu' | 'memory', ReactNode>>;
31
+ controls?: ReactNode;
32
+ }
33
+
34
+ export function WorkloadMetricsSection(props: Props) {
35
+ const [source, setSource] = useState<WorkloadRequestSource | "">("");
36
+ const [helpOpen, setHelpOpen] = useState(false);
37
+ const { data, isLoading, isPlaceholderData, error } = useWorkloadMetrics(
38
+ props.kind,
39
+ props.namespace,
40
+ props.name,
41
+ props.range,
42
+ source,
43
+ true,
44
+ );
45
+ const sameResourceScope = data && ['memory', 'throttling'].every((key) => {
46
+ const scope = data.history[key as 'memory' | 'throttling'];
47
+ return !scope || (scope.mode === data.history.cpu?.mode && scope.reason === data.history.cpu?.reason);
48
+ });
49
+ const nameMatchedFallbacks = (['cpu', 'memory'] as const).filter((key) => {
50
+ const panel = data?.panels[key];
51
+ return (data || error) && props.nameMatchedCharts?.[key] && (!panel || ['detecting', 'error', 'unavailable'].includes(panel.state));
52
+ });
53
+ const resourceNotice = sameResourceScope ? sharedPanelNotice([data?.panels.cpu, data?.panels.memory, data?.panels.throttling]) : undefined;
54
+ const hasHistoricalTotals = (['cpu', 'memory'] as const).some((key) => data?.history[key]?.mode === 'workload-history');
55
+ const hasResourceDetails = hasHistoricalTotals || data?.history.throttling?.mode === 'workload-history' || resourceNotice?.state === 'stale';
56
+ return (
57
+ <div className="metrics-layout min-w-0 px-4 pt-3 space-y-4">
58
+ <div className="flex items-start justify-between gap-3">
59
+ <button type="button" aria-haspopup="dialog" onClick={() => setHelpOpen(true)} className="min-w-0 rounded py-1 text-left text-xs text-theme-text-secondary hover:text-accent-text focus-visible:outline-accent">
60
+ <Info aria-hidden="true" className="mr-1 inline h-3.5 w-3.5" />About these metrics{data?.attribution?.scope && ' · operator-asserted scope'}
61
+ </button>
62
+ {props.controls}
63
+ </div>
64
+ {helpOpen && <WorkloadMetricsHelpDialog kind={props.kind} namespace={props.namespace} name={props.name} range={props.range} data={data} pending={isLoading || isPlaceholderData} error={error} onClose={() => setHelpOpen(false)} />}
65
+ {data?.scopeNotice && <p role="status" className={`text-xs ${SEVERITY_TEXT.warning}`}>{data.scopeNotice}</p>}
66
+ {data?.state === 'partial' && data.reason && <p role="status" className={`text-xs ${SEVERITY_TEXT.warning}`}>{data.reason}</p>}
67
+ {isPlaceholderData && <p role="status" className="text-xs text-theme-text-secondary">Loading selected request source… Resources show previous samples.</p>}
68
+ <WorkloadRequests data={data} isLoading={isLoading || isPlaceholderData} error={error} setSource={setSource} />
69
+ <section aria-label="Workload resource pressure">
70
+ {(data?.panels.cpu || data?.panels.memory || data?.panels.throttling) && <div className="mb-2 flex flex-wrap items-baseline gap-x-3 gap-y-1">
71
+ <h3 className="text-sm font-semibold text-theme-text-primary">Resources</h3>
72
+ {sameResourceScope && <HistoryNotice scope={data?.history.cpu} omitReason={data?.panels.cpu?.reason} />}
73
+ </div>}
74
+ {props.restartLane}
75
+ {resourceNotice && <div className="mb-2"><MetricNotice panel={resourceNotice} label="CPU, memory and throttling" /></div>}
76
+ <div className="metrics-chart-grid">
77
+ {data?.panels.cpu && <WorkloadChart label="CPU usage" population={data.history.cpu?.mode === 'workload-history' ? 'workload' : 'per Pod'} panel={data.panels.cpu} window={data} scope={sameResourceScope ? undefined : data.history.cpu} referenceLines={data.history.cpu?.mode === 'current-pods' ? props.cpuReferenceLines : undefined} sharedNotice={!!resourceNotice} />}
78
+ {data?.panels.memory && <WorkloadChart label="Memory working set" population={data.history.memory?.mode === 'workload-history' ? 'workload' : 'per Pod'} panel={data.panels.memory} window={data} scope={sameResourceScope ? undefined : data.history.memory} referenceLines={data.history.memory?.mode === 'current-pods' ? props.memoryReferenceLines : undefined} sharedNotice={!!resourceNotice} />}
79
+ {data?.panels.throttling && <WorkloadChart
80
+ className="metrics-chart-wide"
81
+ label="CPU throttled periods"
82
+ panel={data.panels.throttling}
83
+ window={data}
84
+ scope={sameResourceScope ? undefined : data.history.throttling}
85
+ footnote="% of CFS periods throttled, not CPU time lost."
86
+ sharedNotice={!!resourceNotice}
87
+ />}
88
+ </div>
89
+ {(data?.panels.cpu || data?.panels.memory || data?.panels.throttling) && <p className="mt-2 text-xs text-theme-text-tertiary">Resource totals include reporting containers and sidecars.</p>}
90
+ {data && hasResourceDetails && <details className="mt-2 text-xs text-theme-text-tertiary">
91
+ <summary className="cursor-pointer">About resource metrics</summary>
92
+ <div className="mt-2 max-w-2xl space-y-2 text-sm leading-relaxed text-theme-text-secondary">
93
+ {hasHistoricalTotals && <p>In workload-history CPU and memory charts, Workload is the total across reporting Pods; Maximum Pod exposes skew.</p>}
94
+ {data.history.throttling?.mode === 'workload-history' && <p>Throttling is weighted by total periods, not an average of Pod percentages.</p>}
95
+ {resourceNotice?.state === 'stale' && <ul className="space-y-1">{(['cpu', 'memory', 'throttling'] as const).map((key) => data.panels[key]?.reason && <li key={key}>{key}: {data.panels[key]!.reason}</li>)}</ul>}
96
+ </div>
97
+ </details>}
98
+ </section>
99
+ {nameMatchedFallbacks.length > 0 && <details aria-label="Name-matched resource metrics">
100
+ <summary className="mb-2 cursor-pointer text-xs text-theme-text-secondary">Basic {nameMatchedFallbacks.map((key) => key === 'cpu' ? 'CPU' : 'memory').join(' / ')} · identity unverified</summary>
101
+ <p className={`mb-2 max-w-2xl text-sm ${SEVERITY_TEXT.warning}`}>These charts match current Pod names, not Pod UIDs or historical workload ownership; matching names in a shared backend may include another cluster.</p>
102
+ <div className="metrics-chart-grid">{nameMatchedFallbacks.map((key) => <div key={key}>{props.nameMatchedCharts?.[key]}</div>)}</div>
103
+ </details>}
104
+ {data && (data.comparison.cpu || data.comparison.memory || data.comparison.throttling) && <PodComparison
105
+ namespace={props.namespace}
106
+ cpu={data.comparison.cpu}
107
+ memory={data.comparison.memory}
108
+ throttle={data.comparison.throttling}
109
+ window={data}
110
+ cpuReferenceLines={props.cpuReferenceLines}
111
+ memoryReferenceLines={props.memoryReferenceLines}
112
+ />}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ function HistoryNotice({ scope, omitReason }: { scope?: WorkloadMetrics['history']['cpu']; omitReason?: string }) {
118
+ if (!scope) return null;
119
+ return <details className={`text-xs ${scope.mode === 'workload-history' ? 'text-theme-text-tertiary' : SEVERITY_TEXT.warning}`}>
120
+ <summary className="cursor-pointer">{scope.mode === 'workload-history' ? 'Workload history' : scope.mode === 'current-pods' ? 'Current Pods only' : 'Workload history unavailable'}</summary>
121
+ <div className="my-2 max-w-2xl space-y-2 text-sm leading-relaxed text-theme-text-secondary">
122
+ <p>{scope.mode === 'workload-history' ? 'Includes previous replicas where ownership and metrics were retained. Missing history produces gaps, not current-Pod substitutes.' : scope.mode === 'current-pods' ? 'Previous replicas are not reconstructed.' : 'Workload history could not be checked.'}</p>
123
+ {scope.reason && scope.reason !== omitReason && <p>{scope.reason}</p>}
124
+ </div>
125
+ </details>;
126
+ }
127
+
128
+ function WorkloadRequests({ data, isLoading, error, setSource }: {
129
+ data?: WorkloadMetrics;
130
+ isLoading: boolean;
131
+ error: Error | null;
132
+ setSource: (source: WorkloadRequestSource) => void;
133
+ }) {
134
+ if (isLoading)
135
+ return (
136
+ <p className="text-xs text-theme-text-tertiary">
137
+ Checking request metrics and resource pressure…
138
+ </p>
139
+ );
140
+ if (error)
141
+ return (
142
+ <p role="status" className={`text-sm ${SEVERITY_TEXT.error}`}>
143
+ Workload metrics could not be loaded: {error.message}
144
+ </p>
145
+ );
146
+ if (!data) return null;
147
+ if (data.state === "detecting" || (data.state === "error" && Object.keys(data.panels).length === 0))
148
+ return <p role="status" className={`text-xs ${data.state === 'error' ? SEVERITY_TEXT.error : 'text-theme-text-secondary'}`}>{data.reason}</p>;
149
+ if (data.state === "unavailable")
150
+ return (
151
+ <div className="text-xs text-theme-text-secondary">
152
+ <p>{data.reason}</p>
153
+ </div>
154
+ );
155
+ const sources = data.sources.filter(
156
+ (s) =>
157
+ s.state === "available" || s.state === "stale" || s.id === data.source,
158
+ );
159
+ const requestPanel = data.panels.requests;
160
+ const unmatchedSources = !data.source && requestPanel?.state === 'unavailable'
161
+ ? data.sources.filter((candidate) => data.attribution?.[candidate.id]) : [];
162
+ const hasRequests =
163
+ requestPanel &&
164
+ requestPanel.series.some((s) => s.dataPoints.some((p) => p.value != null));
165
+ const requestNotice = sharedPanelNotice([data.panels.requests, data.panels.errors, data.panels.p50, data.panels.p95]);
166
+ const observed = data.panels.observedPods?.series[0];
167
+ const reportingPods = observed && latestWorkloadValue(observed, data.end, data.stepSeconds);
168
+ return (
169
+ <div className="space-y-4 min-w-0 break-words">
170
+ <section aria-label="Workload requests">
171
+ <div className="flex flex-wrap items-center justify-between gap-2 mb-2">
172
+ <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"><h3 className="text-sm font-semibold text-theme-text-primary">
173
+ Requests
174
+ </h3>
175
+ {hasRequests ? <HistoryNotice scope={data.history.requests} /> : <details className={`text-xs ${requestPanel?.state === 'error' ? SEVERITY_TEXT.error : requestPanel?.state === 'partial' ? SEVERITY_TEXT.warning : 'text-theme-text-secondary'}`}>
176
+ <summary className="cursor-pointer">{requestPanel?.state === 'error' ? 'Request metrics query failed' : requestPanel?.state === 'partial' ? 'Request metrics withheld' : requestPanel?.state === 'detecting' ? 'Checking request metrics…' : 'No usable HTTP metrics in this window'}</summary>
177
+ {unmatchedSources.length > 0 ? <ul className="mt-2 max-w-2xl space-y-1 text-sm leading-relaxed text-theme-text-secondary">
178
+ {unmatchedSources.map((candidate) => <li key={candidate.id}><span className="font-medium">{candidate.label}:</span> {data.attribution?.[candidate.id]}</li>)}
179
+ </ul> : <p className="mt-2 max-w-2xl text-sm leading-relaxed text-theme-text-secondary">{requestPanel?.reason || 'No usable HTTP request samples were returned for this workload.'}</p>}
180
+ </details>}
181
+ </div>
182
+ {sources.length > 1 ? (
183
+ <select
184
+ aria-label="Request metrics source"
185
+ className="rounded-md border border-theme-border bg-theme-elevated px-2 py-1 text-xs text-theme-text-secondary"
186
+ value={data.source}
187
+ onChange={(e) =>
188
+ setSource(e.target.value as WorkloadRequestSource)
189
+ }
190
+ >
191
+ {sources.map((s) => (
192
+ <option key={s.id} value={s.id}>
193
+ {s.label}
194
+ {s.state === "unavailable" || s.state === "error"
195
+ ? " (unavailable)"
196
+ : ""}
197
+ </option>
198
+ ))}
199
+ </select>
200
+ ) : (
201
+ <span className="text-xs text-theme-text-tertiary">
202
+ {data.sources.find((s) => s.id === data.source)?.label}
203
+ </span>
204
+ )}
205
+ </div>
206
+ {hasRequests ? (
207
+ <>
208
+ {requestNotice && <div className="mb-2"><MetricNotice panel={requestNotice} label="Requests, HTTP 5xx and latency" /></div>}
209
+ <div className="metrics-chart-grid">
210
+ <WorkloadChart
211
+ label="Requests / sec"
212
+ panel={requestPanel}
213
+ window={data}
214
+ sharedNotice={!!requestNotice}
215
+ footnote="Ports combined · includes health checks and admin traffic."
216
+ />
217
+ <WorkloadChart
218
+ label="HTTP 5xx"
219
+ panel={data.panels.errors}
220
+ window={data}
221
+ sharedNotice={!!requestNotice}
222
+ footnote="Not a gRPC error rate · excludes connection failures."
223
+ />
224
+ <WorkloadChart
225
+ className="metrics-chart-wide"
226
+ label="Latency · p50 / p95"
227
+ panel={data.panels.p95}
228
+ secondary={data.panels.p50}
229
+ window={data}
230
+ sharedNotice={!!requestNotice}
231
+ footnote="At the selected observer · not end-to-end latency."
232
+ />
233
+ </div>
234
+ <div className="mt-2 text-xs text-theme-text-tertiary space-y-2">
235
+ <p>{Math.round(data.rateWindowSeconds / 60)}-minute rates{reportingPods != null && <> · {data.history.requests?.mode === 'workload-history' ? `${reportingPods} Pods reporting` : `${reportingPods} of ${data.podsTotal} current Pods reporting`}</>}</p>
236
+ <details>
237
+ <summary className="cursor-pointer">Coverage details</summary>
238
+ <div className="mt-2 max-w-2xl space-y-2 text-sm leading-relaxed text-theme-text-secondary">
239
+ {reportingPods == null && <p>Reporting Pod count unavailable.</p>}
240
+ <p>
241
+ {data.history.requests?.mode === 'workload-history'
242
+ ? 'Queries follow retained ownership at each timestamp, not today’s replica list. Workload identity is cluster, namespace, kind and name, including recreation under that name. Missing ownership produces gaps; when ownership disappears after Pod deletion, that Pod leaves the aggregate even if its last rate window still has samples.'
243
+ : `Queries select ${data.pods} of ${data.podsTotal} current Pods; previous replicas are not reconstructed.`}
244
+ </p>
245
+ {data.source === 'istio' && <p>Istio histograms update separately and can briefly trail the request counter; latency uses the histogram observations.</p>}
246
+ <p>Rates use a {Math.round(data.rateWindowSeconds / 60)}-minute rolling window, evaluated every {Math.round(data.stepSeconds)} seconds. Longer windows smooth short spikes.
247
+ </p>
248
+ {requestNotice?.state === 'stale' && <ul className="space-y-1">{(['requests', 'errors', 'p50', 'p95'] as const).map((key) => data.panels[key]?.reason && <li key={key}>{key}: {data.panels[key]!.reason}</li>)}</ul>}
249
+ </div>
250
+ </details>
251
+ </div>
252
+ </>
253
+ ) : null}
254
+ </section>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ function WorkloadChart({
260
+ className = "",
261
+ label,
262
+ population,
263
+ panel,
264
+ secondary,
265
+ window,
266
+ referenceLines,
267
+ scope,
268
+ footnote,
269
+ sharedNotice,
270
+ }: {
271
+ className?: string;
272
+ label: string;
273
+ population?: 'workload' | 'per Pod';
274
+ panel?: WorkloadMetricPanel;
275
+ secondary?: WorkloadMetricPanel;
276
+ window: Pick<WorkloadMetrics, "start" | "end" | "stepSeconds">;
277
+ referenceLines?: ReferenceLine[];
278
+ scope?: WorkloadMetrics['history']['cpu'];
279
+ footnote?: string;
280
+ sharedNotice?: boolean;
281
+ }) {
282
+ const series = secondary
283
+ ? [
284
+ ...secondary.series.map((s) => ({ ...s, labels: { quantile: "p50" } })),
285
+ ...(panel?.series ?? []).map((s) => ({
286
+ ...s,
287
+ labels: { quantile: "p95" },
288
+ })),
289
+ ]
290
+ : (panel?.series ?? []);
291
+ const hasSamples = series.some((s) =>
292
+ s.dataPoints.some((p) => p.value != null && Number.isFinite(p.value)),
293
+ );
294
+ const headline = panel?.series.find((s) => s.labels.aggregation === 'Workload') ?? (panel?.series.length === 1 ? panel.series[0] : undefined);
295
+ const seriesLabels = secondary ? series.map((s) => s.labels.quantile) : panel?.series.every((s) => s.labels.aggregation) ? panel.series.map((s) => s.labels.aggregation) : series.length === 1 && Object.keys(series[0].labels).length === 0 ? [label] : undefined;
296
+ const last =
297
+ headline
298
+ ? latestWorkloadValue(headline, window.end, window.stepSeconds)
299
+ : undefined;
300
+ return (
301
+ <section className={`metrics-chart flex flex-col rounded-lg border border-theme-border bg-theme-surface/30 p-3 min-w-0 ${className}`}>
302
+ <header className="flex items-baseline justify-between gap-2 mb-2">
303
+ <h4 className="text-xs font-medium text-theme-text-secondary">
304
+ {`${label}${hasSamples && population ? ` · ${population}` : ''}`}
305
+ </h4>
306
+ {last != null && (
307
+ <span className="text-sm font-semibold tabular-nums text-theme-text-primary">
308
+ {formatMetricValue(last, panel!.unit)}
309
+ {secondary && (
310
+ <span className="ml-1 text-xs text-theme-text-tertiary">p95</span>
311
+ )}
312
+ </span>
313
+ )}
314
+ </header>
315
+ <HistoryNotice scope={scope} omitReason={panel?.reason} />
316
+ {hasSamples ? (
317
+ <>
318
+ <AreaChart
319
+ series={series}
320
+ unit={panel!.unit}
321
+ color="var(--accent)"
322
+ fillColor="var(--accent-muted)"
323
+ layout="dashboard"
324
+ seriesLabels={seriesLabels}
325
+ domain={{ start: window.start, end: window.end }}
326
+ stepSeconds={window.stepSeconds}
327
+ referenceLines={referenceLines?.map((line) => ({ ...line, label: `Template ${line.label}` }))}
328
+ />
329
+ {series.length > 1 && (
330
+ <SeriesLegend series={series} color="var(--accent)" seriesLabels={seriesLabels} />
331
+ )}
332
+ </>
333
+ ) : (
334
+ <div className="py-8"><MetricNotice panel={panel} empty /></div>
335
+ )}
336
+ {hasSamples && !sharedNotice && <MetricNotice panel={panel} />}
337
+ {hasSamples && !!referenceLines?.length && <p className="mt-2 text-xs text-theme-text-tertiary">Template lines: current values per Pod, not historical.</p>}
338
+ {secondary && (secondary.state !== panel?.state || secondary.reason !== panel?.reason) && !sharedNotice && <MetricNotice panel={secondary} label="p50" />}
339
+ {hasSamples && footnote && <p className="mt-auto pt-2 text-xs text-theme-text-tertiary">{footnote}</p>}
340
+ </section>
341
+ );
342
+ }
343
+
344
+ function sharedPanelNotice(panels: (WorkloadMetricPanel | undefined)[]) {
345
+ const first = panels[0];
346
+ if (!first || (first.state !== 'stale' && (first.state !== 'partial' || !first.reason))) return undefined;
347
+ return panels.every((panel) => panel?.state === first.state
348
+ && (first.state === 'stale' || panel.reason === first.reason)
349
+ && panel.series.some((series) => series.dataPoints.some((point) => point.value != null && Number.isFinite(point.value)))) ? first : undefined;
350
+ }
351
+
352
+ function MetricNotice({ panel, empty, label }: { panel?: WorkloadMetricPanel; empty?: boolean; label?: string }) {
353
+ if (!empty && !panel?.reason && panel?.state !== 'stale') return null;
354
+ const tone = panel?.state === 'error' ? SEVERITY_TEXT.error : panel?.state === 'partial' || panel?.state === 'stale' ? SEVERITY_TEXT.warning : 'text-theme-text-secondary';
355
+ if (panel?.state === 'stale' && !empty) return <details className={`mt-2 text-xs ${tone}`}>
356
+ <summary className="cursor-pointer">{label && `${label}: `}Historical samples only · no recent samples</summary>
357
+ {panel.reason && <p className="mt-2 max-w-2xl text-sm leading-relaxed text-theme-text-secondary">{panel.reason}</p>}
358
+ </details>;
359
+ return <p role="status" className={`mt-2 text-xs ${tone}`}>{label && `${label}: `}{panel?.reason || 'No usable samples in this window.'}</p>;
360
+ }
361
+
362
+ function PodComparison({
363
+ namespace,
364
+ cpu,
365
+ memory,
366
+ throttle,
367
+ window,
368
+ cpuReferenceLines,
369
+ memoryReferenceLines,
370
+ }: {
371
+ namespace: string;
372
+ cpu?: WorkloadMetricPanel;
373
+ memory?: WorkloadMetricPanel;
374
+ throttle?: WorkloadMetricPanel;
375
+ window: Pick<WorkloadMetrics, "end" | "stepSeconds" | "pods" | "podsTotal">;
376
+ cpuReferenceLines?: ReferenceLine[];
377
+ memoryReferenceLines?: ReferenceLine[];
378
+ }) {
379
+ const [sort, setSort] = useState<"cpu" | "memory" | "throttling">("cpu");
380
+ const values = {
381
+ cpu: workloadPodValues(cpu?.series ?? [], window.end, window.stepSeconds),
382
+ memory: workloadPodValues(
383
+ memory?.series ?? [],
384
+ window.end,
385
+ window.stepSeconds,
386
+ ),
387
+ throttling: workloadPodValues(
388
+ throttle?.series ?? [],
389
+ window.end,
390
+ window.stepSeconds,
391
+ ),
392
+ };
393
+ const pods = [
394
+ ...new Set([
395
+ ...values.cpu.keys(),
396
+ ...values.memory.keys(),
397
+ ...values.throttling.keys(),
398
+ ]),
399
+ ].sort(
400
+ (a, b) =>
401
+ (values[sort].get(b) ?? -1) - (values[sort].get(a) ?? -1) ||
402
+ a.localeCompare(b),
403
+ );
404
+ const show = (pod: string, metric: typeof sort, unit: string) => {
405
+ const value = values[metric].get(pod);
406
+ return value == null ? "—" : formatMetricValue(value, unit);
407
+ };
408
+ const comparisonPanels = [{ label: 'CPU', panel: cpu }, { label: 'Memory', panel: memory }, { label: 'Throttling', panel: throttle }];
409
+ const pending = comparisonPanels.filter(({ panel }) => panel?.state === 'detecting');
410
+ const notices = new Map<string, { labels: string[]; panel: WorkloadMetricPanel }>();
411
+ for (const { label, panel } of comparisonPanels) {
412
+ if (!panel?.reason || panel.state === 'detecting') continue;
413
+ const key = `${panel.state}\0${panel.reason}`;
414
+ const notice = notices.get(key);
415
+ if (notice) notice.labels.push(label);
416
+ else notices.set(key, { labels: [label], panel });
417
+ }
418
+ const hasTemplate = !!cpuReferenceLines?.length || !!memoryReferenceLines?.length;
419
+ return (
420
+ <section className="rounded-lg border border-theme-border bg-theme-surface/30 p-3 min-w-0">
421
+ <h4 className="mb-2 text-xs font-medium text-theme-text-secondary">
422
+ Compare current Pods · latest samples
423
+ </h4>
424
+ {hasTemplate && <details className="mb-2 text-xs text-theme-text-tertiary">
425
+ <summary className="cursor-pointer">Template per Pod · actual Pods may differ</summary>
426
+ <p className="mt-2 max-w-2xl text-sm leading-relaxed text-theme-text-secondary">Actual Pods can differ after injection or rollout. Template values below are not measured allocations for each Pod.</p>
427
+ </details>}
428
+ {window.pods < window.podsTotal && <details className={`mb-2 text-xs ${SEVERITY_TEXT.warning}`}>
429
+ <summary className="cursor-pointer">Comparing {window.pods} of {window.podsTotal} current Pods</summary>
430
+ <p className="mt-2 text-sm text-theme-text-secondary">This comparison cap does not limit workload-history totals.</p>
431
+ </details>}
432
+ {pending.length > 0 && <p role="status" className="mb-2 text-xs text-theme-text-secondary">Matching {pending.map(({ label }) => label).join(' / ')} metrics to current Pods…</p>}
433
+ {Array.from(notices, ([key, { labels, panel }]) => <MetricNotice key={key} panel={panel} label={labels.join(' / ')} />)}
434
+ <div className="max-h-72 overflow-auto">
435
+ <table className="w-full min-w-[480px] text-xs text-left">
436
+ <thead className="text-theme-text-tertiary">
437
+ <tr>
438
+ <th className="py-2 font-medium">Pod</th>
439
+ {(["cpu", "memory", "throttling"] as const).map((metric) => (
440
+ <th
441
+ key={metric}
442
+ className="py-2 pl-3 text-right font-medium"
443
+ aria-sort={sort === metric ? "descending" : "none"}
444
+ >
445
+ <button onClick={() => setSort(metric)}>
446
+ {metric === "cpu"
447
+ ? "CPU"
448
+ : metric === "memory"
449
+ ? "Memory"
450
+ : "Throttled"}
451
+ {sort === metric ? " ↓" : ""}
452
+ </button>
453
+ {(metric === 'cpu' ? cpuReferenceLines : metric === 'memory' ? memoryReferenceLines : undefined)?.map((line) => <div key={line.kind} className="mt-1 font-normal text-theme-text-tertiary">Template {line.label}</div>)}
454
+ </th>
455
+ ))}
456
+ </tr>
457
+ </thead>
458
+ <tbody>
459
+ {pods.map((pod) => (
460
+ <tr
461
+ key={pod}
462
+ className="border-t border-theme-border text-theme-text-secondary"
463
+ >
464
+ <td className="py-2 break-all"><Link className="text-accent hover:underline" to={resourcePath({ kind: "Pod", namespace, name: pod })}>{pod}</Link></td>
465
+ <td className="pl-3 text-right tabular-nums">
466
+ {show(pod, "cpu", "cores")}
467
+ </td>
468
+ <td className="pl-3 text-right tabular-nums">
469
+ {show(pod, "memory", "bytes")}
470
+ </td>
471
+ <td className="pl-3 text-right tabular-nums">
472
+ {show(pod, "throttling", "percent")}
473
+ </td>
474
+ </tr>
475
+ ))}
476
+ </tbody>
477
+ </table>
478
+ {pods.length === 0 && pending.length === 0 && notices.size === 0 && (
479
+ <p className="py-6 text-theme-text-tertiary text-xs">
480
+ No Pod samples available.
481
+ </p>
482
+ )}
483
+ </div>
484
+ </section>
485
+ );
486
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { latestWorkloadValue, workloadPodValues } from "./workloadMetricValues";
3
+
4
+ describe("workload latest samples", () => {
5
+ it("keeps zero but does not walk back across a trailing gap", () => {
6
+ expect(
7
+ latestWorkloadValue(
8
+ { labels: {}, dataPoints: [{ timestamp: 100, value: 0 }] },
9
+ 100,
10
+ 60,
11
+ ),
12
+ ).toBe(0);
13
+ expect(
14
+ latestWorkloadValue(
15
+ {
16
+ labels: {},
17
+ dataPoints: [
18
+ { timestamp: 90, value: 5 },
19
+ { timestamp: 100, value: null },
20
+ ],
21
+ },
22
+ 100,
23
+ 60,
24
+ ),
25
+ ).toBeUndefined();
26
+ });
27
+ it("does not display historical samples as current", () => {
28
+ expect(
29
+ latestWorkloadValue(
30
+ { labels: {}, dataPoints: [{ timestamp: 100, value: 9 }] },
31
+ 1000,
32
+ 60,
33
+ ),
34
+ ).toBeUndefined();
35
+ });
36
+ it("preserves unavailable pods instead of silently dropping them", () => {
37
+ const values = workloadPodValues(
38
+ [
39
+ {
40
+ labels: { pod: "api-0" },
41
+ dataPoints: [{ timestamp: 100, value: null }],
42
+ },
43
+ ],
44
+ 100,
45
+ 60,
46
+ );
47
+ expect(values.has("api-0")).toBe(true);
48
+ expect(values.get("api-0")).toBeUndefined();
49
+ });
50
+ it("does not treat a large gap between samples as the evaluation interval", () => {
51
+ expect(
52
+ latestWorkloadValue(
53
+ {
54
+ labels: {},
55
+ dataPoints: [
56
+ { timestamp: 1, value: 1 },
57
+ { timestamp: 1000, value: 2 },
58
+ ],
59
+ },
60
+ 1500,
61
+ 60,
62
+ ),
63
+ ).toBeUndefined();
64
+ });
65
+ });
@@ -0,0 +1,30 @@
1
+ import type { TimeSeries } from "@skyhook-io/k8s-ui/components/charts";
2
+
3
+ export function latestWorkloadValue(
4
+ series: TimeSeries,
5
+ end: number,
6
+ stepSeconds: number,
7
+ ): number | undefined {
8
+ const points = series.dataPoints;
9
+ const last = points[points.length - 1];
10
+ if (
11
+ !last ||
12
+ last.value == null ||
13
+ !Number.isFinite(last.value) ||
14
+ end - last.timestamp > Math.max(90, 2 * stepSeconds)
15
+ )
16
+ return undefined;
17
+ return last.value;
18
+ }
19
+
20
+ export function workloadPodValues(
21
+ series: TimeSeries[],
22
+ end: number,
23
+ stepSeconds: number,
24
+ ): Map<string, number | undefined> {
25
+ return new Map(
26
+ series
27
+ .filter((s) => s.labels.pod)
28
+ .map((s) => [s.labels.pod, latestWorkloadValue(s, end, stepSeconds)]),
29
+ );
30
+ }