@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
|
@@ -35,7 +35,9 @@ describe('Metrics help', () => {
|
|
|
35
35
|
expect(html).toContain('--prometheus-single-cluster')
|
|
36
36
|
expect(html).toContain('they do not add missing metrics')
|
|
37
37
|
expect(html).toContain('Leave both unset for automatic matching')
|
|
38
|
-
expect(html).toContain('
|
|
38
|
+
expect(html).toContain('It does not scope rightsizing')
|
|
39
|
+
expect(html).toContain('Pod comparison and PVC usage')
|
|
40
|
+
expect(html).toContain('labels must exist on the kubelet volume stats')
|
|
39
41
|
expect(html).toContain('Standalone CLI:')
|
|
40
42
|
expect(html).toContain('when starting Radar')
|
|
41
43
|
expect(html).toContain('traffic.prometheusSingleCluster')
|
|
@@ -133,7 +133,7 @@ export function WorkloadMetricsHelpContent({ data, pending, error }: Pick<Props,
|
|
|
133
133
|
<div><dt className="font-medium">Backend dedicated to this cluster</dt><dd><code className="break-all font-mono text-xs">--prometheus-single-cluster</code><p className="mt-1">Helm: <code className="break-all font-mono text-xs">traffic.prometheusSingleCluster: true</code></p></dd></div>
|
|
134
134
|
<div><dt className="font-medium">Shared backend with a cluster label</dt><dd><code className="break-all font-mono text-xs">--prometheus-cluster-label cluster=your-cluster</code><p className="mt-1">Helm: set the backend’s actual label and value in <code className="break-all font-mono text-xs">traffic.prometheusClusterLabels</code>.</p></dd></div>
|
|
135
135
|
</dl>
|
|
136
|
-
<p>Leave both unset for automatic matching. An override replaces automatic matching for workload requests, resources, history
|
|
136
|
+
<p>Leave both unset for automatic matching. An override replaces automatic matching for workload requests, resources, history, Pod comparison and PVC usage. For PVC usage, its labels must exist on the kubelet volume stats. It does not scope rightsizing, node/HPA or legacy network/storage metrics.</p>
|
|
137
137
|
<h4 className="font-medium text-theme-text-primary">Operator configuration</h4>
|
|
138
138
|
<p><strong className="font-medium">Standalone CLI:</strong> supply the flag when starting Radar. Overrides are not saved; after changing the cluster, backend or credentials, verify the new scope before restarting with a fresh override.</p>
|
|
139
139
|
<p><strong className="font-medium">In-cluster OSS or Radar Cloud:</strong> ask the installation’s operator to update its Helm values or GitOps configuration. These are shared backend settings, not viewer preferences.</p>
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type CrossplaneResourceRef,
|
|
13
13
|
} from '@skyhook-io/k8s-ui/components/resources/resource-utils-crossplane'
|
|
14
14
|
import { getResourceStatus } from '@skyhook-io/k8s-ui'
|
|
15
|
-
import {
|
|
15
|
+
import { kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
16
16
|
import type { ResourceRef as NavRef } from '../../types'
|
|
17
17
|
import { fetchJSON, ApiError } from '../../api/client'
|
|
18
18
|
|
|
@@ -59,8 +59,8 @@ export function CompositeRenderer({ data, onNavigate }: CompositeRendererProps)
|
|
|
59
59
|
],
|
|
60
60
|
queryFn: async () => {
|
|
61
61
|
const ns = boundXRRef!.namespace || '_'
|
|
62
|
-
const plural = kindToPlural(boundXRRef!.kind)
|
|
63
62
|
const group = groupFromApiVersion(boundXRRef!.apiVersion)
|
|
63
|
+
const plural = kindToPluralWithGroup(boundXRRef!.kind, group)
|
|
64
64
|
const query = group ? `?group=${encodeURIComponent(group)}` : ''
|
|
65
65
|
return fetchJSON<{ resource: any }>(`/resources/${plural}/${ns}/${boundXRRef!.name}${query}`)
|
|
66
66
|
},
|
|
@@ -99,7 +99,7 @@ export function CompositeRenderer({ data, onNavigate }: CompositeRendererProps)
|
|
|
99
99
|
queryKey: ['composed-ref', group, ref.kind, ref.namespace ?? '', ref.name],
|
|
100
100
|
queryFn: async () => {
|
|
101
101
|
const ns = ref.namespace || '_'
|
|
102
|
-
const plural =
|
|
102
|
+
const plural = kindToPluralWithGroup(ref.kind, group)
|
|
103
103
|
const query = group ? `?group=${encodeURIComponent(group)}` : ''
|
|
104
104
|
return fetchJSON<{ resource: any }>(`/resources/${plural}/${ns}/${ref.name}${query}`)
|
|
105
105
|
},
|
|
@@ -137,7 +137,7 @@ export function CompositeRenderer({ data, onNavigate }: CompositeRendererProps)
|
|
|
137
137
|
map.set(key, { ref, missing: true })
|
|
138
138
|
return
|
|
139
139
|
}
|
|
140
|
-
const status = getResourceStatus(
|
|
140
|
+
const status = getResourceStatus(kindToPluralWithGroup(ref.kind, groupFromApiVersion(ref.apiVersion)), q.data.resource) ?? undefined
|
|
141
141
|
map.set(key, {
|
|
142
142
|
ref,
|
|
143
143
|
status: status ? { ...status, level: (status as any).level ?? 'unknown' } : undefined,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ResourceDetailDrawer as BaseResourceDetailDrawer } from '@skyhook-io/k8s-ui'
|
|
2
2
|
import type { SelectedResource } from '../../types'
|
|
3
3
|
import { WorkloadView } from '../workload/WorkloadView'
|
|
4
|
-
import {
|
|
4
|
+
import { ResourceDrawerNavigation } from '../../integrations/resourceHosts'
|
|
5
5
|
|
|
6
6
|
interface ResourceDetailDrawerProps {
|
|
7
7
|
resource: SelectedResource
|
|
@@ -33,7 +33,7 @@ export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
|
|
|
33
33
|
<BaseResourceDetailDrawer {...props}>
|
|
34
34
|
{({ resource, expanded, active, initialTab, onClose, onExpand, onExpandIntent, onCancelExpandIntent, onBack, onNavigateToResource, onCollapseToDrawer }) => (
|
|
35
35
|
<div className="flex h-full min-h-0 flex-col">
|
|
36
|
-
{!expanded && <
|
|
36
|
+
{!expanded && <ResourceDrawerNavigation resource={resource} />}
|
|
37
37
|
<div className="min-h-0 flex-1">
|
|
38
38
|
<WorkloadView
|
|
39
39
|
kind={resource.kind}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useCallback
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react'
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useQuery } from '@tanstack/react-query'
|
|
4
4
|
import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, isKindSyncFailed, isStillLoadingError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
|
|
@@ -8,10 +8,11 @@ import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../..
|
|
|
8
8
|
import { readErrorBody } from '../../api/httpErrors'
|
|
9
9
|
import { useAPIResources } from '../../api/apiResources'
|
|
10
10
|
import { useConnection } from '../../context/ConnectionContext'
|
|
11
|
-
import {
|
|
11
|
+
import { getSecretStoreProviderType } from '@skyhook-io/k8s-ui'
|
|
12
12
|
import { usePinnedKinds } from '../../hooks/useFavorites'
|
|
13
13
|
import { useResourceCounts } from '../../hooks/useResourceCounts'
|
|
14
14
|
import { useCNPGSidebarWorkspace } from '../cnpg/useCNPGSidebarWorkspace'
|
|
15
|
+
import { renderResourceKindList, resourceKindListMode, useResourceHostFeatures } from '../../integrations/resourceHosts'
|
|
15
16
|
import { useOpenLogs, useOpenWorkloadLogs } from '../dock'
|
|
16
17
|
import {
|
|
17
18
|
canBulkRestartKind,
|
|
@@ -35,6 +36,9 @@ interface ResourcesViewProps {
|
|
|
35
36
|
onResourceClickYaml?: NavigateToResource
|
|
36
37
|
onKindChange?: () => void
|
|
37
38
|
onClearNamespaces?: () => void
|
|
39
|
+
rememberedKindSearch?: (kind: { name: string; group: string }) => string | undefined
|
|
40
|
+
onKindFiltersChange?: (kind: { name: string; group: string }, search: string) => void
|
|
41
|
+
onApplyNamespacesToAllSections?: (namespaces: string[]) => Promise<unknown> | void
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
type SelectedKindInfo = { name: string; kind: string; group: string } | null
|
|
@@ -70,12 +74,13 @@ function hasResourceCount(counts: Record<string, number> | undefined, key: strin
|
|
|
70
74
|
return Object.prototype.hasOwnProperty.call(counts ?? {}, key)
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange, onClearNamespaces }: ResourcesViewProps) {
|
|
77
|
+
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange, onClearNamespaces, rememberedKindSearch, onKindFiltersChange, onApplyNamespacesToAllSections }: ResourcesViewProps) {
|
|
74
78
|
const location = useLocation()
|
|
75
79
|
const navigate = useNavigate()
|
|
76
80
|
const { connection } = useConnection()
|
|
77
81
|
|
|
78
|
-
const { data: capabilities } = useCapabilities()
|
|
82
|
+
const { data: capabilities, isPending: capabilitiesPending } = useCapabilities()
|
|
83
|
+
const hostFeatures = useResourceHostFeatures()
|
|
79
84
|
const namespaceForCapabilities = namespaces.length === 1 ? namespaces[0] : undefined
|
|
80
85
|
const { data: namespaceCapabilities } = useNamespaceCapabilities(namespaceForCapabilities, capabilities)
|
|
81
86
|
const namespaceCapabilityNames = useMemo(() => namespaces.length > 1 ? [...namespaces].sort() : [], [namespaces])
|
|
@@ -104,15 +109,9 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
104
109
|
// API resources discovery
|
|
105
110
|
const { data: apiResources } = useAPIResources()
|
|
106
111
|
|
|
107
|
-
// Initialize navigation kind↔plural maps from discovered API resources
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
if (apiResources) initNavigationMap(apiResources)
|
|
110
|
-
}, [apiResources])
|
|
111
|
-
|
|
112
|
-
const cnpgSidebarWorkspace = useCNPGSidebarWorkspace({ apiResources, namespaces })
|
|
113
|
-
|
|
114
112
|
// Track the selected kind from the k8s-ui component
|
|
115
113
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(null)
|
|
114
|
+
const cnpgSidebarWorkspace = useCNPGSidebarWorkspace({ apiResources, namespaces, withMetrics: selectedKind?.group === 'postgresql.cnpg.io' })
|
|
116
115
|
const workloadWrites = namespaces.length === 0
|
|
117
116
|
? capabilities?.workloadWrites
|
|
118
117
|
: namespaces.length === 1
|
|
@@ -281,7 +280,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
281
280
|
printerTable: sanitizePrinterTable(body),
|
|
282
281
|
}
|
|
283
282
|
},
|
|
284
|
-
enabled: !!selectedKind && !selectedKindQueryBlocked,
|
|
283
|
+
enabled: !!selectedKind && !selectedKindQueryBlocked && resourceKindListMode(selectedKind, hostFeatures, capabilitiesPending) === 'table',
|
|
285
284
|
staleTime: 30000,
|
|
286
285
|
refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
|
|
287
286
|
retry: (failureCount: number, error: Error) => {
|
|
@@ -385,6 +384,17 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
385
384
|
setCreateDialogOpen(true)
|
|
386
385
|
}, [])
|
|
387
386
|
|
|
387
|
+
const renderKindView = useCallback(
|
|
388
|
+
(kind: SelectedKindInfo) => renderResourceKindList(kind, hostFeatures, capabilitiesPending, {
|
|
389
|
+
namespaces,
|
|
390
|
+
inspected: selectedResource ?? null,
|
|
391
|
+
onInspect: (resource) => onResourceClick?.(resource),
|
|
392
|
+
onClearNamespaces: onClearNamespaces ?? (() => {}),
|
|
393
|
+
onCreate: handleCreateResource,
|
|
394
|
+
}),
|
|
395
|
+
[hostFeatures, capabilitiesPending, namespaces, selectedResource, onResourceClick, onClearNamespaces, handleCreateResource],
|
|
396
|
+
)
|
|
397
|
+
|
|
388
398
|
return (
|
|
389
399
|
<>
|
|
390
400
|
<BaseResourcesView
|
|
@@ -395,6 +405,9 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
395
405
|
onResourceClickYaml={onResourceClickYaml}
|
|
396
406
|
onKindChange={onKindChange}
|
|
397
407
|
onClearNamespaces={onClearNamespaces}
|
|
408
|
+
rememberedKindSearch={rememberedKindSearch}
|
|
409
|
+
onKindFiltersChange={onKindFiltersChange}
|
|
410
|
+
onApplyNamespacesToAllSections={onApplyNamespacesToAllSections}
|
|
398
411
|
// Injected data
|
|
399
412
|
apiResources={apiResources}
|
|
400
413
|
// Lightweight counts for sidebar (replaces 233 parallel queries)
|
|
@@ -434,6 +447,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
434
447
|
onOpenWorkloadLogs={openWorkloadLogs}
|
|
435
448
|
// Create resource
|
|
436
449
|
onCreateResource={handleCreateResource}
|
|
450
|
+
renderKindView={renderKindView}
|
|
437
451
|
// Bulk operations
|
|
438
452
|
onBulkDelete={(items, options) => bulkDeleteMutation.mutate({ items, force: options?.force }, { onSuccess: options?.onSuccess })}
|
|
439
453
|
isBulkDeleting={bulkDeleteMutation.isPending}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import type { useArgoStatus } from '../../api/client'
|
|
4
|
+
import type { IntegrationProfile } from './LocalConnectionSettings'
|
|
5
|
+
import { LocalIntegrationStatus } from './LocalIntegrationStatus'
|
|
6
|
+
|
|
7
|
+
vi.mock('../../api/client', () => ({
|
|
8
|
+
usePrometheusStatus: () => ({}),
|
|
9
|
+
useOpenCostSummary: () => ({}),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const profile: IntegrationProfile = {
|
|
13
|
+
target: {
|
|
14
|
+
binding: 'opaque',
|
|
15
|
+
context: 'development',
|
|
16
|
+
source: '/configs/team',
|
|
17
|
+
inFileName: 'dev',
|
|
18
|
+
fingerprint: 'target',
|
|
19
|
+
clientGeneration: 1,
|
|
20
|
+
operationGeneration: 1,
|
|
21
|
+
identity: { server: 'https://kubernetes', user: 'developer', trust: 'trust', insecureTls: false },
|
|
22
|
+
},
|
|
23
|
+
revision: 'revision',
|
|
24
|
+
state: 'saved',
|
|
25
|
+
mode: 'auto',
|
|
26
|
+
url: '',
|
|
27
|
+
headerKeys: [],
|
|
28
|
+
envHeaderKeys: [],
|
|
29
|
+
headersManaged: false,
|
|
30
|
+
secretSet: true,
|
|
31
|
+
insecureTls: false,
|
|
32
|
+
clusterId: '',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const render = (data: Record<string, unknown>) => {
|
|
36
|
+
const argo = { data, isError: false, isPlaceholderData: false, isFetching: false, dataUpdatedAt: 1, errorUpdatedAt: 0 } as unknown as ReturnType<typeof useArgoStatus>
|
|
37
|
+
return renderToStaticMarkup(<LocalIntegrationStatus kind="argocd" profile={profile} argo={argo} busy={false} changedAt={0} />)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('LocalIntegrationStatus for Argo CD', () => {
|
|
41
|
+
it('says Connecting while the first connection is being made', () => {
|
|
42
|
+
const html = render({ configured: true, connected: false, connecting: true })
|
|
43
|
+
expect(html).toContain('Connecting…')
|
|
44
|
+
expect(html).not.toContain('Not connected')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('shows why the last read failed on a working connection', () => {
|
|
48
|
+
const html = render({ configured: true, connected: true, address: 'https://argocd', reason: "Radar's last read from Argo CD failed: the request timed out." })
|
|
49
|
+
expect(html).toContain('Connected')
|
|
50
|
+
expect(html).toContain('the request timed out')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -26,13 +26,12 @@ export function LocalIntegrationStatus({ kind, profile, argo, busy, changedAt }:
|
|
|
26
26
|
status = prom.data.connected ? 'Connected' : prom.data.discovering ? 'Discovering…' : 'Not connected'
|
|
27
27
|
detail = prom.data.connected ? !profile.url ? `Discovered: ${prom.data.address}` : undefined : prom.data.error
|
|
28
28
|
} else if (kind === 'argocd' && argo.data) {
|
|
29
|
-
status = argo.data.connected ? argo.data.anonymous ? 'Connected · no token needed' : 'Connected' : 'Not connected'
|
|
30
|
-
detail = argo.data.connected
|
|
29
|
+
status = argo.data.connected ? argo.data.anonymous ? 'Connected · no token needed' : 'Connected' : argo.data.connecting ? 'Connecting…' : 'Not connected'
|
|
30
|
+
detail = argo.data.reason ?? (argo.data.connected && !profile.url ? `Discovered: ${argo.data.address}` : undefined)
|
|
31
31
|
} else if (kind === 'cost' && cost.data) {
|
|
32
32
|
status = cost.data.available ? `Available · ${costSourceLabel(cost.data.source)}`
|
|
33
33
|
: ['no_prometheus', 'no_cost_source', 'no_metrics'].includes(cost.data.reason ?? '') ? 'No cost data' : 'Unavailable'
|
|
34
|
-
detail = cost.data.reason ? costIntegrationUnavailableMessage(cost.data.reason)
|
|
35
|
-
?? (cost.data.reason === 'query_error' ? 'The active cost source query failed.' : undefined) : undefined
|
|
34
|
+
detail = cost.data.reason ? costIntegrationUnavailableMessage(cost.data.reason) : undefined
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
const lastDetail = useRef({ text: detail, visible: !!detail })
|
|
@@ -856,11 +856,8 @@ export function SettingsDialog({
|
|
|
856
856
|
anonymous={argoSectionStatus?.connected && argoSectionStatus.anonymous}
|
|
857
857
|
connectedAddress={argoSectionStatus?.connected && !argoSectionStatus.anonymous ? argoSectionStatus.address : undefined}
|
|
858
858
|
active={section === 'argocd'}
|
|
859
|
-
statusReason={
|
|
860
|
-
|
|
861
|
-
? argoSectionStatus.reason
|
|
862
|
-
: undefined
|
|
863
|
-
}
|
|
859
|
+
statusReason={argoSectionStatus?.configured ? argoSectionStatus.reason : undefined}
|
|
860
|
+
connecting={argoSectionStatus?.connecting}
|
|
864
861
|
onChangeUrl={(v) => updateConfigField('argoCdUrl', v || undefined)}
|
|
865
862
|
onChangeInsecureTls={(v) => updateConfigField('argoCdInsecureTls', v || undefined)}
|
|
866
863
|
onApplied={({ url, insecureTls, tokenSet }) => {
|
|
@@ -1218,7 +1215,7 @@ function OperatorSettingsSummary({ section, config }: { section: SettingsSection
|
|
|
1218
1215
|
break
|
|
1219
1216
|
case 'argocd':
|
|
1220
1217
|
rows = [
|
|
1221
|
-
['Status', argoError ? 'Status unavailable' : !argo ? 'Checking…' : argo.connected ? 'Connected' : 'Not connected'],
|
|
1218
|
+
['Status', argoError ? 'Status unavailable' : !argo ? 'Checking…' : argo.connected ? 'Connected' : argo.connecting ? 'Connecting…' : 'Not connected'],
|
|
1222
1219
|
['Endpoint', argo?.address || c.argoCdUrl || 'Auto-discovery'],
|
|
1223
1220
|
['Token', config.argoCdTokenSet ? 'Configured' : 'None configured'],
|
|
1224
1221
|
['TLS verification', c.argoCdInsecureTls ? 'Disabled' : 'Enabled'],
|
|
@@ -1317,7 +1314,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
1317
1314
|
: cost?.reason === 'access_denied'
|
|
1318
1315
|
? 'Your current permissions do not allow cost data.'
|
|
1319
1316
|
: cost?.reason
|
|
1320
|
-
? costIntegrationUnavailableMessage(cost.reason) ?? 'The active cost source
|
|
1317
|
+
? costIntegrationUnavailableMessage(cost.reason) ?? 'The active cost source could not provide data.'
|
|
1321
1318
|
: undefined
|
|
1322
1319
|
|
|
1323
1320
|
const rows: OverviewRow[] = [
|
|
@@ -1347,9 +1344,9 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
1347
1344
|
},
|
|
1348
1345
|
{
|
|
1349
1346
|
id: 'argocd', icon: GitBranch, label: 'Argo CD',
|
|
1350
|
-
tone: argo?.connected ? 'ok' : argo?.configured || argo?.reason ? 'warn' : 'off',
|
|
1351
|
-
value: argo?.connected ? (argo.anonymous ? 'Connected · no token needed' : 'Connected') : 'Not connected',
|
|
1352
|
-
detail: argo?.
|
|
1347
|
+
tone: argo?.connected ? (argo.reason ? 'warn' : 'ok') : argo?.connecting ? 'unknown' : argo?.configured || argo?.reason ? 'warn' : 'off',
|
|
1348
|
+
value: argo?.connected ? (argo.anonymous ? 'Connected · no token needed' : 'Connected') : argo?.connecting ? 'Connecting…' : 'Not connected',
|
|
1349
|
+
detail: argo?.reason ? 'Open Argo CD for connection details.' : argo?.connected ? argo.address : undefined,
|
|
1353
1350
|
},
|
|
1354
1351
|
{
|
|
1355
1352
|
id: 'advanced', icon: Zap, label: 'MCP',
|
|
@@ -2228,6 +2225,7 @@ function ArgoCDConfigField({
|
|
|
2228
2225
|
envError,
|
|
2229
2226
|
cliSession,
|
|
2230
2227
|
statusReason,
|
|
2228
|
+
connecting,
|
|
2231
2229
|
anonymous,
|
|
2232
2230
|
connectedAddress,
|
|
2233
2231
|
active,
|
|
@@ -2242,6 +2240,7 @@ function ArgoCDConfigField({
|
|
|
2242
2240
|
envError?: string
|
|
2243
2241
|
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
2244
2242
|
statusReason?: string
|
|
2243
|
+
connecting?: boolean
|
|
2245
2244
|
anonymous?: boolean
|
|
2246
2245
|
connectedAddress?: string
|
|
2247
2246
|
active?: boolean
|
|
@@ -2250,7 +2249,7 @@ function ArgoCDConfigField({
|
|
|
2250
2249
|
onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
|
|
2251
2250
|
}) {
|
|
2252
2251
|
if (envManaged) {
|
|
2253
|
-
return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} connectedAddress={connectedAddress} statusReason={statusReason} />
|
|
2252
|
+
return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} connectedAddress={connectedAddress} statusReason={statusReason} connecting={connecting} />
|
|
2254
2253
|
}
|
|
2255
2254
|
return (
|
|
2256
2255
|
<ArgoCDEditableField
|
|
@@ -2259,6 +2258,7 @@ function ArgoCDConfigField({
|
|
|
2259
2258
|
tokenSet={tokenSet}
|
|
2260
2259
|
cliSession={cliSession}
|
|
2261
2260
|
statusReason={statusReason}
|
|
2261
|
+
connecting={connecting}
|
|
2262
2262
|
anonymous={anonymous}
|
|
2263
2263
|
connectedAddress={connectedAddress}
|
|
2264
2264
|
active={active}
|
|
@@ -2279,6 +2279,7 @@ function ArgoCDEnvManagedField({
|
|
|
2279
2279
|
envError,
|
|
2280
2280
|
connectedAddress,
|
|
2281
2281
|
statusReason,
|
|
2282
|
+
connecting,
|
|
2282
2283
|
}: {
|
|
2283
2284
|
url: string
|
|
2284
2285
|
insecureTls: boolean
|
|
@@ -2288,6 +2289,7 @@ function ArgoCDEnvManagedField({
|
|
|
2288
2289
|
// so this is the one thing on it that can change after deploy.
|
|
2289
2290
|
connectedAddress?: string
|
|
2290
2291
|
statusReason?: string
|
|
2292
|
+
connecting?: boolean
|
|
2291
2293
|
}) {
|
|
2292
2294
|
if (envError) {
|
|
2293
2295
|
return (
|
|
@@ -2344,10 +2346,15 @@ function ArgoCDEnvManagedField({
|
|
|
2344
2346
|
<dt className="w-24 shrink-0 text-theme-text-tertiary">Status</dt>
|
|
2345
2347
|
<dd className="min-w-0 truncate text-theme-text-secondary">
|
|
2346
2348
|
{connectedAddress ? (
|
|
2347
|
-
|
|
2348
|
-
<
|
|
2349
|
-
|
|
2350
|
-
|
|
2349
|
+
<>
|
|
2350
|
+
<span className="inline-flex items-center gap-1">
|
|
2351
|
+
<Check className="w-3 h-3 shrink-0 text-green-600 dark:text-green-400/80" />
|
|
2352
|
+
Connected at {connectedAddress}
|
|
2353
|
+
</span>
|
|
2354
|
+
{statusReason && <span className="block whitespace-normal text-warning-text">{statusReason}</span>}
|
|
2355
|
+
</>
|
|
2356
|
+
) : connecting ? (
|
|
2357
|
+
'Connecting…'
|
|
2351
2358
|
) : statusReason ? (
|
|
2352
2359
|
<span className="text-warning-text">Not connected — {statusReason}</span>
|
|
2353
2360
|
) : (
|
|
@@ -2377,6 +2384,7 @@ function ArgoCDEditableField({
|
|
|
2377
2384
|
tokenSet,
|
|
2378
2385
|
cliSession,
|
|
2379
2386
|
statusReason,
|
|
2387
|
+
connecting: serverConnecting,
|
|
2380
2388
|
anonymous,
|
|
2381
2389
|
connectedAddress,
|
|
2382
2390
|
active,
|
|
@@ -2389,6 +2397,7 @@ function ArgoCDEditableField({
|
|
|
2389
2397
|
tokenSet: boolean
|
|
2390
2398
|
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
2391
2399
|
statusReason?: string
|
|
2400
|
+
connecting?: boolean
|
|
2392
2401
|
anonymous?: boolean
|
|
2393
2402
|
connectedAddress?: string
|
|
2394
2403
|
active?: boolean
|
|
@@ -2517,6 +2526,10 @@ function ArgoCDEditableField({
|
|
|
2517
2526
|
</p>
|
|
2518
2527
|
)}
|
|
2519
2528
|
|
|
2529
|
+
{serverConnecting && state.status === 'idle' && !anonymous && !connectedAddress && (
|
|
2530
|
+
<p className="mb-3 text-xs text-theme-text-secondary">Connecting to Argo CD…</p>
|
|
2531
|
+
)}
|
|
2532
|
+
|
|
2520
2533
|
{statusReason && state.status !== 'connected' && (
|
|
2521
2534
|
<div className="mb-3 rounded-md border border-theme-border bg-theme-elevated p-3">
|
|
2522
2535
|
<p className="flex items-center gap-1.5 text-sm font-medium text-warning-text">
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
|
-
SEVERITY_TEXT,
|
|
4
3
|
TimelineList as TimelineListUI,
|
|
5
4
|
eventsForApplication,
|
|
6
5
|
isTimelineProblem,
|
|
@@ -16,7 +15,9 @@ import { isManagedTimelineEvent } from '../../api/timelineSource'
|
|
|
16
15
|
import { TimelineRetentionBanner } from './TimelineRetentionNote'
|
|
17
16
|
import { useHasLimitedAccess } from '../../contexts/CapabilitiesContext'
|
|
18
17
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
19
|
-
import {
|
|
18
|
+
import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
|
|
19
|
+
import { useStickyError } from './useStickyError'
|
|
20
|
+
import { ViewModeToggle } from './ViewModeToggle'
|
|
20
21
|
|
|
21
22
|
export type { ActivityTypeFilter, ActivityFilterKey }
|
|
22
23
|
|
|
@@ -93,9 +94,10 @@ interface TimelineListProps {
|
|
|
93
94
|
appScoped?: boolean
|
|
94
95
|
topology?: Topology
|
|
95
96
|
appScopeLoading?: boolean
|
|
97
|
+
onOpenDiagnostics?: () => void
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
export function TimelineList({ namespaces, onViewChange, currentView, onResourceClick, initialFilter, initialTimeRange, showDeleted, onShowDeletedChange, showRoutine, onShowRoutineChange, search, onSearchChange, activityFilter, onActivityFilterChange, kindFilter, onKindFilterChange, selectionWindow, onVisibleWindowChange, scrollToMs, focusedAppIndex, appScoped = false, topology, appScopeLoading = false }: TimelineListProps) {
|
|
100
|
+
export function TimelineList({ namespaces, onViewChange, currentView, onResourceClick, initialFilter, initialTimeRange, showDeleted, onShowDeletedChange, showRoutine, onShowRoutineChange, search, onSearchChange, activityFilter, onActivityFilterChange, kindFilter, onKindFilterChange, selectionWindow, onVisibleWindowChange, scrollToMs, focusedAppIndex, appScoped = false, topology, appScopeLoading = false, onOpenDiagnostics }: TimelineListProps) {
|
|
99
101
|
const hasLimitedAccess = useHasLimitedAccess()
|
|
100
102
|
const timelineSource = useTimelineSource()
|
|
101
103
|
const [queryParams, setQueryParams] = useState<{ timeRange: TimeRange; kinds: string[] }>({
|
|
@@ -112,7 +114,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
|
|
|
112
114
|
const splitRows = !appScoped
|
|
113
115
|
// Uncapped when splitting: each part gets its own cap below. The rows come
|
|
114
116
|
// from the loaded ring, so this costs no extra fetch.
|
|
115
|
-
const { data: fetchedEvents, isLoading, isError, error, refetch } = timelineSource.useEvents({
|
|
117
|
+
const { data: fetchedEvents, isLoading, isFetching, isError, error, refetch } = timelineSource.useEvents({
|
|
116
118
|
namespaces,
|
|
117
119
|
kinds: queryParams.kinds,
|
|
118
120
|
timeRange: queryParams.timeRange,
|
|
@@ -123,6 +125,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
|
|
|
123
125
|
fromMs: selectionWindow?.fromMs,
|
|
124
126
|
toMs: selectionWindow?.toMs,
|
|
125
127
|
})
|
|
128
|
+
const loadError = useStickyError(error, fetchedEvents != null)
|
|
126
129
|
const split = useMemo(
|
|
127
130
|
() => (splitRows && fetchedEvents ? splitRoutineActivity(fetchedEvents, fetchLimit) : undefined),
|
|
128
131
|
[splitRows, fetchedEvents, fetchLimit],
|
|
@@ -141,21 +144,22 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
|
|
|
141
144
|
|
|
142
145
|
// Full-screen error only when nothing is loaded; a failing background poll
|
|
143
146
|
// with data on screen keeps rendering (data before error).
|
|
144
|
-
if (
|
|
147
|
+
if (loadError && !fetchedEvents) {
|
|
145
148
|
return (
|
|
146
|
-
<div className="flex flex-col
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
<div className="flex flex-col h-full">
|
|
150
|
+
{currentView && onViewChange && (
|
|
151
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
|
|
152
|
+
<div />
|
|
153
|
+
<ViewModeToggle viewMode={currentView} onViewModeChange={onViewChange} />
|
|
154
|
+
</div>
|
|
151
155
|
)}
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
<TimelineLoadError
|
|
157
|
+
className="flex-1"
|
|
158
|
+
error={loadError}
|
|
159
|
+
retrying={isFetching}
|
|
160
|
+
onRetry={() => refetch()}
|
|
161
|
+
onOpenDiagnostics={onOpenDiagnostics}
|
|
162
|
+
/>
|
|
159
163
|
</div>
|
|
160
164
|
)
|
|
161
165
|
}
|
|
@@ -165,13 +169,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
|
|
|
165
169
|
// view-level banner already reports the shared failure, and a second note
|
|
166
170
|
// would double up.
|
|
167
171
|
const staleNote = isError && fetchedEvents && !selectionWindow ? (
|
|
168
|
-
<
|
|
169
|
-
<AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
|
|
170
|
-
Live updates are failing — the list may be stale.
|
|
171
|
-
<button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
|
|
172
|
-
Retry now
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
172
|
+
<TimelineStaleNote error={error} subject="list" onRetry={() => refetch()} />
|
|
175
173
|
) : null
|
|
176
174
|
|
|
177
175
|
return (
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { ApiError } from '../../api/client'
|
|
4
|
+
import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
|
|
5
|
+
|
|
6
|
+
const driverError = 'open PostgreSQL timeline database: dial tcp 10.0.0.5:5432: connect: connection refused'
|
|
7
|
+
|
|
8
|
+
function databaseDown(): ApiError {
|
|
9
|
+
return new ApiError('Timeline history is unavailable because Radar can\'t connect to its PostgreSQL database.', 503, {
|
|
10
|
+
error_code: 'timeline_database_unreachable',
|
|
11
|
+
detail: driverError,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function render(error: Error, onOpenDiagnostics?: () => void): string {
|
|
16
|
+
return renderToStaticMarkup(
|
|
17
|
+
<TimelineLoadError error={error} onRetry={() => {}} onOpenDiagnostics={onOpenDiagnostics} />,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('TimelineLoadError', () => {
|
|
22
|
+
it('names the database and points to the logs when PostgreSQL is unreachable', () => {
|
|
23
|
+
const html = render(databaseDown(), () => {})
|
|
24
|
+
expect(html).toContain('Timeline history is unavailable')
|
|
25
|
+
expect(html).toContain('can't connect to its PostgreSQL database')
|
|
26
|
+
expect(html).toContain('[timeline]')
|
|
27
|
+
// The driver's error names the database host; it belongs in the logs and
|
|
28
|
+
// diagnostics, not on the page.
|
|
29
|
+
expect(html).not.toContain(driverError)
|
|
30
|
+
expect(html).toContain('Open diagnostics')
|
|
31
|
+
expect(html).not.toContain('Failed to load timeline data')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('omits the diagnostics link when the host cannot open diagnostics', () => {
|
|
35
|
+
expect(render(databaseDown())).not.toContain('Open diagnostics')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('keeps the generic state, without a diagnostics link, for other failures', () => {
|
|
39
|
+
const html = render(new ApiError('upstream timed out', 502, { error: 'upstream timed out' }), () => {})
|
|
40
|
+
expect(html).toContain('Failed to load timeline data')
|
|
41
|
+
expect(html).toContain('upstream timed out')
|
|
42
|
+
expect(html).not.toContain('Open diagnostics')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('TimelineStaleNote', () => {
|
|
47
|
+
const note = (error: Error) =>
|
|
48
|
+
renderToStaticMarkup(<TimelineStaleNote error={error} subject="timeline" onRetry={() => {}} />)
|
|
49
|
+
|
|
50
|
+
it('names the database and points to the logs when PostgreSQL is the cause', () => {
|
|
51
|
+
const html = note(databaseDown())
|
|
52
|
+
expect(html).toContain('can't connect to its PostgreSQL database, so the timeline may be stale')
|
|
53
|
+
expect(html).toContain('[timeline]')
|
|
54
|
+
expect(html).not.toContain(driverError)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('keeps the generic note for other failures', () => {
|
|
58
|
+
const html = note(new ApiError('upstream timed out', 502, { error: 'upstream timed out' }))
|
|
59
|
+
expect(html).toContain('Live updates are failing, so the timeline may be stale')
|
|
60
|
+
expect(html).not.toContain('PostgreSQL')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { AlertTriangle, Bug, RefreshCw } from 'lucide-react'
|
|
3
|
+
import { timelineDatabaseError } from '../../api/client'
|
|
4
|
+
import { SEVERITY_TEXT } from '@skyhook-io/k8s-ui'
|
|
5
|
+
|
|
6
|
+
const SECONDARY_BUTTON =
|
|
7
|
+
'flex items-center gap-2 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border-light rounded-lg hover:bg-theme-hover transition-colors'
|
|
8
|
+
|
|
9
|
+
// TimelineLoadError is the full-screen state when the timeline has nothing to
|
|
10
|
+
// show. An unreachable PostgreSQL database gets its own copy: it says the
|
|
11
|
+
// database is the problem and where to look. The driver's error stays in
|
|
12
|
+
// Radar's logs and the diagnostics overlay.
|
|
13
|
+
export function TimelineLoadError({
|
|
14
|
+
error,
|
|
15
|
+
onRetry,
|
|
16
|
+
retrying = false,
|
|
17
|
+
onOpenDiagnostics,
|
|
18
|
+
className,
|
|
19
|
+
}: {
|
|
20
|
+
error?: Error | null
|
|
21
|
+
onRetry: () => void
|
|
22
|
+
/** A request is in flight: the error stays up, the button shows the attempt. */
|
|
23
|
+
retrying?: boolean
|
|
24
|
+
onOpenDiagnostics?: () => void
|
|
25
|
+
className?: string
|
|
26
|
+
}) {
|
|
27
|
+
const databaseError = timelineDatabaseError(error)
|
|
28
|
+
const detail = error?.message?.trim()
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={clsx('flex flex-col items-center justify-center text-theme-text-tertiary gap-3 px-6', className)}>
|
|
32
|
+
<AlertTriangle className="w-10 h-10 text-amber-400/70" />
|
|
33
|
+
{databaseError !== null ? (
|
|
34
|
+
<>
|
|
35
|
+
<p className="text-base text-theme-text-secondary">Timeline history is unavailable</p>
|
|
36
|
+
<p className="max-w-md text-center text-sm">
|
|
37
|
+
Radar can't connect to its PostgreSQL database, so changes aren't being recorded. It keeps
|
|
38
|
+
retrying, and this page recovers on its own once the database is back.
|
|
39
|
+
</p>
|
|
40
|
+
<p className="max-w-md text-center text-xs">
|
|
41
|
+
Radar's logs have the details: search them for <code className="font-mono">[timeline]</code>.
|
|
42
|
+
</p>
|
|
43
|
+
</>
|
|
44
|
+
) : (
|
|
45
|
+
<>
|
|
46
|
+
<p className="text-base">Failed to load timeline data</p>
|
|
47
|
+
{detail && <p className="max-w-md text-center text-sm">{detail}</p>}
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<button type="button" onClick={onRetry} disabled={retrying} className={clsx(SECONDARY_BUTTON, 'disabled:opacity-60 disabled:cursor-default')}>
|
|
52
|
+
<RefreshCw className={clsx('w-3.5 h-3.5', retrying && 'animate-spin')} />
|
|
53
|
+
Try again
|
|
54
|
+
</button>
|
|
55
|
+
{databaseError !== null && onOpenDiagnostics && (
|
|
56
|
+
<button type="button" onClick={onOpenDiagnostics} className={SECONDARY_BUTTON}>
|
|
57
|
+
<Bug className="w-3.5 h-3.5" />
|
|
58
|
+
Open diagnostics
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// TimelineStaleNote is the banner over data that is still on screen while its
|
|
67
|
+
// refresh fails. When the database is the cause it says so and where to look,
|
|
68
|
+
// since the data underneath otherwise reads as current.
|
|
69
|
+
export function TimelineStaleNote({
|
|
70
|
+
error,
|
|
71
|
+
subject,
|
|
72
|
+
onRetry,
|
|
73
|
+
}: {
|
|
74
|
+
error?: Error | null
|
|
75
|
+
/** What may be stale: the swimlane view or the list. */
|
|
76
|
+
subject: 'timeline' | 'list'
|
|
77
|
+
onRetry: () => void
|
|
78
|
+
}) {
|
|
79
|
+
const databaseDown = timelineDatabaseError(error) !== null
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
|
|
82
|
+
<AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
|
|
83
|
+
{databaseDown ? (
|
|
84
|
+
<span>
|
|
85
|
+
Radar can't connect to its PostgreSQL database, so the {subject} may be stale. Search Radar's logs
|
|
86
|
+
for <code className="font-mono">[timeline]</code>.
|
|
87
|
+
</span>
|
|
88
|
+
) : (
|
|
89
|
+
<span>Live updates are failing, so the {subject} may be stale.</span>
|
|
90
|
+
)}
|
|
91
|
+
<button type="button" onClick={onRetry} className="underline hover:text-theme-text-primary">
|
|
92
|
+
Retry now
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|