@skyhook-io/radar-app 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/App.tsx +125 -87
- package/src/api/actions.errorCode.test.ts +40 -0
- package/src/api/actions.test.ts +48 -0
- package/src/api/actions.ts +79 -0
- package/src/api/apiResources.navigation.test.tsx +232 -0
- package/src/api/apiResources.ts +46 -3
- package/src/api/client.helmSource.test.ts +39 -0
- package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
- package/src/api/client.namespaceSwitch.test.ts +107 -0
- package/src/api/client.ts +266 -39
- package/src/api/cnpg-ha.live.test.ts +140 -0
- package/src/api/cnpg-ha.test.ts +78 -0
- package/src/api/cnpg-ha.ts +19 -0
- package/src/api/cnpg-history.ts +123 -0
- package/src/api/cnpg-inspect.ts +102 -0
- package/src/api/cnpg-protection.ts +112 -0
- package/src/api/cnpg-recovery.test.tsx +60 -0
- package/src/api/cnpg-recovery.ts +262 -0
- package/src/api/cnpg-sessions.ts +175 -0
- package/src/api/cnpg-storage.ts +138 -0
- package/src/api/cnpg.legacy-reads.test.ts +43 -0
- package/src/api/cnpg.pooler-runtime.test.ts +15 -0
- package/src/api/cnpg.ts +428 -10
- package/src/api/radarFeatures.render.test.tsx +47 -3
- package/src/api/radarFeatures.ts +4 -1
- package/src/api/reviewedActionMutation.test.tsx +164 -0
- package/src/api/reviewedActionMutation.ts +66 -0
- package/src/api/timelineSource.test.ts +9 -0
- package/src/api/timelineSource.ts +8 -0
- package/src/components/audit/AuditView.tsx +9 -0
- package/src/components/capacity/CapacityActivity.tsx +3 -31
- package/src/components/capacity/CapacityDemand.tsx +4 -36
- package/src/components/capacity/CapacityOverview.tsx +217 -56
- package/src/components/capacity/CapacityPoolDetail.tsx +6 -55
- package/src/components/capacity/CapacityView.test.tsx +323 -0
- package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
- package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
- package/src/components/capacity/routes.ts +42 -0
- package/src/components/capacity/shared.tsx +11 -129
- package/src/components/capacity/workspaceRoute.ts +18 -0
- package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
- package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
- package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
- package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
- package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
- package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
- package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
- package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
- package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
- package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
- package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
- package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
- package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
- package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
- package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
- package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
- package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
- package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
- package/src/components/cnpg/CNPGOperator.tsx +78 -30
- package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
- package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
- package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
- package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
- package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
- package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
- package/src/components/cnpg/CNPGOverview.tsx +238 -63
- package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
- package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
- package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
- package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
- package/src/components/cnpg/CNPGPerformance.tsx +477 -0
- package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
- package/src/components/cnpg/CNPGPooling.tsx +97 -40
- package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
- package/src/components/cnpg/CNPGProtection.tsx +228 -122
- package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
- package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
- package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
- package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
- package/src/components/cnpg/CNPGStorage.tsx +619 -0
- package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
- package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
- package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
- package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
- package/src/components/cnpg/CNPGTrends.test.ts +38 -0
- package/src/components/cnpg/CNPGTrends.tsx +702 -0
- package/src/components/cnpg/CNPGView.tsx +34 -21
- package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
- package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
- package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
- package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
- package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
- package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
- package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
- package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
- package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
- package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
- package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
- package/src/components/cnpg/actions/actionModel.test.ts +111 -0
- package/src/components/cnpg/actions/actionModel.ts +127 -0
- package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
- package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
- package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
- package/src/components/cnpg/archivingRepair.test.ts +104 -0
- package/src/components/cnpg/archivingRepair.ts +145 -0
- package/src/components/cnpg/baseBackup.test.ts +36 -0
- package/src/components/cnpg/baseBackup.ts +36 -0
- package/src/components/cnpg/blocking.test.ts +59 -0
- package/src/components/cnpg/blocking.ts +131 -0
- package/src/components/cnpg/coverageText.test.ts +27 -0
- package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
- package/src/components/cnpg/creation/targetModel.test.ts +137 -0
- package/src/components/cnpg/creation/targetModel.ts +149 -0
- package/src/components/cnpg/fleetStatus.test.ts +46 -0
- package/src/components/cnpg/fleetStatus.ts +43 -0
- package/src/components/cnpg/formFields.ts +2 -0
- package/src/components/cnpg/grantText.test.tsx +11 -0
- package/src/components/cnpg/host.test.tsx +24 -0
- package/src/components/cnpg/host.tsx +101 -0
- package/src/components/cnpg/inspectModel.test.ts +120 -0
- package/src/components/cnpg/inspectModel.ts +140 -0
- package/src/components/cnpg/logicalSlots.test.ts +30 -0
- package/src/components/cnpg/logicalSlots.ts +29 -0
- package/src/components/cnpg/lsn.test.ts +40 -0
- package/src/components/cnpg/lsn.ts +43 -0
- package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
- package/src/components/cnpg/operations/model.test.ts +510 -0
- package/src/components/cnpg/operations/model.ts +499 -0
- package/src/components/cnpg/operations/presentation.test.ts +80 -0
- package/src/components/cnpg/operations/presentation.ts +84 -0
- package/src/components/cnpg/operations/store.ts +98 -0
- package/src/components/cnpg/operatorStatus.test.ts +134 -0
- package/src/components/cnpg/operatorStatus.ts +194 -0
- package/src/components/cnpg/paths.test.ts +54 -0
- package/src/components/cnpg/paths.ts +47 -4
- package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
- package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
- package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
- package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
- package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
- package/src/components/cnpg/protection/scheduleModel.ts +16 -0
- package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
- package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
- package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
- package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
- package/src/components/cnpg/recovery/restoreModel.ts +685 -0
- package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
- package/src/components/cnpg/refresh.test.ts +38 -0
- package/src/components/cnpg/refresh.ts +17 -0
- package/src/components/cnpg/refreshNotice.test.tsx +29 -0
- package/src/components/cnpg/routes.test.ts +34 -3
- package/src/components/cnpg/routes.ts +60 -48
- package/src/components/cnpg/runtimeAssessment.ts +198 -0
- package/src/components/cnpg/runtimeModel.test.ts +175 -0
- package/src/components/cnpg/runtimeModel.ts +196 -0
- package/src/components/cnpg/runtimeParts.test.tsx +17 -0
- package/src/components/cnpg/runtimeParts.tsx +79 -0
- package/src/components/cnpg/shared.test.tsx +59 -0
- package/src/components/cnpg/shared.tsx +100 -182
- package/src/components/cnpg/storageModel.test.ts +107 -0
- package/src/components/cnpg/storageModel.ts +101 -0
- package/src/components/cnpg/trendSamples.test.ts +148 -0
- package/src/components/cnpg/trendSamples.ts +222 -0
- package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
- package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
- package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
- package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
- package/src/components/cnpg/useCNPGNavigate.ts +21 -0
- package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
- package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
- package/src/components/cnpg/workspaceRoute.ts +23 -0
- package/src/components/cost/ApplicationCostTab.test.ts +7 -0
- package/src/components/cost/ApplicationCostTab.tsx +7 -7
- package/src/components/cost/CostView.test.ts +2 -1
- package/src/components/cost/CostView.tsx +0 -2
- package/src/components/cost/WorkloadCostTab.tsx +2 -4
- package/src/components/cost/errors.ts +3 -1
- package/src/components/cost/source.test.ts +10 -0
- package/src/components/cost/source.ts +5 -1
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
- package/src/components/dock/BottomDock.tsx +2 -0
- package/src/components/dock/TerminalTab.test.tsx +17 -0
- package/src/components/dock/TerminalTab.tsx +7 -3
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
- package/src/components/helm/HelmCompareRoute.tsx +57 -26
- package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
- package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
- package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
- package/src/components/helm/ValuesViewer.test.tsx +301 -0
- package/src/components/helm/ValuesViewer.tsx +169 -78
- package/src/components/home/ActivitySummary.tsx +2 -1
- package/src/components/home/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- package/src/components/issues/IssuesPane.render.test.tsx +122 -0
- package/src/components/issues/IssuesPane.test.ts +65 -1
- package/src/components/issues/IssuesPane.tsx +188 -25
- package/src/components/issues/uncheckedSources.ts +21 -0
- package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
- package/src/components/resource/PVCUsageBar.test.tsx +24 -0
- package/src/components/resource/PVCUsageBar.tsx +15 -6
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resources/CompositeRenderer.tsx +4 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
- package/src/components/resources/ResourcesView.tsx +26 -12
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +28 -15
- package/src/components/timeline/TimelineList.tsx +22 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineView.tsx +18 -45
- package/src/components/timeline/ViewModeToggle.tsx +20 -0
- package/src/components/timeline/useStickyError.test.tsx +47 -0
- package/src/components/timeline/useStickyError.ts +15 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
- package/src/components/ui/Markdown.test.tsx +65 -0
- package/src/components/ui/Markdown.tsx +144 -127
- package/src/components/whats-new/releaseNotes.ts +63 -1
- package/src/components/workload/WorkloadView.test.ts +10 -1
- package/src/components/workload/WorkloadView.tsx +27 -239
- package/src/components/workspace/layout.test.tsx +29 -0
- package/src/components/workspace/layout.tsx +259 -0
- package/src/components/workspace/table.ts +7 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/integrations/resourceHost.test.tsx +55 -0
- package/src/integrations/resourceHost.ts +99 -0
- package/src/integrations/resourceHosts.test.tsx +141 -0
- package/src/integrations/resourceHosts.tsx +119 -0
- package/src/integrations/workspaceRoute.ts +20 -0
- package/src/integrations/workspaceRoutes.test.ts +40 -0
- package/src/integrations/workspaceRoutes.ts +22 -0
- package/src/integrations/workspaceScreens.tsx +14 -0
- package/src/utils/drawer-trail.ts +35 -0
- package/src/utils/kindFilterStore.test.ts +147 -0
- package/src/utils/kindFilterStore.ts +91 -0
- package/src/utils/navigation.test.ts +13 -1
- package/src/utils/navigation.ts +39 -2
- package/src/utils/page-links.ts +21 -0
- package/src/utils/tabStorage.ts +40 -0
- package/src/utils/viewMemory.test.ts +78 -0
- package/src/utils/viewMemory.ts +54 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
1
2
|
import { describe, expect, it, vi } from "vitest";
|
|
2
3
|
import { renderToString } from "react-dom/server";
|
|
4
|
+
import { act } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
3
6
|
import { MemoryRouter } from "react-router-dom";
|
|
4
7
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
8
|
import type {
|
|
@@ -1337,6 +1340,326 @@ describe("CapacityView overview", () => {
|
|
|
1337
1340
|
});
|
|
1338
1341
|
});
|
|
1339
1342
|
|
|
1343
|
+
describe("Capacity scale-down observations", () => {
|
|
1344
|
+
const noCandidatesExplanation =
|
|
1345
|
+
"The autoscaler found no node it can remove. Usually every node is above its utilization threshold, or pods on emptier nodes can't move (PodDisruptionBudgets, local storage, kube-system pods, safe-to-evict annotations). A node's drain plan shows what would block it.";
|
|
1346
|
+
const candidateExplanation =
|
|
1347
|
+
"Scale-down candidates are observations, not guaranteed removals. PodDisruptionBudgets, local storage, kube-system pods, or safe-to-evict annotations can block removal. Radar can't tell which nodes the autoscaler picked, but a node's drain plan shows what would block removing it.";
|
|
1348
|
+
|
|
1349
|
+
function renderScaleDown(child: CapacityAutoscalerChildObservation) {
|
|
1350
|
+
return renderCapacity("/capacity", (client) =>
|
|
1351
|
+
client.setQueryData(["capacity", "overview"], {
|
|
1352
|
+
...comprehensiveOverview(),
|
|
1353
|
+
groups: [
|
|
1354
|
+
{
|
|
1355
|
+
...gkeGroup,
|
|
1356
|
+
scaling: [
|
|
1357
|
+
{
|
|
1358
|
+
code: "at_min_size",
|
|
1359
|
+
summary: "at minimum size — can't scale down",
|
|
1360
|
+
},
|
|
1361
|
+
],
|
|
1362
|
+
children: [child],
|
|
1363
|
+
childrenMeta: boundedMeta(1),
|
|
1364
|
+
},
|
|
1365
|
+
],
|
|
1366
|
+
orphanAutoscalerGroups: [child],
|
|
1367
|
+
}),
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
it("explains the floor and keeps published zero candidates", () => {
|
|
1372
|
+
const clock = vi.spyOn(Date, "now").mockReturnValue(Date.parse(generatedAt) + 6 * 60 * 1000);
|
|
1373
|
+
const html = renderScaleDown({
|
|
1374
|
+
...gkeChild,
|
|
1375
|
+
scaleDown: {
|
|
1376
|
+
status: "NoCandidates",
|
|
1377
|
+
candidates: 0,
|
|
1378
|
+
lastTransitionTime: generatedAt,
|
|
1379
|
+
asOf: generatedAt,
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
clock.mockRestore();
|
|
1383
|
+
expect(html).toContain("at minimum size");
|
|
1384
|
+
expect(html).toContain(
|
|
1385
|
+
"Every observed autoscaler group has its target at its published minimum.",
|
|
1386
|
+
);
|
|
1387
|
+
expect(html).toContain(
|
|
1388
|
+
"Lowering the pool's minimum is required for it to shrink below this size.",
|
|
1389
|
+
);
|
|
1390
|
+
expect(html).not.toContain("No node can be removed right now");
|
|
1391
|
+
expect(html).toContain(noCandidatesExplanation.replaceAll("'", "'"));
|
|
1392
|
+
expect(html).not.toContain("autoscaler child");
|
|
1393
|
+
expect(html).toContain("No scale-down candidates");
|
|
1394
|
+
expect(html).toContain("Candidate count published by the autoscaler: 0.");
|
|
1395
|
+
expect(html).not.toContain("Candidates:");
|
|
1396
|
+
expect(html).toContain("Status since");
|
|
1397
|
+
expect(html).toContain("(for 6m)");
|
|
1398
|
+
expect(html).not.toContain("As of");
|
|
1399
|
+
expect(html).toContain("PodDisruptionBudgets");
|
|
1400
|
+
expect(html).not.toContain("ETA");
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it("explains no candidates above the minimum on the collapsed pool and both child tables", () => {
|
|
1404
|
+
const child: CapacityAutoscalerChildObservation = {
|
|
1405
|
+
...gkeChild,
|
|
1406
|
+
scaleDown: { status: "NoCandidates", candidates: 0 },
|
|
1407
|
+
};
|
|
1408
|
+
const html = renderCapacity("/capacity", (client) =>
|
|
1409
|
+
client.setQueryData(["capacity", "overview"], {
|
|
1410
|
+
...comprehensiveOverview(),
|
|
1411
|
+
groups: [{ ...gkeGroup, children: [child], childrenMeta: boundedMeta(1) }],
|
|
1412
|
+
orphanAutoscalerGroups: [child],
|
|
1413
|
+
}),
|
|
1414
|
+
);
|
|
1415
|
+
expect(html).toContain('aria-expanded="false"');
|
|
1416
|
+
const explanation = noCandidatesExplanation.replaceAll("'", "'");
|
|
1417
|
+
expect(html).toContain(
|
|
1418
|
+
`tabindex="0" role="note" aria-label="No node can be removed right now. ${explanation}"`,
|
|
1419
|
+
);
|
|
1420
|
+
expect(html.split(`No scale-down candidates. ${explanation}`)).toHaveLength(3);
|
|
1421
|
+
expect(html).not.toContain("Scale-down candidates are observations");
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
it.each([
|
|
1425
|
+
["missing count", { scaleDown: { status: "NoCandidates" } }, false],
|
|
1426
|
+
["missing observation", { scaleDown: undefined }, false],
|
|
1427
|
+
["candidates present", { scaleDown: { status: "CandidatesPresent", candidates: 1 } }, false],
|
|
1428
|
+
["missing minimum", { minSize: undefined }, false],
|
|
1429
|
+
["missing target", { target: undefined }, false],
|
|
1430
|
+
["below the minimum", { target: -1 }, false],
|
|
1431
|
+
["truncated children", {}, true],
|
|
1432
|
+
])("does not assert no removable nodes with %s", (_label, override, truncated) => {
|
|
1433
|
+
const html = renderCapacity("/capacity", (client) =>
|
|
1434
|
+
client.setQueryData(["capacity", "overview"], {
|
|
1435
|
+
...comprehensiveOverview(),
|
|
1436
|
+
groups: [{
|
|
1437
|
+
...gkeGroup,
|
|
1438
|
+
children: [
|
|
1439
|
+
{ ...gkeChild, scaleDown: { status: "NoCandidates", candidates: 0 } },
|
|
1440
|
+
{ ...gkeChildBackoff, scaleDown: { status: "NoCandidates", candidates: 0 }, ...override },
|
|
1441
|
+
],
|
|
1442
|
+
childrenMeta: { total: truncated ? 3 : 2, returned: 2, truncated },
|
|
1443
|
+
}],
|
|
1444
|
+
}),
|
|
1445
|
+
);
|
|
1446
|
+
expect(html).not.toContain("No node can be removed right now");
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it("keeps scale-down probe freshness separate from health in both tables", () => {
|
|
1450
|
+
const clock = vi
|
|
1451
|
+
.spyOn(Date, "now")
|
|
1452
|
+
.mockReturnValue(Date.parse(generatedAt) + 6 * 60 * 1000);
|
|
1453
|
+
try {
|
|
1454
|
+
const html = renderScaleDown({
|
|
1455
|
+
...gkeChild,
|
|
1456
|
+
scaleDown: {
|
|
1457
|
+
status: "InProgress",
|
|
1458
|
+
asOf: "2026-07-13T07:06:00Z",
|
|
1459
|
+
},
|
|
1460
|
+
});
|
|
1461
|
+
expect(html.match(/Health observed<\/th>/g)).toHaveLength(2);
|
|
1462
|
+
expect(html.match(/Probe: <span[^>]*>1h ago<\/span>/g)).toHaveLength(2);
|
|
1463
|
+
expect(html.match(/>6m ago<\/span><\/td>/g)).toHaveLength(2);
|
|
1464
|
+
} finally {
|
|
1465
|
+
clock.mockRestore();
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it("shows an absent scale-down probe independently of published health", () => {
|
|
1470
|
+
const html = renderScaleDown({
|
|
1471
|
+
...gkeChild,
|
|
1472
|
+
scaleDown: { status: "InProgress" },
|
|
1473
|
+
});
|
|
1474
|
+
expect(html.match(/Probe: <span[^>]*>not probed<\/span>/g)).toHaveLength(2);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it.each([
|
|
1478
|
+
"InProgress",
|
|
1479
|
+
"CandidatesPresent",
|
|
1480
|
+
"NoCandidates",
|
|
1481
|
+
"NewControllerStatus",
|
|
1482
|
+
])(
|
|
1483
|
+
"makes the %s removal caveat keyboard reachable without a count glyph",
|
|
1484
|
+
(status) => {
|
|
1485
|
+
const html = renderScaleDown({
|
|
1486
|
+
...gkeChild,
|
|
1487
|
+
scaleDown: { status },
|
|
1488
|
+
});
|
|
1489
|
+
expect(
|
|
1490
|
+
html.match(
|
|
1491
|
+
/tabindex="0" role="note" aria-label="[^"]*PodDisruptionBudgets[^"]*"/g,
|
|
1492
|
+
),
|
|
1493
|
+
).toHaveLength(2);
|
|
1494
|
+
},
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
it("never invents a zero count from NoCandidates or missing observations", () => {
|
|
1498
|
+
const html = renderScaleDown({
|
|
1499
|
+
...gkeChild,
|
|
1500
|
+
scaleDown: { status: "NoCandidates" },
|
|
1501
|
+
});
|
|
1502
|
+
expect(html).toMatch(/Candidates:(?:<!-- -->|\s)*not published/);
|
|
1503
|
+
expect(html).toContain("Candidate count not published by the autoscaler.");
|
|
1504
|
+
expect(html).not.toContain("Probe time not published");
|
|
1505
|
+
expect(html).not.toContain("Status since");
|
|
1506
|
+
expect(html).not.toMatch(/Candidates:(?:<!-- -->|\s)*0/);
|
|
1507
|
+
const older = renderScaleDown(gkeChild);
|
|
1508
|
+
expect(older).not.toContain("Scale-down</th>");
|
|
1509
|
+
expect(older).not.toContain("Not published");
|
|
1510
|
+
expect(older).not.toContain("PodDisruptionBudgets");
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
it("keeps a status-only CandidatesPresent count unknown", () => {
|
|
1514
|
+
const html = renderScaleDown({
|
|
1515
|
+
...gkeChild,
|
|
1516
|
+
scaleDown: { status: "CandidatesPresent" },
|
|
1517
|
+
});
|
|
1518
|
+
expect(html).toContain("Scale-down candidates present");
|
|
1519
|
+
expect(html).toContain("Candidates: not published");
|
|
1520
|
+
expect(html).not.toContain("0 scale-down candidates");
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
it.each(["joined", "orphan"])(
|
|
1524
|
+
"shows both tables when only a %s child has scale-down data",
|
|
1525
|
+
(source) => {
|
|
1526
|
+
const withScaleDown = {
|
|
1527
|
+
...gkeChild,
|
|
1528
|
+
scaleDown: { status: "NoCandidates", candidates: 0 },
|
|
1529
|
+
};
|
|
1530
|
+
const html = renderCapacity("/capacity", (client) =>
|
|
1531
|
+
client.setQueryData(["capacity", "overview"], {
|
|
1532
|
+
...comprehensiveOverview(),
|
|
1533
|
+
groups: [
|
|
1534
|
+
{
|
|
1535
|
+
...gkeGroup,
|
|
1536
|
+
children: [source === "joined" ? withScaleDown : gkeChild],
|
|
1537
|
+
},
|
|
1538
|
+
],
|
|
1539
|
+
orphanAutoscalerGroups: [
|
|
1540
|
+
source === "orphan" ? withScaleDown : gkeChild,
|
|
1541
|
+
],
|
|
1542
|
+
}),
|
|
1543
|
+
);
|
|
1544
|
+
expect(html.match(/Scale-down<\/th>/g)).toHaveLength(2);
|
|
1545
|
+
expect(html).toContain("Not published");
|
|
1546
|
+
expect(html).toContain("No scale-down candidates");
|
|
1547
|
+
},
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
it("opens the removal caveat on status focus and dismisses it on blur", async () => {
|
|
1551
|
+
const client = new QueryClient({
|
|
1552
|
+
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
|
|
1553
|
+
});
|
|
1554
|
+
client.setQueryData(["capacity", "overview"], {
|
|
1555
|
+
...comprehensiveOverview(),
|
|
1556
|
+
groups: [],
|
|
1557
|
+
orphanAutoscalerGroups: [
|
|
1558
|
+
{
|
|
1559
|
+
...gkeChild,
|
|
1560
|
+
scaleDown: { status: "InProgress", candidates: 2 },
|
|
1561
|
+
},
|
|
1562
|
+
],
|
|
1563
|
+
});
|
|
1564
|
+
const container = document.createElement("div");
|
|
1565
|
+
document.body.append(container);
|
|
1566
|
+
const root = createRoot(container);
|
|
1567
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
1568
|
+
vi.useFakeTimers();
|
|
1569
|
+
try {
|
|
1570
|
+
await act(async () => {
|
|
1571
|
+
root.render(
|
|
1572
|
+
<MemoryRouter initialEntries={["/capacity"]}>
|
|
1573
|
+
<QueryClientProvider client={client}>
|
|
1574
|
+
<CapacityView onOpenResource={() => {}} />
|
|
1575
|
+
</QueryClientProvider>
|
|
1576
|
+
</MemoryRouter>,
|
|
1577
|
+
);
|
|
1578
|
+
});
|
|
1579
|
+
const trigger = container.querySelector<HTMLElement>(
|
|
1580
|
+
'[role="note"][aria-label^="Scale-down in progress."]',
|
|
1581
|
+
);
|
|
1582
|
+
expect(trigger).not.toBeNull();
|
|
1583
|
+
expect(trigger!.textContent).toBe("Scale-down in progress");
|
|
1584
|
+
expect(trigger!.querySelector('[tabindex="0"]')).toBeNull();
|
|
1585
|
+
await act(async () => {
|
|
1586
|
+
trigger!.focus();
|
|
1587
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
1588
|
+
});
|
|
1589
|
+
expect(document.activeElement).toBe(trigger);
|
|
1590
|
+
expect(document.querySelector('[role="tooltip"]')?.textContent).toContain(
|
|
1591
|
+
"PodDisruptionBudgets",
|
|
1592
|
+
);
|
|
1593
|
+
await act(async () => {
|
|
1594
|
+
trigger!.blur();
|
|
1595
|
+
});
|
|
1596
|
+
expect(document.querySelector('[role="tooltip"]')).toBeNull();
|
|
1597
|
+
} finally {
|
|
1598
|
+
await act(async () => {
|
|
1599
|
+
root.unmount();
|
|
1600
|
+
});
|
|
1601
|
+
client.clear();
|
|
1602
|
+
container.remove();
|
|
1603
|
+
vi.useRealTimers();
|
|
1604
|
+
vi.unstubAllGlobals();
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
it("renders the server candidate fact on the collapsed pool", () => {
|
|
1609
|
+
const html = renderCapacity("/capacity", (client) =>
|
|
1610
|
+
client.setQueryData(["capacity", "overview"], {
|
|
1611
|
+
...comprehensiveOverview(),
|
|
1612
|
+
groups: [
|
|
1613
|
+
{
|
|
1614
|
+
...gkeGroup,
|
|
1615
|
+
scaling: [
|
|
1616
|
+
{
|
|
1617
|
+
code: "scale_down_candidates",
|
|
1618
|
+
summary: "3 scale-down candidates",
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
},
|
|
1622
|
+
],
|
|
1623
|
+
}),
|
|
1624
|
+
);
|
|
1625
|
+
expect(html).toContain("3 scale-down candidates");
|
|
1626
|
+
expect(html).toMatch(/tabindex="0" role="note" aria-label="3 scale-down candidates\. [^"]*PodDisruptionBudgets/);
|
|
1627
|
+
expect(html).toContain(candidateExplanation.replaceAll("'", "'"));
|
|
1628
|
+
expect(html).not.toContain("scale-down delay");
|
|
1629
|
+
expect(html).not.toContain("Radar can't see those blockers");
|
|
1630
|
+
expect(html).toContain('aria-expanded="false"');
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it.each([
|
|
1634
|
+
["CandidatesPresent", "2 scale-down candidates"],
|
|
1635
|
+
["InProgress", "Scale-down in progress"],
|
|
1636
|
+
["NewControllerStatus", "NewControllerStatus"],
|
|
1637
|
+
["constructor", "constructor"],
|
|
1638
|
+
])("renders %s without promising a removal time", (status, label) => {
|
|
1639
|
+
const html = renderScaleDown({
|
|
1640
|
+
...gkeChild,
|
|
1641
|
+
scaleDown: { status, candidates: 2 },
|
|
1642
|
+
});
|
|
1643
|
+
expect(html).toContain(label);
|
|
1644
|
+
expect(html).toContain("2 scale-down candidates");
|
|
1645
|
+
expect(html).not.toContain("unneeded");
|
|
1646
|
+
expect(html).not.toContain("Candidates:");
|
|
1647
|
+
expect(html).not.toContain("Status since");
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
it("names a single published candidate the same way the pool row does", () => {
|
|
1651
|
+
const html = renderScaleDown({
|
|
1652
|
+
...gkeChild,
|
|
1653
|
+
scaleDown: { status: "CandidatesPresent", candidates: 1 },
|
|
1654
|
+
});
|
|
1655
|
+
expect(html).toMatch(
|
|
1656
|
+
/role="note" aria-label="1 scale-down candidate\. Scale-down candidates are observations/,
|
|
1657
|
+
);
|
|
1658
|
+
expect(html).toContain(candidateExplanation.replaceAll("'", "'"));
|
|
1659
|
+
expect(html).not.toContain("1 scale-down candidates");
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1340
1663
|
describe("CapacityView pool detail", () => {
|
|
1341
1664
|
it("renders the capacity ledger with headers and a certainty legend", () => {
|
|
1342
1665
|
const html = renderCapacity("/capacity/pools/default", (client) =>
|
|
@@ -9,16 +9,8 @@ import {
|
|
|
9
9
|
parseCPUToNanocores,
|
|
10
10
|
parseQuantityToNumber,
|
|
11
11
|
} from "@skyhook-io/k8s-ui/utils/format";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
LinkButton,
|
|
15
|
-
SectionCard,
|
|
16
|
-
certaintyGlyph,
|
|
17
|
-
certaintyValueLabel,
|
|
18
|
-
formatQuantity,
|
|
19
|
-
quantityResourceRank,
|
|
20
|
-
resourceLabel,
|
|
21
|
-
} from "./shared";
|
|
12
|
+
import { LinkButton, SectionCard, formatQuantity, quantityResourceRank, resourceLabel } from "./shared";
|
|
13
|
+
import { CertaintyGlyph, certaintyGlyph, certaintyValueLabel } from "@skyhook-io/k8s-ui";
|
|
22
14
|
|
|
23
15
|
// Karpenter scheduling capacity — the fleet-level "how full is Karpenter, and
|
|
24
16
|
// how much is knocking at the door" bar. Karpenter-pooled nodes only: on mixed
|
|
@@ -4,15 +4,8 @@ import type {
|
|
|
4
4
|
CapacityActivityState,
|
|
5
5
|
CapacityQuantityObservation,
|
|
6
6
|
} from "@skyhook-io/k8s-ui";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
ActivityStateBadge,
|
|
10
|
-
CertaintyGlyph,
|
|
11
|
-
InventoryQuantityCell,
|
|
12
|
-
QuantityInline,
|
|
13
|
-
certaintyGlyph,
|
|
14
|
-
managerStatusRank,
|
|
15
|
-
} from "./shared";
|
|
7
|
+
import { DeniedCapacityState, ActivityStateBadge, InventoryQuantityCell, QuantityInline, managerStatusRank } from "./shared";
|
|
8
|
+
import { CertaintyGlyph, certaintyGlyph } from "@skyhook-io/k8s-ui";
|
|
16
9
|
|
|
17
10
|
function observation(
|
|
18
11
|
certainty: CapacityQuantityObservation["certainty"],
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type CapacityTopTab = "overview" | "demand" | "activity";
|
|
2
|
+
export type PoolSection = "summary" | "workloads" | "members" | "configuration";
|
|
3
|
+
|
|
4
|
+
export interface CapacityRoute {
|
|
5
|
+
topTab: CapacityTopTab;
|
|
6
|
+
poolName?: string;
|
|
7
|
+
poolSection: PoolSection;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseCapacityRoute(pathname: string): CapacityRoute {
|
|
11
|
+
const segments = pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
12
|
+
if (segments[0] !== "capacity")
|
|
13
|
+
return { topTab: "overview", poolSection: "summary" };
|
|
14
|
+
if (segments[1] === "pools" && segments[2]) {
|
|
15
|
+
const section = segments[3];
|
|
16
|
+
return {
|
|
17
|
+
topTab: "overview",
|
|
18
|
+
poolName: decodePathSegment(segments[2]),
|
|
19
|
+
poolSection:
|
|
20
|
+
section === "workloads" ||
|
|
21
|
+
section === "members" ||
|
|
22
|
+
section === "configuration"
|
|
23
|
+
? section
|
|
24
|
+
: "summary",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (segments[1] === "demand" || segments[1] === "activity")
|
|
28
|
+
return { topTab: segments[1], poolSection: "summary" };
|
|
29
|
+
return { topTab: "overview", poolSection: "summary" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodePathSegment(value: string): string {
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function capacityPoolPath(name: string): string {
|
|
41
|
+
return `/capacity/pools/${encodeURIComponent(name)}`;
|
|
42
|
+
}
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
4
|
useState,
|
|
5
|
-
type ComponentType,
|
|
6
5
|
type ReactNode,
|
|
7
6
|
} from "react";
|
|
8
7
|
import {
|
|
@@ -25,6 +24,8 @@ import {
|
|
|
25
24
|
ResourceBar,
|
|
26
25
|
Tooltip,
|
|
27
26
|
WithTooltip,
|
|
27
|
+
certaintyValueLabel,
|
|
28
|
+
CertaintyGlyph,
|
|
28
29
|
type CapacityActivityEpisode,
|
|
29
30
|
type CapacityCertainty,
|
|
30
31
|
type CapacityClaimStage,
|
|
@@ -61,22 +62,19 @@ import {
|
|
|
61
62
|
} from "../../api/client";
|
|
62
63
|
import type { SelectedResource } from "../../types";
|
|
63
64
|
import { refToSelectedResource } from "../../utils/navigation";
|
|
65
|
+
import { Notice, ScreenEmptyState } from "../workspace/layout";
|
|
64
66
|
|
|
65
67
|
// ============================================================================
|
|
66
68
|
// Route model + navigation
|
|
67
69
|
// ============================================================================
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
export
|
|
71
|
+
import type { PoolSection } from './routes'
|
|
72
|
+
export { capacityPoolPath, parseCapacityRoute } from './routes'
|
|
73
|
+
export type { CapacityRoute, CapacityTopTab, PoolSection } from './routes'
|
|
74
|
+
|
|
71
75
|
export type CapacityConnectionState =
|
|
72
76
|
"connected" | "disconnected" | "connecting";
|
|
73
77
|
|
|
74
|
-
export interface CapacityRoute {
|
|
75
|
-
topTab: CapacityTopTab;
|
|
76
|
-
poolName?: string;
|
|
77
|
-
poolSection: PoolSection;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
78
|
export const POOL_SECTIONS: { id: PoolSection; label: string }[] = [
|
|
81
79
|
{ id: "summary", label: "Summary" },
|
|
82
80
|
{ id: "workloads", label: "Workloads" },
|
|
@@ -84,40 +82,6 @@ export const POOL_SECTIONS: { id: PoolSection; label: string }[] = [
|
|
|
84
82
|
{ id: "configuration", label: "Configuration" },
|
|
85
83
|
];
|
|
86
84
|
|
|
87
|
-
export function parseCapacityRoute(pathname: string): CapacityRoute {
|
|
88
|
-
const segments = pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
89
|
-
if (segments[0] !== "capacity")
|
|
90
|
-
return { topTab: "overview", poolSection: "summary" };
|
|
91
|
-
if (segments[1] === "pools" && segments[2]) {
|
|
92
|
-
const section = segments[3];
|
|
93
|
-
return {
|
|
94
|
-
topTab: "overview",
|
|
95
|
-
poolName: decodePathSegment(segments[2]),
|
|
96
|
-
poolSection:
|
|
97
|
-
section === "workloads" ||
|
|
98
|
-
section === "members" ||
|
|
99
|
-
section === "configuration"
|
|
100
|
-
? section
|
|
101
|
-
: "summary",
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (segments[1] === "demand" || segments[1] === "activity")
|
|
105
|
-
return { topTab: segments[1], poolSection: "summary" };
|
|
106
|
-
return { topTab: "overview", poolSection: "summary" };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function decodePathSegment(value: string): string {
|
|
110
|
-
try {
|
|
111
|
-
return decodeURIComponent(value);
|
|
112
|
-
} catch {
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function capacityPoolPath(name: string): string {
|
|
118
|
-
return `/capacity/pools/${encodeURIComponent(name)}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
85
|
export function identityToSelectedResource(
|
|
122
86
|
identity: CapacityResourceIdentity,
|
|
123
87
|
): SelectedResource {
|
|
@@ -266,20 +230,6 @@ function missingUsageMessage(coverage?: CapacitySourceCoverage): string {
|
|
|
266
230
|
// = exact · ≥ lower bound · ≤ upper bound · ? unknown
|
|
267
231
|
// ============================================================================
|
|
268
232
|
|
|
269
|
-
export function certaintyGlyph(certainty: CapacityCertainty): string {
|
|
270
|
-
if (certainty === "exact") return "=";
|
|
271
|
-
if (certainty === "lower_bound") return "≥";
|
|
272
|
-
if (certainty === "upper_bound") return "≤";
|
|
273
|
-
return "?";
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function certaintyValueLabel(certainty: CapacityCertainty): string {
|
|
277
|
-
if (certainty === "exact") return "Exact";
|
|
278
|
-
if (certainty === "lower_bound") return "Lower bound";
|
|
279
|
-
if (certainty === "upper_bound") return "Upper bound";
|
|
280
|
-
return "Unknown certainty";
|
|
281
|
-
}
|
|
282
|
-
|
|
283
233
|
export function observationTitle(
|
|
284
234
|
observation: CapacityQuantityObservation,
|
|
285
235
|
definition?: string,
|
|
@@ -295,28 +245,6 @@ export function observationTitle(
|
|
|
295
245
|
return parts.join(" ");
|
|
296
246
|
}
|
|
297
247
|
|
|
298
|
-
/** Small bordered mono glyph chip; hover reveals certainty/source/asOf. */
|
|
299
|
-
export function CertaintyGlyph({
|
|
300
|
-
certainty,
|
|
301
|
-
title,
|
|
302
|
-
}: {
|
|
303
|
-
certainty: CapacityCertainty;
|
|
304
|
-
title?: string;
|
|
305
|
-
}) {
|
|
306
|
-
return (
|
|
307
|
-
<WithTooltip tip={title ?? certaintyValueLabel(certainty)}>
|
|
308
|
-
<span
|
|
309
|
-
tabIndex={0}
|
|
310
|
-
role="note"
|
|
311
|
-
aria-label={title ?? certaintyValueLabel(certainty)}
|
|
312
|
-
className="cursor-help rounded border border-theme-border-light px-1 font-mono text-[10px] leading-tight text-theme-text-tertiary focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
|
|
313
|
-
>
|
|
314
|
-
{certaintyGlyph(certainty)}
|
|
315
|
-
</span>
|
|
316
|
-
</WithTooltip>
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
248
|
// ============================================================================
|
|
321
249
|
// Quantity / resource formatting
|
|
322
250
|
// ============================================================================
|
|
@@ -1241,15 +1169,6 @@ export function TokenGroup({
|
|
|
1241
1169
|
);
|
|
1242
1170
|
}
|
|
1243
1171
|
|
|
1244
|
-
export function Notice({ children }: { children: ReactNode }) {
|
|
1245
|
-
return (
|
|
1246
|
-
<div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-sm text-theme-text-secondary">
|
|
1247
|
-
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
1248
|
-
<div>{children}</div>
|
|
1249
|
-
</div>
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
1172
|
export function RefreshError({ message }: { message: string }) {
|
|
1254
1173
|
return (
|
|
1255
1174
|
<Notice>
|
|
@@ -1327,31 +1246,6 @@ export function InlineEmpty({
|
|
|
1327
1246
|
);
|
|
1328
1247
|
}
|
|
1329
1248
|
|
|
1330
|
-
export function EmptyState({
|
|
1331
|
-
icon: Icon,
|
|
1332
|
-
title,
|
|
1333
|
-
detail,
|
|
1334
|
-
action,
|
|
1335
|
-
}: {
|
|
1336
|
-
icon: ComponentType<{ className?: string }>;
|
|
1337
|
-
title: string;
|
|
1338
|
-
detail: ReactNode;
|
|
1339
|
-
action?: ReactNode;
|
|
1340
|
-
}) {
|
|
1341
|
-
return (
|
|
1342
|
-
<div className="flex min-h-0 flex-1 items-center justify-center bg-theme-base p-6">
|
|
1343
|
-
<div className="max-w-md text-center">
|
|
1344
|
-
<Icon className="mx-auto h-9 w-9 text-theme-text-tertiary/50" />
|
|
1345
|
-
<h2 className="mt-3 text-lg font-medium text-theme-text-primary">
|
|
1346
|
-
{title}
|
|
1347
|
-
</h2>
|
|
1348
|
-
<p className="mt-1 text-sm text-theme-text-secondary">{detail}</p>
|
|
1349
|
-
{action}
|
|
1350
|
-
</div>
|
|
1351
|
-
</div>
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
1249
|
export function ScrollableContent({ children }: { children: ReactNode }) {
|
|
1356
1250
|
// Expanding/collapsing cards can toggle the scrollbar; a stable gutter
|
|
1357
1251
|
// keeps the centered column from shifting sideways when that happens.
|
|
@@ -1460,7 +1354,7 @@ rules:
|
|
|
1460
1354
|
export function DeniedCapacityState({ detail }: { detail: string }) {
|
|
1461
1355
|
const [copied, setCopied] = useState(false);
|
|
1462
1356
|
return (
|
|
1463
|
-
<
|
|
1357
|
+
<ScreenEmptyState
|
|
1464
1358
|
icon={Shield}
|
|
1465
1359
|
title="Capacity access denied"
|
|
1466
1360
|
detail={detail}
|
|
@@ -1519,7 +1413,7 @@ export function integrationBlock(
|
|
|
1519
1413
|
const upgrade = getRadarUpgradeRequirement(error);
|
|
1520
1414
|
if (upgrade)
|
|
1521
1415
|
return (
|
|
1522
|
-
<
|
|
1416
|
+
<ScreenEmptyState
|
|
1523
1417
|
icon={Gauge}
|
|
1524
1418
|
title={radarUpgradeHeadline("Capacity")}
|
|
1525
1419
|
detail={radarUpgradeDetail(upgrade)}
|
|
@@ -1531,7 +1425,7 @@ export function integrationBlock(
|
|
|
1531
1425
|
/>
|
|
1532
1426
|
);
|
|
1533
1427
|
return (
|
|
1534
|
-
<
|
|
1428
|
+
<ScreenEmptyState
|
|
1535
1429
|
icon={AlertTriangle}
|
|
1536
1430
|
title="Capacity unavailable"
|
|
1537
1431
|
detail={errorMessage(error)}
|
|
@@ -1549,7 +1443,7 @@ export function integrationBlock(
|
|
|
1549
1443
|
);
|
|
1550
1444
|
if (response.state === "not_detected")
|
|
1551
1445
|
return (
|
|
1552
|
-
<
|
|
1446
|
+
<ScreenEmptyState
|
|
1553
1447
|
icon={Gauge}
|
|
1554
1448
|
title="Karpenter not detected"
|
|
1555
1449
|
detail="Capacity appears automatically when this cluster exposes Karpenter NodePools."
|
|
@@ -1838,15 +1732,3 @@ export function PoolSelector({
|
|
|
1838
1732
|
</div>
|
|
1839
1733
|
);
|
|
1840
1734
|
}
|
|
1841
|
-
|
|
1842
|
-
// ============================================================================
|
|
1843
|
-
// Shared table cell classes (keeps every capacity table visually identical)
|
|
1844
|
-
// ============================================================================
|
|
1845
|
-
|
|
1846
|
-
export const TABLE_WRAP = "overflow-x-auto";
|
|
1847
|
-
export const TABLE_HEAD =
|
|
1848
|
-
"border-b border-theme-border bg-theme-base/60 text-[11px] uppercase tracking-wide text-theme-text-tertiary";
|
|
1849
|
-
export const TH = "px-3 py-2.5 text-left font-medium whitespace-nowrap";
|
|
1850
|
-
export const TD = "px-3 py-2.5 align-top text-sm text-theme-text-primary";
|
|
1851
|
-
export const TBODY = "table-divide-subtle";
|
|
1852
|
-
export const ROW_HOVER = "transition-colors hover:bg-theme-hover/50";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkspaceRoute } from '../../integrations/workspaceRoute'
|
|
2
|
+
import { parseCapacityRoute } from './routes'
|
|
3
|
+
|
|
4
|
+
export const capacityWorkspaceRoute: WorkspaceRoute = {
|
|
5
|
+
id: 'capacity',
|
|
6
|
+
path: '/capacity',
|
|
7
|
+
label: 'Capacity',
|
|
8
|
+
navView: 'capacity',
|
|
9
|
+
namespaceScope: 'cluster',
|
|
10
|
+
namespaceFilterTooltip: 'Capacity is reported across the cluster — the namespace filter doesn’t apply here.',
|
|
11
|
+
pageTitle(pathname) {
|
|
12
|
+
const route = parseCapacityRoute(pathname)
|
|
13
|
+
if (route.poolName) return route.poolName
|
|
14
|
+
if (route.topTab === 'demand') return 'Capacity Demand'
|
|
15
|
+
if (route.topTab === 'activity') return 'Capacity Activity'
|
|
16
|
+
return 'Capacity'
|
|
17
|
+
},
|
|
18
|
+
}
|