@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
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This package is source-only — it ships TypeScript + TSX files under `src/`. Co
|
|
|
10
10
|
npm install @skyhook-io/radar-app
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Peer deps: `react >=19`, `react-dom >=19`, `react-router-dom >=7`, `@tanstack/react-query >=5`, `@skyhook-io/k8s-ui >=1.
|
|
13
|
+
Peer deps: `react >=19`, `react-dom >=19`, `react-router-dom >=7`, `@tanstack/react-query >=5`, `@skyhook-io/k8s-ui >=1.17.1`, plus `clsx`, `tailwind-merge`, `lucide-react`, `@xyflow/react`, `elkjs`.
|
|
14
14
|
|
|
15
15
|
## Use
|
|
16
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@fontsource/dm-mono": "^5.3.0",
|
|
35
35
|
"@monaco-editor/react": "^4.7.0",
|
|
36
36
|
"diff": "^9.0.0",
|
|
37
|
-
"monaco-editor": "0.
|
|
37
|
+
"monaco-editor": "0.57.0",
|
|
38
38
|
"react-markdown": "^10.1.0",
|
|
39
39
|
"react-virtuoso": "^4.18.16",
|
|
40
40
|
"remark-gfm": "^4.0.1",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"yaml": "^2.9.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@skyhook-io/k8s-ui": ">=1.17.
|
|
45
|
+
"@skyhook-io/k8s-ui": ">=1.17.1",
|
|
46
46
|
"@tanstack/react-query": ">=5",
|
|
47
47
|
"@xyflow/react": ">=12.0.0",
|
|
48
48
|
"clsx": ">=2",
|
package/src/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
1
|
+
import { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } from 'react'
|
|
2
2
|
import { flushSync } from 'react-dom'
|
|
3
3
|
import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
|
|
4
4
|
import { englishPlural } from '@skyhook-io/k8s-ui/utils/pluralize'
|
|
@@ -10,7 +10,7 @@ import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
|
|
|
10
10
|
import { investigationWorkspaceSearch, isInvestigationWorkspacePath, useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
|
|
11
11
|
import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
|
|
12
12
|
import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
|
|
13
|
-
import {
|
|
13
|
+
import { resetNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
14
14
|
import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
|
|
15
15
|
import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
|
|
16
16
|
import { TimelineView } from './components/timeline/TimelineView'
|
|
@@ -23,7 +23,6 @@ import { HelmView } from './components/helm/HelmView'
|
|
|
23
23
|
import { HelmCompareRoute } from './components/helm/HelmCompareRoute'
|
|
24
24
|
import { TrafficView } from './components/traffic/TrafficView'
|
|
25
25
|
import { CostView } from './components/cost/CostView'
|
|
26
|
-
import { CapacityView } from './components/capacity/CapacityView'
|
|
27
26
|
import { AuditView } from './components/audit/AuditView'
|
|
28
27
|
import { IssuesPane } from './components/issues/IssuesPane'
|
|
29
28
|
import { GitOpsView } from './components/gitops/GitOpsView'
|
|
@@ -38,9 +37,10 @@ import { CloudFunnelButton } from './components/CloudFunnelButton'
|
|
|
38
37
|
import { useNavCustomization } from './context/NavCustomization'
|
|
39
38
|
import type { FleetTakeoverTarget } from './context/NavCustomization'
|
|
40
39
|
import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
40
|
+
import { resourceExpandedPath, resourceKindListTitle } from './integrations/resourceHosts'
|
|
41
|
+
import { workspaceContextSwitchSearch, workspaceForPath, workspaceForView, workspaceLabels } from './integrations/workspaceRoutes'
|
|
42
|
+
import { WorkspaceScreen } from './integrations/workspaceScreens'
|
|
43
|
+
import { currentPageLabel } from './utils/page-links'
|
|
44
44
|
import { navigateFromPrimaryRail } from './components/nav/navigation'
|
|
45
45
|
import { useNavRailPinned } from './hooks/useNavRailPinned'
|
|
46
46
|
import { useMediaQuery } from './hooks/useMediaQuery'
|
|
@@ -58,12 +58,12 @@ import { openWhatsNew, useWhatsNewStatus, WhatsNew } from './components/whats-ne
|
|
|
58
58
|
import { useUsageData, useUsageRecording } from './api/usage-data'
|
|
59
59
|
import { UsageDataPrompt } from './components/usage-data/UsageDataPrompt'
|
|
60
60
|
import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
|
|
61
|
-
import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
|
|
61
|
+
import { DiagnosticsOverlay, type DiagnosticsFocus } from './components/ui/DiagnosticsOverlay'
|
|
62
62
|
import { useEventSource } from './hooks/useEventSource'
|
|
63
|
-
import { debugNamespaceLog, refetchOnResourceEvents, resourceEventKey, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
|
|
63
|
+
import { debugNamespaceLog, refetchOnResourceEvents, resourceEventKey, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit, useClusterInfo } from './api/client'
|
|
64
64
|
import { buildAuditSeverityMap } from './utils/auditBadges'
|
|
65
65
|
import { isInNamespaceScope, scopeNodesToNamespaces } from './utils/topology-namespace'
|
|
66
|
-
import { routePath, apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
|
|
66
|
+
import { routePath, apiUrl, getApiBase, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
|
|
67
67
|
import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts, useSuppressBaseShortcuts } from './hooks/useKeyboardShortcuts'
|
|
68
68
|
import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
69
69
|
import { useDocumentTitle } from './hooks/useDocumentTitle'
|
|
@@ -75,7 +75,9 @@ import { Tooltip } from './components/ui/Tooltip'
|
|
|
75
75
|
import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
|
|
76
76
|
import { SettingsDialog, type SettingsSectionId } from './components/settings/SettingsDialog'
|
|
77
77
|
import type { APIResource, TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
|
|
78
|
-
import {
|
|
78
|
+
import { createViewMemory } from './utils/viewMemory'
|
|
79
|
+
import { createKindFilterStore } from './utils/kindFilterStore'
|
|
80
|
+
import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource, withCrossViewParams, writeViewParams, CROSS_VIEW_PARAMS } from './utils/navigation'
|
|
79
81
|
import { findSelectedTopologyNode } from './utils/topology-selection'
|
|
80
82
|
import { type OmnibarHandle } from './components/ui/Omnibar'
|
|
81
83
|
import { RadarOmnibar } from './components/ui/RadarOmnibar'
|
|
@@ -83,7 +85,7 @@ import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
|
83
85
|
|
|
84
86
|
// All possible node kinds (core + GitOps)
|
|
85
87
|
const ALL_NODE_KINDS: NodeKind[] = [
|
|
86
|
-
'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
|
|
88
|
+
'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute', 'UDPRoute',
|
|
87
89
|
'Service', 'Deployment', 'Rollout', 'DaemonSet', 'StatefulSet',
|
|
88
90
|
'ReplicaSet', 'Pod', 'PodGroup', 'ConfigMap', 'Secret', 'ServiceAccount', 'SealedSecret', 'ServiceMonitor', 'PodMonitor', 'HorizontalPodAutoscaler', 'Job', 'CronJob', 'PersistentVolumeClaim', 'Namespace',
|
|
89
91
|
'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
|
|
@@ -141,8 +143,8 @@ function getViewFromPath(pathname: string): ExtendedMainView {
|
|
|
141
143
|
if (path === 'helm') return 'helm'
|
|
142
144
|
if (path === 'traffic') return 'traffic'
|
|
143
145
|
if (path === 'cost') return 'cost'
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
+
const workspace = workspaceForPath(pathname)
|
|
147
|
+
if (workspace) return workspace.id
|
|
146
148
|
if (path === 'workload') return 'workload'
|
|
147
149
|
if (path === 'checks' || path === 'audit') return 'checks' // /audit = legacy → checks
|
|
148
150
|
if (path === 'gitops') return 'gitops'
|
|
@@ -167,13 +169,14 @@ function usageView(pathname: string, view: ExtendedMainView, upgrade: boolean):
|
|
|
167
169
|
|
|
168
170
|
// The screen a crash is counted under. Fixed names, because release builds
|
|
169
171
|
// shorten component names and the component stack can't say which screen.
|
|
170
|
-
const CRASH_LABELS
|
|
172
|
+
const CRASH_LABELS = {
|
|
171
173
|
home: 'Home', topology: 'Topology', resources: 'Resources', timeline: 'Timeline',
|
|
172
174
|
issues: 'Issues', helm: 'Helm', helmCompare: 'HelmCompare', traffic: 'Traffic',
|
|
173
|
-
cost: 'Cost',
|
|
175
|
+
cost: 'Cost', checks: 'Checks', gitops: 'GitOps',
|
|
174
176
|
applications: 'Applications', workload: 'Workload', compare: 'Compare',
|
|
175
177
|
investigations: 'Investigations',
|
|
176
|
-
|
|
178
|
+
...workspaceLabels,
|
|
179
|
+
} satisfies Record<ExtendedMainView, string>
|
|
177
180
|
|
|
178
181
|
// The namespace scope filter is meaningful only on namespaced surfaces. On
|
|
179
182
|
// cluster-scoped views it does nothing, so we disable it with an explanation
|
|
@@ -203,12 +206,8 @@ function namespaceFilterDisabled(
|
|
|
203
206
|
tooltip: 'Cost is reported per namespace across the whole cluster — the namespace filter doesn’t apply here.',
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
disabled: true,
|
|
209
|
-
tooltip: 'Capacity is reported across the cluster — the namespace filter doesn’t apply here.',
|
|
210
|
-
}
|
|
211
|
-
}
|
|
209
|
+
const workspace = workspaceForView(view)
|
|
210
|
+
if (workspace?.namespaceScope === 'cluster') return { disabled: true, tooltip: workspace.namespaceFilterTooltip }
|
|
212
211
|
if (view === 'checks' && pathname.startsWith('/checks/upgrade')) {
|
|
213
212
|
return {
|
|
214
213
|
disabled: true,
|
|
@@ -263,6 +262,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: APIResourc
|
|
|
263
262
|
const resourceName = decode(pathSegments[1] ?? '')
|
|
264
263
|
if (!resourceName) return 'Resources'
|
|
265
264
|
const group = new URLSearchParams(search).get('apiGroup') || ''
|
|
265
|
+
const hostTitle = resourceKindListTitle({ name: resourceName, group })
|
|
266
|
+
if (hostTitle) return hostTitle
|
|
266
267
|
const match = findAPIResourceForRoute(apiResources, resourceName, group)
|
|
267
268
|
return pluralKindTitle(match?.kind ?? pluralToKind(resourceName), resourceName)
|
|
268
269
|
}
|
|
@@ -281,18 +282,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: APIResourc
|
|
|
281
282
|
if (view === 'checks' && pathSegments[1] === 'upgrade') return 'Upgrade impact'
|
|
282
283
|
|
|
283
284
|
// The landing view reads "Overview" rather than "Home" in the tab.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (pathSegments[1] === 'demand') return 'Capacity Demand'
|
|
287
|
-
if (pathSegments[1] === 'activity') return 'Capacity Activity'
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (view === 'cnpg') {
|
|
291
|
-
const route = parseCNPGRoute(pathname)
|
|
292
|
-
if (route.detail) return route.detail.name
|
|
293
|
-
const screen = CNPG_SCREENS.find((s) => s.id === route.screen)
|
|
294
|
-
return `CloudNativePG ${screen?.label ?? 'Overview'}`
|
|
295
|
-
}
|
|
285
|
+
const workspace = workspaceForView(view)
|
|
286
|
+
if (workspace) return workspace.pageTitle(pathname)
|
|
296
287
|
if (view === 'home') return 'Overview'
|
|
297
288
|
// Every other view's label is its id capitalized — getViewFromPath has already
|
|
298
289
|
// normalized aliases (e.g. /audit → 'checks'), so no lookup table is needed.
|
|
@@ -351,6 +342,7 @@ interface AppProps {
|
|
|
351
342
|
}
|
|
352
343
|
|
|
353
344
|
function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
345
|
+
useLayoutEffect(() => () => resetNavigationMap(), [])
|
|
354
346
|
const navigate = useNavigate()
|
|
355
347
|
const location = useLocation()
|
|
356
348
|
const navigationType = useNavigationType()
|
|
@@ -452,6 +444,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
452
444
|
|
|
453
445
|
// Get mainView from URL path
|
|
454
446
|
const mainView = getViewFromPath(location.pathname)
|
|
447
|
+
const activeWorkspace = workspaceForView(mainView)
|
|
455
448
|
const upgradeReadinessRoute = location.pathname.startsWith('/checks/upgrade')
|
|
456
449
|
|
|
457
450
|
// Opt-in usage data. Embedded hosts own their own consent, so Radar never asks there.
|
|
@@ -459,12 +452,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
459
452
|
// A status cached by another screen must not start recording inside a host.
|
|
460
453
|
useUsageRecording(usageView(location.pathname, mainView, upgradeReadinessRoute), navCustomization.embedded ? undefined : usageData.data)
|
|
461
454
|
|
|
462
|
-
//
|
|
463
|
-
// mount) so the omnibar can open a CRD hit with an irregular plural from any
|
|
464
|
-
// view — kindToPlural would otherwise English-guess the route before a
|
|
465
|
-
// resources view has run initNavigationMap().
|
|
455
|
+
// The omnibar needs discovered plurals even before ResourcesView mounts.
|
|
466
456
|
const { data: navApiResources } = useAPIResources()
|
|
467
|
-
useEffect(() => { if (navApiResources) initNavigationMap(navApiResources) }, [navApiResources])
|
|
468
457
|
|
|
469
458
|
// View-aware namespace scope: disabled on cluster-scoped surfaces so the
|
|
470
459
|
// chip isn't a dead control next to the cluster switcher.
|
|
@@ -482,15 +471,45 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
482
471
|
return slug || 'pods'
|
|
483
472
|
}, [location.pathname])
|
|
484
473
|
|
|
485
|
-
//
|
|
474
|
+
// Section memory and Resources filters are kept per cluster; inert until the context is known.
|
|
475
|
+
const { data: clusterInfoData, isLoading: clusterInfoLoading } = useClusterInfo()
|
|
476
|
+
const clusterContext = clusterInfoData?.context
|
|
477
|
+
const viewMemory = useMemo(
|
|
478
|
+
() => (clusterContext ? createViewMemory(`${getApiBase()}|${clusterContext}`) : undefined),
|
|
479
|
+
[clusterContext],
|
|
480
|
+
)
|
|
481
|
+
const kindFilterStore = useMemo(
|
|
482
|
+
() => (clusterContext ? createKindFilterStore(`${getApiBase()}|${clusterContext}`) : undefined),
|
|
483
|
+
[clusterContext],
|
|
484
|
+
)
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
viewMemory?.record(mainView, location.pathname, location.search)
|
|
487
|
+
if (mainView === 'resources') kindFilterStore?.recordLastKind(location.pathname, location.search)
|
|
488
|
+
}, [viewMemory, kindFilterStore, mainView, location.pathname, location.search])
|
|
489
|
+
const rememberedKindSearch = useMemo(
|
|
490
|
+
() => kindFilterStore && ((kind: { name: string; group: string }) => kindFilterStore.get(kind.name, kind.group)),
|
|
491
|
+
[kindFilterStore],
|
|
492
|
+
)
|
|
493
|
+
const saveKindFilters = useMemo(
|
|
494
|
+
() => kindFilterStore && ((kind: { name: string; group: string }, search: string) => kindFilterStore.set(kind.name, kind.group, search)),
|
|
495
|
+
[kindFilterStore],
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
// Canonical URL — `/resources` is not stable for bookmarks/sharing; normalize to
|
|
499
|
+
// the last kind (hosts such as Radar Hub link to bare `/resources`), else Pods.
|
|
486
500
|
useEffect(() => {
|
|
487
501
|
const path = location.pathname.replace(/\/+$/, '') || '/'
|
|
488
|
-
|
|
502
|
+
// Wait for the cluster so its last kind can be read; landing on Pods first would overwrite it.
|
|
503
|
+
if (path !== '/resources' || clusterInfoLoading) return
|
|
504
|
+
const current = new URLSearchParams(location.search)
|
|
505
|
+
const onlyCrossView = [...current.keys()].every(key => (CROSS_VIEW_PARAMS as readonly string[]).includes(key))
|
|
506
|
+
const destination = new URL((onlyCrossView && kindFilterStore?.lastKind()) || '/resources/pods', 'http://radar.invalid')
|
|
507
|
+
current.forEach((value, key) => destination.searchParams.set(key, value))
|
|
489
508
|
navigate(
|
|
490
|
-
{ pathname:
|
|
509
|
+
{ pathname: destination.pathname, search: destination.search, hash: location.hash },
|
|
491
510
|
{ replace: true },
|
|
492
511
|
)
|
|
493
|
-
}, [location.pathname, location.search, location.hash, navigate])
|
|
512
|
+
}, [location.pathname, location.search, location.hash, navigate, kindFilterStore, clusterInfoLoading])
|
|
494
513
|
|
|
495
514
|
// Set mainView by navigating to the path
|
|
496
515
|
const setMainView = useCallback((view: ExtendedMainView, params?: Record<string, string>) => {
|
|
@@ -508,6 +527,15 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
508
527
|
}
|
|
509
528
|
}
|
|
510
529
|
|
|
530
|
+
// Explicit params are a link with its own state; otherwise go back to where the user left this section.
|
|
531
|
+
const remembered = params
|
|
532
|
+
? undefined
|
|
533
|
+
: viewMemory?.sectionPath(view) ?? (view === 'resources' ? kindFilterStore?.lastKind() : undefined)
|
|
534
|
+
if (remembered) {
|
|
535
|
+
navigate(withCrossViewParams(remembered, location.search))
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
|
|
511
539
|
const path = view === 'home' ? '/' : `/${view}`
|
|
512
540
|
|
|
513
541
|
// Start fresh — keep only cross-view params, discard view-specific ones.
|
|
@@ -532,7 +560,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
532
560
|
}
|
|
533
561
|
|
|
534
562
|
navigate({ pathname: path, search: newParams.toString() })
|
|
535
|
-
}, [location.search, navigate, takeover, goHost])
|
|
563
|
+
}, [location.search, navigate, takeover, goHost, viewMemory, kindFilterStore])
|
|
536
564
|
|
|
537
565
|
const whatsNewStatus = useWhatsNewStatus()
|
|
538
566
|
|
|
@@ -637,6 +665,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
637
665
|
|
|
638
666
|
// Diagnostics overlay state
|
|
639
667
|
const [showDiagnostics, setShowDiagnostics] = useState(false)
|
|
668
|
+
const [diagnosticsFocus, setDiagnosticsFocus] = useState<DiagnosticsFocus>()
|
|
640
669
|
|
|
641
670
|
// The peek drawer "expanded" into a fullscreen overlay = ?full=1 with a selected
|
|
642
671
|
// resource, on ANY view (resources list, topology graph, GitOps, Applications…) —
|
|
@@ -953,7 +982,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
953
982
|
category: 'General' as const,
|
|
954
983
|
scope: 'global' as const,
|
|
955
984
|
allowInInputs: true,
|
|
956
|
-
handler: () => setShowDiagnostics(prev => !prev),
|
|
985
|
+
handler: () => { setDiagnosticsFocus(undefined); setShowDiagnostics(prev => !prev) },
|
|
957
986
|
},
|
|
958
987
|
// Settings exposes local-binary controls that don't apply to embedded hosts.
|
|
959
988
|
// Register the shortcut only when standalone (matching the gear button) —
|
|
@@ -1205,6 +1234,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1205
1234
|
onContextSwitchComplete: endSwitch,
|
|
1206
1235
|
onContextSwitchProgress: updateProgress,
|
|
1207
1236
|
onContextChanged: (context) => {
|
|
1237
|
+
resetNavigationMap()
|
|
1238
|
+
|
|
1208
1239
|
// Clear all React Query caches when cluster context changes
|
|
1209
1240
|
// This ensures helm releases, resources, etc. are refetched from the new cluster
|
|
1210
1241
|
// removeQueries clears cached data, invalidateQueries triggers refetch
|
|
@@ -1248,10 +1279,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1248
1279
|
const nextParams = new URLSearchParams()
|
|
1249
1280
|
const diagnoseRun = new URLSearchParams(location.search).get('ai-run')
|
|
1250
1281
|
if (diagnoseRun) nextParams.set('ai-run', diagnoseRun)
|
|
1251
|
-
|
|
1252
|
-
// not in the new one instead of loading a same-named object.
|
|
1253
|
-
const pinnedCtx = new URLSearchParams(location.search).get('ctx')
|
|
1254
|
-
if (pinnedCtx && location.pathname.startsWith('/cnpg/')) nextParams.set('ctx', pinnedCtx)
|
|
1282
|
+
for (const [key, value] of workspaceContextSwitchSearch(location.pathname, new URLSearchParams(location.search))) nextParams.set(key, value)
|
|
1255
1283
|
navigate(
|
|
1256
1284
|
{ pathname: location.pathname, search: nextParams.toString() },
|
|
1257
1285
|
{ replace: true, state: location.state },
|
|
@@ -1432,6 +1460,34 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1432
1460
|
{ onSuccess: () => setNamespaces([]) },
|
|
1433
1461
|
)
|
|
1434
1462
|
}, [namespaceScope?.cacheScoped, namespaces.length, setActiveNamespace])
|
|
1463
|
+
// Same write as the header picker, which then mirrors it into `namespaces`.
|
|
1464
|
+
// The flash shows where that scope lives from now on. The picker owns the
|
|
1465
|
+
// namespace scope from then on, so no kind keeps a Namespace column filter.
|
|
1466
|
+
const [namespacePickerFlash, setNamespacePickerFlash] = useState(0)
|
|
1467
|
+
const applyNamespacesToAllSections = useCallback(
|
|
1468
|
+
(picked: string[]) => setActiveNamespace.mutateAsync({ namespaces: picked }).then(() => {
|
|
1469
|
+
kindFilterStore?.clearColumn('namespace')
|
|
1470
|
+
setNamespacePickerFlash(n => n + 1)
|
|
1471
|
+
}),
|
|
1472
|
+
[setActiveNamespace, kindFilterStore],
|
|
1473
|
+
)
|
|
1474
|
+
const issuesShowNamespace = useMemo(() => {
|
|
1475
|
+
if (namespaceScope?.cacheScoped) {
|
|
1476
|
+
if (!namespaceScope.namespaceRescope) return undefined
|
|
1477
|
+
return {
|
|
1478
|
+
mode: 'switch' as const,
|
|
1479
|
+
show: (ns: string) => { setNamespaces([ns]); setActiveNamespace.mutate({ namespaces: [ns] }) },
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
mode: 'add' as const,
|
|
1484
|
+
show: (ns: string) => {
|
|
1485
|
+
const next = namespaces.includes(ns) ? namespaces : [...namespaces, ns]
|
|
1486
|
+
setNamespaces(next)
|
|
1487
|
+
setActiveNamespace.mutate({ namespaces: next })
|
|
1488
|
+
},
|
|
1489
|
+
}
|
|
1490
|
+
}, [namespaceScope?.cacheScoped, namespaceScope?.namespaceRescope, namespaces, setActiveNamespace])
|
|
1435
1491
|
const initialBookmarkReconciledRef = useRef(false)
|
|
1436
1492
|
const scopeActives = useMemo(() => namespaceScope?.actives ?? [], [namespaceScope?.actives])
|
|
1437
1493
|
const namespaceScopeKey = useMemo(() => namespaceScope ? [...scopeActives].sort().join(',') : null, [namespaceScope, scopeActives])
|
|
@@ -1510,32 +1566,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1510
1566
|
if (skipUrlWriteAfterPopRef.current) return
|
|
1511
1567
|
const currentSearch = window.location.search
|
|
1512
1568
|
const params = new URLSearchParams(currentSearch)
|
|
1513
|
-
|
|
1514
|
-
// Update namespaces param
|
|
1515
|
-
if (namespaces.length > 0) {
|
|
1516
|
-
params.set('namespaces', namespaces.join(','))
|
|
1517
|
-
} else {
|
|
1518
|
-
params.delete('namespaces')
|
|
1519
|
-
}
|
|
1520
|
-
// Remove legacy 'namespace' param if present
|
|
1521
|
-
params.delete('namespace')
|
|
1522
|
-
|
|
1523
|
-
// Topology-specific params: only set when on topology view, clean up otherwise
|
|
1524
|
-
if (mainView === 'topology') {
|
|
1525
|
-
if (topologyMode !== 'resources') {
|
|
1526
|
-
params.set('mode', topologyMode)
|
|
1527
|
-
} else {
|
|
1528
|
-
params.delete('mode')
|
|
1529
|
-
}
|
|
1530
|
-
if (groupingMode !== 'none' && (namespaces.length === 0 || groupingMode !== 'namespace')) {
|
|
1531
|
-
params.set('group', groupingMode)
|
|
1532
|
-
} else {
|
|
1533
|
-
params.delete('group')
|
|
1534
|
-
}
|
|
1535
|
-
} else {
|
|
1536
|
-
params.delete('mode')
|
|
1537
|
-
params.delete('group')
|
|
1538
|
-
}
|
|
1569
|
+
writeViewParams(params, namespaces, mainView === 'topology' ? { mode: topologyMode, grouping: groupingMode } : null)
|
|
1539
1570
|
|
|
1540
1571
|
// Only update if params actually changed vs current URL
|
|
1541
1572
|
if (params.toString() !== new URLSearchParams(currentSearch).toString()) {
|
|
@@ -1778,7 +1809,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1778
1809
|
setVisibleKinds(new Set())
|
|
1779
1810
|
}, [])
|
|
1780
1811
|
|
|
1781
|
-
const navActiveView = mainView === 'helmCompare' ? 'helm' :
|
|
1812
|
+
const navActiveView = mainView === 'helmCompare' ? 'helm' : activeWorkspace?.navView ?? mainView
|
|
1782
1813
|
|
|
1783
1814
|
return (
|
|
1784
1815
|
<PortForwardProvider>
|
|
@@ -1821,7 +1852,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1821
1852
|
<ScopePill>
|
|
1822
1853
|
<ContextSwitcher ref={contextSwitcherRef} variant="segment" />
|
|
1823
1854
|
<NamespaceSwitcher
|
|
1855
|
+
key={namespacePickerFlash}
|
|
1824
1856
|
ref={namespaceSwitcherRef}
|
|
1857
|
+
className={namespacePickerFlash ? 'reach-flash' : undefined}
|
|
1825
1858
|
variant="segment"
|
|
1826
1859
|
disabled={namespaceFilter.disabled}
|
|
1827
1860
|
disabledTooltip={namespaceFilter.tooltip}
|
|
@@ -2237,7 +2270,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2237
2270
|
)}
|
|
2238
2271
|
|
|
2239
2272
|
{/* Resources view */}
|
|
2240
|
-
{
|
|
2273
|
+
{/* Bare /resources waits for App's redirect to the last kind; mounted, the view would redirect to Pods first. */}
|
|
2274
|
+
{mainView === 'resources' && (location.pathname.replace(/\/+$/, '') || '/') !== '/resources' && (
|
|
2241
2275
|
<ResourcesView
|
|
2242
2276
|
namespaces={namespaces}
|
|
2243
2277
|
selectedResource={routeSelectedResource}
|
|
@@ -2245,6 +2279,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2245
2279
|
onResourceClickYaml={(res) => navigateToResource(res, 'yaml')}
|
|
2246
2280
|
onKindChange={() => setSelectedResource(null)}
|
|
2247
2281
|
onClearNamespaces={clearAllNamespaces}
|
|
2282
|
+
rememberedKindSearch={rememberedKindSearch}
|
|
2283
|
+
onKindFiltersChange={saveKindFilters}
|
|
2284
|
+
// Embedded hosts own their namespace picker, and a cache-scoped
|
|
2285
|
+
// Radar can't take an arbitrary set.
|
|
2286
|
+
onApplyNamespacesToAllSections={namespaceScope?.cacheScoped || !kindFilterStore ? undefined : applyNamespacesToAllSections}
|
|
2248
2287
|
/>
|
|
2249
2288
|
)}
|
|
2250
2289
|
|
|
@@ -2264,6 +2303,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2264
2303
|
setNamespaces([ns])
|
|
2265
2304
|
setActiveNamespace.mutate({ namespaces: [ns] })
|
|
2266
2305
|
}}
|
|
2306
|
+
onOpenDiagnostics={() => { setDiagnosticsFocus('timeline'); setShowDiagnostics(true) }}
|
|
2267
2307
|
/>
|
|
2268
2308
|
)}
|
|
2269
2309
|
|
|
@@ -2328,12 +2368,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2328
2368
|
<CostView namespaces={namespaces} onBack={() => setMainView('home')} onOpenResource={navigateToResource} />
|
|
2329
2369
|
)}
|
|
2330
2370
|
|
|
2331
|
-
{!viewsSyncGated &&
|
|
2332
|
-
<
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
{!viewsSyncGated && mainView === 'cnpg' && (
|
|
2336
|
-
<CNPGView
|
|
2371
|
+
{!viewsSyncGated && activeWorkspace && (
|
|
2372
|
+
<WorkspaceScreen
|
|
2373
|
+
workspace={activeWorkspace}
|
|
2337
2374
|
namespaces={namespaces}
|
|
2338
2375
|
selectedResource={routeSelectedResource}
|
|
2339
2376
|
onOpenResource={navigateToResource}
|
|
@@ -2372,6 +2409,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2372
2409
|
<IssuesPane
|
|
2373
2410
|
namespaces={namespaces}
|
|
2374
2411
|
onNavigateToResource={navigateFromIssue}
|
|
2412
|
+
showNamespace={issuesShowNamespace}
|
|
2375
2413
|
/>
|
|
2376
2414
|
)}
|
|
2377
2415
|
|
|
@@ -2410,10 +2448,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2410
2448
|
onNavigate={(res) => navigateToResource(res)}
|
|
2411
2449
|
canCollapseToDrawer={!isMobile}
|
|
2412
2450
|
onExpand={(res, opts) => {
|
|
2413
|
-
const
|
|
2414
|
-
if (
|
|
2451
|
+
const detailPath = resourceExpandedPath(res, connection.context || undefined, opts?.yaml ? 'yaml' : undefined)
|
|
2452
|
+
if (detailPath) {
|
|
2415
2453
|
navigate(
|
|
2416
|
-
|
|
2454
|
+
detailPath,
|
|
2417
2455
|
{ state: { returnLabel: currentPageLabel(), returnCtx: connection.context } },
|
|
2418
2456
|
)
|
|
2419
2457
|
return
|
|
@@ -2541,7 +2579,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2541
2579
|
{helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
|
|
2542
2580
|
|
|
2543
2581
|
{/* Diagnostics overlay */}
|
|
2544
|
-
{diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
|
|
2582
|
+
{diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} focus={diagnosticsFocus} onClose={() => { setShowDiagnostics(false); setDiagnosticsFocus(undefined) }} />}
|
|
2545
2583
|
|
|
2546
2584
|
{/* Settings dialog — My permissions is rendered inline in its own section */}
|
|
2547
2585
|
<SettingsDialog
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ApiError } from './client'
|
|
3
|
+
import { cnpgActionErrorCode } from './cnpg'
|
|
4
|
+
import { actionCompleted, actionOutcomeLocked } from './actions'
|
|
5
|
+
|
|
6
|
+
describe('cnpgActionErrorCode', () => {
|
|
7
|
+
it('keeps the server code when there is one', () => {
|
|
8
|
+
expect(cnpgActionErrorCode(new ApiError('changed', 409, { code: 'changed' }))).toBe('changed')
|
|
9
|
+
})
|
|
10
|
+
it('treats a lost connection or an apiserver timeout as an unknown outcome', () => {
|
|
11
|
+
expect(cnpgActionErrorCode(new TypeError('Failed to fetch'))).toBe('outcome_unknown')
|
|
12
|
+
expect(cnpgActionErrorCode(new ApiError('timeout', 504))).toBe('outcome_unknown')
|
|
13
|
+
expect(cnpgActionErrorCode(new ApiError('internal', 500))).toBe('outcome_unknown')
|
|
14
|
+
})
|
|
15
|
+
it('treats an uncoded 503 as unknown: a proxy may have forwarded the write before the upstream reset', () => {
|
|
16
|
+
const reset = new ApiError('upstream connect error or disconnect/reset before headers', 503)
|
|
17
|
+
expect(cnpgActionErrorCode(reset)).toBe('outcome_unknown')
|
|
18
|
+
expect(actionOutcomeLocked(reset)).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
it('treats refusals before any write as known failures', () => {
|
|
21
|
+
expect(cnpgActionErrorCode(new ApiError('webhook down', 503, { code: 'operator_webhook_unavailable' }))).toBe('operator_webhook_unavailable')
|
|
22
|
+
expect(actionOutcomeLocked(new ApiError('webhook down', 503, { code: 'operator_webhook_unavailable' }))).toBe(false)
|
|
23
|
+
expect(actionOutcomeLocked(new ApiError('Not connected to cluster', 503, { error: 'Not connected to cluster', code: 'unavailable' }))).toBe(false)
|
|
24
|
+
expect(cnpgActionErrorCode(new ApiError('forbidden', 403))).toBeUndefined()
|
|
25
|
+
expect(cnpgActionErrorCode(null)).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('partial outcomes', () => {
|
|
30
|
+
const partial = new ApiError('stopped', 403, { code: 'partial', completed: ['deleted PVC pg-2', 'deleted PVC pg-2-wal'] })
|
|
31
|
+
it('locks confirm and lists what already happened, whatever the status', () => {
|
|
32
|
+
expect(cnpgActionErrorCode(partial)).toBe('partial')
|
|
33
|
+
expect(actionOutcomeLocked(partial)).toBe(true)
|
|
34
|
+
expect(actionCompleted(partial)).toEqual(['deleted PVC pg-2', 'deleted PVC pg-2-wal'])
|
|
35
|
+
})
|
|
36
|
+
it('leaves known refusals unlocked', () => {
|
|
37
|
+
expect(actionOutcomeLocked(new ApiError('changed', 409, { code: 'changed' }))).toBe(false)
|
|
38
|
+
expect(actionCompleted(new ApiError('changed', 409, { code: 'changed' }))).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { capabilityReason, actionErrorCode, actionOutcomeLocked, actionDialogResult } from './actions'
|
|
3
|
+
import { ApiError } from './client'
|
|
4
|
+
import { RadarFeatureUnsupportedError } from './radarFeatures'
|
|
5
|
+
|
|
6
|
+
describe('capabilityReason', () => {
|
|
7
|
+
it('says nothing for an allowed action, and the server reason when there is one', () => {
|
|
8
|
+
expect(capabilityReason({ allowed: true, permission: 'allowed' })).toBeUndefined()
|
|
9
|
+
expect(capabilityReason({ allowed: false, permission: 'allowed', reason: 'The Cluster is hibernated' })).toBe('The Cluster is hibernated')
|
|
10
|
+
})
|
|
11
|
+
it('names the missing grant when the reason is absent', () => {
|
|
12
|
+
expect(capabilityReason({ allowed: false, permission: 'denied', grant: { verb: 'create', group: 'postgresql.cnpg.io', resource: 'backups', namespace: 'db' } })).toBe(
|
|
13
|
+
'Your account may not do this (create backups (postgresql.cnpg.io) in namespace db)',
|
|
14
|
+
)
|
|
15
|
+
expect(capabilityReason({ allowed: false, permission: 'unknown' })).toBe('Not available')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('does not call a pre-request feature refusal an unknown write outcome', () => {
|
|
20
|
+
const error = new RadarFeatureUnsupportedError('cnpgWorkspace', {})
|
|
21
|
+
expect(actionErrorCode(error)).toBeUndefined()
|
|
22
|
+
expect(actionOutcomeLocked(error)).toBe(false)
|
|
23
|
+
expect(actionDialogResult(error)?.state).toBe('error')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('actionDialogResult', () => {
|
|
27
|
+
it.each([
|
|
28
|
+
[new ApiError('Review changed', 409, { code: 'changed' }), 'error'],
|
|
29
|
+
[new ApiError('upstream connect error or disconnect/reset before headers', 503), 'unknown'],
|
|
30
|
+
[new ApiError('The operator webhook did not answer', 503, { code: 'operator_webhook_unavailable' }), 'error'],
|
|
31
|
+
[new ApiError('Gateway timeout', 504), 'unknown'],
|
|
32
|
+
[new Error('Connection lost'), 'unknown'],
|
|
33
|
+
[new ApiError('Write result missing', 500, { code: 'outcome_unknown' }), 'unknown'],
|
|
34
|
+
[new ApiError('One volume removed', 409, { code: 'partial', completed: ['delete volume'] }), 'partial'],
|
|
35
|
+
] as const)('keeps the result classification for %s', (error, state) => {
|
|
36
|
+
expect(actionDialogResult(error)?.state).toBe(state)
|
|
37
|
+
expect(actionDialogResult(error)?.message).toBe(error.message)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('selects the heading belonging to the result and retains no empty result', () => {
|
|
41
|
+
const titles = { error: 'Review could not be loaded', unknown: 'The Cluster may have changed', partial: 'Earlier writes partially completed' }
|
|
42
|
+
expect(actionDialogResult(new ApiError('Refused', 409), titles)?.title).toBe(titles.error)
|
|
43
|
+
expect(actionDialogResult(new Error('Connection lost'), titles)?.title).toBe(titles.unknown)
|
|
44
|
+
expect(actionDialogResult(new ApiError('Partial', 409, { code: 'partial' }), titles)?.title).toBe(titles.partial)
|
|
45
|
+
expect(actionDialogResult(new ApiError('Partial', 409, { code: 'partial' }), { unknown: 'May have changed' })?.title).toBeUndefined()
|
|
46
|
+
expect(actionDialogResult(null)).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
})
|