@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,49 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import type { TimelineEventsResult, TimelineSource } from '../../api/timelineSource'
|
|
4
|
+
|
|
5
|
+
// The list reads its source from context; hand it a fixed empty result so the
|
|
6
|
+
// test is about the note row alone.
|
|
7
|
+
const eventsResult: TimelineEventsResult = { data: [], isLoading: false, isFetching: false, isError: false, refetch: () => {} }
|
|
8
|
+
const fakeSource: TimelineSource = {
|
|
9
|
+
capabilities: { mode: 'local', ringLimit: 10_000 },
|
|
10
|
+
useEvents: () => eventsResult,
|
|
11
|
+
}
|
|
12
|
+
vi.mock('../../context/TimelineSource', () => ({ useTimelineSource: () => fakeSource }))
|
|
13
|
+
|
|
14
|
+
import { TimelineRetentionNoteProvider } from '../../context/TimelineCustomization'
|
|
15
|
+
import { TimelineList } from './TimelineList'
|
|
16
|
+
|
|
17
|
+
describe('TimelineList history-limit row', () => {
|
|
18
|
+
const props = {
|
|
19
|
+
namespaces: [],
|
|
20
|
+
showDeleted: false,
|
|
21
|
+
onShowDeletedChange: () => {},
|
|
22
|
+
showRoutine: false,
|
|
23
|
+
onShowRoutineChange: () => {},
|
|
24
|
+
search: '',
|
|
25
|
+
onSearchChange: () => {},
|
|
26
|
+
activityFilter: [],
|
|
27
|
+
onActivityFilterChange: () => {},
|
|
28
|
+
kindFilter: [],
|
|
29
|
+
onKindFilterChange: () => {},
|
|
30
|
+
}
|
|
31
|
+
const withNote = (node: React.ReactNode) => (
|
|
32
|
+
<TimelineRetentionNoteProvider value={<a href="/plans">Host note</a>}>{node}</TimelineRetentionNoteProvider>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
it('shows the host note in its own row', () => {
|
|
36
|
+
const html = renderToStaticMarkup(withNote(<TimelineList {...props} />))
|
|
37
|
+
expect(html).toContain('data-testid="timeline-retention-note"')
|
|
38
|
+
expect(html).toContain('<a href="/plans">Host note</a>')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders no row without a host note (standalone Radar)', () => {
|
|
42
|
+
expect(renderToStaticMarkup(<TimelineList {...props} />)).not.toContain('timeline-retention-note')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('leaves the note to the view when a scrubber owns the range', () => {
|
|
46
|
+
const html = renderToStaticMarkup(withNote(<TimelineList {...props} selectionWindow={{ fromMs: 0, toMs: 1 }} />))
|
|
47
|
+
expect(html).not.toContain('timeline-retention-note')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Clock } from 'lucide-react'
|
|
2
|
+
import { useTimelineRetentionNote } from '../../context/TimelineCustomization'
|
|
3
|
+
|
|
4
|
+
// The host's history-limit note as a full-width row above the timeline,
|
|
5
|
+
// beside the other history-completeness rows (truncation, stale live
|
|
6
|
+
// updates). Informational, not a warning: nothing is broken, the server
|
|
7
|
+
// serves this much. Renders nothing when the host has no note.
|
|
8
|
+
export function TimelineRetentionBanner() {
|
|
9
|
+
const note = useTimelineRetentionNote()
|
|
10
|
+
if (!note) return null
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className="flex flex-wrap items-center gap-x-1.5 gap-y-1 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary"
|
|
14
|
+
data-testid="timeline-retention-note"
|
|
15
|
+
>
|
|
16
|
+
<Clock className="h-3.5 w-3.5 shrink-0" />
|
|
17
|
+
{note}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
|
|
2
2
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
-
import { Network, AlertTriangle,
|
|
4
|
+
import { Network, AlertTriangle, Boxes, X } from 'lucide-react'
|
|
5
5
|
import {
|
|
6
6
|
clampLensToSelection,
|
|
7
7
|
deriveLiveSelection,
|
|
@@ -21,10 +21,14 @@ import {
|
|
|
21
21
|
useDebouncedValue,
|
|
22
22
|
} from '@skyhook-io/k8s-ui'
|
|
23
23
|
import { TimelineList } from './TimelineList'
|
|
24
|
+
import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
|
|
25
|
+
import { ViewModeToggle } from './ViewModeToggle'
|
|
26
|
+
import { useStickyError } from './useStickyError'
|
|
24
27
|
import type { ActivityFilterKey } from './TimelineList'
|
|
25
28
|
import { TimelineSwimlanes } from './TimelineSwimlanes'
|
|
26
29
|
import { RetainedTimelineScrubber, extendSelection, type ScrubberDomainInfo } from './RetainedTimelineScrubber'
|
|
27
30
|
import { LocalTimelineScrubber } from './LocalTimelineScrubber'
|
|
31
|
+
import { TimelineRetentionBanner } from './TimelineRetentionNote'
|
|
28
32
|
import { useTopology, useApplications } from '../../api/client'
|
|
29
33
|
import { useTimelineSource } from '../../context/TimelineSource'
|
|
30
34
|
import type { Topology } from '../../types'
|
|
@@ -274,6 +278,7 @@ interface TimelineViewProps {
|
|
|
274
278
|
requiresNamespaceFilter?: boolean
|
|
275
279
|
availableNamespaces?: { name: string }[]
|
|
276
280
|
onNamespaceSelect?: (ns: string) => void
|
|
281
|
+
onOpenDiagnostics?: () => void
|
|
277
282
|
}
|
|
278
283
|
|
|
279
284
|
export function resolveApplicationTimelineScope(searchParams: URLSearchParams, namespaces: string[]) {
|
|
@@ -292,7 +297,7 @@ export function resolveApplicationTimelineScope(searchParams: URLSearchParams, n
|
|
|
292
297
|
}
|
|
293
298
|
}
|
|
294
299
|
|
|
295
|
-
export function TimelineView({ namespaces, onResourceClick, initialViewMode, initialFilter, initialTimeRange, requiresNamespaceFilter, availableNamespaces, onNamespaceSelect }: TimelineViewProps) {
|
|
300
|
+
export function TimelineView({ namespaces, onResourceClick, initialViewMode, initialFilter, initialTimeRange, requiresNamespaceFilter, availableNamespaces, onNamespaceSelect, onOpenDiagnostics }: TimelineViewProps) {
|
|
296
301
|
// URL is the source of truth for every control below (deep-linkable +
|
|
297
302
|
// back/forward-restorable). Read on mount, written on user change.
|
|
298
303
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
@@ -682,7 +687,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
682
687
|
// Fetch all activity - zoom controls what's visible in the UI. This ring feeds
|
|
683
688
|
// the swimlanes and the local strip's histogram, so it also runs in list mode
|
|
684
689
|
// when that strip is shown; the list itself fetches its own 2000.
|
|
685
|
-
const { data: activity, isLoading, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
|
|
690
|
+
const { data: activity, isLoading, isFetching, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
|
|
686
691
|
namespaces: timelineNamespaces,
|
|
687
692
|
timeRange: 'all',
|
|
688
693
|
includeK8sEvents: true,
|
|
@@ -700,6 +705,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
700
705
|
fromMs: isRetained ? selection.fromMs : undefined,
|
|
701
706
|
toMs: isRetained ? selection.toMs : undefined,
|
|
702
707
|
})
|
|
708
|
+
const loadError = useStickyError(error, activity != null)
|
|
703
709
|
|
|
704
710
|
// Topology powers both swimlane hierarchy and application-scoped attribution.
|
|
705
711
|
const { data: rawTopology } = useTopology(timelineNamespaces, 'resources', {
|
|
@@ -829,11 +835,16 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
829
835
|
// The scrubber sits above whichever view is active, sharing one selection
|
|
830
836
|
// across list + swimlane. Retained draws its server-overview strip; local
|
|
831
837
|
// derives the strip client-side from the loaded ring and omits the gap band.
|
|
838
|
+
// The host's note about how far back history goes: without it the stretch
|
|
839
|
+
// before the limit reads as "nothing happened". Without a scrubber the list
|
|
840
|
+
// view shows it itself, above its own range selector.
|
|
841
|
+
const retentionBanner = showScrubber || viewMode === 'swimlane' ? <TimelineRetentionBanner /> : null
|
|
832
842
|
const wrap = (node: ReactNode): ReactNode => {
|
|
833
843
|
if (!showScrubber) {
|
|
834
844
|
return (
|
|
835
845
|
<div className="flex-1 flex flex-col min-h-0">
|
|
836
846
|
{appScopeBar}
|
|
847
|
+
{retentionBanner}
|
|
837
848
|
<div className="flex-1 flex flex-col min-h-0">{node}</div>
|
|
838
849
|
</div>
|
|
839
850
|
)
|
|
@@ -850,6 +861,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
850
861
|
return (
|
|
851
862
|
<div className="flex-1 flex flex-col min-h-0">
|
|
852
863
|
{appScopeBar}
|
|
864
|
+
{retentionBanner}
|
|
853
865
|
{/* A row-capped ring load means the OLDEST part of the retention
|
|
854
866
|
window is not loaded — say so, or an old selection reads as
|
|
855
867
|
"nothing happened back then". */}
|
|
@@ -862,13 +874,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
862
874
|
{/* Failing background polls with a loaded ring: keep the data, say
|
|
863
875
|
it's going stale. The full-screen error is reserved for no-data. */}
|
|
864
876
|
{isError && activity && (
|
|
865
|
-
<
|
|
866
|
-
<AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
|
|
867
|
-
Live updates are failing — the timeline may be stale.
|
|
868
|
-
<button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
|
|
869
|
-
Retry now
|
|
870
|
-
</button>
|
|
871
|
-
</div>
|
|
877
|
+
<TimelineStaleNote error={error} subject="timeline" onRetry={() => refetch()} />
|
|
872
878
|
)}
|
|
873
879
|
{isRetained ? (
|
|
874
880
|
<RetainedTimelineScrubber
|
|
@@ -960,29 +966,23 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
960
966
|
// a load failure. With a loaded ring on screen, a failing background poll
|
|
961
967
|
// must NOT blank it (gate on data before error); the stale-data banner
|
|
962
968
|
// below carries the warning instead.
|
|
963
|
-
if (
|
|
969
|
+
if (loadError && !activity) {
|
|
964
970
|
// Surface the server's own message — a generic "failed to load" would
|
|
965
971
|
// swallow whatever the hub said. "Try again" is a full resync: the
|
|
966
972
|
// retained source drops its delta cursor and reloads the whole ring.
|
|
967
|
-
const detail = error?.message?.trim()
|
|
968
973
|
return wrap(
|
|
969
974
|
<div className="flex-1 flex flex-col">
|
|
970
975
|
<div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
|
|
971
976
|
<div />
|
|
972
977
|
<ViewModeToggle viewMode={viewMode} onViewModeChange={setViewMode} />
|
|
973
978
|
</div>
|
|
974
|
-
<
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
>
|
|
982
|
-
<RefreshCw className="w-3.5 h-3.5" />
|
|
983
|
-
Try again
|
|
984
|
-
</button>
|
|
985
|
-
</div>
|
|
979
|
+
<TimelineLoadError
|
|
980
|
+
className="flex-1"
|
|
981
|
+
error={loadError}
|
|
982
|
+
retrying={isFetching}
|
|
983
|
+
onRetry={() => refetch()}
|
|
984
|
+
onOpenDiagnostics={onOpenDiagnostics}
|
|
985
|
+
/>
|
|
986
986
|
</div>
|
|
987
987
|
)
|
|
988
988
|
}
|
|
@@ -1057,27 +1057,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
1057
1057
|
appScoped={Boolean(focusedAppKey)}
|
|
1058
1058
|
topology={stableTopology}
|
|
1059
1059
|
appScopeLoading={focusedAppLoading}
|
|
1060
|
+
onOpenDiagnostics={onOpenDiagnostics}
|
|
1060
1061
|
/>
|
|
1061
1062
|
)
|
|
1062
1063
|
}
|
|
1063
|
-
|
|
1064
|
-
function ViewModeToggle({ viewMode, onViewModeChange }: { viewMode: TimelineViewMode, onViewModeChange: (mode: TimelineViewMode) => void }) {
|
|
1065
|
-
return (
|
|
1066
|
-
<div className="flex items-center gap-1 bg-theme-base rounded-lg p-0.5 border border-theme-border">
|
|
1067
|
-
<button
|
|
1068
|
-
type="button"
|
|
1069
|
-
onClick={() => onViewModeChange('list')}
|
|
1070
|
-
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'list' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
1071
|
-
>
|
|
1072
|
-
List
|
|
1073
|
-
</button>
|
|
1074
|
-
<button
|
|
1075
|
-
type="button"
|
|
1076
|
-
onClick={() => onViewModeChange('swimlane')}
|
|
1077
|
-
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'swimlane' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
1078
|
-
>
|
|
1079
|
-
Swimlane
|
|
1080
|
-
</button>
|
|
1081
|
-
</div>
|
|
1082
|
-
)
|
|
1083
|
-
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function ViewModeToggle({ viewMode, onViewModeChange }: { viewMode: 'list' | 'swimlane', onViewModeChange: (mode: 'list' | 'swimlane') => void }) {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center gap-1 bg-theme-base rounded-lg p-0.5 border border-theme-border">
|
|
4
|
+
<button
|
|
5
|
+
type="button"
|
|
6
|
+
onClick={() => onViewModeChange('list')}
|
|
7
|
+
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'list' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
8
|
+
>
|
|
9
|
+
List
|
|
10
|
+
</button>
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
onClick={() => onViewModeChange('swimlane')}
|
|
14
|
+
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'swimlane' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
15
|
+
>
|
|
16
|
+
Swimlane
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { useStickyError } from './useStickyError'
|
|
6
|
+
|
|
7
|
+
const seen: (Error | null)[] = []
|
|
8
|
+
function Harness({ error, hasData }: { error: Error | null; hasData: boolean }) {
|
|
9
|
+
seen.push(useStickyError(error, hasData))
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let root: Root
|
|
14
|
+
let container: HTMLDivElement
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
seen.length = 0
|
|
17
|
+
container = document.createElement('div')
|
|
18
|
+
root = createRoot(container)
|
|
19
|
+
})
|
|
20
|
+
afterEach(() => act(() => root.unmount()))
|
|
21
|
+
const render = (error: Error | null, hasData: boolean) => act(() => root.render(<Harness error={error} hasData={hasData} />))
|
|
22
|
+
const last = () => seen[seen.length - 1]
|
|
23
|
+
|
|
24
|
+
describe('useStickyError', () => {
|
|
25
|
+
it('keeps the error while a refetch with no data clears it', () => {
|
|
26
|
+
const down = new Error('database down')
|
|
27
|
+
render(down, false)
|
|
28
|
+
expect(last()).toBe(down)
|
|
29
|
+
render(null, false)
|
|
30
|
+
expect(last()).toBe(down)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('drops the error once data arrives', () => {
|
|
34
|
+
render(new Error('database down'), false)
|
|
35
|
+
render(null, true)
|
|
36
|
+
expect(last()).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// A page with data on screen whose poll starts failing: the hook must settle,
|
|
40
|
+
// not bounce between the error and null until React gives up.
|
|
41
|
+
it('settles when an error arrives while data is on screen', () => {
|
|
42
|
+
render(null, true)
|
|
43
|
+
render(new Error('database down'), true)
|
|
44
|
+
expect(last()).toBeNull()
|
|
45
|
+
expect(seen.length).toBeLessThan(10)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
// React Query clears the error of a query that has never loaded each time it
|
|
4
|
+
// refetches, so a failure that persists across polls would flicker to a loader
|
|
5
|
+
// on every interval. This keeps the last error on screen until data arrives.
|
|
6
|
+
// Once data is loaded it returns null: a failing poll then belongs to the
|
|
7
|
+
// stale-data banner, not the full-screen error.
|
|
8
|
+
export function useStickyError(error: Error | null | undefined, hasData: boolean): Error | null {
|
|
9
|
+
const [shown, setShown] = useState<Error | null>(null)
|
|
10
|
+
const next = hasData ? null : (error ?? shown)
|
|
11
|
+
// Adjusting state during render: converges in one extra pass because `next`
|
|
12
|
+
// is a fixed point once stored.
|
|
13
|
+
if (next !== shown) setShown(next)
|
|
14
|
+
return next
|
|
15
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { Virtuoso } from 'react-virtuoso'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { ArrowRight, Crosshair, Minus } from 'lucide-react'
|
|
5
|
+
import type { AggregatedFlow } from '../../types'
|
|
6
|
+
import { SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
8
|
+
import type { TrafficGraphSelection } from './TrafficGraph'
|
|
9
|
+
import {
|
|
10
|
+
connectionRows, displayVolume, edgesOnPort, flowDrops, flowErrors, formatRate, graphEndpointId, hasStatusData, isExternalKind,
|
|
11
|
+
type GraphFlow, type TrafficFocus,
|
|
12
|
+
} from './trafficFilters'
|
|
13
|
+
|
|
14
|
+
interface TrafficConnectionsTableProps {
|
|
15
|
+
flows: AggregatedFlow[]
|
|
16
|
+
isRateBased: boolean
|
|
17
|
+
selection: TrafficGraphSelection | null
|
|
18
|
+
onSelect: (selection: TrafficGraphSelection | null) => void
|
|
19
|
+
onFocus: (focus: TrafficFocus) => void
|
|
20
|
+
/** The focused endpoint, whose name is not offered as a focus again. */
|
|
21
|
+
focusedId?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const GRID = 'grid grid-cols-[minmax(0,1fr)_1rem_minmax(0,1fr)_6.5rem_5rem_4.5rem_4.5rem] items-center gap-x-3'
|
|
25
|
+
// Without HTTP status anywhere in view (plain TCP) a 5xx column would read as
|
|
26
|
+
// "no errors" rather than "not measured".
|
|
27
|
+
const GRID_NO_STATUS = 'grid grid-cols-[minmax(0,1fr)_1rem_minmax(0,1fr)_6.5rem_5rem_4.5rem] items-center gap-x-3'
|
|
28
|
+
|
|
29
|
+
function rowSelection(flow: AggregatedFlow): TrafficGraphSelection {
|
|
30
|
+
return {
|
|
31
|
+
type: 'edge',
|
|
32
|
+
sourceId: graphEndpointId(flow.source),
|
|
33
|
+
destId: graphEndpointId(flow.destination),
|
|
34
|
+
port: flow.port,
|
|
35
|
+
directionUnknown: !!flow.directionUnknown,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function selectedRows(rows: GraphFlow[], selection: TrafficGraphSelection | null): Set<GraphFlow> {
|
|
40
|
+
if (selection?.type !== 'edge' || selection.port === undefined) return new Set()
|
|
41
|
+
const between = rows.filter(flow =>
|
|
42
|
+
graphEndpointId(flow.source) === selection.sourceId && graphEndpointId(flow.destination) === selection.destId &&
|
|
43
|
+
!!flow.directionUnknown === !!selection.directionUnknown)
|
|
44
|
+
return new Set(edgesOnPort(between, f => f, selection.port))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The view's connections as rows, for when there are too many to draw. Where
|
|
49
|
+
* the problems are comes first. Selecting a row loads its records into the
|
|
50
|
+
* flow list, as selecting an edge does; a name narrows the view to it.
|
|
51
|
+
*/
|
|
52
|
+
export function TrafficConnectionsTable({ flows, isRateBased, selection, onSelect, onFocus, focusedId }: TrafficConnectionsTableProps) {
|
|
53
|
+
const rows = useMemo(() => connectionRows(flows), [flows])
|
|
54
|
+
const selected = useMemo(() => selectedRows(rows, selection), [rows, selection])
|
|
55
|
+
const showStatus = useMemo(() => flows.some(hasStatusData), [flows])
|
|
56
|
+
const grid = showStatus ? GRID : GRID_NO_STATUS
|
|
57
|
+
|
|
58
|
+
const endpoint = (e: AggregatedFlow['source']) => {
|
|
59
|
+
const id = graphEndpointId(e)
|
|
60
|
+
const label = (
|
|
61
|
+
<span className="min-w-0">
|
|
62
|
+
<span className="block truncate text-theme-text-primary">{e.name}</span>
|
|
63
|
+
{e.namespace && <span className="block truncate text-[10px] text-theme-text-tertiary">{e.namespace}</span>}
|
|
64
|
+
</span>
|
|
65
|
+
)
|
|
66
|
+
if (id === focusedId || e.kind === 'Internet') return <span className="flex min-w-0 items-center">{label}</span>
|
|
67
|
+
return (
|
|
68
|
+
<Tooltip content={`Focus on ${e.name}`} wrapperClassName="min-w-0">
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={ev => {
|
|
72
|
+
ev.stopPropagation()
|
|
73
|
+
onFocus({ namespace: e.namespace || undefined, name: e.name })
|
|
74
|
+
}}
|
|
75
|
+
className="group flex min-w-0 max-w-full items-center gap-1 text-left hover:underline"
|
|
76
|
+
>
|
|
77
|
+
{label}
|
|
78
|
+
<Crosshair className="h-3 w-3 shrink-0 opacity-0 group-hover:opacity-70" />
|
|
79
|
+
</button>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
86
|
+
<div className={clsx(grid, 'px-3 py-1.5 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary border-b border-theme-border')}>
|
|
87
|
+
<span>Source</span>
|
|
88
|
+
<span />
|
|
89
|
+
<span>Destination</span>
|
|
90
|
+
<span>Port</span>
|
|
91
|
+
<span className="text-right">{isRateBased ? 'Req/s' : 'Conns'}</span>
|
|
92
|
+
{showStatus && <span className="text-right">5xx</span>}
|
|
93
|
+
<span className="text-right">Dropped</span>
|
|
94
|
+
</div>
|
|
95
|
+
<Virtuoso
|
|
96
|
+
className="flex-1"
|
|
97
|
+
data={rows}
|
|
98
|
+
computeItemKey={(_, flow) => `${graphEndpointId(flow.source)}->${graphEndpointId(flow.destination)}:${flow.port}${flow.directionUnknown ? ':u' : ''}`}
|
|
99
|
+
itemContent={(_, flow) => {
|
|
100
|
+
const sel = rowSelection(flow)
|
|
101
|
+
const isSelected = selected.has(flow)
|
|
102
|
+
const errors = flowErrors(flow)
|
|
103
|
+
const drops = flowDrops(flow)
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
role="row"
|
|
107
|
+
aria-selected={isSelected}
|
|
108
|
+
onClick={() => onSelect(isSelected ? null : sel)}
|
|
109
|
+
className={clsx(
|
|
110
|
+
grid,
|
|
111
|
+
'cursor-pointer px-3 py-1.5 text-xs border-b border-theme-border/50 transition-colors',
|
|
112
|
+
isSelected ? 'bg-theme-elevated' : 'hover:bg-theme-hover',
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{endpoint(flow.source)}
|
|
116
|
+
{flow.directionUnknown
|
|
117
|
+
? <Tooltip content="Direction unknown"><Minus className="h-3 w-3 text-theme-text-tertiary" /></Tooltip>
|
|
118
|
+
: <ArrowRight className="h-3 w-3 text-theme-text-tertiary" />}
|
|
119
|
+
{endpoint(flow.destination)}
|
|
120
|
+
<span className="truncate text-theme-text-secondary tabular-nums">
|
|
121
|
+
{flow.port > 0 ? flow.port : '—'}
|
|
122
|
+
{(flow.l7Protocol || flow.protocol) && (
|
|
123
|
+
<span className="ml-1 text-theme-text-tertiary">{flow.l7Protocol || flow.protocol.toUpperCase()}</span>
|
|
124
|
+
)}
|
|
125
|
+
{isExternalKind(flow.destination.kind) && <span className="ml-1 text-theme-text-tertiary">ext</span>}
|
|
126
|
+
</span>
|
|
127
|
+
<span className="text-right tabular-nums text-theme-text-secondary">
|
|
128
|
+
{isRateBased ? formatRate(displayVolume(flow, true)) : flow.connections.toLocaleString()}
|
|
129
|
+
</span>
|
|
130
|
+
{showStatus && (
|
|
131
|
+
<span className={clsx('text-right tabular-nums', errors > 0 ? clsx(SEVERITY_TEXT.error, 'font-medium') : 'text-theme-text-tertiary')}>
|
|
132
|
+
{errors > 0 ? (isRateBased ? formatRate(errors) : errors.toLocaleString()) : hasStatusData(flow) ? '0' : '—'}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
<span className={clsx('text-right tabular-nums', drops > 0 ? clsx(SEVERITY_TEXT.error, 'font-medium') : 'text-theme-text-tertiary')}>
|
|
136
|
+
{drops > 0 ? drops.toLocaleString() : '—'}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Filter,
|
|
11
11
|
Info,
|
|
12
12
|
Puzzle,
|
|
13
|
+
Boxes,
|
|
13
14
|
} from 'lucide-react'
|
|
14
15
|
import { clsx } from 'clsx'
|
|
15
16
|
import { SEVERITY_BADGE } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
@@ -41,6 +42,8 @@ interface TrafficFilterSidebarProps {
|
|
|
41
42
|
setShowNamespaceGroups: (v: boolean) => void
|
|
42
43
|
collapseInternet: boolean
|
|
43
44
|
setCollapseInternet: (v: boolean) => void
|
|
45
|
+
groupByWorkload: boolean
|
|
46
|
+
setGroupByWorkload: (v: boolean) => void
|
|
44
47
|
addonMode: AddonMode
|
|
45
48
|
setAddonMode: (v: AddonMode) => void
|
|
46
49
|
|
|
@@ -83,6 +86,9 @@ interface TrafficFilterSidebarProps {
|
|
|
83
86
|
namespaces: Array<{ name: string; nodeCount: number }>
|
|
84
87
|
hiddenNamespaces: Set<string>
|
|
85
88
|
onToggleNamespace: (ns: string) => void
|
|
89
|
+
/** Set while the view is focused on an endpoint: the namespace choices are
|
|
90
|
+
* kept but not applied, so a neighbor in a hidden namespace still shows. */
|
|
91
|
+
namespacesPausedFor?: string
|
|
86
92
|
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -150,6 +156,8 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
150
156
|
setShowNamespaceGroups,
|
|
151
157
|
collapseInternet,
|
|
152
158
|
setCollapseInternet,
|
|
159
|
+
groupByWorkload,
|
|
160
|
+
setGroupByWorkload,
|
|
153
161
|
addonMode,
|
|
154
162
|
setAddonMode,
|
|
155
163
|
aggregateExternal,
|
|
@@ -177,6 +185,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
177
185
|
namespaces,
|
|
178
186
|
hiddenNamespaces,
|
|
179
187
|
onToggleNamespace,
|
|
188
|
+
namespacesPausedFor,
|
|
180
189
|
}: TrafficFilterSidebarProps) {
|
|
181
190
|
const [namespacesExpanded, setNamespacesExpanded] = useState(false)
|
|
182
191
|
|
|
@@ -298,6 +307,13 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
298
307
|
onToggle={() => setCollapseInternet(!collapseInternet)}
|
|
299
308
|
icon={Globe}
|
|
300
309
|
/>
|
|
310
|
+
<ToggleOption
|
|
311
|
+
label="Group by Workload"
|
|
312
|
+
description="Draw a workload's pods as one node. Off, each pod gets its own node"
|
|
313
|
+
enabled={groupByWorkload}
|
|
314
|
+
onToggle={() => setGroupByWorkload(!groupByWorkload)}
|
|
315
|
+
icon={Boxes}
|
|
316
|
+
/>
|
|
301
317
|
</div>
|
|
302
318
|
</div>
|
|
303
319
|
|
|
@@ -494,7 +510,12 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
494
510
|
</button>
|
|
495
511
|
</div>
|
|
496
512
|
</div>
|
|
497
|
-
|
|
513
|
+
{namespacesPausedFor && (
|
|
514
|
+
<p className="mb-1.5 text-[10px] text-theme-text-tertiary">
|
|
515
|
+
Paused while focused on {namespacesPausedFor}: its neighbors show whatever their namespace.
|
|
516
|
+
</p>
|
|
517
|
+
)}
|
|
518
|
+
<div className={clsx('space-y-0.5', namespacesPausedFor && 'opacity-50 pointer-events-none')} aria-disabled={!!namespacesPausedFor}>
|
|
498
519
|
{visibleNamespaces.map(({ name, nodeCount }) => {
|
|
499
520
|
const isHidden = hiddenNamespaces.has(name)
|
|
500
521
|
return (
|
|
@@ -7,10 +7,16 @@ interface TrafficFlowListContextValue {
|
|
|
7
7
|
responsesCallerOriented: boolean
|
|
8
8
|
graphSelection: TrafficGraphSelection | null
|
|
9
9
|
clearSelection: () => void
|
|
10
|
+
/** The records for the current selection are still being fetched. */
|
|
11
|
+
loading?: boolean
|
|
12
|
+
/** Says which records the list holds when it is not all of them. */
|
|
13
|
+
note?: string
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
const emptyValue: TrafficFlowListContextValue = { flows: [], responsesCallerOriented: false, graphSelection: null, clearSelection: () => {} }
|
|
17
|
+
|
|
12
18
|
// Module-level store — TrafficView writes, dock tab reads.
|
|
13
|
-
let currentValue: TrafficFlowListContextValue =
|
|
19
|
+
let currentValue: TrafficFlowListContextValue = emptyValue
|
|
14
20
|
const listeners = new Set<() => void>()
|
|
15
21
|
|
|
16
22
|
function setValue(val: TrafficFlowListContextValue) {
|
|
@@ -53,15 +59,17 @@ export function TrafficFlowListProvider({
|
|
|
53
59
|
responsesCallerOriented,
|
|
54
60
|
graphSelection,
|
|
55
61
|
clearSelection,
|
|
62
|
+
loading,
|
|
63
|
+
note,
|
|
56
64
|
children,
|
|
57
65
|
}: TrafficFlowListContextValue & { children: ReactNode }) {
|
|
58
66
|
useEffect(() => {
|
|
59
|
-
setValue({ flows, responsesCallerOriented, graphSelection, clearSelection })
|
|
60
|
-
}, [flows, responsesCallerOriented, graphSelection, clearSelection])
|
|
67
|
+
setValue({ flows, responsesCallerOriented, graphSelection, clearSelection, loading, note })
|
|
68
|
+
}, [flows, responsesCallerOriented, graphSelection, clearSelection, loading, note])
|
|
61
69
|
|
|
62
70
|
useEffect(() => {
|
|
63
71
|
return () => {
|
|
64
|
-
setValue(
|
|
72
|
+
setValue(emptyValue)
|
|
65
73
|
setFlowSearch('')
|
|
66
74
|
}
|
|
67
75
|
}, [])
|