@skyhook-io/radar-app 1.16.3 → 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 +138 -93
- package/src/RadarApp.tsx +27 -17
- 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.goneResource.test.ts +49 -0
- 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 +381 -47
- 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/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +65 -3
- package/src/api/radarFeatures.ts +6 -0
- 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/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/applications/ApplicationsView.tsx +3 -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 +24 -62
- package/src/components/capacity/CapacityView.test.tsx +354 -1
- 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 +23 -150
- 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 +230 -124
- 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/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- 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/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/ChartBrowser.tsx +3 -3
- 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/RevisionHistory.tsx +3 -2
- 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/ClusterHealthCard.tsx +7 -0
- 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/home/mcpToolCatalog.ts +1 -1
- 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/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +150 -34
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.tsx +25 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +25 -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/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -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 +11 -1
- package/src/components/workload/WorkloadView.tsx +162 -529
- 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/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- 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,256 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
getGitOpsResourceStatus,
|
|
4
|
+
gitOpsOwnerFromRelationships,
|
|
5
|
+
type GitOpsOwnerRef,
|
|
6
|
+
type GitOpsStatus,
|
|
7
|
+
type HelmOwnerRef,
|
|
8
|
+
} from '@skyhook-io/k8s-ui'
|
|
9
|
+
import type { Relationships, ResourceRef } from '../types'
|
|
10
|
+
import { kindToPluralWithGroup } from '../utils/navigation'
|
|
11
|
+
import { useResource, useResourceWithRelationships, useResources } from '../api/client'
|
|
12
|
+
|
|
13
|
+
export interface ResolvedGitOpsOwner {
|
|
14
|
+
/** GitOps owner, with an Argo Application's namespace filled in when the
|
|
15
|
+
* tracking label omitted it and exactly one Application matches. */
|
|
16
|
+
owner: GitOpsOwnerRef | null
|
|
17
|
+
/** Owner as reported by relationships, before namespace resolution. */
|
|
18
|
+
rawOwner: GitOpsOwnerRef | null
|
|
19
|
+
/** The owner CR (Application / Kustomization / HelmRelease). */
|
|
20
|
+
ownerObject: any
|
|
21
|
+
ownerPending: boolean
|
|
22
|
+
ownerVerified: boolean
|
|
23
|
+
/** The Argo CD tracking label named no namespace and Radar filled it in
|
|
24
|
+
* from a single Application of that name. */
|
|
25
|
+
ownerMatchedByName: boolean
|
|
26
|
+
ownerStatus: GitOpsStatus | null
|
|
27
|
+
/** The label/annotation that ties the object to its owner. */
|
|
28
|
+
ownerSource: string | null
|
|
29
|
+
helmOwner: HelmOwnerRef | null
|
|
30
|
+
helmSource: string | null
|
|
31
|
+
/** A parent (e.g. the Deployment of a ReplicaSet) consulted when the
|
|
32
|
+
* object carries no GitOps ownership itself. */
|
|
33
|
+
inheritedLookupRef: ResourceRef | null
|
|
34
|
+
lookupPending: boolean
|
|
35
|
+
lookupError: boolean
|
|
36
|
+
/** The parent was read but the server sent no relationships for it (its
|
|
37
|
+
* topology isn't built yet), so an inherited owner is unknown, not absent. */
|
|
38
|
+
relationshipsUnavailable: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolves who manages a resource: its own GitOps/Helm tracking metadata or,
|
|
42
|
+
// failing that, the metadata of the workload that owns it.
|
|
43
|
+
export function useResolvedGitOpsOwner({
|
|
44
|
+
kind,
|
|
45
|
+
group,
|
|
46
|
+
namespace,
|
|
47
|
+
name,
|
|
48
|
+
relationships,
|
|
49
|
+
resource,
|
|
50
|
+
enabled = true,
|
|
51
|
+
cacheScope,
|
|
52
|
+
}: {
|
|
53
|
+
kind: string
|
|
54
|
+
group?: string
|
|
55
|
+
namespace: string
|
|
56
|
+
name: string
|
|
57
|
+
relationships: Relationships | undefined
|
|
58
|
+
resource: any
|
|
59
|
+
enabled?: boolean
|
|
60
|
+
/** Keeps these reads apart from the shared resource cache (see useResource). */
|
|
61
|
+
cacheScope?: string
|
|
62
|
+
}): ResolvedGitOpsOwner {
|
|
63
|
+
const active = enabled && Boolean(relationships)
|
|
64
|
+
const relationshipGitopsOwner = useMemo(
|
|
65
|
+
() => (active ? gitOpsOwnerFromRelationships(relationships) : null),
|
|
66
|
+
[active, relationships],
|
|
67
|
+
)
|
|
68
|
+
const inheritedLookupRef = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
active
|
|
71
|
+
? findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, { kind, namespace, name, group })
|
|
72
|
+
: null,
|
|
73
|
+
[active, relationships, relationshipGitopsOwner, kind, namespace, name, group],
|
|
74
|
+
)
|
|
75
|
+
const inherited = useResourceWithRelationships<any>(
|
|
76
|
+
inheritedLookupRef ? kindToPluralWithGroup(inheritedLookupRef.kind, inheritedLookupRef.group ?? '') : '',
|
|
77
|
+
inheritedLookupRef?.namespace ?? '',
|
|
78
|
+
inheritedLookupRef?.name ?? '',
|
|
79
|
+
inheritedLookupRef?.group,
|
|
80
|
+
{ cacheScope },
|
|
81
|
+
)
|
|
82
|
+
const inheritedGitopsOwner = useMemo(
|
|
83
|
+
() => (inheritedLookupRef ? gitOpsOwnerFromRelationships(inherited.data?.relationships) : null),
|
|
84
|
+
[inheritedLookupRef, inherited.data?.relationships],
|
|
85
|
+
)
|
|
86
|
+
const relationshipHelmOwner = useMemo(
|
|
87
|
+
() => (active ? nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace) : null),
|
|
88
|
+
[active, relationships, resource?.metadata?.namespace, namespace],
|
|
89
|
+
)
|
|
90
|
+
const inheritedHelmOwner = useMemo(
|
|
91
|
+
() =>
|
|
92
|
+
inheritedLookupRef
|
|
93
|
+
? nativeHelmOwnerFromRelationships(
|
|
94
|
+
inherited.data?.relationships,
|
|
95
|
+
inherited.data?.resource?.metadata?.namespace ?? namespace,
|
|
96
|
+
)
|
|
97
|
+
: null,
|
|
98
|
+
[inheritedLookupRef, inherited.data?.relationships, inherited.data?.resource?.metadata?.namespace, namespace],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const rawOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
|
|
102
|
+
const gitOpsSourceResource = relationshipGitopsOwner ? resource : inherited.data?.resource
|
|
103
|
+
const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
|
|
104
|
+
const helmSourceResource = relationshipHelmOwner ? resource : inherited.data?.resource
|
|
105
|
+
|
|
106
|
+
const shouldResolveArgoNamespace = rawOwner?.tool === 'argocd' && !rawOwner.namespace
|
|
107
|
+
const argoQuery = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
108
|
+
enabled: shouldResolveArgoNamespace,
|
|
109
|
+
cacheScope,
|
|
110
|
+
})
|
|
111
|
+
const argoApplications = argoQuery.data
|
|
112
|
+
const owner = useMemo(() => resolveGitOpsOwner(rawOwner, argoApplications), [rawOwner, argoApplications])
|
|
113
|
+
|
|
114
|
+
const ownerGroup = owner ? gitOpsOwnerGroup(owner) : ''
|
|
115
|
+
const shouldFetchOwner = Boolean(owner?.namespace)
|
|
116
|
+
const ownerQuery = useResource<any>(
|
|
117
|
+
shouldFetchOwner ? owner!.kind : '',
|
|
118
|
+
owner?.namespace ?? '',
|
|
119
|
+
owner?.name ?? '',
|
|
120
|
+
ownerGroup,
|
|
121
|
+
{ enabled: shouldFetchOwner, cacheScope },
|
|
122
|
+
)
|
|
123
|
+
const ownerObject = shouldFetchOwner ? ownerQuery.data : undefined
|
|
124
|
+
|
|
125
|
+
const ownerStatus = useMemo(() => deriveGitOpsOwnerStatus(owner, ownerObject), [owner, ownerObject])
|
|
126
|
+
const ownerSource = useMemo(
|
|
127
|
+
() => describeGitOpsOwnerSource(rawOwner, gitOpsSourceResource),
|
|
128
|
+
[rawOwner, gitOpsSourceResource],
|
|
129
|
+
)
|
|
130
|
+
const helmSource = useMemo(() => describeHelmOwnerSource(helmOwner, helmSourceResource), [helmOwner, helmSourceResource])
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
owner,
|
|
134
|
+
rawOwner,
|
|
135
|
+
ownerObject,
|
|
136
|
+
ownerPending: Boolean(shouldFetchOwner && ownerQuery.isLoading && !ownerQuery.data),
|
|
137
|
+
ownerVerified: Boolean(shouldFetchOwner && ownerQuery.data),
|
|
138
|
+
ownerMatchedByName: Boolean(shouldResolveArgoNamespace && owner?.namespace),
|
|
139
|
+
ownerStatus,
|
|
140
|
+
ownerSource,
|
|
141
|
+
helmOwner,
|
|
142
|
+
helmSource,
|
|
143
|
+
inheritedLookupRef,
|
|
144
|
+
lookupPending: Boolean((inheritedLookupRef && inherited.isPending) || (shouldResolveArgoNamespace && argoQuery.isPending)),
|
|
145
|
+
lookupError: Boolean(inheritedLookupRef && inherited.isError),
|
|
146
|
+
relationshipsUnavailable: Boolean(inheritedLookupRef && inherited.data && !inherited.data.relationships),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function gitOpsOwnerGroup(owner: GitOpsOwnerRef): string {
|
|
151
|
+
if (owner.tool === 'argocd') return 'argoproj.io'
|
|
152
|
+
if (owner.kind === 'kustomizations') return 'kustomize.toolkit.fluxcd.io'
|
|
153
|
+
return 'helm.toolkit.fluxcd.io'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The owner as the singular Kind + group the server's APIs take. */
|
|
157
|
+
export function gitOpsOwnerKindRef(owner: GitOpsOwnerRef): { kind: string; group: string } {
|
|
158
|
+
if (owner.tool === 'argocd') return { kind: 'Application', group: 'argoproj.io' }
|
|
159
|
+
if (owner.kind === 'kustomizations') return { kind: 'Kustomization', group: 'kustomize.toolkit.fluxcd.io' }
|
|
160
|
+
return { kind: 'HelmRelease', group: 'helm.toolkit.fluxcd.io' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveGitOpsOwner(owner: GitOpsOwnerRef | null, argoApplications: any[] | undefined): GitOpsOwnerRef | null {
|
|
164
|
+
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
165
|
+
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
166
|
+
if (matches.length !== 1) return owner
|
|
167
|
+
const namespace = matches[0]?.metadata?.namespace
|
|
168
|
+
return namespace ? { ...owner, namespace } : owner
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function findInheritedGitOpsLookupRef(
|
|
172
|
+
relationships: Relationships | undefined,
|
|
173
|
+
directOwner: GitOpsOwnerRef | null,
|
|
174
|
+
current: ResourceRef,
|
|
175
|
+
): ResourceRef | null {
|
|
176
|
+
if (directOwner) return null
|
|
177
|
+
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
178
|
+
(ref) => !gitOpsOwnerFromRelationships({ managedBy: [ref] }) && !isNativeHelmManager(ref),
|
|
179
|
+
)
|
|
180
|
+
const candidates = [relationships?.deployment, ...inheritedManagerRefs, relationships?.owner].filter(
|
|
181
|
+
Boolean,
|
|
182
|
+
) as ResourceRef[]
|
|
183
|
+
|
|
184
|
+
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function nativeHelmOwnerFromRelationships(
|
|
188
|
+
relationships: Relationships | undefined,
|
|
189
|
+
fallbackNamespace: string,
|
|
190
|
+
): HelmOwnerRef | null {
|
|
191
|
+
const ref = relationships?.managedBy?.[0]
|
|
192
|
+
if (!ref || !isNativeHelmManager(ref)) return null
|
|
193
|
+
return {
|
|
194
|
+
namespace: ref.namespace || fallbackNamespace,
|
|
195
|
+
name: ref.name,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
200
|
+
return (
|
|
201
|
+
kindToPluralWithGroup(ref.kind, ref.group ?? '') === kindToPluralWithGroup(current.kind, current.group ?? '') &&
|
|
202
|
+
ref.namespace === current.namespace &&
|
|
203
|
+
ref.name === current.name &&
|
|
204
|
+
(ref.group ?? '') === (current.group ?? '')
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isNativeHelmManager(ref: ResourceRef): boolean {
|
|
209
|
+
return ref.kind === 'HelmRelease' && ref.group !== 'helm.toolkit.fluxcd.io'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any): string | null {
|
|
213
|
+
if (!owner || !resource) return null
|
|
214
|
+
const labels = resource.metadata?.labels ?? {}
|
|
215
|
+
const annotations = resource.metadata?.annotations ?? {}
|
|
216
|
+
|
|
217
|
+
if (owner.tool === 'fluxcd') {
|
|
218
|
+
const nameKey = owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/name' : 'kustomize.toolkit.fluxcd.io/name'
|
|
219
|
+
const nsKey =
|
|
220
|
+
owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/namespace' : 'kustomize.toolkit.fluxcd.io/namespace'
|
|
221
|
+
if (labels[nameKey] || labels[nsKey]) {
|
|
222
|
+
return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const trackingID = annotations['argocd.argoproj.io/tracking-id']
|
|
227
|
+
if (trackingID) return `argocd.argoproj.io/tracking-id=${trackingID}`
|
|
228
|
+
const argoInstance = labels['argocd.argoproj.io/instance']
|
|
229
|
+
if (argoInstance) return `argocd.argoproj.io/instance=${argoInstance}`
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function describeHelmOwnerSource(owner: HelmOwnerRef | null, resource: any): string | null {
|
|
234
|
+
if (!owner || !resource) return null
|
|
235
|
+
const annotations = resource.metadata?.annotations ?? {}
|
|
236
|
+
const releaseName = annotations['meta.helm.sh/release-name']
|
|
237
|
+
const releaseNamespace = annotations['meta.helm.sh/release-namespace']
|
|
238
|
+
if (releaseName || releaseNamespace) {
|
|
239
|
+
return `meta.helm.sh/release-name=${releaseName ?? ''}, meta.helm.sh/release-namespace=${releaseNamespace ?? ''}`
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function deriveGitOpsOwnerStatus(owner: GitOpsOwnerRef | null, resource: any): GitOpsStatus | null {
|
|
245
|
+
if (!owner || !resource || !hasGitOpsStatusPayload(owner, resource)) return null
|
|
246
|
+
return getGitOpsResourceStatus(owner.kind, resource)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
250
|
+
if (owner.kind === 'applications') {
|
|
251
|
+
const status = resource.status ?? {}
|
|
252
|
+
return Boolean(status.sync?.status || status.health?.status || status.operationState?.phase)
|
|
253
|
+
}
|
|
254
|
+
if (resource.spec?.suspend === true) return true
|
|
255
|
+
return Array.isArray(resource.status?.conditions) && resource.status.conditions.length > 0
|
|
256
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
composeDetailSlots, composeResourceHosts, hostsForResource, resourceHostForSlot,
|
|
5
|
+
type ResourceHost,
|
|
6
|
+
} from './resourceHost'
|
|
7
|
+
|
|
8
|
+
const job = { kind: 'Job', group: 'batch', namespace: 'ml', name: 'run' }
|
|
9
|
+
const jobs = [{ name: 'jobs', group: 'batch' }]
|
|
10
|
+
const Admission = () => <span>admission</span>
|
|
11
|
+
|
|
12
|
+
describe('resource host composition', () => {
|
|
13
|
+
it('allows batch and admission to extend the same Job without hiding the destination owner', () => {
|
|
14
|
+
const hosts: ResourceHost[] = [
|
|
15
|
+
{ id: 'admission', resources: jobs, detailSlots: () => ({ renderHeaderActions: () => <Admission /> }) },
|
|
16
|
+
{ id: 'execution', resources: jobs, detailOwnership: ['destination'], expandedPath: () => '/execution/run', detailSlots: () => ({ renderHeaderActions: () => <span>execution</span>, extraTabs: [{ id: 'runs', label: 'Runs', render: () => null }] }) },
|
|
17
|
+
]
|
|
18
|
+
composeResourceHosts(hosts)
|
|
19
|
+
expect(hostsForResource(hosts, job).map(host => host.id)).toEqual(['admission', 'execution'])
|
|
20
|
+
expect(resourceHostForSlot(hosts, job, 'destination')?.expandedPath?.(job)).toBe('/execution/run')
|
|
21
|
+
const slots = composeDetailSlots(hosts.map(host => ({ id: host.id, slots: host.detailSlots?.(job) })), [{ id: 'history', label: 'History', render: () => null }])
|
|
22
|
+
const html = renderToStaticMarkup(<>{slots.renderHeaderActions?.({ resource: {}, context: 'drawer' })}</>)
|
|
23
|
+
expect(html).toBe('<span>admission</span><span>execution</span>')
|
|
24
|
+
expect(slots.extraTabs?.map(tab => tab.id)).toEqual(['history', 'runs'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('requires resolved group identity and normalizes Kind versus resource plural', () => {
|
|
28
|
+
const hosts: ResourceHost[] = [{ id: 'batch', resources: jobs }]
|
|
29
|
+
expect(hostsForResource(hosts, job)).toHaveLength(1)
|
|
30
|
+
expect(hostsForResource(hosts, { ...job, kind: 'jobs' })).toHaveLength(1)
|
|
31
|
+
expect(hostsForResource(hosts, { ...job, group: undefined })).toEqual([])
|
|
32
|
+
expect(hostsForResource(hosts, { ...job, group: '' })).toEqual([])
|
|
33
|
+
expect(hostsForResource(hosts, { ...job, group: 'batch.volcano.sh' })).toEqual([])
|
|
34
|
+
expect(hostsForResource([{ id: 'core', resources: [{ name: 'pods', group: '' }] }], { ...job, kind: 'Pod', group: '' })).toHaveLength(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('rejects competing exclusive ownership instead of letting registration order win', () => {
|
|
38
|
+
const owner: ResourceHost = { id: 'execution', resources: jobs, detailOwnership: ['destination'], expandedPath: () => '/execution' }
|
|
39
|
+
const other: ResourceHost = { ...owner, id: 'other' }
|
|
40
|
+
expect(() => composeResourceHosts([owner, other])).toThrow('ownership conflict')
|
|
41
|
+
expect(() => composeResourceHosts([{ id: 'missing', resources: jobs, logs: () => null }])).toThrow('must declare logs ownership')
|
|
42
|
+
expect(() => composeResourceHosts([{ id: 'missing', resources: jobs, expandedPath: () => '/' }])).toThrow('must declare destination ownership')
|
|
43
|
+
expect(() => composeResourceHosts([
|
|
44
|
+
{ id: 'one', renderers: { JobRenderer: Admission } },
|
|
45
|
+
{ id: 'two', renderers: { JobRenderer: Admission } },
|
|
46
|
+
])).toThrow('renderer:JobRenderer')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('catches summary and tab conflicts even if a contribution omitted its declaration', () => {
|
|
50
|
+
expect(() => composeDetailSlots(['one', 'two'].map(id => ({ id, slots: { renderSummary: () => null } })))).toThrow('summary ownership conflict')
|
|
51
|
+
const tab = { id: 'runs', label: 'Runs', render: () => null }
|
|
52
|
+
expect(composeDetailSlots([], [tab, tab]).extraTabs).toEqual([tab, tab])
|
|
53
|
+
expect(() => composeDetailSlots([{ id: 'execution', slots: { extraTabs: [tab] } }], [tab])).toThrow('tab ownership conflict: runs')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createElement, Fragment, type ComponentProps, type ComponentType, type ReactNode } from 'react'
|
|
2
|
+
import type { RendererOverrides, WorkloadExtraTab, WorkloadView } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import type { RadarFeature, RadarFeatureSupport } from '../api/radarFeatures'
|
|
4
|
+
import type { RenderDiagnoseAction } from '../context/DiagnoseCustomization'
|
|
5
|
+
import type { SelectedResource } from '../types'
|
|
6
|
+
import { kindToPluralWithGroup } from '../utils/navigation'
|
|
7
|
+
|
|
8
|
+
export type HostResource = Pick<SelectedResource, 'kind' | 'group' | 'namespace' | 'name'>
|
|
9
|
+
export type DetailSlots = Pick<ComponentProps<typeof WorkloadView>, 'renderSummary' | 'renderHeaderActions' | 'extraTabs'>
|
|
10
|
+
export type HostFeatures = Partial<Record<RadarFeature, RadarFeatureSupport>>
|
|
11
|
+
export type KindListMode = 'table' | 'wait' | 'view'
|
|
12
|
+
export interface HostKind { name: string; kind: string; group: string }
|
|
13
|
+
export interface KindListProps {
|
|
14
|
+
namespaces: string[]
|
|
15
|
+
inspected: SelectedResource | null
|
|
16
|
+
onInspect: (resource: SelectedResource) => void
|
|
17
|
+
onClearNamespaces: () => void
|
|
18
|
+
onCreate: (kind: HostKind) => void
|
|
19
|
+
}
|
|
20
|
+
type ExclusiveDetailSlot = 'summary' | 'destination' | 'logs'
|
|
21
|
+
export interface ResourceHost {
|
|
22
|
+
id: string
|
|
23
|
+
feature?: RadarFeature
|
|
24
|
+
diagnoseAction?: (render: RenderDiagnoseAction | undefined) => RenderDiagnoseAction | undefined
|
|
25
|
+
kindLists?: readonly {
|
|
26
|
+
kind: HostKind
|
|
27
|
+
title: string
|
|
28
|
+
mode: (support: RadarFeatureSupport, pending: boolean) => KindListMode
|
|
29
|
+
Component: ComponentType<KindListProps>
|
|
30
|
+
}[]
|
|
31
|
+
renderers?: RendererOverrides
|
|
32
|
+
resources?: readonly { name: string; group: string }[]
|
|
33
|
+
expandedPath?: (resource: HostResource, context?: string, tab?: 'yaml') => string | null
|
|
34
|
+
detailRedirect?: (resource: HostResource, search: URLSearchParams) => string | null
|
|
35
|
+
detailSlots?: (resource: HostResource) => DetailSlots
|
|
36
|
+
detailOwnership?: readonly ExclusiveDetailSlot[]
|
|
37
|
+
logs?: (resource: HostResource, data: { apiVersion?: string } | null | undefined) => ReactNode
|
|
38
|
+
drawerNavigation?: {
|
|
39
|
+
ownsPath: (pathname: string) => boolean
|
|
40
|
+
Component: ComponentType<{ resource: SelectedResource }>
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hostSupport(host: ResourceHost, features: HostFeatures): RadarFeatureSupport {
|
|
45
|
+
return host.feature ? features[host.feature] ?? 'unknown' : 'supported'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resourceKey(name: string, group: string): string { return `${group}\u0000${name}` }
|
|
49
|
+
|
|
50
|
+
export function hostsForResource(hosts: readonly ResourceHost[], resource: HostResource): ResourceHost[] {
|
|
51
|
+
// An unresolved group must not select a colliding CRD, or be treated as core.
|
|
52
|
+
if (resource.group === undefined) return []
|
|
53
|
+
const plural = kindToPluralWithGroup(resource.kind, resource.group)
|
|
54
|
+
return hosts.filter(host => host.resources?.some(kind => kind.name === plural && kind.group === resource.group))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resourceHostForSlot(hosts: readonly ResourceHost[], resource: HostResource, slot: ExclusiveDetailSlot): ResourceHost | undefined {
|
|
58
|
+
return hostsForResource(hosts, resource).find(host => host.detailOwnership?.includes(slot))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function composeResourceHosts(hosts: readonly ResourceHost[]): RendererOverrides {
|
|
62
|
+
const renderers: RendererOverrides = {}
|
|
63
|
+
const owners = new Map<string, string>()
|
|
64
|
+
const claim = (key: string, id: string) => {
|
|
65
|
+
const previous = owners.get(key)
|
|
66
|
+
if (previous) throw new Error(`Resource host ownership conflict: ${previous} and ${id} own ${key}`)
|
|
67
|
+
owners.set(key, id)
|
|
68
|
+
}
|
|
69
|
+
for (const host of hosts) {
|
|
70
|
+
claim(`host:${host.id}`, host.id)
|
|
71
|
+
if ((host.expandedPath || host.detailRedirect) && !host.detailOwnership?.includes('destination')) throw new Error(`Resource host ${host.id} must declare destination ownership`)
|
|
72
|
+
if (host.logs && !host.detailOwnership?.includes('logs')) throw new Error(`Resource host ${host.id} must declare logs ownership`)
|
|
73
|
+
for (const name of Object.keys(host.renderers ?? {})) claim(`renderer:${name}`, host.id)
|
|
74
|
+
Object.assign(renderers, host.renderers)
|
|
75
|
+
for (const kind of host.resources ?? []) {
|
|
76
|
+
for (const slot of host.detailOwnership ?? []) claim(`${slot}:${resourceKey(kind.name, kind.group)}`, host.id)
|
|
77
|
+
}
|
|
78
|
+
for (const list of host.kindLists ?? []) claim(`kind-list:${resourceKey(list.kind.name, list.kind.group)}`, host.id)
|
|
79
|
+
}
|
|
80
|
+
return renderers
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function composeDetailSlots(contributions: readonly { id: string; slots: DetailSlots | undefined }[], inheritedTabs: WorkloadExtraTab[] = []): DetailSlots {
|
|
84
|
+
const summaries = contributions.filter(host => host.slots?.renderSummary)
|
|
85
|
+
if (summaries.length > 1) throw new Error(`Resource host summary ownership conflict: ${summaries.map(host => host.id).join(', ')}`)
|
|
86
|
+
const summary = summaries[0]?.slots?.renderSummary
|
|
87
|
+
const actions = contributions.filter(host => host.slots?.renderHeaderActions)
|
|
88
|
+
const tabs = [...inheritedTabs, ...contributions.flatMap(host => host.slots?.extraTabs ?? [])]
|
|
89
|
+
const tabIds = new Set(inheritedTabs.map(tab => tab.id))
|
|
90
|
+
for (const tab of contributions.flatMap(host => host.slots?.extraTabs ?? [])) {
|
|
91
|
+
if (tabIds.has(tab.id)) throw new Error(`Resource host tab ownership conflict: ${tab.id}`)
|
|
92
|
+
tabIds.add(tab.id)
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...(summary ? { renderSummary: summary } : {}),
|
|
96
|
+
...(actions.length ? { renderHeaderActions: (props: Parameters<NonNullable<DetailSlots['renderHeaderActions']>>[0]) => actions.map(host => createElement(Fragment, { key: host.id }, host.slots!.renderHeaderActions!(props))) } : {}),
|
|
97
|
+
...(tabs.length ? { extraTabs: tabs } : {}),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { isValidElement } from 'react'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ResourceRendererDispatch } from '@skyhook-io/k8s-ui'
|
|
6
|
+
import { encodeDrawerTrail } from '../utils/drawer-trail'
|
|
7
|
+
import { kindToPluralWithGroup } from '../utils/navigation'
|
|
8
|
+
import {
|
|
9
|
+
decorateResourceDiagnose, renderResourceKindList, resourceKindListMode, resourceKindListTitle, ResourceDrawerNavigation,
|
|
10
|
+
resourceDetailRedirect, resourceDetailSlots, resourceExpandedPath,
|
|
11
|
+
resourceLogs, resourceRendererOverrides,
|
|
12
|
+
} from './resourceHosts'
|
|
13
|
+
|
|
14
|
+
vi.mock('../api/client', () => ({
|
|
15
|
+
ApiError: class extends Error { status = 500 },
|
|
16
|
+
useKueueAdmission: () => ({ data: { uid: 'job-id', installed: true, workloads: [], total: 0 }, isLoading: false, refetch: () => {} }),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
import type { HostFeatures } from './resourceHost'
|
|
20
|
+
|
|
21
|
+
const cluster = { kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'pg' }
|
|
22
|
+
|
|
23
|
+
const cnpgKind = { name: 'clusters', group: cluster.group }
|
|
24
|
+
const supported: HostFeatures = { cnpgWorkspace: 'supported' }
|
|
25
|
+
const unsupported: HostFeatures = { cnpgWorkspace: 'unsupported' }
|
|
26
|
+
const unknown: HostFeatures = { cnpgWorkspace: 'unknown' }
|
|
27
|
+
const listProps = { namespaces: ['db'], inspected: null, onInspect: () => {}, onClearNamespaces: () => {}, onCreate: () => {} }
|
|
28
|
+
|
|
29
|
+
function renderResource(data: any) {
|
|
30
|
+
const group = data.apiVersion.split('/')[0]
|
|
31
|
+
return renderToStaticMarkup(<MemoryRouter><ResourceRendererDispatch
|
|
32
|
+
resource={{ kind: kindToPluralWithGroup(data.kind, group), namespace: 'ml', name: 'run', group }}
|
|
33
|
+
data={data} onCopy={() => {}} copied={null} showCommonSections={false}
|
|
34
|
+
rendererOverrides={resourceRendererOverrides}
|
|
35
|
+
/></MemoryRouter>)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('application resource hosts', () => {
|
|
39
|
+
it('preserves existing CNPG access and exact group collisions without a new feature flag', () => {
|
|
40
|
+
expect(resourceExpandedPath(cluster, 'kind-demo', 'yaml')).toBe('/cnpg/clusters/db/pg?ctx=kind-demo&tab=yaml')
|
|
41
|
+
expect(resourceDetailSlots(cluster).renderSummary).toBeDefined()
|
|
42
|
+
for (const group of [undefined, '', 'cluster.x-k8s.io']) {
|
|
43
|
+
expect(resourceExpandedPath({ ...cluster, group })).toBeNull()
|
|
44
|
+
expect(resourceDetailSlots({ ...cluster, group })).toEqual({})
|
|
45
|
+
}
|
|
46
|
+
expect(resourceExpandedPath({ kind: 'NodePool', group: 'karpenter.sh', namespace: '', name: 'pool' })).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('retains every existing renderer override at a stable module reference', () => {
|
|
50
|
+
expect(Object.keys(resourceRendererOverrides).sort()).toEqual([
|
|
51
|
+
'RayJobRenderer', 'JobRenderer', 'JobSetRenderer', 'RayServiceRenderer', 'RayClusterRenderer',
|
|
52
|
+
'KueueWorkloadRenderer', 'CAPIClusterRenderer', 'PodRenderer', 'KarpenterNodePoolRenderer',
|
|
53
|
+
'NodeRenderer', 'ServiceRenderer', 'WorkloadRenderer', 'CompositeRenderer', 'ServiceAccountRenderer',
|
|
54
|
+
'RoleRenderer', 'RoleBindingRenderer', 'NamespaceRenderer', 'HPARenderer', 'PVCRenderer',
|
|
55
|
+
'RolloutRenderer', 'KyvernoPolicyCoverage', 'KyvernoPolicyQueued', 'CNPGObjectStoreRenderer',
|
|
56
|
+
'VeleroBSLRenderer', 'VeleroBackupRenderer', 'VeleroRestoreRenderer', 'CNPGClusterRenderer',
|
|
57
|
+
'CNPGDatabaseRenderer', 'CNPGPublicationRenderer', 'CNPGSubscriptionRenderer', 'CNPGImageCatalogRenderer',
|
|
58
|
+
].sort())
|
|
59
|
+
const inherited = [{ id: 'local', label: 'Local', render: () => null }]
|
|
60
|
+
expect(resourceDetailSlots({ kind: 'Pod', group: '', namespace: 'db', name: 'pg-1' }, inherited).extraTabs).toEqual(inherited)
|
|
61
|
+
expect(resourceExpandedPath({ ...cluster, group: undefined })).toBeNull()
|
|
62
|
+
expect(resourceDetailRedirect({ ...cluster, group: '' }, new URLSearchParams('tab=events'))).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('keeps query, tab, context and drawer state when redirecting a generic detail link', () => {
|
|
66
|
+
const search = new URLSearchParams('apiGroup=postgresql.cnpg.io&ctx=kind-demo&tab=events&drawer=pods%3Apg-1&ai-run=run')
|
|
67
|
+
const path = resourceDetailRedirect({ ...cluster, kind: 'clusters' }, search)!
|
|
68
|
+
const url = new URL(path, 'http://radar.test')
|
|
69
|
+
expect(url.pathname).toBe('/cnpg/clusters/db/pg')
|
|
70
|
+
expect(Object.fromEntries(url.searchParams)).toEqual({ ctx: 'kind-demo', tab: 'activity', drawer: 'pods:pg-1', 'ai-run': 'run' })
|
|
71
|
+
expect(search.get('tab')).toBe('events')
|
|
72
|
+
expect(search.get('apiGroup')).toBe('postgresql.cnpg.io')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('keeps integration log selection confined to observed CNPG Clusters and falls back for other resources', () => {
|
|
76
|
+
expect(isValidElement(resourceLogs(cluster, { apiVersion: 'postgresql.cnpg.io/v1' }))).toBe(true)
|
|
77
|
+
expect(isValidElement(resourceLogs({ ...cluster, group: undefined }, { apiVersion: 'postgresql.cnpg.io/v1' }))).toBe(true)
|
|
78
|
+
expect(resourceLogs(cluster, { apiVersion: 'cluster.x-k8s.io/v1beta1' })).toBeNull()
|
|
79
|
+
expect(resourceLogs({ ...cluster, kind: 'Pooler' }, { apiVersion: 'postgresql.cnpg.io/v1' })).toBeNull()
|
|
80
|
+
expect(resourceLogs({ kind: 'Pod', group: '', namespace: 'db', name: 'pg-1' }, { apiVersion: 'v1' })).toBeNull()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('mounts drawer trail navigation for any inspected resource in its owning workspace, including embeds', () => {
|
|
84
|
+
const previous = { kind: 'backups', group: 'postgresql.cnpg.io', namespace: 'db', name: 'base' }
|
|
85
|
+
const pod = { kind: 'pods', group: '', namespace: 'db', name: 'pg-1' }
|
|
86
|
+
const search = `?drawer=${encodeURIComponent(encodeDrawerTrail([previous, pod]))}`
|
|
87
|
+
const html = renderToStaticMarkup(<MemoryRouter basename="/c/demo" initialEntries={[`/c/demo/cnpg/protection${search}`]}><ResourceDrawerNavigation resource={pod} /></MemoryRouter>)
|
|
88
|
+
expect(html).toContain('Backup base')
|
|
89
|
+
const ordinary = renderToStaticMarkup(<MemoryRouter initialEntries={[`/resources/pods${search}`]}><ResourceDrawerNavigation resource={pod} /></MemoryRouter>)
|
|
90
|
+
expect(ordinary).toBe('')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('preserves shared version dispatch and simultaneous batch/Kueue detail in the stable override map', () => {
|
|
94
|
+
const data = { kind: 'Job', apiVersion: 'batch/v1', metadata: { name: 'run', namespace: 'ml', uid: 'job-id', labels: { 'kueue.x-k8s.io/queue-name': 'queue' } }, spec: { completions: 1 }, status: { active: 1 } }
|
|
95
|
+
const job = renderResource(data)
|
|
96
|
+
expect(job).toContain('Completions')
|
|
97
|
+
expect(job).toContain('No controller-owned Kueue Workload observed')
|
|
98
|
+
const jobSet = { ...data, kind: 'JobSet', apiVersion: 'jobset.x-k8s.io/v1alpha2', spec: { replicatedJobs: [] } }
|
|
99
|
+
const current = renderResource(jobSet)
|
|
100
|
+
expect(current).toContain('JobSet status')
|
|
101
|
+
expect(current).toContain('No controller-owned Kueue Workload observed')
|
|
102
|
+
for (const apiVersion of ['jobset.x-k8s.io/v1beta1', 'example.io/v1alpha2']) {
|
|
103
|
+
const generic = renderResource({ ...jobSet, apiVersion })
|
|
104
|
+
expect(generic).not.toContain('JobSet status')
|
|
105
|
+
expect(generic).not.toContain('No controller-owned Kueue Workload observed')
|
|
106
|
+
}
|
|
107
|
+
const foreignJob = renderResource({ ...data, apiVersion: 'batch.volcano.sh/v1alpha1' })
|
|
108
|
+
expect(foreignJob).toContain('Specification')
|
|
109
|
+
expect(foreignJob).not.toContain('No controller-owned Kueue Workload observed')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('makes the same kind-list decision for fetching and rendering across feature states', () => {
|
|
113
|
+
for (const [features, pending, mode] of [
|
|
114
|
+
[supported, true, 'view'], [supported, false, 'view'],
|
|
115
|
+
[unknown, true, 'wait'], [unknown, false, 'table'],
|
|
116
|
+
[unsupported, true, 'table'], [unsupported, false, 'table'],
|
|
117
|
+
] as const) {
|
|
118
|
+
expect(resourceKindListMode(cnpgKind, features, pending)).toBe(mode)
|
|
119
|
+
const view = renderResourceKindList(cnpgKind, features, pending, listProps)
|
|
120
|
+
expect(view !== null).toBe(mode !== 'table')
|
|
121
|
+
if (mode === 'wait') expect(renderToStaticMarkup(view)).toContain('Loading…')
|
|
122
|
+
}
|
|
123
|
+
for (const group of ['', 'cluster.x-k8s.io']) {
|
|
124
|
+
expect(resourceKindListMode({ ...cnpgKind, group }, supported, true)).toBe('table')
|
|
125
|
+
expect(renderResourceKindList({ ...cnpgKind, group }, supported, true, listProps)).toBeNull()
|
|
126
|
+
expect(resourceKindListTitle({ ...cnpgKind, group })).toBeNull()
|
|
127
|
+
}
|
|
128
|
+
expect(resourceKindListTitle(cnpgKind)).toBe('CloudNativePG Clusters')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('decorates Diagnose only for the integration-owned resource context', () => {
|
|
132
|
+
const render = vi.fn(() => <span>diagnose</span>)
|
|
133
|
+
const decorated = decorateResourceDiagnose(render)!
|
|
134
|
+
const context = { ...cluster, compact: false }
|
|
135
|
+
expect(isValidElement(decorated(context))).toBe(true)
|
|
136
|
+
expect(render).not.toHaveBeenCalled()
|
|
137
|
+
decorated({ ...context, group: 'cluster.x-k8s.io' })
|
|
138
|
+
expect(render).toHaveBeenCalledOnce()
|
|
139
|
+
expect(decorateResourceDiagnose(undefined)).toBeUndefined()
|
|
140
|
+
})
|
|
141
|
+
})
|