@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useLocation } from 'react-router-dom'
|
|
3
|
+
import { PaneLoader, type WorkloadExtraTab } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { useRadarFeature } from '../api/client'
|
|
5
|
+
import type { RenderDiagnoseAction } from '../context/DiagnoseCustomization'
|
|
6
|
+
import { apiVersionToGroup } from '../utils/navigation'
|
|
7
|
+
import type { SelectedResource } from '../types'
|
|
8
|
+
import { cnpgResourceHost } from '../components/cnpg/host'
|
|
9
|
+
import {
|
|
10
|
+
composeDetailSlots, composeResourceHosts, hostsForResource, hostSupport, resourceHostForSlot,
|
|
11
|
+
type DetailSlots, type HostResource, type HostFeatures, type HostKind, type KindListProps,
|
|
12
|
+
type ResourceHost,
|
|
13
|
+
} from './resourceHost'
|
|
14
|
+
import { RayJobRenderer } from '../components/resources/renderers/RayJobRenderer'
|
|
15
|
+
import { JobRenderer, JobSetRenderer } from '../components/resources/renderers/JobAdmissionRenderers'
|
|
16
|
+
import { RayClusterRenderer } from '../components/resources/renderers/RayClusterRenderer'
|
|
17
|
+
import { RayServiceRenderer } from '../components/resources/renderers/RayServiceRenderer'
|
|
18
|
+
import { KueueWorkloadRenderer } from '../components/resources/renderers/KueueWorkloadRenderer'
|
|
19
|
+
import { PodRenderer } from '../components/resources/renderers/PodRenderer'
|
|
20
|
+
import { KarpenterNodePoolRenderer } from '../components/resources/renderers/KarpenterNodePoolRenderer'
|
|
21
|
+
import { NodeRenderer } from '../components/resources/renderers/NodeRenderer'
|
|
22
|
+
import { ServiceRenderer } from '../components/resources/renderers/ServiceRenderer'
|
|
23
|
+
import { WorkloadRenderer } from '../components/resources/renderers/WorkloadRenderer'
|
|
24
|
+
import { CompositeRenderer } from '../components/resources/CompositeRenderer'
|
|
25
|
+
import { ServiceAccountRenderer } from '../components/resources/renderers/ServiceAccountRenderer'
|
|
26
|
+
import { RoleRenderer } from '../components/resources/renderers/RoleRenderer'
|
|
27
|
+
import { RoleBindingRenderer } from '../components/resources/renderers/RoleBindingRenderer'
|
|
28
|
+
import { NamespaceRenderer } from '../components/resources/renderers/NamespaceRenderer'
|
|
29
|
+
import { CAPIClusterRenderer } from '../components/resources/renderers/CAPIClusterRenderer'
|
|
30
|
+
import { HPARenderer } from '../components/resources/renderers/HPARenderer'
|
|
31
|
+
import { PVCRenderer } from '../components/resources/renderers/PVCRenderer'
|
|
32
|
+
import { RolloutRenderer } from '../components/resources/renderers/RolloutRenderer'
|
|
33
|
+
import { KyvernoPolicyCoverage } from '../components/resources/renderers/KyvernoPolicyCoverage'
|
|
34
|
+
import { KyvernoPolicyQueued } from '../components/resources/renderers/KyvernoPolicyQueued'
|
|
35
|
+
import { VeleroBSLRenderer } from '../components/resources/renderers/VeleroBSLRenderer'
|
|
36
|
+
import { VeleroBackupRenderer } from '../components/resources/renderers/VeleroBackupRenderer'
|
|
37
|
+
import { VeleroRestoreRenderer } from '../components/resources/renderers/VeleroRestoreRenderer'
|
|
38
|
+
|
|
39
|
+
export const resourceHosts: readonly ResourceHost[] = [
|
|
40
|
+
{ id: 'kubernetes', renderers: { PodRenderer, NodeRenderer, ServiceRenderer, WorkloadRenderer, HPARenderer, PVCRenderer } },
|
|
41
|
+
{ id: 'rbac', renderers: { ServiceAccountRenderer, RoleRenderer, RoleBindingRenderer, NamespaceRenderer } },
|
|
42
|
+
{ id: 'batch-and-admission', renderers: { JobRenderer, JobSetRenderer, KueueWorkloadRenderer } },
|
|
43
|
+
{ id: 'ray', renderers: { RayJobRenderer, RayServiceRenderer, RayClusterRenderer } },
|
|
44
|
+
{ id: 'capacity', renderers: { KarpenterNodePoolRenderer } },
|
|
45
|
+
{ id: 'capi', renderers: { CAPIClusterRenderer } },
|
|
46
|
+
{ id: 'crossplane', renderers: { CompositeRenderer } },
|
|
47
|
+
{ id: 'rollouts', renderers: { RolloutRenderer } },
|
|
48
|
+
{ id: 'kyverno', renderers: { KyvernoPolicyCoverage, KyvernoPolicyQueued } },
|
|
49
|
+
{ id: 'velero', renderers: { VeleroBSLRenderer, VeleroBackupRenderer, VeleroRestoreRenderer } },
|
|
50
|
+
cnpgResourceHost,
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
export const resourceRendererOverrides = composeResourceHosts(resourceHosts)
|
|
54
|
+
|
|
55
|
+
export function resourceExpandedPath(resource: HostResource, context?: string, tab?: 'yaml'): string | null {
|
|
56
|
+
return resourceHostForSlot(resourceHosts, resource, 'destination')?.expandedPath?.(resource, context, tab) ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resourceDetailRedirect(resource: HostResource, search: URLSearchParams): string | null {
|
|
60
|
+
return resourceHostForSlot(resourceHosts, resource, 'destination')?.detailRedirect?.(resource, search) ?? null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resourceDetailSlots(resource: HostResource, inheritedTabs?: WorkloadExtraTab[]): DetailSlots {
|
|
64
|
+
const slots = hostsForResource(resourceHosts, resource)
|
|
65
|
+
.map(host => ({ id: host.id, slots: host.detailSlots?.(resource) }))
|
|
66
|
+
return composeDetailSlots(slots, inheritedTabs)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resourceLogs(resource: HostResource, data: { apiVersion?: string } | null | undefined) {
|
|
70
|
+
const observed = { ...resource, group: data?.apiVersion ? apiVersionToGroup(data.apiVersion) : resource.group }
|
|
71
|
+
const host = resourceHostForSlot(resourceHosts, observed, 'logs')
|
|
72
|
+
return host?.logs?.(observed, data) ?? null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ResourceDrawerNavigation({ resource }: { resource: SelectedResource }) {
|
|
76
|
+
const { pathname } = useLocation()
|
|
77
|
+
return <>{resourceHosts.map(host => {
|
|
78
|
+
const navigation = host.drawerNavigation
|
|
79
|
+
if (!navigation?.ownsPath(pathname)) return null
|
|
80
|
+
const Component = navigation.Component
|
|
81
|
+
return <Component key={host.id} resource={resource} />
|
|
82
|
+
})}</>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useResourceHostFeatures(): HostFeatures {
|
|
86
|
+
// Hook order is fixed here; selected adapters mount components that own their data hooks.
|
|
87
|
+
const { support } = useRadarFeature('cnpgWorkspace')
|
|
88
|
+
return useMemo(() => ({ cnpgWorkspace: support }), [support])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
export function decorateResourceDiagnose(render: RenderDiagnoseAction | undefined): RenderDiagnoseAction | undefined {
|
|
93
|
+
return resourceHosts.reduce((current, host) => host.diagnoseAction?.(current) ?? current, render)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
function kindListHost(kind: Pick<HostKind, 'name' | 'group'> | null) {
|
|
98
|
+
return kind ? resourceHosts.flatMap(host => (host.kindLists ?? []).map(list => ({ host, list })))
|
|
99
|
+
.find(({ list }) => list.kind.name === kind.name && list.kind.group === kind.group) : undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resourceKindListMode(kind: Pick<HostKind, 'name' | 'group'> | null, features: HostFeatures, pending: boolean) {
|
|
103
|
+
const contribution = kindListHost(kind)
|
|
104
|
+
return contribution?.list.mode(hostSupport(contribution.host, features), pending) ?? 'table'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function resourceKindListTitle(kind: Pick<HostKind, 'name' | 'group'>): string | null {
|
|
108
|
+
return kindListHost(kind)?.list.title ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function renderResourceKindList(kind: Pick<HostKind, 'name' | 'group'> | null, features: HostFeatures, pending: boolean, props: KindListProps) {
|
|
112
|
+
const contribution = kindListHost(kind)
|
|
113
|
+
if (!contribution) return null
|
|
114
|
+
const mode = contribution.list.mode(hostSupport(contribution.host, features), pending)
|
|
115
|
+
if (mode === 'wait') return <PaneLoader label="Loading…" className="flex-1" />
|
|
116
|
+
if (mode === 'table') return null
|
|
117
|
+
const Component = contribution.list.Component
|
|
118
|
+
return <Component {...props} />
|
|
119
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SelectedResource } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceScreenProps {
|
|
4
|
+
namespaces: string[]
|
|
5
|
+
selectedResource: SelectedResource | null
|
|
6
|
+
onOpenResource: (resource: SelectedResource) => void
|
|
7
|
+
onCloseResource: () => void
|
|
8
|
+
onClearNamespaces: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkspaceRoute {
|
|
12
|
+
id: 'cnpg' | 'capacity'
|
|
13
|
+
path: string
|
|
14
|
+
label: string
|
|
15
|
+
navView: 'resources' | 'capacity'
|
|
16
|
+
namespaceScope: 'view' | 'cluster'
|
|
17
|
+
namespaceFilterTooltip?: string
|
|
18
|
+
pageTitle: (pathname: string) => string
|
|
19
|
+
contextSwitchSearch?: (pathname: string, search: URLSearchParams) => URLSearchParams
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { workspaceContextSwitchSearch, workspaceForPath, workspaceForView } from './workspaceRoutes'
|
|
3
|
+
|
|
4
|
+
describe('workspace route ownership', () => {
|
|
5
|
+
it('preserves distinct placement, scope, route parsers and title/breadcrumb labels', () => {
|
|
6
|
+
const cnpg = workspaceForPath('/cnpg/backups/db/backup%2F1')!
|
|
7
|
+
expect(cnpg.id).toBe('cnpg')
|
|
8
|
+
expect(cnpg.navView).toBe('resources')
|
|
9
|
+
expect(cnpg.namespaceScope).toBe('view')
|
|
10
|
+
expect(cnpg.pageTitle('/cnpg/backups/db/backup%2F1')).toBe('backup/1')
|
|
11
|
+
expect(cnpg.pageTitle('/cnpg/protection')).toBe('CloudNativePG Backups')
|
|
12
|
+
expect(cnpg.pageTitle('/cnpg/unknown')).toBe('CloudNativePG Clusters')
|
|
13
|
+
const capacity = workspaceForView('capacity')!
|
|
14
|
+
expect(capacity.navView).toBe('capacity')
|
|
15
|
+
expect(capacity.namespaceScope).toBe('cluster')
|
|
16
|
+
expect(capacity.pageTitle('/capacity/pools/pool%2Fone/members')).toBe('pool/one')
|
|
17
|
+
expect(capacity.pageTitle('/capacity/demand')).toBe('Capacity Demand')
|
|
18
|
+
expect(capacity.pageTitle('/capacity/activity')).toBe('Capacity Activity')
|
|
19
|
+
expect(capacity.pageTitle('/capacity/unknown')).toBe('Capacity')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('only owns whole route prefixes inside either OSS or an embedded router basename', () => {
|
|
23
|
+
expect(workspaceForPath('/cnpg')?.id).toBe('cnpg')
|
|
24
|
+
expect(workspaceForPath('/cnpg/')).toBeDefined()
|
|
25
|
+
expect(workspaceForPath('/cnpg-other')).toBeUndefined()
|
|
26
|
+
expect(workspaceForPath('/capacity-other')).toBeUndefined()
|
|
27
|
+
expect(workspaceForPath('/workload/clusters/db/pg')).toBeUndefined()
|
|
28
|
+
// React Router supplies basename-relative locations to the application.
|
|
29
|
+
expect(workspaceForPath('/c/demo/cnpg')).toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('pins only CNPG detail context on a switch, without adopting another workspace’s query policy', () => {
|
|
33
|
+
const search = new URLSearchParams('ctx=old-cluster&tab=storage&drawer=pod&namespaces=db&ai-run=run')
|
|
34
|
+
expect(workspaceContextSwitchSearch('/cnpg/clusters/db/pg', search).toString()).toBe('ctx=old-cluster')
|
|
35
|
+
expect(workspaceContextSwitchSearch('/cnpg', search).toString()).toBe('')
|
|
36
|
+
expect(workspaceContextSwitchSearch('/capacity/pools/default', search).toString()).toBe('')
|
|
37
|
+
expect(workspaceContextSwitchSearch('/resources/pods', search).toString()).toBe('')
|
|
38
|
+
expect(search.get('tab')).toBe('storage')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { capacityWorkspaceRoute } from '../components/capacity/workspaceRoute'
|
|
2
|
+
import { cnpgWorkspaceRoute } from '../components/cnpg/workspaceRoute'
|
|
3
|
+
import type { WorkspaceRoute } from './workspaceRoute'
|
|
4
|
+
|
|
5
|
+
export const workspaceRoutes = [cnpgWorkspaceRoute, capacityWorkspaceRoute] as const
|
|
6
|
+
|
|
7
|
+
export const workspaceLabels = {
|
|
8
|
+
cnpg: cnpgWorkspaceRoute.label,
|
|
9
|
+
capacity: capacityWorkspaceRoute.label,
|
|
10
|
+
} satisfies Record<WorkspaceRoute['id'], string>
|
|
11
|
+
|
|
12
|
+
export function workspaceForPath(pathname: string) {
|
|
13
|
+
return workspaceRoutes.find(route => pathname === route.path || pathname.startsWith(`${route.path}/`))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function workspaceForView(view: string) {
|
|
17
|
+
return workspaceRoutes.find(route => route.id === view)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function workspaceContextSwitchSearch(pathname: string, search: URLSearchParams): URLSearchParams {
|
|
21
|
+
return workspaceForPath(pathname)?.contextSwitchSearch?.(pathname, search) ?? new URLSearchParams()
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
import { CapacityView } from '../components/capacity/CapacityView'
|
|
3
|
+
import { CNPGView } from '../components/cnpg/CNPGView'
|
|
4
|
+
import type { WorkspaceRoute, WorkspaceScreenProps } from './workspaceRoute'
|
|
5
|
+
|
|
6
|
+
const screens: Record<WorkspaceRoute['id'], ComponentType<WorkspaceScreenProps>> = {
|
|
7
|
+
cnpg: CNPGView,
|
|
8
|
+
capacity: ({ onOpenResource }) => <CapacityView onOpenResource={onOpenResource} />,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function WorkspaceScreen({ workspace, ...props }: WorkspaceScreenProps & { workspace: WorkspaceRoute }) {
|
|
12
|
+
const Component = screens[workspace.id]
|
|
13
|
+
return <Component {...props} />
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SelectedResource } from '../types'
|
|
2
|
+
|
|
3
|
+
// Drawer identity in the URL: kind:group:namespace:name, chained with "~" for
|
|
4
|
+
// the in-drawer trail (last entry is the one shown). Kubernetes names and API
|
|
5
|
+
// groups cannot contain ":" or "~", and the group is mandatory: CRD kinds
|
|
6
|
+
// collide across groups (CloudNativePG's Cluster and Backup with CAPI,
|
|
7
|
+
// KubeBlocks and Velero kinds).
|
|
8
|
+
export function encodeDrawerRef(r: SelectedResource): string {
|
|
9
|
+
return [r.kind, r.group ?? '', r.namespace ?? '', r.name].join(':')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decodeDrawerRef(s: string): SelectedResource | null {
|
|
13
|
+
const parts = s.split(':')
|
|
14
|
+
if (parts.length !== 4 || !parts[0] || !parts[3]) return null
|
|
15
|
+
return { kind: parts[0], group: parts[1], namespace: parts[2], name: parts[3] }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function decodeDrawerTrail(param: string | null): SelectedResource[] {
|
|
19
|
+
if (!param) return []
|
|
20
|
+
return param.split('~').map(decodeDrawerRef).filter((r): r is SelectedResource => r !== null)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function encodeDrawerTrail(trail: SelectedResource[]): string {
|
|
24
|
+
return trail.map(encodeDrawerRef).join('~')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sameSelectedResource(a: SelectedResource | null | undefined, b: SelectedResource | null | undefined): boolean {
|
|
28
|
+
if (!a || !b) return false
|
|
29
|
+
return (
|
|
30
|
+
a.kind.toLowerCase() === b.kind.toLowerCase() &&
|
|
31
|
+
(a.group ?? '') === (b.group ?? '') &&
|
|
32
|
+
(a.namespace ?? '') === (b.namespace ?? '') &&
|
|
33
|
+
a.name === b.name
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createKindFilterStore } from './kindFilterStore'
|
|
3
|
+
|
|
4
|
+
const storage = new Map<string, string>()
|
|
5
|
+
vi.stubGlobal('localStorage', {
|
|
6
|
+
getItem: (k: string) => storage.get(k) ?? null,
|
|
7
|
+
setItem: (k: string, v: string) => void storage.set(k, v),
|
|
8
|
+
removeItem: (k: string) => void storage.delete(k),
|
|
9
|
+
get length() { return storage.size },
|
|
10
|
+
key: (i: number) => [...storage.keys()][i] ?? null,
|
|
11
|
+
})
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
storage.clear()
|
|
14
|
+
vi.stubGlobal('sessionStorage', undefined)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function fakeSessionStorage() {
|
|
18
|
+
const items = new Map<string, string>()
|
|
19
|
+
return {
|
|
20
|
+
getItem: (k: string) => items.get(k) ?? null,
|
|
21
|
+
setItem: (k: string, v: string) => void items.set(k, v),
|
|
22
|
+
get length() { return items.size },
|
|
23
|
+
key: (i: number) => [...items.keys()][i] ?? null,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('createKindFilterStore', () => {
|
|
28
|
+
it('keeps each cluster and each API group apart', () => {
|
|
29
|
+
const a = createKindFilterStore('/api|ctx-a')
|
|
30
|
+
const b = createKindFilterStore('/api|ctx-b')
|
|
31
|
+
a.set('services', '', 'filters=type%3AClusterIP')
|
|
32
|
+
a.set('services', 'serving.knative.dev', 'problems=failed')
|
|
33
|
+
|
|
34
|
+
expect(a.get('services', '')).toBe('filters=type%3AClusterIP')
|
|
35
|
+
expect(a.get('services', 'serving.knative.dev')).toBe('problems=failed')
|
|
36
|
+
expect(b.get('services', '')).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('removes the entry when the filters are cleared', () => {
|
|
40
|
+
const store = createKindFilterStore('/api|ctx')
|
|
41
|
+
store.set('pods', '', 'problems=failed')
|
|
42
|
+
store.set('pods', '', '')
|
|
43
|
+
|
|
44
|
+
expect(store.get('pods', '')).toBeUndefined()
|
|
45
|
+
expect(storage.size).toBe(0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('remembers the last Resources list per cluster, with its API group only', () => {
|
|
49
|
+
const a = createKindFilterStore('/api|ctx-a')
|
|
50
|
+
a.recordLastKind('/resources/services', '?apiGroup=serving.knative.dev&filters=x&namespaces=n')
|
|
51
|
+
a.recordLastKind('/resources', '')
|
|
52
|
+
a.recordLastKind('/helm', '')
|
|
53
|
+
|
|
54
|
+
expect(a.lastKind()).toBe('/resources/services?apiGroup=serving.knative.dev')
|
|
55
|
+
expect(createKindFilterStore('/api|ctx-b').lastKind()).toBeUndefined()
|
|
56
|
+
|
|
57
|
+
a.recordLastKind('/resources/pods/', '?problems=failed')
|
|
58
|
+
expect(a.lastKind()).toBe('/resources/pods')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('keeps a tab on its own filters while another tab saves different ones', () => {
|
|
62
|
+
const tabA = createKindFilterStore('/api|ctx')
|
|
63
|
+
const tabB = createKindFilterStore('/api|ctx')
|
|
64
|
+
tabA.set('pods', '', 'filters=namespace%3Aa')
|
|
65
|
+
tabB.set('pods', '', 'filters=namespace%3Ab')
|
|
66
|
+
|
|
67
|
+
expect(tabA.get('pods', '')).toBe('filters=namespace%3Aa')
|
|
68
|
+
expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Ab')
|
|
69
|
+
|
|
70
|
+
tabA.set('pods', '', '')
|
|
71
|
+
tabB.set('pods', '', 'filters=namespace%3Ac')
|
|
72
|
+
expect(tabA.get('pods', '')).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('keeps what a tab restored when another tab saves later', () => {
|
|
76
|
+
createKindFilterStore('/api|ctx').set('pods', '', 'filters=namespace%3Aa')
|
|
77
|
+
const tabA = createKindFilterStore('/api|ctx')
|
|
78
|
+
expect(tabA.get('pods', '')).toBe('filters=namespace%3Aa')
|
|
79
|
+
expect(tabA.get('services', '')).toBeUndefined()
|
|
80
|
+
|
|
81
|
+
const tabB = createKindFilterStore('/api|ctx')
|
|
82
|
+
tabB.set('pods', '', 'filters=namespace%3Ab')
|
|
83
|
+
tabB.set('services', '', 'filters=type%3AClusterIP')
|
|
84
|
+
|
|
85
|
+
expect(tabA.get('pods', '')).toBe('filters=namespace%3Aa')
|
|
86
|
+
expect(tabA.get('services', '')).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('clears one column on every kind of the cluster, in the tab and in storage', () => {
|
|
90
|
+
const tab = createKindFilterStore('/api|ctx')
|
|
91
|
+
tab.set('pods', '', 'filters=namespace%3Aa%7Cstatus%3ARunning&problems=failed')
|
|
92
|
+
tab.set('services', '', 'filters=namespace%3Aexclude%3Ab')
|
|
93
|
+
tab.set('deployments', '', 'problems=failed')
|
|
94
|
+
// Another tab saved this kind; this tab never read it.
|
|
95
|
+
createKindFilterStore('/api|ctx').set('configmaps', '', 'filters=namespace%3Ac')
|
|
96
|
+
const otherCluster = createKindFilterStore('/api|ctx:2')
|
|
97
|
+
otherCluster.set('pods', '', 'filters=namespace%3Aa')
|
|
98
|
+
|
|
99
|
+
tab.clearColumn('namespace')
|
|
100
|
+
|
|
101
|
+
expect(tab.get('pods', '')).toBe('filters=status%3ARunning&problems=failed')
|
|
102
|
+
expect(tab.get('services', '')).toBeUndefined()
|
|
103
|
+
expect(tab.get('deployments', '')).toBe('problems=failed')
|
|
104
|
+
expect(tab.get('configmaps', '')).toBeUndefined()
|
|
105
|
+
const fresh = createKindFilterStore('/api|ctx')
|
|
106
|
+
expect(fresh.get('pods', '')).toBe('filters=status%3ARunning&problems=failed')
|
|
107
|
+
expect(fresh.get('services', '')).toBeUndefined()
|
|
108
|
+
expect(fresh.get('configmaps', '')).toBeUndefined()
|
|
109
|
+
expect(createKindFilterStore('/api|ctx:2').get('pods', '')).toBe('filters=namespace%3Aa')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("keeps the tab's own filters on reload and in a duplicated tab; a new tab starts from the last saved", () => {
|
|
113
|
+
const tabA = fakeSessionStorage()
|
|
114
|
+
vi.stubGlobal('sessionStorage', tabA)
|
|
115
|
+
createKindFilterStore('/api|ctx').set('pods', '', 'filters=namespace%3Aa')
|
|
116
|
+
|
|
117
|
+
// Tab B saves a newer choice.
|
|
118
|
+
vi.stubGlobal('sessionStorage', fakeSessionStorage())
|
|
119
|
+
createKindFilterStore('/api|ctx').set('pods', '', 'filters=namespace%3Ab')
|
|
120
|
+
|
|
121
|
+
vi.stubGlobal('sessionStorage', tabA)
|
|
122
|
+
expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Aa')
|
|
123
|
+
|
|
124
|
+
const duplicate = fakeSessionStorage()
|
|
125
|
+
for (let i = 0; i < tabA.length; i++) duplicate.setItem(tabA.key(i)!, tabA.getItem(tabA.key(i)!)!)
|
|
126
|
+
vi.stubGlobal('sessionStorage', duplicate)
|
|
127
|
+
expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Aa')
|
|
128
|
+
|
|
129
|
+
vi.stubGlobal('sessionStorage', fakeSessionStorage())
|
|
130
|
+
expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Ab')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('remembers filters in the tab only when storage is unavailable', () => {
|
|
134
|
+
vi.stubGlobal('localStorage', {
|
|
135
|
+
getItem: () => { throw new Error('denied') },
|
|
136
|
+
setItem: () => { throw new Error('denied') },
|
|
137
|
+
removeItem: () => { throw new Error('denied') },
|
|
138
|
+
})
|
|
139
|
+
const store = createKindFilterStore('/api|ctx')
|
|
140
|
+
store.set('pods', '', 'problems=failed')
|
|
141
|
+
store.recordLastKind('/resources/pods', '')
|
|
142
|
+
|
|
143
|
+
expect(store.get('pods', '')).toBe('problems=failed')
|
|
144
|
+
expect(createKindFilterStore('/api|ctx').get('pods', '')).toBeUndefined()
|
|
145
|
+
expect(store.lastKind()).toBeUndefined()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { kindFiltersFromSearch, kindFiltersToSearch } from '../components/resources/resource-utils'
|
|
2
|
+
import { RESOURCE_LIST_PATH } from './navigation'
|
|
3
|
+
import { createTabStorage } from './tabStorage'
|
|
4
|
+
|
|
5
|
+
const FILTERS_PREFIX = 'radar-filters:'
|
|
6
|
+
const LAST_KIND_PREFIX = 'radar-last-kind:'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Per-cluster Resources filters (one query string per kind) and the last
|
|
10
|
+
* Resources kind, kept in localStorage so they survive reloads and new tabs.
|
|
11
|
+
* Storage can be unavailable (private mode, blocked site data): every access
|
|
12
|
+
* degrades to "nothing remembered".
|
|
13
|
+
*
|
|
14
|
+
* Filters are read from the tab's own copy first (sessionStorage: kept on
|
|
15
|
+
* reload, copied to a duplicated tab), so a tab keeps showing what happened
|
|
16
|
+
* in it while another tab saves different filters; localStorage only seeds a
|
|
17
|
+
* kind the first time the tab reads it.
|
|
18
|
+
*/
|
|
19
|
+
export function createKindFilterStore(cluster: string) {
|
|
20
|
+
const filtersKey = (plural: string, group: string) => `${FILTERS_PREFIX}${cluster}:${plural}/${group}`
|
|
21
|
+
const lastKindKey = LAST_KIND_PREFIX + cluster
|
|
22
|
+
const tab = createTabStorage()
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
get(plural: string, group: string): string | undefined {
|
|
26
|
+
const key = filtersKey(plural, group)
|
|
27
|
+
const own = tab.get(key)
|
|
28
|
+
if (own !== undefined) return own || undefined
|
|
29
|
+
let saved: string | undefined
|
|
30
|
+
try {
|
|
31
|
+
saved = localStorage.getItem(key) ?? undefined
|
|
32
|
+
} catch { /* unavailable: nothing saved */ }
|
|
33
|
+
// What the tab was given is now the tab's own, whatever other tabs save later.
|
|
34
|
+
tab.set(key, saved ?? '')
|
|
35
|
+
return saved
|
|
36
|
+
},
|
|
37
|
+
/** An empty search removes the entry. */
|
|
38
|
+
set(plural: string, group: string, search: string) {
|
|
39
|
+
tab.set(filtersKey(plural, group), search)
|
|
40
|
+
try {
|
|
41
|
+
if (search) localStorage.setItem(filtersKey(plural, group), search)
|
|
42
|
+
else localStorage.removeItem(filtersKey(plural, group))
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
},
|
|
45
|
+
/** Removes one column's filter from every kind of this cluster, in the tab and in localStorage. */
|
|
46
|
+
clearColumn(column: string) {
|
|
47
|
+
const withoutColumn = (search: string) => {
|
|
48
|
+
const filters = kindFiltersFromSearch(search)
|
|
49
|
+
delete filters.columnFilters[column]
|
|
50
|
+
delete filters.columnFilterExcludes[column]
|
|
51
|
+
return kindFiltersToSearch(filters)
|
|
52
|
+
}
|
|
53
|
+
const prefix = `${FILTERS_PREFIX}${cluster}:`
|
|
54
|
+
// The rest is `<plural>/<group>`, never a colon: guards against a context named `<cluster>:…`.
|
|
55
|
+
const ofCluster = (key: string | null): key is string => !!key?.startsWith(prefix) && !key.slice(prefix.length).includes(':')
|
|
56
|
+
for (const key of tab.keys().filter(ofCluster)) tab.set(key, withoutColumn(tab.get(key) ?? ''))
|
|
57
|
+
try {
|
|
58
|
+
const keys: string[] = []
|
|
59
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
60
|
+
const key = localStorage.key(i)
|
|
61
|
+
if (ofCluster(key)) keys.push(key)
|
|
62
|
+
}
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const next = withoutColumn(localStorage.getItem(key) ?? '')
|
|
65
|
+
if (next) localStorage.setItem(key, next)
|
|
66
|
+
else localStorage.removeItem(key)
|
|
67
|
+
}
|
|
68
|
+
} catch { /* ignore */ }
|
|
69
|
+
},
|
|
70
|
+
/** The last Resources list path (`/resources/<plural>[?apiGroup=…]`). */
|
|
71
|
+
lastKind(): string | undefined {
|
|
72
|
+
try {
|
|
73
|
+
return localStorage.getItem(lastKindKey) ?? undefined
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
/** Records a location; only a Resources list path counts. */
|
|
79
|
+
recordLastKind(pathname: string, search: string) {
|
|
80
|
+
const path = pathname.replace(/\/+$/, '')
|
|
81
|
+
if (!RESOURCE_LIST_PATH.test(path)) return
|
|
82
|
+
const group = new URLSearchParams(search).get('apiGroup')
|
|
83
|
+
const value = group ? `${path}?${new URLSearchParams({ apiGroup: group })}` : path
|
|
84
|
+
try {
|
|
85
|
+
localStorage.setItem(lastKindKey, value)
|
|
86
|
+
} catch { /* ignore */ }
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type KindFilterStore = ReturnType<typeof createKindFilterStore>
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
initNavigationMap,
|
|
5
5
|
resetNavigationMap,
|
|
6
6
|
} from '@skyhook-io/k8s-ui/utils/navigation'
|
|
7
|
-
import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath, withCrossViewParams } from './navigation'
|
|
7
|
+
import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath, withCrossViewParams, writeViewParams } from './navigation'
|
|
8
8
|
|
|
9
9
|
afterEach(resetNavigationMap)
|
|
10
10
|
|
|
@@ -76,3 +76,15 @@ describe('withCrossViewParams', () => {
|
|
|
76
76
|
expect(withCrossViewParams('/helm', '')).toBe('/helm')
|
|
77
77
|
})
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
describe('writeViewParams', () => {
|
|
81
|
+
it('keeps topology mode and grouping only on topology', () => {
|
|
82
|
+
const topology = new URLSearchParams('namespace=old&apiGroup=example.com')
|
|
83
|
+
writeViewParams(topology, [], { mode: 'traffic', grouping: 'app' })
|
|
84
|
+
expect(topology.toString()).toBe('apiGroup=example.com&mode=traffic&group=app')
|
|
85
|
+
|
|
86
|
+
const issues = new URLSearchParams('mode=traffic&group=app&kind=Widget&apiGroup=example.com&resource=ns%2Fw')
|
|
87
|
+
writeViewParams(issues, ['ns'], null)
|
|
88
|
+
expect(issues.toString()).toBe('kind=Widget&apiGroup=example.com&resource=ns%2Fw&namespaces=ns')
|
|
89
|
+
})
|
|
90
|
+
})
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
2
|
import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
3
3
|
import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
|
|
4
|
-
import type { SelectedResource, Topology } from '@skyhook-io/k8s-ui/types/core'
|
|
4
|
+
import type { GroupingMode, SelectedResource, Topology, TopologyMode } from '@skyhook-io/k8s-ui/types/core'
|
|
5
5
|
import type { SearchHit } from '../api/client'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -137,7 +137,44 @@ export function openExternal(url: string): void {
|
|
|
137
137
|
// setMainView's cross-view params. A navigation whose search omits
|
|
138
138
|
// ?namespaces= reads as an empty pick to App's URL sync, which clears the
|
|
139
139
|
// user's namespace scope.
|
|
140
|
-
const CROSS_VIEW_PARAMS = ['namespaces', 'ai-run'] as const
|
|
140
|
+
export const CROSS_VIEW_PARAMS = ['namespaces', 'ai-run'] as const
|
|
141
|
+
|
|
142
|
+
/** A Resources list route (`/resources/<plural>`), as opposed to bare `/resources` or a detail page. */
|
|
143
|
+
export const RESOURCE_LIST_PATH = /^\/resources\/[^/]+$/
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Writes App's view state into the query string. Topology's `mode` and
|
|
147
|
+
* `group` are dropped on every other view, so no other view can use those
|
|
148
|
+
* names for params of its own.
|
|
149
|
+
*/
|
|
150
|
+
export function writeViewParams(
|
|
151
|
+
params: URLSearchParams,
|
|
152
|
+
namespaces: string[],
|
|
153
|
+
topology: { mode: TopologyMode; grouping: GroupingMode } | null,
|
|
154
|
+
): void {
|
|
155
|
+
if (namespaces.length > 0) {
|
|
156
|
+
params.set('namespaces', namespaces.join(','))
|
|
157
|
+
} else {
|
|
158
|
+
params.delete('namespaces')
|
|
159
|
+
}
|
|
160
|
+
// Legacy single-namespace param
|
|
161
|
+
params.delete('namespace')
|
|
162
|
+
if (!topology) {
|
|
163
|
+
params.delete('mode')
|
|
164
|
+
params.delete('group')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (topology.mode !== 'resources') {
|
|
168
|
+
params.set('mode', topology.mode)
|
|
169
|
+
} else {
|
|
170
|
+
params.delete('mode')
|
|
171
|
+
}
|
|
172
|
+
if (topology.grouping !== 'none' && (namespaces.length === 0 || topology.grouping !== 'namespace')) {
|
|
173
|
+
params.set('group', topology.grouping)
|
|
174
|
+
} else {
|
|
175
|
+
params.delete('group')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
141
178
|
|
|
142
179
|
/** Carries the current cross-view params onto an in-app path that doesn't set them itself. */
|
|
143
180
|
export function withCrossViewParams(path: string, currentSearch: string): string {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The label for "← back" on the page a push lands on: the title of the page
|
|
3
|
+
* being left, which Radar keeps in the document title.
|
|
4
|
+
*/
|
|
5
|
+
export function currentPageLabel(): string {
|
|
6
|
+
return document.title.replace(/\s*·\s*Radar$/, '') || 'previous page'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Radar's Issues page narrowed to one subject (it has no link to a single issue).
|
|
11
|
+
* The subject travels as `resource=ns/name`, never `namespace=`: App reads a bare
|
|
12
|
+
* `namespace` as the view filter, and a URL without `namespaces` clears it.
|
|
13
|
+
*/
|
|
14
|
+
export function issuesPathForSubject(subject: { kind: string; group?: string; namespace: string; name: string }, viewNamespaces?: string | null): string {
|
|
15
|
+
const params = new URLSearchParams()
|
|
16
|
+
if (viewNamespaces) params.set('namespaces', viewNamespaces)
|
|
17
|
+
params.set('kind', subject.kind)
|
|
18
|
+
if (subject.group) params.set('apiGroup', subject.group)
|
|
19
|
+
params.set('resource', subject.namespace ? `${subject.namespace}/${subject.name}` : subject.name)
|
|
20
|
+
return `/issues?${params}`
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tab strings in sessionStorage: they survive a reload and are copied
|
|
3
|
+
* when the tab is duplicated, while a tab opened from scratch starts empty.
|
|
4
|
+
* Without sessionStorage (private mode, blocked site data) they live in
|
|
5
|
+
* memory, so a reload forgets them.
|
|
6
|
+
*/
|
|
7
|
+
export function createTabStorage() {
|
|
8
|
+
let session: Storage | undefined
|
|
9
|
+
try {
|
|
10
|
+
session = sessionStorage
|
|
11
|
+
session.getItem('')
|
|
12
|
+
} catch {
|
|
13
|
+
session = undefined
|
|
14
|
+
}
|
|
15
|
+
const memory = new Map<string, string>()
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
get(key: string): string | undefined {
|
|
19
|
+
return session ? session.getItem(key) ?? undefined : memory.get(key)
|
|
20
|
+
},
|
|
21
|
+
set(key: string, value: string) {
|
|
22
|
+
if (!session) {
|
|
23
|
+
memory.set(key, value)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
session.setItem(key, value)
|
|
28
|
+
} catch { /* over quota: keep the previous value */ }
|
|
29
|
+
},
|
|
30
|
+
keys(): string[] {
|
|
31
|
+
if (!session) return [...memory.keys()]
|
|
32
|
+
const keys: string[] = []
|
|
33
|
+
for (let i = 0; i < session.length; i++) {
|
|
34
|
+
const key = session.key(i)
|
|
35
|
+
if (key !== null) keys.push(key)
|
|
36
|
+
}
|
|
37
|
+
return keys
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|