@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.
- package/package.json +8 -8
- package/src/App.tsx +60 -24
- package/src/RadarApp.tsx +5 -0
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +325 -16
- package/src/api/diagnose.ts +62 -15
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +179 -42
- package/src/components/CloudFunnelButton.tsx +107 -36
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +140 -0
- package/src/components/cloudConnectHandoff.ts +124 -0
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.tsx +2 -1
- package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
- package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
- package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
- package/src/components/diagnose/InvestigationView.tsx +222 -5
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
- package/src/components/diagnose/investigationEvidence/types.ts +512 -0
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +2 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +482 -0
- package/src/components/diagnose/parts.tsx +418 -41
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +94 -14
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +15 -2
- package/src/index.css +17 -0
- package/src/index.ts +1 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
- package/src/components/diagnose/investigationEvidence.ts +0 -4784
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- package/src/components/ui/ResourceBar.tsx +0 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { useMemo
|
|
2
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
import { Loader2, Wifi, WifiOff } from "lucide-react";
|
|
3
4
|
import {
|
|
4
5
|
AreaChart,
|
|
5
6
|
SeriesLegend,
|
|
6
7
|
computeSaturation,
|
|
8
|
+
describePodCoverage,
|
|
7
9
|
type ReferenceLine,
|
|
8
|
-
} from
|
|
9
|
-
import {
|
|
10
|
+
} from "@skyhook-io/k8s-ui/components/charts";
|
|
11
|
+
import {
|
|
12
|
+
SEVERITY_BADGE,
|
|
13
|
+
SEVERITY_TEXT,
|
|
14
|
+
type Severity,
|
|
15
|
+
} from "@skyhook-io/k8s-ui/utils/badge-colors";
|
|
10
16
|
import {
|
|
11
17
|
usePrometheusStatus,
|
|
12
18
|
usePrometheusConnect,
|
|
@@ -14,7 +20,7 @@ import {
|
|
|
14
20
|
useAutoPromConnect,
|
|
15
21
|
type PrometheusMetricCategory,
|
|
16
22
|
type PrometheusTimeRange,
|
|
17
|
-
} from
|
|
23
|
+
} from "../../api/client";
|
|
18
24
|
import {
|
|
19
25
|
MetricsSummary,
|
|
20
26
|
TIME_RANGES,
|
|
@@ -22,24 +28,34 @@ import {
|
|
|
22
28
|
NODE_CATEGORIES,
|
|
23
29
|
computeRequestLimitLines,
|
|
24
30
|
type CategoryDef,
|
|
25
|
-
} from
|
|
26
|
-
import { RestartEventLane } from
|
|
27
|
-
import { Tooltip } from
|
|
31
|
+
} from "./PrometheusCharts";
|
|
32
|
+
import { RestartEventLane } from "./RestartChart";
|
|
33
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
34
|
+
import { WorkloadMetricsSection } from "./WorkloadMetricsSection";
|
|
35
|
+
import { RightsizingStrip } from "./RightsizingStrip";
|
|
36
|
+
import { useNavCustomization } from "../../context/NavCustomization";
|
|
28
37
|
|
|
29
38
|
// Used when MetricsTabContent is in expanded (full-screen) mode. Drawer mode
|
|
30
39
|
// uses the single-chart tabbed `PrometheusCharts` instead — drawer width
|
|
31
40
|
// can't fit the grid cleanly.
|
|
32
41
|
export interface PrometheusChartsGridProps {
|
|
33
|
-
kind: string
|
|
34
|
-
namespace: string
|
|
35
|
-
name: string
|
|
42
|
+
kind: string;
|
|
43
|
+
namespace: string;
|
|
44
|
+
name: string;
|
|
36
45
|
/** Optional full K8s resource for request/limit overlay derivation. */
|
|
37
|
-
resource?: any
|
|
46
|
+
resource?: any;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
const SUPPORTED_KINDS = new Set([
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
"Pod",
|
|
51
|
+
"Deployment",
|
|
52
|
+
"StatefulSet",
|
|
53
|
+
"DaemonSet",
|
|
54
|
+
"ReplicaSet",
|
|
55
|
+
"Job",
|
|
56
|
+
"CronJob",
|
|
57
|
+
"Node",
|
|
58
|
+
]);
|
|
43
59
|
|
|
44
60
|
export function PrometheusChartsGrid({
|
|
45
61
|
kind,
|
|
@@ -47,29 +63,34 @@ export function PrometheusChartsGrid({
|
|
|
47
63
|
name,
|
|
48
64
|
resource,
|
|
49
65
|
}: PrometheusChartsGridProps) {
|
|
50
|
-
useAutoPromConnect()
|
|
51
|
-
const { data: status, isLoading: statusLoading } = usePrometheusStatus()
|
|
52
|
-
const connectMutation = usePrometheusConnect()
|
|
53
|
-
const isConnected = status?.connected === true
|
|
54
|
-
const isSupported = SUPPORTED_KINDS.has(kind)
|
|
55
|
-
const
|
|
66
|
+
useAutoPromConnect();
|
|
67
|
+
const { data: status, isLoading: statusLoading } = usePrometheusStatus();
|
|
68
|
+
const connectMutation = usePrometheusConnect();
|
|
69
|
+
const isConnected = status?.connected === true;
|
|
70
|
+
const isSupported = SUPPORTED_KINDS.has(kind);
|
|
71
|
+
const isWorkload = ["Deployment", "StatefulSet", "DaemonSet"].includes(kind);
|
|
72
|
+
const showRestartLane = isSupported && kind !== "Node";
|
|
56
73
|
|
|
57
|
-
const
|
|
74
|
+
const settingsAvailable = !useNavCustomization().embedded;
|
|
75
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
76
|
+
const timeRange = TIME_RANGES.find((range) => range.value === searchParams.get("metricsRange"))?.value ?? "1h";
|
|
58
77
|
|
|
59
|
-
const categories = kind ===
|
|
78
|
+
const categories = kind === "Node" ? NODE_CATEGORIES : WORKLOAD_CATEGORIES;
|
|
60
79
|
|
|
61
80
|
// CPU + memory get reference-line overlays when a resource is provided.
|
|
62
81
|
// Computed once at the parent so each panel can stay otherwise generic.
|
|
63
82
|
const cpuRefLines = useMemo<ReferenceLine[] | undefined>(
|
|
64
|
-
() =>
|
|
83
|
+
() =>
|
|
84
|
+
resource ? computeRequestLimitLines(resource, kind, "cpu") : undefined,
|
|
65
85
|
[resource, kind],
|
|
66
|
-
)
|
|
86
|
+
);
|
|
67
87
|
const memRefLines = useMemo<ReferenceLine[] | undefined>(
|
|
68
|
-
() =>
|
|
88
|
+
() =>
|
|
89
|
+
resource ? computeRequestLimitLines(resource, kind, "memory") : undefined,
|
|
69
90
|
[resource, kind],
|
|
70
|
-
)
|
|
91
|
+
);
|
|
71
92
|
|
|
72
|
-
if (!isSupported) return null
|
|
93
|
+
if (!isSupported) return null;
|
|
73
94
|
|
|
74
95
|
if (statusLoading) {
|
|
75
96
|
return (
|
|
@@ -77,7 +98,16 @@ export function PrometheusChartsGrid({
|
|
|
77
98
|
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
78
99
|
Checking Prometheus availability...
|
|
79
100
|
</div>
|
|
80
|
-
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!isConnected && status?.discovering) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex items-center justify-center py-12 text-theme-text-tertiary">
|
|
107
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
108
|
+
Discovering Prometheus…
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
if (!isConnected) {
|
|
@@ -85,10 +115,14 @@ export function PrometheusChartsGrid({
|
|
|
85
115
|
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
|
86
116
|
<WifiOff className="w-10 h-10 text-theme-text-quaternary" />
|
|
87
117
|
<div className="text-center">
|
|
88
|
-
<p className="text-sm text-theme-text-secondary mb-1">
|
|
118
|
+
<p className="text-sm text-theme-text-secondary mb-1">
|
|
119
|
+
Prometheus not connected
|
|
120
|
+
</p>
|
|
89
121
|
<p className="text-xs text-theme-text-tertiary mb-4">
|
|
90
|
-
{status?.error ||
|
|
122
|
+
{status?.error ||
|
|
123
|
+
"Connect to view historical CPU, memory, and network metrics"}
|
|
91
124
|
</p>
|
|
125
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
92
126
|
<button
|
|
93
127
|
onClick={() => connectMutation.mutate()}
|
|
94
128
|
disabled={connectMutation.isPending}
|
|
@@ -101,107 +135,193 @@ export function PrometheusChartsGrid({
|
|
|
101
135
|
)}
|
|
102
136
|
Discover Prometheus
|
|
103
137
|
</button>
|
|
138
|
+
{settingsAvailable && <button
|
|
139
|
+
type="button"
|
|
140
|
+
className="text-sm text-accent hover:underline"
|
|
141
|
+
onClick={() => window.dispatchEvent(new CustomEvent('radar:open-settings', { detail: { section: 'prometheus' } }))}
|
|
142
|
+
>Configure metrics</button>}
|
|
143
|
+
</div>
|
|
144
|
+
{!settingsAvailable && <p className="mt-3 text-xs text-theme-text-tertiary">Ask your operator to configure the metrics connection for this cluster.</p>}
|
|
145
|
+
{isWorkload && <a className="mt-3 inline-block text-xs text-accent hover:underline" href="https://github.com/skyhook-io/radar/blob/main/docs/workload-metrics.md#what-each-chart-needs" target="_blank" rel="noopener noreferrer">What each chart needs</a>}
|
|
104
146
|
</div>
|
|
105
147
|
</div>
|
|
106
|
-
)
|
|
148
|
+
);
|
|
107
149
|
}
|
|
108
150
|
|
|
109
|
-
const findCategory = (
|
|
110
|
-
|
|
151
|
+
const findCategory = (
|
|
152
|
+
key: PrometheusMetricCategory,
|
|
153
|
+
): CategoryDef | undefined => categories.find((c) => c.key === key);
|
|
111
154
|
|
|
112
|
-
const primaryCats: { def: CategoryDef; refLines?: ReferenceLine[] }[] = []
|
|
113
|
-
const cpu = findCategory(
|
|
114
|
-
if (cpu) primaryCats.push({ def: cpu, refLines: cpuRefLines })
|
|
115
|
-
const mem = findCategory(
|
|
116
|
-
if (mem) primaryCats.push({ def: mem, refLines: memRefLines })
|
|
117
|
-
if (kind !==
|
|
118
|
-
const rx = findCategory(
|
|
119
|
-
if (rx) primaryCats.push({ def: rx })
|
|
120
|
-
const tx = findCategory(
|
|
121
|
-
if (tx) primaryCats.push({ def: tx })
|
|
155
|
+
const primaryCats: { def: CategoryDef; refLines?: ReferenceLine[] }[] = [];
|
|
156
|
+
const cpu = findCategory("cpu");
|
|
157
|
+
if (cpu) primaryCats.push({ def: cpu, refLines: cpuRefLines });
|
|
158
|
+
const mem = findCategory("memory");
|
|
159
|
+
if (mem) primaryCats.push({ def: mem, refLines: memRefLines });
|
|
160
|
+
if (kind !== "Node") {
|
|
161
|
+
const rx = findCategory("network_rx");
|
|
162
|
+
if (rx) primaryCats.push({ def: rx });
|
|
163
|
+
const tx = findCategory("network_tx");
|
|
164
|
+
if (tx) primaryCats.push({ def: tx });
|
|
122
165
|
}
|
|
123
|
-
const disk = findCategory(
|
|
124
|
-
const chartPanels = disk ? [...primaryCats, { def: disk }] : primaryCats
|
|
166
|
+
const disk = findCategory("filesystem");
|
|
167
|
+
const chartPanels = disk ? [...primaryCats, { def: disk }] : primaryCats;
|
|
168
|
+
const renderPanel = ({ def, refLines }: { def: CategoryDef; refLines?: ReferenceLine[] }) => (
|
|
169
|
+
<MetricsPanel key={def.key} category={def} kind={kind} namespace={namespace} name={name} timeRange={timeRange} referenceLines={refLines} />
|
|
170
|
+
);
|
|
125
171
|
|
|
126
|
-
|
|
127
|
-
<div className="flex flex-col h-full overflow-auto">
|
|
128
|
-
<div className="flex shrink-0 justify-end px-4 pt-3">
|
|
129
|
-
<select
|
|
172
|
+
const rangeControl = <select
|
|
130
173
|
aria-label="Metrics time range"
|
|
131
174
|
value={timeRange}
|
|
132
|
-
onChange={e =>
|
|
133
|
-
|
|
175
|
+
onChange={(e) => {
|
|
176
|
+
const next = new URLSearchParams(searchParams);
|
|
177
|
+
next.set("metricsRange", e.target.value);
|
|
178
|
+
setSearchParams(next, { replace: true });
|
|
179
|
+
}}
|
|
180
|
+
className="ml-auto rounded-md border border-theme-border bg-theme-elevated px-2 py-1 text-xs text-theme-text-secondary shadow-theme-sm focus:outline-none focus:ring-1 focus:ring-accent/50"
|
|
134
181
|
>
|
|
135
|
-
{TIME_RANGES.map(tr => (
|
|
136
|
-
<option key={tr.value} value={tr.value}>
|
|
182
|
+
{TIME_RANGES.map((tr) => (
|
|
183
|
+
<option key={tr.value} value={tr.value}>
|
|
184
|
+
{tr.label}
|
|
185
|
+
</option>
|
|
137
186
|
))}
|
|
138
|
-
</select
|
|
139
|
-
|
|
187
|
+
</select>;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex flex-col min-w-0 w-full h-full overflow-auto">
|
|
191
|
+
{!isWorkload && <div className="flex shrink-0 items-center justify-between gap-3 px-4 pt-3">{rangeControl}</div>}
|
|
192
|
+
|
|
193
|
+
{isWorkload && (
|
|
194
|
+
<WorkloadMetricsSection key={`${kind}/${namespace}/${name}`} kind={kind} namespace={namespace} name={name} range={timeRange}
|
|
195
|
+
controls={rangeControl}
|
|
196
|
+
cpuReferenceLines={cpuRefLines} memoryReferenceLines={memRefLines}
|
|
197
|
+
nameMatchedCharts={{ cpu: cpu && renderPanel({ def: cpu }), memory: mem && renderPanel({ def: mem }) }}
|
|
198
|
+
restartLane={showRestartLane && <div className="mb-3"><RestartEventLane kind={kind} namespace={namespace} name={name} range={timeRange} /></div>} />
|
|
199
|
+
)}
|
|
140
200
|
|
|
141
201
|
{/* Restart lane sits above the grid so its markers visually align with
|
|
142
202
|
the time axis of the charts below. */}
|
|
143
|
-
{showRestartLane && (
|
|
203
|
+
{showRestartLane && !isWorkload && (
|
|
144
204
|
<div className="px-4 pt-3">
|
|
145
|
-
<RestartEventLane
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
|
|
149
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-4">
|
|
150
|
-
{chartPanels.map(({ def, refLines }) => (
|
|
151
|
-
<MetricsPanel
|
|
152
|
-
key={def.key}
|
|
153
|
-
category={def}
|
|
205
|
+
<RestartEventLane
|
|
154
206
|
kind={kind}
|
|
155
207
|
namespace={namespace}
|
|
156
208
|
name={name}
|
|
157
|
-
|
|
158
|
-
referenceLines={refLines}
|
|
209
|
+
range={timeRange}
|
|
159
210
|
/>
|
|
160
|
-
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
<div className="metrics-layout min-w-0 px-4 pt-4">
|
|
215
|
+
{isWorkload && <h3 className="mb-2 text-sm font-semibold text-theme-text-primary">Network and storage</h3>}
|
|
216
|
+
{isWorkload && <details className="mb-2 text-xs text-theme-text-secondary">
|
|
217
|
+
<summary className="cursor-pointer">Pod-name matched · identity unverified</summary>
|
|
218
|
+
<p className="mt-2 max-w-2xl text-sm leading-relaxed">These charts match current Pod names, independently of the identity-checked charts above. Matching names in a shared backend may include another cluster.</p>
|
|
219
|
+
</details>}
|
|
220
|
+
<div className="metrics-chart-grid">
|
|
221
|
+
{chartPanels.filter(({ def }) => !isWorkload || (def.key !== "cpu" && def.key !== "memory")).map(renderPanel)}
|
|
222
|
+
</div>
|
|
161
223
|
</div>
|
|
224
|
+
|
|
225
|
+
<PodCoverageNote
|
|
226
|
+
kind={kind}
|
|
227
|
+
namespace={namespace}
|
|
228
|
+
name={name}
|
|
229
|
+
timeRange={timeRange}
|
|
230
|
+
category={isWorkload ? "network_rx" : "cpu"}
|
|
231
|
+
/>
|
|
232
|
+
{["Deployment", "StatefulSet", "DaemonSet"].includes(kind) && (
|
|
233
|
+
<div className="px-4 pb-4"><RightsizingStrip kind={kind} namespace={namespace} name={name} /></div>
|
|
234
|
+
)}
|
|
162
235
|
</div>
|
|
163
|
-
)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function PodCoverageNote({
|
|
240
|
+
kind,
|
|
241
|
+
namespace,
|
|
242
|
+
name,
|
|
243
|
+
timeRange,
|
|
244
|
+
category,
|
|
245
|
+
}: {
|
|
246
|
+
kind: string;
|
|
247
|
+
namespace: string;
|
|
248
|
+
name: string;
|
|
249
|
+
timeRange: PrometheusTimeRange;
|
|
250
|
+
category: PrometheusMetricCategory;
|
|
251
|
+
}) {
|
|
252
|
+
const { data } = usePrometheusResourceMetrics(
|
|
253
|
+
kind,
|
|
254
|
+
namespace,
|
|
255
|
+
name,
|
|
256
|
+
category,
|
|
257
|
+
timeRange,
|
|
258
|
+
true,
|
|
259
|
+
);
|
|
260
|
+
const note = data ? describePodCoverage(data) : undefined;
|
|
261
|
+
if (!note) return null;
|
|
262
|
+
return <p className="px-4 pb-4 text-xs text-theme-text-tertiary">{note}</p>;
|
|
164
263
|
}
|
|
165
264
|
|
|
166
265
|
interface MetricsPanelProps {
|
|
167
|
-
category: CategoryDef
|
|
168
|
-
kind: string
|
|
169
|
-
namespace: string
|
|
170
|
-
name: string
|
|
171
|
-
timeRange: PrometheusTimeRange
|
|
172
|
-
referenceLines?: ReferenceLine[]
|
|
266
|
+
category: CategoryDef;
|
|
267
|
+
kind: string;
|
|
268
|
+
namespace: string;
|
|
269
|
+
name: string;
|
|
270
|
+
timeRange: PrometheusTimeRange;
|
|
271
|
+
referenceLines?: ReferenceLine[];
|
|
173
272
|
}
|
|
174
273
|
|
|
175
|
-
function MetricsPanel({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
274
|
+
function MetricsPanel({
|
|
275
|
+
category,
|
|
276
|
+
kind,
|
|
277
|
+
namespace,
|
|
278
|
+
name,
|
|
279
|
+
timeRange,
|
|
280
|
+
referenceLines,
|
|
281
|
+
}: MetricsPanelProps) {
|
|
282
|
+
const {
|
|
283
|
+
data: metrics,
|
|
284
|
+
isLoading,
|
|
285
|
+
error,
|
|
286
|
+
} = usePrometheusResourceMetrics(
|
|
287
|
+
kind,
|
|
288
|
+
namespace,
|
|
289
|
+
name,
|
|
290
|
+
category.key,
|
|
291
|
+
timeRange,
|
|
292
|
+
true,
|
|
293
|
+
);
|
|
179
294
|
|
|
180
|
-
const series = metrics?.result?.series
|
|
181
|
-
const hasData = (series?.length ?? 0) > 0
|
|
295
|
+
const series = metrics?.result?.series;
|
|
296
|
+
const hasData = (series?.length ?? 0) > 0;
|
|
182
297
|
|
|
183
298
|
// "% of limit / request" derived from current peak vs reference lines.
|
|
184
299
|
// Without this, a low-utilization workload with a high limit looks like
|
|
185
300
|
// an empty chart — the user can't tell healthy from starved at a glance.
|
|
186
|
-
const saturation =
|
|
187
|
-
|
|
188
|
-
|
|
301
|
+
const saturation =
|
|
302
|
+
hasData && series && referenceLines
|
|
303
|
+
? computeSaturation(series, referenceLines)
|
|
304
|
+
: undefined;
|
|
189
305
|
|
|
190
306
|
return (
|
|
191
|
-
<section className="rounded-lg border border-theme-border bg-theme-surface/30 p-3 flex flex-col min-
|
|
192
|
-
<header className="flex items-center justify-between mb-2 gap-3">
|
|
193
|
-
<div className="flex items-center gap-2">
|
|
307
|
+
<section className="metrics-chart rounded-lg border border-theme-border bg-theme-surface/30 p-3 flex flex-col min-w-0">
|
|
308
|
+
<header className="flex flex-wrap items-center justify-between mb-2 gap-3">
|
|
309
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
194
310
|
<h3 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wide">
|
|
195
311
|
{category.label}
|
|
196
312
|
</h3>
|
|
197
313
|
{saturation && <SaturationChip {...saturation} />}
|
|
198
314
|
</div>
|
|
199
315
|
{hasData && series && (
|
|
200
|
-
<MetricsSummary
|
|
316
|
+
<MetricsSummary
|
|
317
|
+
series={series}
|
|
318
|
+
category={category}
|
|
319
|
+
unit={metrics!.unit}
|
|
320
|
+
/>
|
|
201
321
|
)}
|
|
202
322
|
</header>
|
|
203
323
|
|
|
204
|
-
<div className="
|
|
324
|
+
<div className="min-w-0">
|
|
205
325
|
{isLoading ? (
|
|
206
326
|
<PanelLoading />
|
|
207
327
|
) : error ? (
|
|
@@ -214,6 +334,7 @@ function MetricsPanel({ category, kind, namespace, name, timeRange, referenceLin
|
|
|
214
334
|
fillColor={category.fillColor}
|
|
215
335
|
unit={metrics!.unit}
|
|
216
336
|
referenceLines={referenceLines}
|
|
337
|
+
layout="dashboard"
|
|
217
338
|
/>
|
|
218
339
|
{series.length > 1 && (
|
|
219
340
|
<div className="mt-1.5">
|
|
@@ -226,45 +347,66 @@ function MetricsPanel({ category, kind, namespace, name, timeRange, referenceLin
|
|
|
226
347
|
)}
|
|
227
348
|
</div>
|
|
228
349
|
</section>
|
|
229
|
-
)
|
|
350
|
+
);
|
|
230
351
|
}
|
|
231
352
|
|
|
232
|
-
function SaturationChip({
|
|
353
|
+
function SaturationChip({
|
|
354
|
+
ratio,
|
|
355
|
+
against,
|
|
356
|
+
}: {
|
|
357
|
+
ratio: number;
|
|
358
|
+
against: "limit" | "request";
|
|
359
|
+
}) {
|
|
233
360
|
// Thresholds match the rightsizing tone vocabulary: amber from 75% (start
|
|
234
361
|
// watching), red at 90% (the same OOM-risk boundary the backend uses for
|
|
235
362
|
// memory in classifyRightsizing).
|
|
236
|
-
const tone: Severity =
|
|
237
|
-
|
|
238
|
-
|
|
363
|
+
const tone: Severity =
|
|
364
|
+
ratio >= 0.9
|
|
365
|
+
? "error"
|
|
366
|
+
: ratio >= 0.75
|
|
367
|
+
? "warning"
|
|
368
|
+
: ratio < 0.05
|
|
369
|
+
? "info"
|
|
370
|
+
: "neutral";
|
|
371
|
+
const label = `${(ratio * 100).toFixed(ratio < 0.1 ? 1 : 0)}% of ${against}`;
|
|
372
|
+
return (
|
|
373
|
+
<span className={`badge badge-sm ${SEVERITY_BADGE[tone]}`}>{label}</span>
|
|
374
|
+
);
|
|
239
375
|
}
|
|
240
376
|
|
|
241
377
|
function PanelLoading() {
|
|
242
378
|
return (
|
|
243
|
-
<div className="flex items-center justify-center h-
|
|
379
|
+
<div className="flex items-center justify-center h-[240px] text-theme-text-tertiary text-xs">
|
|
244
380
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
245
381
|
Loading…
|
|
246
382
|
</div>
|
|
247
|
-
)
|
|
383
|
+
);
|
|
248
384
|
}
|
|
249
385
|
|
|
250
386
|
function PanelError({ message }: { message: string }) {
|
|
251
387
|
return (
|
|
252
|
-
<div
|
|
388
|
+
<div
|
|
389
|
+
className={`flex flex-col items-center justify-center h-[240px] ${SEVERITY_TEXT.warning} text-xs px-3 text-center`}
|
|
390
|
+
>
|
|
253
391
|
Query failed
|
|
254
392
|
<Tooltip content={message} wrapperClassName="!block w-full">
|
|
255
|
-
|
|
393
|
+
<span className="text-theme-text-quaternary mt-0.5 line-clamp-2">
|
|
394
|
+
{message}
|
|
395
|
+
</span>
|
|
256
396
|
</Tooltip>
|
|
257
397
|
</div>
|
|
258
|
-
)
|
|
398
|
+
);
|
|
259
399
|
}
|
|
260
400
|
|
|
261
401
|
function PanelNoData({ hint }: { hint?: string }) {
|
|
262
402
|
return (
|
|
263
|
-
<div className="flex flex-col items-center justify-center h-
|
|
403
|
+
<div className="flex flex-col items-center justify-center h-[240px] text-theme-text-tertiary text-xs px-3 text-center">
|
|
264
404
|
No data
|
|
265
|
-
{hint &&
|
|
405
|
+
{hint && (
|
|
406
|
+
<span className="text-theme-text-quaternary mt-1 max-w-xs">{hint}</span>
|
|
407
|
+
)}
|
|
266
408
|
</div>
|
|
267
|
-
)
|
|
409
|
+
);
|
|
268
410
|
}
|
|
269
411
|
|
|
270
|
-
export { isPrometheusSupported } from
|
|
412
|
+
export { isPrometheusSupported } from "./PrometheusCharts";
|
|
@@ -107,3 +107,41 @@ describe('rightsizing presentation', () => {
|
|
|
107
107
|
expect(RIGHTSIZING_DOCS_URL).toContain('/features/rightsizing')
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
describe('live pod inventory', () => {
|
|
112
|
+
it('qualifies a memory row when the pod list could not be read', () => {
|
|
113
|
+
const explanation = getRightsizingExplanation(
|
|
114
|
+
row({ resource: 'memory', liveInventoryUnavailable: true }),
|
|
115
|
+
)
|
|
116
|
+
expect(explanation).toContain('could not read')
|
|
117
|
+
expect(explanation).toContain('out-of-memory restarts')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('adds the caveat to a suppression reason instead of replacing it', () => {
|
|
121
|
+
const explanation = getRightsizingExplanation(
|
|
122
|
+
row({ resource: 'memory', recommendationReason: 'oom_evidence', liveInventoryUnavailable: true }),
|
|
123
|
+
)
|
|
124
|
+
expect(explanation).toContain('OOM evidence exists')
|
|
125
|
+
expect(explanation).toContain('could not read')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('keeps the caveat on a staged reduction, where an unchecked pod matters most', () => {
|
|
129
|
+
const explanation = getRightsizingExplanation(
|
|
130
|
+
row({
|
|
131
|
+
resource: 'memory',
|
|
132
|
+
liveInventoryUnavailable: true,
|
|
133
|
+
reductionLimited: true,
|
|
134
|
+
calculatedRequest: '256Mi',
|
|
135
|
+
recommendedRequest: '384Mi',
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
expect(explanation).toContain('conservative next step')
|
|
139
|
+
expect(explanation).toContain('could not read')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('says nothing when the pods were read', () => {
|
|
143
|
+
expect(getRightsizingExplanation(row({ resource: 'memory' })) ?? '').not.toContain(
|
|
144
|
+
'could not read',
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -328,16 +328,28 @@ export function getRightsizingExplanation(row: RightsizingRow): string | undefin
|
|
|
328
328
|
recommended_request_exceeds_limit:
|
|
329
329
|
'The evidence-based request would exceed the current limit; review the limit first.',
|
|
330
330
|
}
|
|
331
|
+
// A denied pod read leaves currentPodOOM unknown rather than false, so it
|
|
332
|
+
// qualifies whatever else is said instead of replacing it — including a
|
|
333
|
+
// staged reduction, which is exactly where an unchecked pod matters most.
|
|
334
|
+
const inventoryNote =
|
|
335
|
+
row.liveInventoryUnavailable && row.resource === 'memory'
|
|
336
|
+
? "Radar could not read this workload's pods, so it could not check them for out-of-memory restarts."
|
|
337
|
+
: undefined
|
|
338
|
+
const qualified = (base: string) => (inventoryNote ? `${base} ${inventoryNote}` : base)
|
|
331
339
|
if (row.reductionLimited && row.calculatedRequest && row.recommendedRequest) {
|
|
332
340
|
const burstNote = row.bursty && row.peak ? ` CPU P99 reached ${row.peak.formatted}.` : ''
|
|
333
|
-
return
|
|
341
|
+
return qualified(
|
|
342
|
+
`Demand-based target: ${row.calculatedRequest}. Radar suggests ${row.recommendedRequest} as a conservative next step; observe another full window before reducing further.${burstNote}`,
|
|
343
|
+
)
|
|
334
344
|
}
|
|
335
345
|
const message = row.queryError
|
|
336
346
|
? `Partial result: ${row.queryError}.`
|
|
337
347
|
: messages[row.recommendationReason ?? '']
|
|
338
348
|
const throttleUnavailable = row.resource === 'cpu' && !row.throttleAvailable
|
|
339
|
-
if (!message && !throttleUnavailable) return
|
|
340
|
-
return
|
|
349
|
+
if (!message && !throttleUnavailable) return inventoryNote
|
|
350
|
+
return qualified(
|
|
351
|
+
message ?? 'CPU throttling metrics are unavailable; no throttling conclusion was drawn.',
|
|
352
|
+
)
|
|
341
353
|
}
|
|
342
354
|
|
|
343
355
|
function confidenceLabel(row: RightsizingRow) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { WorkloadMetricsHelpContent } from './WorkloadMetricsHelpDialog'
|
|
4
|
+
import type { WorkloadMetrics } from '../../api/workloadMetrics'
|
|
5
|
+
|
|
6
|
+
const data = (attribution: Record<string, string>): WorkloadMetrics => ({
|
|
7
|
+
attribution, state: 'available', sources: [], history: {}, comparison: {}, panels: {},
|
|
8
|
+
pods: 2, podsTotal: 2, start: 100, end: 160, stepSeconds: 60, rateWindowSeconds: 300,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('Metrics help', () => {
|
|
12
|
+
it('groups only identical resource evidence, not HTTP observers or different methods', () => {
|
|
13
|
+
const html = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} data={data({
|
|
14
|
+
cpu: 'UID match', memory: 'UID match', throttling: 'Label match', beyla: 'UID match', istio: 'No observations',
|
|
15
|
+
})} />)
|
|
16
|
+
expect(html).toContain('CPU / Memory')
|
|
17
|
+
expect(html).not.toContain('CPU / Memory / Throttling')
|
|
18
|
+
expect(html).toContain('Beyla · HTTP requests')
|
|
19
|
+
expect(html).toContain('Istio · HTTP requests')
|
|
20
|
+
expect(html.match(/UID match/g)).toHaveLength(2)
|
|
21
|
+
expect(html).toContain('Label match')
|
|
22
|
+
expect(html).toContain('No observations')
|
|
23
|
+
})
|
|
24
|
+
it('separates asserted scope and history from current-Pod matching', () => {
|
|
25
|
+
const html = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} data={data({ scope: 'Operator assertion', history: 'Retained ownership' })} />)
|
|
26
|
+
expect(html).toContain('Operator-asserted scope')
|
|
27
|
+
expect(html).toContain('Historical ownership')
|
|
28
|
+
expect(html).not.toContain('<table')
|
|
29
|
+
expect(html).not.toContain('No current-Pod matching evidence')
|
|
30
|
+
expect(html).toContain('Each chart’s history label')
|
|
31
|
+
})
|
|
32
|
+
it('keeps operator overrides collapsed and explains their limits', () => {
|
|
33
|
+
const html = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} data={data({})} />)
|
|
34
|
+
expect(html).toContain('Troubleshooting identity matching')
|
|
35
|
+
expect(html).toContain('--prometheus-single-cluster')
|
|
36
|
+
expect(html).toContain('they do not add missing metrics')
|
|
37
|
+
expect(html).toContain('Leave both unset for automatic matching')
|
|
38
|
+
expect(html).toContain('it does not scope rightsizing')
|
|
39
|
+
expect(html).toContain('Standalone CLI:')
|
|
40
|
+
expect(html).toContain('when starting Radar')
|
|
41
|
+
expect(html).toContain('traffic.prometheusSingleCluster')
|
|
42
|
+
expect(html).toContain('traffic.prometheusClusterLabels')
|
|
43
|
+
expect(html).toContain('In-cluster OSS or Radar Cloud:')
|
|
44
|
+
expect(html).toContain('not viewer preferences')
|
|
45
|
+
expect(html).toContain('Desktop does not expose these overrides yet')
|
|
46
|
+
expect(html).toContain('only alongside a verified scope override')
|
|
47
|
+
expect(html).not.toMatch(/<details[^>]*\bopen[=> ]/)
|
|
48
|
+
})
|
|
49
|
+
it('distinguishes loading, failed refresh and absent evidence', () => {
|
|
50
|
+
const pending = renderToStaticMarkup(<WorkloadMetricsHelpContent pending />)
|
|
51
|
+
expect(pending).toContain('Checking the selected metrics')
|
|
52
|
+
expect(pending).not.toContain('No current-Pod matching evidence')
|
|
53
|
+
const failed = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} error={new Error('Query failed')} data={data({ cpu: 'Previous evidence' })} />)
|
|
54
|
+
expect(failed).toContain('Query failed')
|
|
55
|
+
expect(failed).toContain('Evidence below is from the previous response')
|
|
56
|
+
const empty = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} />)
|
|
57
|
+
expect(empty).toContain('No current-Pod matching evidence is available yet')
|
|
58
|
+
})
|
|
59
|
+
it('does not deny historical evidence when there are no current-Pod matches', () => {
|
|
60
|
+
const html = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} data={data({ history: 'Retained ownership is available' })} />)
|
|
61
|
+
expect(html).toContain('No current-Pod matching evidence')
|
|
62
|
+
expect(html).toContain('Retained ownership is available')
|
|
63
|
+
})
|
|
64
|
+
it.each(['detecting', 'error'] as const)('shows %s comparison independently of usable history', (state) => {
|
|
65
|
+
const response = data({ history: 'Historical ownership is available' })
|
|
66
|
+
response.reason = 'History follows retained workload ownership'
|
|
67
|
+
response.comparison = { cpu: { state, reason: 'Current identity status', unit: 'cores', series: [] } }
|
|
68
|
+
const html = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} data={response} />)
|
|
69
|
+
expect(html).toContain('Current identity status')
|
|
70
|
+
expect(html).toContain('Historical ownership is available')
|
|
71
|
+
expect(html).not.toContain(response.reason)
|
|
72
|
+
expect(html).not.toContain('No current-Pod matching evidence')
|
|
73
|
+
})
|
|
74
|
+
it('describes a first-load failure without implying previous evidence', () => {
|
|
75
|
+
const html = renderToStaticMarkup(<WorkloadMetricsHelpContent pending={false} error={new Error('Query failed')} />)
|
|
76
|
+
expect(html).toContain('could not be loaded')
|
|
77
|
+
expect(html).not.toContain('refreshed')
|
|
78
|
+
})
|
|
79
|
+
})
|