@skyhook-io/radar-app 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/App.tsx +125 -87
- package/src/api/actions.errorCode.test.ts +40 -0
- package/src/api/actions.test.ts +48 -0
- package/src/api/actions.ts +79 -0
- package/src/api/apiResources.navigation.test.tsx +232 -0
- package/src/api/apiResources.ts +46 -3
- package/src/api/client.helmSource.test.ts +39 -0
- package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
- package/src/api/client.namespaceSwitch.test.ts +107 -0
- package/src/api/client.ts +266 -39
- package/src/api/cnpg-ha.live.test.ts +140 -0
- package/src/api/cnpg-ha.test.ts +78 -0
- package/src/api/cnpg-ha.ts +19 -0
- package/src/api/cnpg-history.ts +123 -0
- package/src/api/cnpg-inspect.ts +102 -0
- package/src/api/cnpg-protection.ts +112 -0
- package/src/api/cnpg-recovery.test.tsx +60 -0
- package/src/api/cnpg-recovery.ts +262 -0
- package/src/api/cnpg-sessions.ts +175 -0
- package/src/api/cnpg-storage.ts +138 -0
- package/src/api/cnpg.legacy-reads.test.ts +43 -0
- package/src/api/cnpg.pooler-runtime.test.ts +15 -0
- package/src/api/cnpg.ts +428 -10
- package/src/api/radarFeatures.render.test.tsx +47 -3
- package/src/api/radarFeatures.ts +4 -1
- package/src/api/reviewedActionMutation.test.tsx +164 -0
- package/src/api/reviewedActionMutation.ts +66 -0
- package/src/api/timelineSource.test.ts +9 -0
- package/src/api/timelineSource.ts +8 -0
- package/src/components/audit/AuditView.tsx +9 -0
- package/src/components/capacity/CapacityActivity.tsx +3 -31
- package/src/components/capacity/CapacityDemand.tsx +4 -36
- package/src/components/capacity/CapacityOverview.tsx +217 -56
- package/src/components/capacity/CapacityPoolDetail.tsx +6 -55
- package/src/components/capacity/CapacityView.test.tsx +323 -0
- package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
- package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
- package/src/components/capacity/routes.ts +42 -0
- package/src/components/capacity/shared.tsx +11 -129
- package/src/components/capacity/workspaceRoute.ts +18 -0
- package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
- package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
- package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
- package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
- package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
- package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
- package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
- package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
- package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
- package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
- package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
- package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
- package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
- package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
- package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
- package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
- package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
- package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
- package/src/components/cnpg/CNPGOperator.tsx +78 -30
- package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
- package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
- package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
- package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
- package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
- package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
- package/src/components/cnpg/CNPGOverview.tsx +238 -63
- package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
- package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
- package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
- package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
- package/src/components/cnpg/CNPGPerformance.tsx +477 -0
- package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
- package/src/components/cnpg/CNPGPooling.tsx +97 -40
- package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
- package/src/components/cnpg/CNPGProtection.tsx +228 -122
- package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
- package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
- package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
- package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
- package/src/components/cnpg/CNPGStorage.tsx +619 -0
- package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
- package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
- package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
- package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
- package/src/components/cnpg/CNPGTrends.test.ts +38 -0
- package/src/components/cnpg/CNPGTrends.tsx +702 -0
- package/src/components/cnpg/CNPGView.tsx +34 -21
- package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
- package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
- package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
- package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
- package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
- package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
- package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
- package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
- package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
- package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
- package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
- package/src/components/cnpg/actions/actionModel.test.ts +111 -0
- package/src/components/cnpg/actions/actionModel.ts +127 -0
- package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
- package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
- package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
- package/src/components/cnpg/archivingRepair.test.ts +104 -0
- package/src/components/cnpg/archivingRepair.ts +145 -0
- package/src/components/cnpg/baseBackup.test.ts +36 -0
- package/src/components/cnpg/baseBackup.ts +36 -0
- package/src/components/cnpg/blocking.test.ts +59 -0
- package/src/components/cnpg/blocking.ts +131 -0
- package/src/components/cnpg/coverageText.test.ts +27 -0
- package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
- package/src/components/cnpg/creation/targetModel.test.ts +137 -0
- package/src/components/cnpg/creation/targetModel.ts +149 -0
- package/src/components/cnpg/fleetStatus.test.ts +46 -0
- package/src/components/cnpg/fleetStatus.ts +43 -0
- package/src/components/cnpg/formFields.ts +2 -0
- package/src/components/cnpg/grantText.test.tsx +11 -0
- package/src/components/cnpg/host.test.tsx +24 -0
- package/src/components/cnpg/host.tsx +101 -0
- package/src/components/cnpg/inspectModel.test.ts +120 -0
- package/src/components/cnpg/inspectModel.ts +140 -0
- package/src/components/cnpg/logicalSlots.test.ts +30 -0
- package/src/components/cnpg/logicalSlots.ts +29 -0
- package/src/components/cnpg/lsn.test.ts +40 -0
- package/src/components/cnpg/lsn.ts +43 -0
- package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
- package/src/components/cnpg/operations/model.test.ts +510 -0
- package/src/components/cnpg/operations/model.ts +499 -0
- package/src/components/cnpg/operations/presentation.test.ts +80 -0
- package/src/components/cnpg/operations/presentation.ts +84 -0
- package/src/components/cnpg/operations/store.ts +98 -0
- package/src/components/cnpg/operatorStatus.test.ts +134 -0
- package/src/components/cnpg/operatorStatus.ts +194 -0
- package/src/components/cnpg/paths.test.ts +54 -0
- package/src/components/cnpg/paths.ts +47 -4
- package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
- package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
- package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
- package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
- package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
- package/src/components/cnpg/protection/scheduleModel.ts +16 -0
- package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
- package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
- package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
- package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
- package/src/components/cnpg/recovery/restoreModel.ts +685 -0
- package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
- package/src/components/cnpg/refresh.test.ts +38 -0
- package/src/components/cnpg/refresh.ts +17 -0
- package/src/components/cnpg/refreshNotice.test.tsx +29 -0
- package/src/components/cnpg/routes.test.ts +34 -3
- package/src/components/cnpg/routes.ts +60 -48
- package/src/components/cnpg/runtimeAssessment.ts +198 -0
- package/src/components/cnpg/runtimeModel.test.ts +175 -0
- package/src/components/cnpg/runtimeModel.ts +196 -0
- package/src/components/cnpg/runtimeParts.test.tsx +17 -0
- package/src/components/cnpg/runtimeParts.tsx +79 -0
- package/src/components/cnpg/shared.test.tsx +59 -0
- package/src/components/cnpg/shared.tsx +100 -182
- package/src/components/cnpg/storageModel.test.ts +107 -0
- package/src/components/cnpg/storageModel.ts +101 -0
- package/src/components/cnpg/trendSamples.test.ts +148 -0
- package/src/components/cnpg/trendSamples.ts +222 -0
- package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
- package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
- package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
- package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
- package/src/components/cnpg/useCNPGNavigate.ts +21 -0
- package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
- package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
- package/src/components/cnpg/workspaceRoute.ts +23 -0
- package/src/components/cost/ApplicationCostTab.test.ts +7 -0
- package/src/components/cost/ApplicationCostTab.tsx +7 -7
- package/src/components/cost/CostView.test.ts +2 -1
- package/src/components/cost/CostView.tsx +0 -2
- package/src/components/cost/WorkloadCostTab.tsx +2 -4
- package/src/components/cost/errors.ts +3 -1
- package/src/components/cost/source.test.ts +10 -0
- package/src/components/cost/source.ts +5 -1
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
- package/src/components/dock/BottomDock.tsx +2 -0
- package/src/components/dock/TerminalTab.test.tsx +17 -0
- package/src/components/dock/TerminalTab.tsx +7 -3
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
- package/src/components/helm/HelmCompareRoute.tsx +57 -26
- package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
- package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
- package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
- package/src/components/helm/ValuesViewer.test.tsx +301 -0
- package/src/components/helm/ValuesViewer.tsx +169 -78
- package/src/components/home/ActivitySummary.tsx +2 -1
- package/src/components/home/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- package/src/components/issues/IssuesPane.render.test.tsx +122 -0
- package/src/components/issues/IssuesPane.test.ts +65 -1
- package/src/components/issues/IssuesPane.tsx +188 -25
- package/src/components/issues/uncheckedSources.ts +21 -0
- package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
- package/src/components/resource/PVCUsageBar.test.tsx +24 -0
- package/src/components/resource/PVCUsageBar.tsx +15 -6
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resources/CompositeRenderer.tsx +4 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
- package/src/components/resources/ResourcesView.tsx +26 -12
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +28 -15
- package/src/components/timeline/TimelineList.tsx +22 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineView.tsx +18 -45
- package/src/components/timeline/ViewModeToggle.tsx +20 -0
- package/src/components/timeline/useStickyError.test.tsx +47 -0
- package/src/components/timeline/useStickyError.ts +15 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
- package/src/components/ui/Markdown.test.tsx +65 -0
- package/src/components/ui/Markdown.tsx +144 -127
- package/src/components/whats-new/releaseNotes.ts +63 -1
- package/src/components/workload/WorkloadView.test.ts +10 -1
- package/src/components/workload/WorkloadView.tsx +27 -239
- package/src/components/workspace/layout.test.tsx +29 -0
- package/src/components/workspace/layout.tsx +259 -0
- package/src/components/workspace/table.ts +7 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/integrations/resourceHost.test.tsx +55 -0
- package/src/integrations/resourceHost.ts +99 -0
- package/src/integrations/resourceHosts.test.tsx +141 -0
- package/src/integrations/resourceHosts.tsx +119 -0
- package/src/integrations/workspaceRoute.ts +20 -0
- package/src/integrations/workspaceRoutes.test.ts +40 -0
- package/src/integrations/workspaceRoutes.ts +22 -0
- package/src/integrations/workspaceScreens.tsx +14 -0
- package/src/utils/drawer-trail.ts +35 -0
- package/src/utils/kindFilterStore.test.ts +147 -0
- package/src/utils/kindFilterStore.ts +91 -0
- package/src/utils/navigation.test.ts +13 -1
- package/src/utils/navigation.ts +39 -2
- package/src/utils/page-links.ts +21 -0
- package/src/utils/tabStorage.ts +40 -0
- package/src/utils/viewMemory.test.ts +78 -0
- package/src/utils/viewMemory.ts +54 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { useReviewedActionMutation } from './reviewedActionMutation'
|
|
7
|
+
import { ApiError } from './client'
|
|
8
|
+
import { actionErrorCode, actionCompleted } from './actions'
|
|
9
|
+
import { RadarFeatureUnsupportedError } from './radarFeatures'
|
|
10
|
+
|
|
11
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
12
|
+
|
|
13
|
+
describe('reviewed action lifetime', () => {
|
|
14
|
+
it.each([
|
|
15
|
+
new TypeError('Connection lost'),
|
|
16
|
+
new ApiError('One write completed', 409, { code: 'partial', completed: ['Pod removed'] }),
|
|
17
|
+
new ApiError('upstream connect error or disconnect/reset before headers', 503),
|
|
18
|
+
])('retains an ambiguous result through remount and prevents another request: %s', async (failure) => {
|
|
19
|
+
const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
|
20
|
+
const host = document.body.appendChild(document.createElement('div'))
|
|
21
|
+
const root = createRoot(host)
|
|
22
|
+
const request = vi.fn(async () => { throw failure })
|
|
23
|
+
let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
|
|
24
|
+
function Dialog({ target }: { target: string }) {
|
|
25
|
+
current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>(target, 'ctx', { mutationFn: request })
|
|
26
|
+
return <div>{current.outcomeError?.message}</div>
|
|
27
|
+
}
|
|
28
|
+
const mount = (target?: string) => act(() => root.render(<QueryClientProvider client={client}>{target && <Dialog target={target} />}</QueryClientProvider>))
|
|
29
|
+
const attempt = () => act(async () => {
|
|
30
|
+
await current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {})
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
32
|
+
})
|
|
33
|
+
try {
|
|
34
|
+
mount('/api/cnpg/clusters/jobs/orders')
|
|
35
|
+
await attempt()
|
|
36
|
+
mount()
|
|
37
|
+
client.removeQueries()
|
|
38
|
+
client.clear()
|
|
39
|
+
mount('/api/cnpg/clusters/jobs/orders')
|
|
40
|
+
expect(current!.outcomeError?.message).toContain('reload this page')
|
|
41
|
+
expect(actionErrorCode(current!.outcomeError)).toBe(actionErrorCode(failure))
|
|
42
|
+
expect(actionCompleted(current!.outcomeError)).toEqual(actionCompleted(failure))
|
|
43
|
+
await attempt()
|
|
44
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
45
|
+
mount('/other-cluster/api/cnpg/clusters/jobs/orders')
|
|
46
|
+
expect(current!.outcomeError).toBeNull()
|
|
47
|
+
mount('/api/cnpg/scheduledbackups/jobs/orders')
|
|
48
|
+
expect(current!.outcomeError).toBeNull()
|
|
49
|
+
} finally {
|
|
50
|
+
act(() => root.unmount())
|
|
51
|
+
client.clear()
|
|
52
|
+
host.remove()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
it.each([
|
|
56
|
+
{ nextTarget: '/api/target', nextContext: 'other-context' },
|
|
57
|
+
{ nextTarget: '/other-cluster/api/target', nextContext: 'ctx' },
|
|
58
|
+
])('keeps a late result on the original cluster binding: %s', async ({ nextTarget, nextContext }) => {
|
|
59
|
+
const client = new QueryClient()
|
|
60
|
+
const host = document.body.appendChild(document.createElement('div'))
|
|
61
|
+
const root = createRoot(host)
|
|
62
|
+
let fail!: (error: Error) => void
|
|
63
|
+
let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
|
|
64
|
+
function Dialog({ target, context }: { target: string; context: string }) {
|
|
65
|
+
current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>(target, context, {
|
|
66
|
+
mutationFn: () => new Promise<null>((_, reject) => { fail = reject }),
|
|
67
|
+
})
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const mount = (target: string, context: string) => act(() => root.render(<QueryClientProvider client={client}><Dialog target={target} context={context} /></QueryClientProvider>))
|
|
71
|
+
try {
|
|
72
|
+
mount('/api/target', 'ctx')
|
|
73
|
+
let attempted!: Promise<unknown>
|
|
74
|
+
await act(async () => {
|
|
75
|
+
attempted = current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {})
|
|
76
|
+
await Promise.resolve()
|
|
77
|
+
})
|
|
78
|
+
mount(nextTarget, nextContext)
|
|
79
|
+
await act(async () => {
|
|
80
|
+
fail(new TypeError('Connection lost'))
|
|
81
|
+
await attempted
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
83
|
+
})
|
|
84
|
+
expect(current!.outcomeError).toBeNull()
|
|
85
|
+
mount('/api/target', 'ctx')
|
|
86
|
+
expect(current!.outcomeError?.message).toContain('Connection lost')
|
|
87
|
+
} finally {
|
|
88
|
+
act(() => root.unmount())
|
|
89
|
+
client.clear()
|
|
90
|
+
host.remove()
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
it.each([
|
|
94
|
+
new ApiError('Facts changed', 409, { code: 'changed' }),
|
|
95
|
+
new ApiError('The operator webhook did not answer', 503, { code: 'operator_webhook_unavailable' }),
|
|
96
|
+
new RadarFeatureUnsupportedError('cnpgWorkspace', {}),
|
|
97
|
+
])('keeps a known refusal retryable after remount: %s', async (failure) => {
|
|
98
|
+
const client = new QueryClient()
|
|
99
|
+
const host = document.body.appendChild(document.createElement('div'))
|
|
100
|
+
const root = createRoot(host)
|
|
101
|
+
let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
|
|
102
|
+
const request = vi.fn(async () => { throw failure })
|
|
103
|
+
function Dialog() {
|
|
104
|
+
current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>('/api/target', 'ctx', { mutationFn: request })
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
const render = (open: boolean) => act(() => root.render(<QueryClientProvider client={client}>{open && <Dialog />}</QueryClientProvider>))
|
|
108
|
+
try {
|
|
109
|
+
render(true)
|
|
110
|
+
await act(async () => { await current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {}) })
|
|
111
|
+
render(false)
|
|
112
|
+
render(true)
|
|
113
|
+
expect(current!.outcomeError).toBeNull()
|
|
114
|
+
await act(async () => { await current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {}) })
|
|
115
|
+
expect(request).toHaveBeenCalledTimes(2)
|
|
116
|
+
} finally {
|
|
117
|
+
act(() => root.unmount())
|
|
118
|
+
client.clear()
|
|
119
|
+
host.remove()
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('retains an in-flight request through remount and cache clearing without sending twice', async () => {
|
|
124
|
+
const client = new QueryClient()
|
|
125
|
+
const host = document.body.appendChild(document.createElement('div'))
|
|
126
|
+
const root = createRoot(host)
|
|
127
|
+
let complete!: (data: null) => void
|
|
128
|
+
const request = vi.fn(() => new Promise<null>(resolve => { complete = resolve }))
|
|
129
|
+
let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
|
|
130
|
+
function Dialog() {
|
|
131
|
+
current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>('/api/target', 'ctx', { mutationFn: request })
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
const render = (open: boolean) => act(() => root.render(<QueryClientProvider client={client}>{open && <Dialog />}</QueryClientProvider>))
|
|
135
|
+
try {
|
|
136
|
+
render(true)
|
|
137
|
+
let attempted!: Promise<null>
|
|
138
|
+
await act(async () => {
|
|
139
|
+
attempted = current.mutateAsync({ request: { reviewedContext: 'ctx' } })
|
|
140
|
+
await Promise.resolve()
|
|
141
|
+
})
|
|
142
|
+
render(false)
|
|
143
|
+
client.removeQueries()
|
|
144
|
+
client.clear()
|
|
145
|
+
render(true)
|
|
146
|
+
expect(current!.isPending).toBe(true)
|
|
147
|
+
await act(async () => {
|
|
148
|
+
await expect(current.mutateAsync({ request: { reviewedContext: 'ctx' } })).rejects.toThrow('still in progress')
|
|
149
|
+
})
|
|
150
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
151
|
+
await act(async () => {
|
|
152
|
+
complete(null)
|
|
153
|
+
await attempted
|
|
154
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
155
|
+
})
|
|
156
|
+
expect(current!.isPending).toBe(false)
|
|
157
|
+
expect(current!.outcomeError).toBeNull()
|
|
158
|
+
} finally {
|
|
159
|
+
act(() => root.unmount())
|
|
160
|
+
client.clear()
|
|
161
|
+
host.remove()
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
2
|
+
import { useMutation, useQueryClient, type MutationFunction, type QueryClient, type UseMutationOptions } from '@tanstack/react-query'
|
|
3
|
+
import { actionOutcomeLocked } from './actions'
|
|
4
|
+
import { ApiError } from './client'
|
|
5
|
+
|
|
6
|
+
interface Outcome {
|
|
7
|
+
pending: boolean
|
|
8
|
+
error: Error | null
|
|
9
|
+
}
|
|
10
|
+
const EMPTY: Outcome = { pending: false, error: null }
|
|
11
|
+
const stores = new WeakMap<QueryClient, { entries: Map<string, Outcome>; listeners: Set<() => void> }>()
|
|
12
|
+
|
|
13
|
+
/** Result memory belongs to the host, so dismissing a dialog cannot unlock a second write. */
|
|
14
|
+
export function useReviewedActionMutation<Data, Variables extends { request: { reviewedContext: string } }>(
|
|
15
|
+
target: string,
|
|
16
|
+
reviewedContext: string | undefined,
|
|
17
|
+
options: Omit<UseMutationOptions<Data, Error, Variables>, 'mutationFn' | 'mutationKey'> & { mutationFn: MutationFunction<Data, Variables> },
|
|
18
|
+
) {
|
|
19
|
+
const client = useQueryClient()
|
|
20
|
+
let store = stores.get(client)
|
|
21
|
+
if (!store) {
|
|
22
|
+
store = { entries: new Map(), listeners: new Set() }
|
|
23
|
+
stores.set(client, store)
|
|
24
|
+
}
|
|
25
|
+
const outcomes = store
|
|
26
|
+
const keyFor = (context: string | undefined) => JSON.stringify([target, context])
|
|
27
|
+
const key = keyFor(reviewedContext)
|
|
28
|
+
const subscribe = useCallback((listener: () => void) => {
|
|
29
|
+
outcomes.listeners.add(listener)
|
|
30
|
+
return () => { outcomes.listeners.delete(listener) }
|
|
31
|
+
}, [outcomes])
|
|
32
|
+
const snapshot = useCallback(() => outcomes.entries.get(key) ?? EMPTY, [outcomes, key])
|
|
33
|
+
// Connection recovery purges query/mutation caches. It must not forget a
|
|
34
|
+
// request still in flight or a result that may already have changed the target.
|
|
35
|
+
const retained = useSyncExternalStore(subscribe, snapshot, () => EMPTY)
|
|
36
|
+
const publish = (key: string, value: Outcome) => {
|
|
37
|
+
if (value.pending || value.error) outcomes.entries.set(key, value)
|
|
38
|
+
else outcomes.entries.delete(key)
|
|
39
|
+
for (const listener of outcomes.listeners) listener()
|
|
40
|
+
}
|
|
41
|
+
const mutation = useMutation<Data, Error, Variables>({
|
|
42
|
+
...options,
|
|
43
|
+
mutationKey: ['reviewed-action', target, reviewedContext],
|
|
44
|
+
mutationFn: async (variables, context) => {
|
|
45
|
+
const boundKey = keyFor(variables.request.reviewedContext)
|
|
46
|
+
const previous = outcomes.entries.get(boundKey)
|
|
47
|
+
if (previous?.error) throw previous.error
|
|
48
|
+
if (previous?.pending) throw new ApiError('A write to this target is still in progress. Wait for its result.', 409, { code: 'blocked' })
|
|
49
|
+
publish(boundKey, { pending: true, error: null })
|
|
50
|
+
let retainedError: Error | null = null
|
|
51
|
+
try {
|
|
52
|
+
return await options.mutationFn(variables, context)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (actionOutcomeLocked(error)) {
|
|
55
|
+
const failure = error as Error
|
|
56
|
+
const message = `${failure.message} Inspect the current state, then reload this page and review the action again.`
|
|
57
|
+
retainedError = failure instanceof ApiError ? new ApiError(message, failure.status, failure.data) : new Error(message)
|
|
58
|
+
}
|
|
59
|
+
throw error
|
|
60
|
+
} finally {
|
|
61
|
+
publish(boundKey, { pending: false, error: retainedError })
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
return { ...mutation, isPending: mutation.isPending || retained.pending, outcomeError: retained.error }
|
|
66
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
applyClientFilters,
|
|
20
20
|
localOverviewFromEvents,
|
|
21
21
|
needsWindowFetch,
|
|
22
|
+
frozenWindowRefetchInterval,
|
|
22
23
|
type RetainedRing,
|
|
23
24
|
} from './timelineSource'
|
|
24
25
|
import type { TimelineEvent } from '../types'
|
|
@@ -650,3 +651,11 @@ describe('retained ring fetch (the OSS-identical accumulate model)', () => {
|
|
|
650
651
|
// cursor is intact on the cached ring — nothing was committed
|
|
651
652
|
})
|
|
652
653
|
})
|
|
654
|
+
|
|
655
|
+
describe('frozenWindowRefetchInterval', () => {
|
|
656
|
+
it('polls a failed window until it loads, and leaves a loaded one alone', () => {
|
|
657
|
+
expect(frozenWindowRefetchInterval('error')).toBe(10_000)
|
|
658
|
+
expect(frozenWindowRefetchInterval('success')).toBe(false)
|
|
659
|
+
expect(frozenWindowRefetchInterval('pending')).toBe(false)
|
|
660
|
+
})
|
|
661
|
+
})
|
|
@@ -544,6 +544,13 @@ export function needsWindowFetch(
|
|
|
544
544
|
// refresh.
|
|
545
545
|
const FROZEN_WINDOW_STALE_MS = 10 * 60 * 1000
|
|
546
546
|
|
|
547
|
+
// A frozen window that failed to load does poll, at the ring's cadence, until
|
|
548
|
+
// it loads: the error page tells the user it recovers on its own once the
|
|
549
|
+
// timeline's database is back, and without a poll only "Try again" would.
|
|
550
|
+
export function frozenWindowRefetchInterval(status: 'pending' | 'error' | 'success'): number | false {
|
|
551
|
+
return status === 'error' ? RETAINED_POLL_MS : false
|
|
552
|
+
}
|
|
553
|
+
|
|
547
554
|
// Ring keys whose next fetch must be a full reload (manual refresh). A side
|
|
548
555
|
// flag, not a side cursor: consuming it only switches the PATH of the next
|
|
549
556
|
// fetch — all sync state that must stay consistent with the data lives inside
|
|
@@ -750,6 +757,7 @@ function createRingEventsHook(opts: {
|
|
|
750
757
|
},
|
|
751
758
|
enabled: windowNeeded,
|
|
752
759
|
staleTime: FROZEN_WINDOW_STALE_MS,
|
|
760
|
+
refetchInterval: (q) => frozenWindowRefetchInterval(q.state.status),
|
|
753
761
|
})
|
|
754
762
|
|
|
755
763
|
const kindsKey = query.kinds?.join(',')
|
|
@@ -83,6 +83,9 @@ function BestPracticesView({ namespaces, onNavigateToResource }: AuditViewProps)
|
|
|
83
83
|
)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const annotationIgnored = Object.values(data.annotationIgnoredByNamespace ?? {})
|
|
87
|
+
.reduce((sum, byNS) => sum + Object.values(byNS).reduce((a, n) => a + n, 0), 0)
|
|
88
|
+
|
|
86
89
|
const onResourceClick = (ref: CheckResourceRef) =>
|
|
87
90
|
onNavigateToResource({ kind: ref.kind, namespace: ref.namespace, name: ref.name, group: ref.group })
|
|
88
91
|
|
|
@@ -108,6 +111,11 @@ function BestPracticesView({ namespaces, onNavigateToResource }: AuditViewProps)
|
|
|
108
111
|
{ignoredCount > 0 && (
|
|
109
112
|
<button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
|
|
110
113
|
)}
|
|
114
|
+
{annotationIgnored > 0 && (
|
|
115
|
+
<Tooltip content={`Resources that opted out of a check with the ${data.ignoreChecksAnnotation} annotation`}>
|
|
116
|
+
<span className="text-xs text-theme-text-tertiary">{annotationIgnored} {annotationIgnored === 1 ? 'finding' : 'findings'} hidden by annotation</span>
|
|
117
|
+
</Tooltip>
|
|
118
|
+
)}
|
|
111
119
|
<Tooltip content="Checks settings">
|
|
112
120
|
<button
|
|
113
121
|
onClick={() => setShowSettings(true)}
|
|
@@ -129,6 +137,7 @@ function BestPracticesView({ namespaces, onNavigateToResource }: AuditViewProps)
|
|
|
129
137
|
onResourceClick={onResourceClick}
|
|
130
138
|
onHideCheck={canEdit ? hideCheck : undefined}
|
|
131
139
|
onHideCategory={canEdit ? hideCategory : undefined}
|
|
140
|
+
ignoreChecksAnnotation={data.ignoreChecksAnnotation}
|
|
132
141
|
/>
|
|
133
142
|
|
|
134
143
|
{showSettings && <AuditSettingsDialog namespaces={namespaces} onClose={() => setShowSettings(false)} />}
|
|
@@ -17,37 +17,9 @@ import {
|
|
|
17
17
|
useCapacityActivity,
|
|
18
18
|
} from "../../api/client";
|
|
19
19
|
import type { SelectedResource } from "../../types";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
InlineEmpty,
|
|
24
|
-
LinkButton,
|
|
25
|
-
Notice,
|
|
26
|
-
PageControls,
|
|
27
|
-
PoolSelector,
|
|
28
|
-
ROW_HOVER,
|
|
29
|
-
ScopeBadges,
|
|
30
|
-
ScrollableContent,
|
|
31
|
-
TABLE_HEAD,
|
|
32
|
-
TABLE_WRAP,
|
|
33
|
-
TBODY,
|
|
34
|
-
TD,
|
|
35
|
-
TH,
|
|
36
|
-
activityTypeLabel,
|
|
37
|
-
activityWindowPreset,
|
|
38
|
-
coverageHasObservations,
|
|
39
|
-
coverageIsLowerBound,
|
|
40
|
-
coverageMessage,
|
|
41
|
-
errorMessage,
|
|
42
|
-
formatTimestamp,
|
|
43
|
-
identityKey,
|
|
44
|
-
identityToSelectedResource,
|
|
45
|
-
integrationBlock,
|
|
46
|
-
relativeTime,
|
|
47
|
-
retentionLabel,
|
|
48
|
-
useCapacityCursorRecovery,
|
|
49
|
-
useCapacityPagination,
|
|
50
|
-
} from "./shared";
|
|
20
|
+
import { ActivityStateBadge, CapacityFreshness, InlineEmpty, LinkButton, PageControls, PoolSelector, ScopeBadges, ScrollableContent, activityTypeLabel, activityWindowPreset, coverageHasObservations, coverageIsLowerBound, coverageMessage, errorMessage, formatTimestamp, identityKey, identityToSelectedResource, integrationBlock, relativeTime, retentionLabel, useCapacityCursorRecovery, useCapacityPagination } from "./shared";
|
|
21
|
+
import { Notice } from "../workspace/layout";
|
|
22
|
+
import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from "../workspace/table";
|
|
51
23
|
|
|
52
24
|
const WINDOW_PILLS: [number | undefined, string][] = [
|
|
53
25
|
[undefined, "Retained"],
|
|
@@ -21,41 +21,9 @@ import {
|
|
|
21
21
|
} from "../../api/client";
|
|
22
22
|
import type { SelectedResource } from "../../types";
|
|
23
23
|
import { refToSelectedResource } from "../../utils/navigation";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
DemandStateBadge,
|
|
28
|
-
EmptyState,
|
|
29
|
-
InlineEmpty,
|
|
30
|
-
LinkButton,
|
|
31
|
-
Notice,
|
|
32
|
-
PageControls,
|
|
33
|
-
PoolEvaluationBadge,
|
|
34
|
-
PoolSelector,
|
|
35
|
-
QuantityInline,
|
|
36
|
-
ResourceLink,
|
|
37
|
-
ROW_HOVER,
|
|
38
|
-
ScopeBadges,
|
|
39
|
-
ScrollableContent,
|
|
40
|
-
TABLE_HEAD,
|
|
41
|
-
TABLE_WRAP,
|
|
42
|
-
TBODY,
|
|
43
|
-
TD,
|
|
44
|
-
TH,
|
|
45
|
-
coverageHasObservations,
|
|
46
|
-
coverageIsLowerBound,
|
|
47
|
-
coverageMessage,
|
|
48
|
-
demandStateLabel,
|
|
49
|
-
errorMessage,
|
|
50
|
-
formatTimestamp,
|
|
51
|
-
humanizeCode,
|
|
52
|
-
identityKey,
|
|
53
|
-
integrationBlock,
|
|
54
|
-
namespaceCoverageDescription,
|
|
55
|
-
quantityText,
|
|
56
|
-
useCapacityCursorRecovery,
|
|
57
|
-
useCapacityPagination,
|
|
58
|
-
} from "./shared";
|
|
24
|
+
import { CapacityFreshness, CapacityIssueEvidence, DemandStateBadge, InlineEmpty, LinkButton, PageControls, PoolEvaluationBadge, PoolSelector, QuantityInline, ResourceLink, ScopeBadges, ScrollableContent, coverageHasObservations, coverageIsLowerBound, coverageMessage, demandStateLabel, errorMessage, formatTimestamp, humanizeCode, identityKey, integrationBlock, namespaceCoverageDescription, quantityText, useCapacityCursorRecovery, useCapacityPagination } from "./shared";
|
|
25
|
+
import { Notice, ScreenEmptyState } from "../workspace/layout";
|
|
26
|
+
import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from "../workspace/table";
|
|
59
27
|
|
|
60
28
|
const STATE_PILLS: [CapacityDemandState | undefined, string][] = [
|
|
61
29
|
[undefined, "All states"],
|
|
@@ -184,7 +152,7 @@ export function CapacityDemand({
|
|
|
184
152
|
const clearPodFilter = () => updateSearchParam("pod", undefined);
|
|
185
153
|
if (poolFilter && !responseData && isNotFoundError(query.error)) {
|
|
186
154
|
return (
|
|
187
|
-
<
|
|
155
|
+
<ScreenEmptyState
|
|
188
156
|
icon={AlertTriangle}
|
|
189
157
|
title="NodePool not found"
|
|
190
158
|
detail={`This link evaluates demand against NodePool “${poolFilter}”, which no longer exists. It may have been removed or belongs to another cluster context.`}
|