@oneuptime/common 13.0.3 → 13.0.5
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/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/RumApplication.ts +1 -1
- package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
- package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
- package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
- package/Server/API/AIInvestigationAPI.ts +484 -24
- package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
- package/Server/API/SecurityEventConnectionAPI.ts +250 -0
- package/Server/API/TelemetryAPI.ts +402 -265
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/TelemetryIngest.ts +101 -6
- package/Server/Services/ExceptionAggregationService.ts +7 -16
- package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/InventoryItemService.ts +28 -0
- package/Server/Services/LogAggregationService.ts +17 -23
- package/Server/Services/MetricAggregationService.ts +4 -14
- package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
- package/Server/Services/SecurityEventConnectionService.ts +538 -0
- package/Server/Services/TraceAggregationService.ts +77 -28
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
- package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
- package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
- package/Server/Utils/AI/Toolbox/Index.ts +38 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
- package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
- package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
- package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
- package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
- package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
- package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
- package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
- package/Server/Utils/Telemetry.ts +18 -0
- package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
- package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
- package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
- package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
- package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
- package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
- package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
- package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
- package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
- package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
- package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
- package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
- package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
- package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
- package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
- package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
- package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
- package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
- package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
- package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
- package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
- package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
- package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
- package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
- package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
- package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
- package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
- package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
- package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
- package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
- package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
- package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
- package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
- package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
- package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
- package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
- package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
- package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
- package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
- package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
- package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
- package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
- package/Tests/Models/InventoryItemNaming.test.ts +132 -16
- package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
- package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
- package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
- package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
- package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
- package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
- package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
- package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
- package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
- package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
- package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
- package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
- package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
- package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
- package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
- package/Tests/Server/Utils/LoggerCore.test.ts +597 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
- package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
- package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
- package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +868 -0
- package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
- package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
- package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
- package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
- package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
- package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
- package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
- package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
- package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
- package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
- package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
- package/Tests/UI/Components/Card.test.tsx +312 -1
- package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
- package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
- package/Tests/UI/Components/Checkbox.test.tsx +79 -0
- package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
- package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
- package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
- package/Tests/UI/Components/FacetSections.test.tsx +581 -0
- package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
- package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
- package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
- package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
- package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
- package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
- package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
- package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
- package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
- package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
- package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
- package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
- package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
- package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
- package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
- package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
- package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
- package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
- package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
- package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
- package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
- package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
- package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
- package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
- package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
- package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
- package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
- package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
- package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
- package/Tests/UI/Utils/Clipboard.test.ts +154 -0
- package/Tests/UI/Utils/Navigation.test.ts +24 -0
- package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
- package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
- package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
- package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
- package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
- package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
- package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
- package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
- package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
- package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
- package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
- package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
- package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
- package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
- package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
- package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
- package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
- package/Types/AI/AIChatTypes.ts +8 -0
- package/Types/AI/InvestigationEvidence.ts +96 -0
- package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
- package/Types/OnCallDutyPolicy/Layer.ts +12 -0
- package/Types/Permission.ts +2 -2
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
- package/Types/Rum/SessionReplay.ts +109 -0
- package/Types/Rum/SessionReplayApi.ts +31 -0
- package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
- package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
- package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
- package/Types/Telemetry/EntityType.ts +14 -0
- package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
- package/Types/Telemetry/LockedFilterDetail.ts +46 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
- package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
- package/UI/Components/Card/Card.tsx +118 -83
- package/UI/Components/Checkbox/Checkbox.tsx +8 -5
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
- package/UI/Components/Detail/Detail.tsx +137 -22
- package/UI/Components/ErrorBoundary.tsx +38 -0
- package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
- package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
- package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
- package/UI/Components/LogsViewer/types.ts +6 -0
- package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
- package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
- package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
- package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
- package/UI/Components/ModelList/ModelList.tsx +33 -8
- package/UI/Components/Page/ModelPage.tsx +119 -54
- package/UI/Components/Table/Table.tsx +8 -0
- package/UI/Components/Table/TableRow.tsx +13 -5
- package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
- package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
- package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
- package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
- package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
- package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
- package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
- package/UI/Components/TelemetryViewer/types.ts +19 -0
- package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
- package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
- package/UI/Utils/Clipboard.ts +68 -2
- package/UI/Utils/Navigation.ts +17 -2
- package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
- package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
- package/Utils/AI/InvestigationReport.ts +1789 -0
- package/Utils/API.ts +90 -1
- package/Utils/Rum/SessionReplayHealth.ts +11 -0
- package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
- package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
- package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
- package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
- package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
- package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
- package/Utils/Telemetry/CrossSignalScope.ts +22 -8
- package/Utils/Telemetry/EntityRelationship.ts +8 -0
- package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
- package/Utils/Telemetry/NetworkHost.ts +43 -0
- package/Utils/Telemetry/OriginAllowList.ts +139 -4
- package/Utils/Traces/CriticalPath.ts +10 -4
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
- package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
- package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +248 -170
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +22 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +8 -17
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +4 -14
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
- package/build/dist/Server/Services/TraceAggregationService.js +51 -25
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -0
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +11 -5
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
- package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/Permission.js +2 -2
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +95 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +14 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
- package/build/dist/UI/Components/Card/Card.js +34 -17
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +63 -13
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ErrorBoundary.js +34 -0
- package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +62 -34
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +3 -1
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +8 -1
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
- package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
- package/build/dist/UI/Utils/Clipboard.js +59 -2
- package/build/dist/UI/Utils/Clipboard.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +11 -2
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
- package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
- package/build/dist/Utils/API.js +25 -0
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
- package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
- package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
- package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
- package/build/dist/Utils/Traces/CriticalPath.js +9 -4
- package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
- package/package.json +1 -1
- package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
- package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
- package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
- package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
- package/UI/Components/GanttChart/Index.tsx +0 -90
- package/UI/Components/GanttChart/Row/Index.tsx +0 -22
- package/UI/Components/GanttChart/Row/Row.tsx +0 -231
- package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
- package/UI/Components/GanttChart/Rows.tsx +0 -59
- package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
- package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
- package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
- package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
- package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Index.js +0 -39
- package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
- package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
- package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
- package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js.map +0 -1
|
@@ -0,0 +1,2354 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
RenderResult,
|
|
16
|
+
screen,
|
|
17
|
+
waitFor,
|
|
18
|
+
within,
|
|
19
|
+
} from "@testing-library/react";
|
|
20
|
+
import React, { ReactElement, ReactNode } from "react";
|
|
21
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* The incident and alert overview pages, rendered for real with every heavy
|
|
25
|
+
* card stubbed. What is under test is the page itself: the first load, the
|
|
26
|
+
* background refresh that must never unmount a card, moving to another event
|
|
27
|
+
* on the same route (the page is not remounted, so nothing of the previous
|
|
28
|
+
* event may leak into the next), the stat bar's numbers, the header facts and
|
|
29
|
+
* AI summary it assembles, and the order of the cards.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
33
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
34
|
+
const updateByIdMock: MockFunction = getJestMockFunction();
|
|
35
|
+
|
|
36
|
+
const recordedProps: Record<string, Array<Record<string, unknown>>> = {};
|
|
37
|
+
const mountCounts: Record<string, number> = {};
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* Function declarations, so they are hoisted along with the jest.mock calls
|
|
41
|
+
* that use them. Everything they touch is dereferenced lazily, at render
|
|
42
|
+
* time, after the module's consts are initialised.
|
|
43
|
+
*/
|
|
44
|
+
function recordProps(key: string, props: Record<string, unknown>): void {
|
|
45
|
+
if (!recordedProps[key]) {
|
|
46
|
+
recordedProps[key] = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
recordedProps[key]!.push(props);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stubModule(key: string): {
|
|
53
|
+
__esModule: boolean;
|
|
54
|
+
default: (props: Record<string, unknown>) => ReactElement;
|
|
55
|
+
} {
|
|
56
|
+
return {
|
|
57
|
+
__esModule: true,
|
|
58
|
+
default: (props: Record<string, unknown>): ReactElement => {
|
|
59
|
+
recordProps(key, props);
|
|
60
|
+
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
mountCounts[key] = (mountCounts[key] || 0) + 1;
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
return React.createElement("div", { "data-testid": `stub-${key}` });
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
jest.mock("react-i18next", () => {
|
|
71
|
+
return {
|
|
72
|
+
useTranslation: () => {
|
|
73
|
+
return {
|
|
74
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
75
|
+
return options?.defaultValue ?? key;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
83
|
+
return {
|
|
84
|
+
__esModule: true,
|
|
85
|
+
default: {
|
|
86
|
+
getList: (...args: Array<unknown>) => {
|
|
87
|
+
return getListMock(...args);
|
|
88
|
+
},
|
|
89
|
+
getItem: (...args: Array<unknown>) => {
|
|
90
|
+
return getItemMock(...args);
|
|
91
|
+
},
|
|
92
|
+
updateById: (...args: Array<unknown>) => {
|
|
93
|
+
return updateByIdMock(...args);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
|
|
100
|
+
return {
|
|
101
|
+
__esModule: true,
|
|
102
|
+
default: (props: Record<string, unknown>): ReactElement => {
|
|
103
|
+
recordProps(`CardModelDetail:${props["name"] as string}`, props);
|
|
104
|
+
|
|
105
|
+
return React.createElement("div", {
|
|
106
|
+
"data-testid": `stub-card-${props["name"] as string}`,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
jest.mock(
|
|
113
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AppLink/AppLink",
|
|
114
|
+
() => {
|
|
115
|
+
return {
|
|
116
|
+
__esModule: true,
|
|
117
|
+
default: (props: {
|
|
118
|
+
to?: { toString: () => string };
|
|
119
|
+
children: ReactNode;
|
|
120
|
+
className?: string;
|
|
121
|
+
}): ReactElement => {
|
|
122
|
+
return React.createElement(
|
|
123
|
+
"a",
|
|
124
|
+
{ href: props.to?.toString(), className: props.className },
|
|
125
|
+
props.children,
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
jest.mock(
|
|
133
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Incident/ChangeState",
|
|
134
|
+
() => {
|
|
135
|
+
return stubModule("ChangeIncidentState");
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
jest.mock(
|
|
139
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Alert/ChangeState",
|
|
140
|
+
() => {
|
|
141
|
+
return stubModule("ChangeAlertState");
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
jest.mock(
|
|
145
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AI/InvestigationPanel",
|
|
146
|
+
() => {
|
|
147
|
+
return stubModule("InvestigationPanel");
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
jest.mock(
|
|
151
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Incident/IncidentFeed",
|
|
152
|
+
() => {
|
|
153
|
+
return stubModule("Feed");
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
jest.mock(
|
|
157
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Alert/AlertFeed",
|
|
158
|
+
() => {
|
|
159
|
+
return stubModule("Feed");
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
jest.mock(
|
|
163
|
+
"../../../../App/FeatureSet/Dashboard/src/Pages/Incidents/View/AffectedResources",
|
|
164
|
+
() => {
|
|
165
|
+
return stubModule("SeriesResource");
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
jest.mock(
|
|
169
|
+
"../../../../App/FeatureSet/Dashboard/src/Pages/Alerts/View/AffectedResources",
|
|
170
|
+
() => {
|
|
171
|
+
return stubModule("SeriesResource");
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
jest.mock(
|
|
175
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Monitor/MonitorSummarySnapshotCard",
|
|
176
|
+
() => {
|
|
177
|
+
return stubModule("MonitorSummary");
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
jest.mock(
|
|
181
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Runbook/EntityRunbooks",
|
|
182
|
+
() => {
|
|
183
|
+
return stubModule("Runbooks");
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
jest.mock(
|
|
187
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AutoRemediation/RemediationSuggestionCard",
|
|
188
|
+
() => {
|
|
189
|
+
return stubModule("Remediation");
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
jest.mock(
|
|
193
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Incident/IncidentMemberRoleAssignment",
|
|
194
|
+
() => {
|
|
195
|
+
return stubModule("Roles");
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
jest.mock(
|
|
199
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/CustomFields/OverviewCustomFields",
|
|
200
|
+
() => {
|
|
201
|
+
return stubModule("CustomFields");
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
jest.mock(
|
|
205
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/TelemetryCompanionSignalTabs",
|
|
206
|
+
() => {
|
|
207
|
+
return stubModule("Telemetry");
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
jest.mock(
|
|
211
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/TelemetrySnapshotWindowAlert",
|
|
212
|
+
() => {
|
|
213
|
+
return stubModule("SnapshotWindow");
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
jest.mock(
|
|
217
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer",
|
|
218
|
+
() => {
|
|
219
|
+
return stubModule("LogsViewer");
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
jest.mock(
|
|
223
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Traces/TracesViewer",
|
|
224
|
+
() => {
|
|
225
|
+
return stubModule("TracesViewer");
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
jest.mock(
|
|
229
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricView",
|
|
230
|
+
() => {
|
|
231
|
+
return stubModule("MetricView");
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
jest.mock(
|
|
235
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsViewer",
|
|
236
|
+
() => {
|
|
237
|
+
return stubModule("ExceptionsViewer");
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
jest.mock(
|
|
241
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AffectedResources/AffectedResourcesDisplay",
|
|
242
|
+
() => {
|
|
243
|
+
return stubModule("AffectedResourcesDisplay");
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
jest.mock(
|
|
247
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AffectedResources/AffectedResourcesPicker",
|
|
248
|
+
() => {
|
|
249
|
+
return {
|
|
250
|
+
...stubModule("AffectedResourcesPicker"),
|
|
251
|
+
isAffectedResourcesPayload: (): boolean => {
|
|
252
|
+
return false;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
jest.mock(
|
|
258
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/StatusPageSubscribers/SubscriberNotificationStatus",
|
|
259
|
+
() => {
|
|
260
|
+
return stubModule("SubscriberNotificationStatus");
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
jest.mock(
|
|
264
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/OnCallPolicies",
|
|
265
|
+
() => {
|
|
266
|
+
return stubModule("OnCallPolicies");
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
jest.mock(
|
|
270
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/User/User",
|
|
271
|
+
() => {
|
|
272
|
+
return stubModule("User");
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
jest.mock(
|
|
276
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Monitor/Monitor",
|
|
277
|
+
() => {
|
|
278
|
+
return {
|
|
279
|
+
__esModule: true,
|
|
280
|
+
default: (props: { monitor: { name?: string } }): ReactElement => {
|
|
281
|
+
return React.createElement(
|
|
282
|
+
"span",
|
|
283
|
+
{ "data-testid": "monitor-element" },
|
|
284
|
+
props.monitor.name,
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
jest.mock(
|
|
291
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/AlertEpisode/AlertEpisode",
|
|
292
|
+
() => {
|
|
293
|
+
return {
|
|
294
|
+
__esModule: true,
|
|
295
|
+
default: (props: { alertEpisode: { title?: string } }): ReactElement => {
|
|
296
|
+
return React.createElement(
|
|
297
|
+
"span",
|
|
298
|
+
{ "data-testid": "episode-element" },
|
|
299
|
+
props.alertEpisode.title,
|
|
300
|
+
);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
import IncidentView from "../../../../App/FeatureSet/Dashboard/src/Pages/Incidents/View/Index";
|
|
307
|
+
import AlertView from "../../../../App/FeatureSet/Dashboard/src/Pages/Alerts/View/Index";
|
|
308
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
309
|
+
import { EventStatusFact } from "../../../../App/FeatureSet/Dashboard/src/Components/EventView/EventStatusPanel";
|
|
310
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
311
|
+
import AlertEpisode from "../../../Models/DatabaseModels/AlertEpisode";
|
|
312
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
313
|
+
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
|
314
|
+
import AlertStateTimeline from "../../../Models/DatabaseModels/AlertStateTimeline";
|
|
315
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
316
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
317
|
+
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
|
|
318
|
+
import IncidentStateTimeline from "../../../Models/DatabaseModels/IncidentStateTimeline";
|
|
319
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
320
|
+
import Probe from "../../../Models/DatabaseModels/Probe";
|
|
321
|
+
import AIRunStatus from "../../../Types/AI/AIRunStatus";
|
|
322
|
+
import Route from "../../../Types/API/Route";
|
|
323
|
+
import Color from "../../../Types/Color";
|
|
324
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
325
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
326
|
+
import StatusPageSubscriberNotificationStatus from "../../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
|
327
|
+
import { DetailStyle } from "../../../UI/Components/Detail/Detail";
|
|
328
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
329
|
+
|
|
330
|
+
const EVENT_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
331
|
+
const OTHER_EVENT_ID: string = "22222222-2222-4222-8222-222222222222";
|
|
332
|
+
const CREATED_STATE_ID: string = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1";
|
|
333
|
+
const ACKNOWLEDGED_STATE_ID: string = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2";
|
|
334
|
+
const RESOLVED_STATE_ID: string = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3";
|
|
335
|
+
const MONITOR_ID: string = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1";
|
|
336
|
+
|
|
337
|
+
const START: Date = new Date("2026-09-14T18:00:00.000Z");
|
|
338
|
+
|
|
339
|
+
const minutesAfterStart: (minutes: number) => Date = (
|
|
340
|
+
minutes: number,
|
|
341
|
+
): Date => {
|
|
342
|
+
return new Date(START.getTime() + minutes * 60 * 1000);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const pageProps: PageComponentProps = {
|
|
346
|
+
pageRoute: new Route("/overview"),
|
|
347
|
+
currentProject: null,
|
|
348
|
+
hasPaymentMethod: false,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
interface Deferred<T> {
|
|
352
|
+
promise: Promise<T>;
|
|
353
|
+
resolve: (value: T) => void;
|
|
354
|
+
reject: (reason: unknown) => void;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function createDeferred<T>(): Deferred<T> {
|
|
358
|
+
let resolve: (value: T) => void = (): void => {};
|
|
359
|
+
let reject: (reason: unknown) => void = (): void => {};
|
|
360
|
+
|
|
361
|
+
const promise: Promise<T> = new Promise<T>(
|
|
362
|
+
(
|
|
363
|
+
promiseResolve: (value: T) => void,
|
|
364
|
+
promiseReject: (reason: unknown) => void,
|
|
365
|
+
) => {
|
|
366
|
+
resolve = promiseResolve;
|
|
367
|
+
reject = promiseReject;
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
return { promise: promise, resolve: resolve, reject: reject };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function propsHistory<T>(key: string): Array<T> {
|
|
375
|
+
return (recordedProps[key] || []) as unknown as Array<T>;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function latestProps<T>(key: string): T {
|
|
379
|
+
const history: Array<T> = propsHistory<T>(key);
|
|
380
|
+
|
|
381
|
+
if (history.length === 0) {
|
|
382
|
+
throw new Error(`${key} was never rendered`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return history[history.length - 1]!;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
interface ListResultShape {
|
|
389
|
+
data: Array<unknown>;
|
|
390
|
+
count: number;
|
|
391
|
+
skip: number;
|
|
392
|
+
limit: number;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const listResult: (data: Array<unknown>) => ListResultShape = (
|
|
396
|
+
data: Array<unknown>,
|
|
397
|
+
): ListResultShape => {
|
|
398
|
+
return { data: data, count: data.length, skip: 0, limit: 10000 };
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
interface ChangeStateProps {
|
|
402
|
+
title?: string;
|
|
403
|
+
eventNumber?: string;
|
|
404
|
+
eventStartsAt?: Date;
|
|
405
|
+
isPrivate?: boolean;
|
|
406
|
+
severity?: { name: string; color: Color };
|
|
407
|
+
facts?: Array<EventStatusFact>;
|
|
408
|
+
aiInvestigationStatus?: AIRunStatus | null;
|
|
409
|
+
aiInvestigationSummary?: string | null;
|
|
410
|
+
onActionComplete: () => void;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface InvestigationPanelProps {
|
|
414
|
+
subjectType: string;
|
|
415
|
+
onStatusChange: (status: AIRunStatus | null) => void;
|
|
416
|
+
onReportSummaryChange: (summary: string | null) => void;
|
|
417
|
+
onAnalysisAvailable: () => void;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
interface DetailFieldProps {
|
|
421
|
+
title?: string;
|
|
422
|
+
field?: Record<string, unknown>;
|
|
423
|
+
getElement?: (item: unknown) => ReactElement;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
interface CardModelDetailProps {
|
|
427
|
+
refresher?: boolean;
|
|
428
|
+
onSaveSuccess?: () => void;
|
|
429
|
+
modelDetailProps: {
|
|
430
|
+
style?: DetailStyle;
|
|
431
|
+
onBeforeFetch?: unknown;
|
|
432
|
+
onItemLoaded?: (item: unknown) => void;
|
|
433
|
+
fields: Array<DetailFieldProps>;
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
interface PageCase {
|
|
438
|
+
noun: "incident" | "alert";
|
|
439
|
+
pageElement: () => ReactElement;
|
|
440
|
+
renderPage: () => RenderResult;
|
|
441
|
+
eventModel: typeof Incident | typeof Alert;
|
|
442
|
+
stateModel: typeof IncidentState | typeof AlertState;
|
|
443
|
+
timelineModel: typeof IncidentStateTimeline | typeof AlertStateTimeline;
|
|
444
|
+
changeStateKey: string;
|
|
445
|
+
buildEvent: (title: string) => Incident | Alert;
|
|
446
|
+
buildStates: () => Array<IncidentState | AlertState>;
|
|
447
|
+
buildTimeline: (
|
|
448
|
+
entries: Array<[string, number]>,
|
|
449
|
+
) => Array<IncidentStateTimeline | AlertStateTimeline>;
|
|
450
|
+
startFactLabel: string;
|
|
451
|
+
statBarName: string;
|
|
452
|
+
detailsCardName: string;
|
|
453
|
+
expectedDetailTitles: Array<string>;
|
|
454
|
+
leftColumnOrder: Array<string>;
|
|
455
|
+
rightColumnOrder: Array<string>;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
interface StateSpec {
|
|
459
|
+
id: string;
|
|
460
|
+
name: string;
|
|
461
|
+
flag: "isCreatedState" | "isAcknowledgedState" | "isResolvedState";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const STATE_SPECS: Array<StateSpec> = [
|
|
465
|
+
{ id: CREATED_STATE_ID, name: "Created", flag: "isCreatedState" },
|
|
466
|
+
{
|
|
467
|
+
id: ACKNOWLEDGED_STATE_ID,
|
|
468
|
+
name: "Acknowledged",
|
|
469
|
+
flag: "isAcknowledgedState",
|
|
470
|
+
},
|
|
471
|
+
{ id: RESOLVED_STATE_ID, name: "Resolved", flag: "isResolvedState" },
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const INCIDENT_CASE: PageCase = {
|
|
475
|
+
noun: "incident",
|
|
476
|
+
pageElement: (): ReactElement => {
|
|
477
|
+
return <IncidentView {...pageProps} />;
|
|
478
|
+
},
|
|
479
|
+
renderPage: (): RenderResult => {
|
|
480
|
+
return render(<IncidentView {...pageProps} />);
|
|
481
|
+
},
|
|
482
|
+
eventModel: Incident,
|
|
483
|
+
stateModel: IncidentState,
|
|
484
|
+
timelineModel: IncidentStateTimeline,
|
|
485
|
+
changeStateKey: "ChangeIncidentState",
|
|
486
|
+
buildEvent: (title: string): Incident => {
|
|
487
|
+
const incident: Incident = new Incident();
|
|
488
|
+
incident.id = new ObjectID(EVENT_ID);
|
|
489
|
+
incident.title = title;
|
|
490
|
+
incident.incidentNumber = 42;
|
|
491
|
+
incident.incidentNumberWithPrefix = "INC-42";
|
|
492
|
+
incident.declaredAt = START;
|
|
493
|
+
incident.isPrivate = true;
|
|
494
|
+
incident.seriesLabels = { "host.name": "prod-01" };
|
|
495
|
+
|
|
496
|
+
const severity: IncidentSeverity = new IncidentSeverity();
|
|
497
|
+
severity.name = "Critical";
|
|
498
|
+
severity.color = new Color("#ef4444");
|
|
499
|
+
incident.incidentSeverity = severity;
|
|
500
|
+
|
|
501
|
+
return incident;
|
|
502
|
+
},
|
|
503
|
+
buildStates: (): Array<IncidentState> => {
|
|
504
|
+
return STATE_SPECS.map((spec: StateSpec): IncidentState => {
|
|
505
|
+
const state: IncidentState = new IncidentState();
|
|
506
|
+
state.id = new ObjectID(spec.id);
|
|
507
|
+
state.name = spec.name;
|
|
508
|
+
state[spec.flag] = true;
|
|
509
|
+
return state;
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
buildTimeline: (
|
|
513
|
+
entries: Array<[string, number]>,
|
|
514
|
+
): Array<IncidentStateTimeline> => {
|
|
515
|
+
return entries.map(
|
|
516
|
+
([stateId, minutes]: [string, number]): IncidentStateTimeline => {
|
|
517
|
+
const timeline: IncidentStateTimeline = new IncidentStateTimeline();
|
|
518
|
+
timeline.incidentStateId = new ObjectID(stateId);
|
|
519
|
+
timeline.startsAt = minutesAfterStart(minutes);
|
|
520
|
+
return timeline;
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
},
|
|
524
|
+
startFactLabel: "Declared",
|
|
525
|
+
statBarName: "Incident response times",
|
|
526
|
+
detailsCardName: "Incident Details",
|
|
527
|
+
expectedDetailTitles: [
|
|
528
|
+
"Declared At",
|
|
529
|
+
"Declared By",
|
|
530
|
+
"On-Call Duty Policies",
|
|
531
|
+
"Subscriber Notification Status",
|
|
532
|
+
"Labels",
|
|
533
|
+
"Incident Number",
|
|
534
|
+
"Incident ID",
|
|
535
|
+
],
|
|
536
|
+
leftColumnOrder: [
|
|
537
|
+
"stub-InvestigationPanel",
|
|
538
|
+
"stub-MonitorSummary",
|
|
539
|
+
"stub-SeriesResource",
|
|
540
|
+
"stub-Remediation",
|
|
541
|
+
"stub-Runbooks",
|
|
542
|
+
"stub-Feed",
|
|
543
|
+
],
|
|
544
|
+
rightColumnOrder: [
|
|
545
|
+
"stub-card-Incident Details",
|
|
546
|
+
"stub-Roles",
|
|
547
|
+
"stub-card-Affected Resources",
|
|
548
|
+
"stub-CustomFields",
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const ALERT_CASE: PageCase = {
|
|
553
|
+
noun: "alert",
|
|
554
|
+
pageElement: (): ReactElement => {
|
|
555
|
+
return <AlertView {...pageProps} />;
|
|
556
|
+
},
|
|
557
|
+
renderPage: (): RenderResult => {
|
|
558
|
+
return render(<AlertView {...pageProps} />);
|
|
559
|
+
},
|
|
560
|
+
eventModel: Alert,
|
|
561
|
+
stateModel: AlertState,
|
|
562
|
+
timelineModel: AlertStateTimeline,
|
|
563
|
+
changeStateKey: "ChangeAlertState",
|
|
564
|
+
buildEvent: (title: string): Alert => {
|
|
565
|
+
const alert: Alert = new Alert();
|
|
566
|
+
alert.id = new ObjectID(EVENT_ID);
|
|
567
|
+
alert.title = title;
|
|
568
|
+
alert.alertNumber = 12;
|
|
569
|
+
alert.alertNumberWithPrefix = "ALR-12";
|
|
570
|
+
alert.createdAt = START;
|
|
571
|
+
alert.isPrivate = true;
|
|
572
|
+
alert.seriesLabels = { "host.name": "prod-01" };
|
|
573
|
+
|
|
574
|
+
const severity: AlertSeverity = new AlertSeverity();
|
|
575
|
+
severity.name = "Critical";
|
|
576
|
+
severity.color = new Color("#ef4444");
|
|
577
|
+
alert.alertSeverity = severity;
|
|
578
|
+
|
|
579
|
+
return alert;
|
|
580
|
+
},
|
|
581
|
+
buildStates: (): Array<AlertState> => {
|
|
582
|
+
return STATE_SPECS.map((spec: StateSpec): AlertState => {
|
|
583
|
+
const state: AlertState = new AlertState();
|
|
584
|
+
state.id = new ObjectID(spec.id);
|
|
585
|
+
state.name = spec.name;
|
|
586
|
+
state[spec.flag] = true;
|
|
587
|
+
return state;
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
buildTimeline: (
|
|
591
|
+
entries: Array<[string, number]>,
|
|
592
|
+
): Array<AlertStateTimeline> => {
|
|
593
|
+
return entries.map(
|
|
594
|
+
([stateId, minutes]: [string, number]): AlertStateTimeline => {
|
|
595
|
+
const timeline: AlertStateTimeline = new AlertStateTimeline();
|
|
596
|
+
timeline.alertStateId = new ObjectID(stateId);
|
|
597
|
+
timeline.startsAt = minutesAfterStart(minutes);
|
|
598
|
+
return timeline;
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
},
|
|
602
|
+
startFactLabel: "Created",
|
|
603
|
+
statBarName: "Alert response times",
|
|
604
|
+
detailsCardName: "Alert Details",
|
|
605
|
+
expectedDetailTitles: [
|
|
606
|
+
"Created At",
|
|
607
|
+
"Created By",
|
|
608
|
+
"Monitor",
|
|
609
|
+
"Episode",
|
|
610
|
+
"On-Call Duty Policies",
|
|
611
|
+
"Labels",
|
|
612
|
+
"Alert Number",
|
|
613
|
+
"Alert ID",
|
|
614
|
+
],
|
|
615
|
+
leftColumnOrder: [
|
|
616
|
+
"stub-InvestigationPanel",
|
|
617
|
+
"stub-MonitorSummary",
|
|
618
|
+
"stub-SeriesResource",
|
|
619
|
+
"stub-Remediation",
|
|
620
|
+
"stub-Runbooks",
|
|
621
|
+
"stub-Feed",
|
|
622
|
+
],
|
|
623
|
+
rightColumnOrder: [
|
|
624
|
+
"stub-card-Alert Details",
|
|
625
|
+
"stub-card-Affected Resources",
|
|
626
|
+
"stub-CustomFields",
|
|
627
|
+
],
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// Created, acknowledged at 5m, resolved at 20m, reopened at 60m, acknowledged again at 70m.
|
|
631
|
+
const REOPENED_TIMELINE: Array<[string, number]> = [
|
|
632
|
+
[CREATED_STATE_ID, 0],
|
|
633
|
+
[ACKNOWLEDGED_STATE_ID, 5],
|
|
634
|
+
[RESOLVED_STATE_ID, 20],
|
|
635
|
+
[CREATED_STATE_ID, 60],
|
|
636
|
+
[ACKNOWLEDGED_STATE_ID, 70],
|
|
637
|
+
];
|
|
638
|
+
|
|
639
|
+
interface FakeServer {
|
|
640
|
+
timeline: Array<[string, number]>;
|
|
641
|
+
title: string;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
type ServeFunction = (pageCase: PageCase, server: FakeServer) => void;
|
|
645
|
+
|
|
646
|
+
const serve: ServeFunction = (pageCase: PageCase, server: FakeServer): void => {
|
|
647
|
+
getListMock.mockImplementation((...args: Array<unknown>) => {
|
|
648
|
+
const request: { modelType: unknown } = args[0] as { modelType: unknown };
|
|
649
|
+
|
|
650
|
+
if (request.modelType === pageCase.stateModel) {
|
|
651
|
+
return Promise.resolve(listResult(pageCase.buildStates()));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (request.modelType === pageCase.timelineModel) {
|
|
655
|
+
return Promise.resolve(
|
|
656
|
+
listResult(pageCase.buildTimeline(server.timeline)),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return Promise.reject(new Error("Unexpected list request"));
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
getItemMock.mockImplementation(() => {
|
|
664
|
+
return Promise.resolve(pageCase.buildEvent(server.title));
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
updateByIdMock.mockImplementation(() => {
|
|
668
|
+
return Promise.resolve(undefined);
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const countItemReads: () => number = (): number => {
|
|
673
|
+
return getItemMock.mock.calls.length;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const factsOf: (pageCase: PageCase) => Array<EventStatusFact> = (
|
|
677
|
+
pageCase: PageCase,
|
|
678
|
+
): Array<EventStatusFact> => {
|
|
679
|
+
return latestProps<ChangeStateProps>(pageCase.changeStateKey).facts || [];
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const factLabelsOf: (pageCase: PageCase) => Array<string> = (
|
|
683
|
+
pageCase: PageCase,
|
|
684
|
+
): Array<string> => {
|
|
685
|
+
return factsOf(pageCase).map((fact: EventStatusFact) => {
|
|
686
|
+
return fact.label;
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
type WaitForPageFunction = () => Promise<void>;
|
|
691
|
+
|
|
692
|
+
const waitForPage: WaitForPageFunction = async (): Promise<void> => {
|
|
693
|
+
await screen.findByTestId("stub-InvestigationPanel");
|
|
694
|
+
/*
|
|
695
|
+
* The stub counts its mount in a passive effect, which can land after the
|
|
696
|
+
* DOM query resolves on a loaded machine. Wait for it so mount-count
|
|
697
|
+
* assertions never race the effect.
|
|
698
|
+
*/
|
|
699
|
+
await waitFor(() => {
|
|
700
|
+
expect(mountCounts["InvestigationPanel"]).toBeGreaterThanOrEqual(1);
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
async function flush(): Promise<void> {
|
|
705
|
+
await act(async () => {
|
|
706
|
+
for (let index: number = 0; index < 20; index++) {
|
|
707
|
+
await Promise.resolve();
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// The id in the URL. Tests move the reader to another event by changing it.
|
|
713
|
+
let currentEventId: string = EVENT_ID;
|
|
714
|
+
|
|
715
|
+
/*
|
|
716
|
+
* Every stub that was ever rendered with `id` in an id prop: its own
|
|
717
|
+
* incidentId / alertId / subjectId / modelId, or a detail card's
|
|
718
|
+
* modelDetailProps.modelId.
|
|
719
|
+
*/
|
|
720
|
+
function stubsRenderedWithId(id: string): Array<string> {
|
|
721
|
+
const keys: Array<string> = [];
|
|
722
|
+
|
|
723
|
+
for (const key of Object.keys(recordedProps)) {
|
|
724
|
+
const rendersWithId: boolean = (recordedProps[key] || []).some(
|
|
725
|
+
(props: Record<string, unknown>): boolean => {
|
|
726
|
+
const detailProps: Record<string, unknown> | undefined = props[
|
|
727
|
+
"modelDetailProps"
|
|
728
|
+
] as Record<string, unknown> | undefined;
|
|
729
|
+
|
|
730
|
+
return [
|
|
731
|
+
props["incidentId"],
|
|
732
|
+
props["alertId"],
|
|
733
|
+
props["subjectId"],
|
|
734
|
+
props["modelId"],
|
|
735
|
+
detailProps?.["modelId"],
|
|
736
|
+
].some((value: unknown): boolean => {
|
|
737
|
+
return value instanceof ObjectID && value.toString() === id;
|
|
738
|
+
});
|
|
739
|
+
},
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
if (rendersWithId) {
|
|
743
|
+
keys.push(key);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return keys;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
type ItemRequestIdFunction = (args: Array<unknown>) => string;
|
|
751
|
+
|
|
752
|
+
const itemRequestId: ItemRequestIdFunction = (args: Array<unknown>): string => {
|
|
753
|
+
return (args[0] as { id: ObjectID }).id.toString();
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
beforeEach(() => {
|
|
757
|
+
currentEventId = EVENT_ID;
|
|
758
|
+
|
|
759
|
+
jest
|
|
760
|
+
.spyOn(Navigation, "getLastParamAsObjectID")
|
|
761
|
+
.mockImplementation((): ObjectID => {
|
|
762
|
+
return new ObjectID(currentEventId);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
afterEach(() => {
|
|
767
|
+
cleanup();
|
|
768
|
+
getListMock.mockReset();
|
|
769
|
+
getItemMock.mockReset();
|
|
770
|
+
updateByIdMock.mockReset();
|
|
771
|
+
|
|
772
|
+
for (const key of Object.keys(recordedProps)) {
|
|
773
|
+
delete recordedProps[key];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
for (const key of Object.keys(mountCounts)) {
|
|
777
|
+
delete mountCounts[key];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
jest.restoreAllMocks();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
describe.each([
|
|
784
|
+
["incident", INCIDENT_CASE],
|
|
785
|
+
["alert", ALERT_CASE],
|
|
786
|
+
])("%s overview page", (_name: string, pageCase: PageCase) => {
|
|
787
|
+
describe("first load", () => {
|
|
788
|
+
test("starts on the skeleton and mounts no card until the data lands", async () => {
|
|
789
|
+
const item: Deferred<Incident | Alert> = createDeferred<
|
|
790
|
+
Incident | Alert
|
|
791
|
+
>();
|
|
792
|
+
|
|
793
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
794
|
+
getItemMock.mockImplementation(() => {
|
|
795
|
+
return item.promise;
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
pageCase.renderPage();
|
|
799
|
+
|
|
800
|
+
const skeleton: HTMLElement = screen.getByRole("status");
|
|
801
|
+
|
|
802
|
+
expect(skeleton).toHaveTextContent(`Loading ${pageCase.noun}`);
|
|
803
|
+
expect(screen.queryByTestId("stub-InvestigationPanel")).toBeNull();
|
|
804
|
+
expect(mountCounts["InvestigationPanel"]).toBeUndefined();
|
|
805
|
+
expect(document.querySelector(".mt-52")).toBeNull();
|
|
806
|
+
|
|
807
|
+
await act(async () => {
|
|
808
|
+
item.resolve(pageCase.buildEvent("Checkout slow"));
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
await waitForPage();
|
|
812
|
+
|
|
813
|
+
expect(screen.queryByText(`Loading ${pageCase.noun}`)).toBeNull();
|
|
814
|
+
// Mounted once: the page no longer renders its cards and then unmounts them.
|
|
815
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
816
|
+
expect(mountCounts["Feed"]).toBe(1);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test("reads the timeline, the states and the row in parallel", async () => {
|
|
820
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
821
|
+
|
|
822
|
+
const item: Deferred<Incident | Alert> = createDeferred<
|
|
823
|
+
Incident | Alert
|
|
824
|
+
>();
|
|
825
|
+
const pendingLists: Array<Deferred<unknown>> = [];
|
|
826
|
+
|
|
827
|
+
getListMock.mockImplementation(() => {
|
|
828
|
+
const deferred: Deferred<unknown> = createDeferred<unknown>();
|
|
829
|
+
pendingLists.push(deferred);
|
|
830
|
+
return deferred.promise;
|
|
831
|
+
});
|
|
832
|
+
getItemMock.mockImplementation(() => {
|
|
833
|
+
return item.promise;
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
pageCase.renderPage();
|
|
837
|
+
|
|
838
|
+
await waitFor(() => {
|
|
839
|
+
expect(getListMock).toHaveBeenCalledTimes(2);
|
|
840
|
+
});
|
|
841
|
+
expect(getItemMock).toHaveBeenCalledTimes(1);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("a failed first load shows the error with a retry that recovers", async () => {
|
|
845
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
846
|
+
getItemMock.mockImplementationOnce(() => {
|
|
847
|
+
return Promise.reject(new Error("The database is asleep"));
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
pageCase.renderPage();
|
|
851
|
+
|
|
852
|
+
expect(
|
|
853
|
+
await screen.findByText("The database is asleep"),
|
|
854
|
+
).toBeInTheDocument();
|
|
855
|
+
expect(screen.queryByTestId("stub-InvestigationPanel")).toBeNull();
|
|
856
|
+
|
|
857
|
+
fireEvent.click(screen.getByTestId("refresh-button"));
|
|
858
|
+
|
|
859
|
+
await waitForPage();
|
|
860
|
+
|
|
861
|
+
expect(screen.queryByText("The database is asleep")).toBeNull();
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
describe("header", () => {
|
|
866
|
+
test("hands the header the row's title, number, severity, privacy and start", async () => {
|
|
867
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
868
|
+
|
|
869
|
+
pageCase.renderPage();
|
|
870
|
+
await waitForPage();
|
|
871
|
+
|
|
872
|
+
const props: ChangeStateProps = latestProps<ChangeStateProps>(
|
|
873
|
+
pageCase.changeStateKey,
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
expect(props.title).toBe("Checkout slow");
|
|
877
|
+
expect(props.eventNumber).toBe(
|
|
878
|
+
pageCase.noun === "incident" ? "INC-42" : "ALR-12",
|
|
879
|
+
);
|
|
880
|
+
expect(props.severity?.name).toBe("Critical");
|
|
881
|
+
expect(props.isPrivate).toBe(true);
|
|
882
|
+
expect(props.eventStartsAt).toEqual(START);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test("leads the facts with the absolute start time", async () => {
|
|
886
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
887
|
+
|
|
888
|
+
pageCase.renderPage();
|
|
889
|
+
await waitForPage();
|
|
890
|
+
|
|
891
|
+
const startFact: EventStatusFact | undefined = factsOf(pageCase)[0];
|
|
892
|
+
|
|
893
|
+
expect(startFact?.label).toBe(pageCase.startFactLabel);
|
|
894
|
+
expect(startFact?.value).toBe(
|
|
895
|
+
OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(START),
|
|
896
|
+
);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test("lifts the investigation status and summary into the header", async () => {
|
|
900
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
901
|
+
|
|
902
|
+
pageCase.renderPage();
|
|
903
|
+
await waitForPage();
|
|
904
|
+
|
|
905
|
+
const panel: InvestigationPanelProps =
|
|
906
|
+
latestProps<InvestigationPanelProps>("InvestigationPanel");
|
|
907
|
+
|
|
908
|
+
expect(panel.subjectType).toBe(pageCase.noun);
|
|
909
|
+
expect(
|
|
910
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey)
|
|
911
|
+
.aiInvestigationStatus,
|
|
912
|
+
).toBeNull();
|
|
913
|
+
|
|
914
|
+
act(() => {
|
|
915
|
+
panel.onStatusChange(AIRunStatus.Running);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
expect(
|
|
919
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey)
|
|
920
|
+
.aiInvestigationStatus,
|
|
921
|
+
).toBe(AIRunStatus.Running);
|
|
922
|
+
|
|
923
|
+
act(() => {
|
|
924
|
+
latestProps<InvestigationPanelProps>(
|
|
925
|
+
"InvestigationPanel",
|
|
926
|
+
).onStatusChange(AIRunStatus.Completed);
|
|
927
|
+
latestProps<InvestigationPanelProps>(
|
|
928
|
+
"InvestigationPanel",
|
|
929
|
+
).onReportSummaryChange("Pool exhaustion in checkout-api.");
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
let header: ChangeStateProps = latestProps<ChangeStateProps>(
|
|
933
|
+
pageCase.changeStateKey,
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
expect(header.aiInvestigationStatus).toBe(AIRunStatus.Completed);
|
|
937
|
+
expect(header.aiInvestigationSummary).toBe(
|
|
938
|
+
"Pool exhaustion in checkout-api.",
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
act(() => {
|
|
942
|
+
latestProps<InvestigationPanelProps>(
|
|
943
|
+
"InvestigationPanel",
|
|
944
|
+
).onReportSummaryChange(null);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
header = latestProps<ChangeStateProps>(pageCase.changeStateKey);
|
|
948
|
+
|
|
949
|
+
expect(header.aiInvestigationSummary).toBeNull();
|
|
950
|
+
// Status and summary changes never remount the panel.
|
|
951
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
describe("stat bar", () => {
|
|
956
|
+
test("counts to the FIRST acknowledgement and resolution of a reopened event", async () => {
|
|
957
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
958
|
+
|
|
959
|
+
pageCase.renderPage();
|
|
960
|
+
await waitForPage();
|
|
961
|
+
|
|
962
|
+
const statBar: HTMLElement = screen.getByRole("group", {
|
|
963
|
+
name: pageCase.statBarName,
|
|
964
|
+
});
|
|
965
|
+
const cells: Array<Element> = Array.from(statBar.children);
|
|
966
|
+
|
|
967
|
+
expect(cells).toHaveLength(3);
|
|
968
|
+
expect(cells[0]).toHaveTextContent("Acknowledged in");
|
|
969
|
+
expect(cells[0]).toHaveTextContent("5 minutes");
|
|
970
|
+
expect(cells[0]).toHaveTextContent(
|
|
971
|
+
OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
972
|
+
minutesAfterStart(5),
|
|
973
|
+
),
|
|
974
|
+
);
|
|
975
|
+
expect(cells[1]).toHaveTextContent("Resolved in");
|
|
976
|
+
expect(cells[1]).toHaveTextContent("20 minutes");
|
|
977
|
+
expect(cells[2]).toHaveTextContent("Duration");
|
|
978
|
+
// Reopened, so the duration is still running and has no end.
|
|
979
|
+
expect(cells[2]).not.toHaveTextContent("Ended");
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test("says not yet for an event nobody has answered", async () => {
|
|
983
|
+
serve(pageCase, {
|
|
984
|
+
timeline: [[CREATED_STATE_ID, 0]],
|
|
985
|
+
title: "Checkout slow",
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
pageCase.renderPage();
|
|
989
|
+
await waitForPage();
|
|
990
|
+
|
|
991
|
+
const statBar: HTMLElement = screen.getByRole("group", {
|
|
992
|
+
name: pageCase.statBarName,
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
expect(within(statBar).getByText("Not yet acknowledged")).toBeVisible();
|
|
996
|
+
expect(within(statBar).getByText("Not yet resolved")).toBeVisible();
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test("ends the duration at the current resolution", async () => {
|
|
1000
|
+
serve(pageCase, {
|
|
1001
|
+
timeline: [
|
|
1002
|
+
[CREATED_STATE_ID, 0],
|
|
1003
|
+
[RESOLVED_STATE_ID, 90],
|
|
1004
|
+
],
|
|
1005
|
+
title: "Checkout slow",
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
pageCase.renderPage();
|
|
1009
|
+
await waitForPage();
|
|
1010
|
+
|
|
1011
|
+
const cells: Array<Element> = Array.from(
|
|
1012
|
+
screen.getByRole("group", { name: pageCase.statBarName }).children,
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
// Resolved without an acknowledgement: the resolution answered it.
|
|
1016
|
+
expect(cells[0]).toHaveTextContent("1 hour, 30 minutes");
|
|
1017
|
+
expect(cells[1]).toHaveTextContent("1 hour, 30 minutes");
|
|
1018
|
+
expect(cells[2]).toHaveTextContent("1 hour, 30 minutes");
|
|
1019
|
+
expect(cells[2]).toHaveTextContent(
|
|
1020
|
+
`Ended ${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
1021
|
+
minutesAfterStart(90),
|
|
1022
|
+
)}`,
|
|
1023
|
+
);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test("sits between the header and the card grid", async () => {
|
|
1027
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1028
|
+
|
|
1029
|
+
pageCase.renderPage();
|
|
1030
|
+
await waitForPage();
|
|
1031
|
+
|
|
1032
|
+
const header: HTMLElement = screen.getByTestId(
|
|
1033
|
+
`stub-${pageCase.changeStateKey}`,
|
|
1034
|
+
);
|
|
1035
|
+
const statBar: HTMLElement = screen.getByRole("group", {
|
|
1036
|
+
name: pageCase.statBarName,
|
|
1037
|
+
});
|
|
1038
|
+
const panel: HTMLElement = screen.getByTestId("stub-InvestigationPanel");
|
|
1039
|
+
|
|
1040
|
+
expect(
|
|
1041
|
+
header.compareDocumentPosition(statBar) &
|
|
1042
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1043
|
+
).toBeTruthy();
|
|
1044
|
+
expect(
|
|
1045
|
+
statBar.compareDocumentPosition(panel) &
|
|
1046
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1047
|
+
).toBeTruthy();
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
describe("layout", () => {
|
|
1052
|
+
const expectInDocumentOrder: (testIds: Array<string>) => void = (
|
|
1053
|
+
testIds: Array<string>,
|
|
1054
|
+
): void => {
|
|
1055
|
+
const elements: Array<HTMLElement> = testIds.map((testId: string) => {
|
|
1056
|
+
return screen.getByTestId(testId);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
for (let index: number = 1; index < elements.length; index++) {
|
|
1060
|
+
expect(
|
|
1061
|
+
elements[index - 1]!.compareDocumentPosition(elements[index]!) &
|
|
1062
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1063
|
+
).toBeTruthy();
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
test("the left column leads with the AI investigation", async () => {
|
|
1068
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1069
|
+
|
|
1070
|
+
pageCase.renderPage();
|
|
1071
|
+
await waitForPage();
|
|
1072
|
+
|
|
1073
|
+
expectInDocumentOrder(pageCase.leftColumnOrder);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
test("the right column orders details, then the rest, custom fields last", async () => {
|
|
1077
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1078
|
+
|
|
1079
|
+
pageCase.renderPage();
|
|
1080
|
+
await waitForPage();
|
|
1081
|
+
|
|
1082
|
+
expectInDocumentOrder(pageCase.rightColumnOrder);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("the details card is compact, fetches nothing extra, and lists when and who first", async () => {
|
|
1086
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1087
|
+
|
|
1088
|
+
pageCase.renderPage();
|
|
1089
|
+
await waitForPage();
|
|
1090
|
+
|
|
1091
|
+
const details: CardModelDetailProps = latestProps<CardModelDetailProps>(
|
|
1092
|
+
`CardModelDetail:${pageCase.detailsCardName}`,
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
expect(details.modelDetailProps.style).toBe(DetailStyle.Compact);
|
|
1096
|
+
expect(details.modelDetailProps.onBeforeFetch).toBeUndefined();
|
|
1097
|
+
expect(
|
|
1098
|
+
details.modelDetailProps.fields.map((field: DetailFieldProps) => {
|
|
1099
|
+
return field.title;
|
|
1100
|
+
}),
|
|
1101
|
+
).toEqual(pageCase.expectedDetailTitles);
|
|
1102
|
+
|
|
1103
|
+
const resources: CardModelDetailProps = latestProps<CardModelDetailProps>(
|
|
1104
|
+
"CardModelDetail:Affected Resources",
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
expect(resources.modelDetailProps.style).toBe(DetailStyle.Compact);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
/*
|
|
1111
|
+
* The right column is about 300px wide. Side-by-side headers squeezed
|
|
1112
|
+
* each card's title into a one-word-per-line column beside a long
|
|
1113
|
+
* "Edit Incident" button, and two resource tiles per row clipped every
|
|
1114
|
+
* name, so every card there stacks its header and the resources render
|
|
1115
|
+
* in one column.
|
|
1116
|
+
*/
|
|
1117
|
+
test("every right-column card stacks its header, edit buttons say Edit, resources use one column", async () => {
|
|
1118
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1119
|
+
|
|
1120
|
+
pageCase.renderPage();
|
|
1121
|
+
await waitForPage();
|
|
1122
|
+
|
|
1123
|
+
interface SidebarCardProps {
|
|
1124
|
+
editButtonText?: string;
|
|
1125
|
+
isEditable?: boolean;
|
|
1126
|
+
cardProps: {
|
|
1127
|
+
title?: string;
|
|
1128
|
+
description?: string;
|
|
1129
|
+
headerLayout?: string;
|
|
1130
|
+
buttons?: Array<unknown>;
|
|
1131
|
+
};
|
|
1132
|
+
modelDetailProps: {
|
|
1133
|
+
fields: Array<DetailFieldProps>;
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const details: SidebarCardProps = latestProps<SidebarCardProps>(
|
|
1138
|
+
`CardModelDetail:${pageCase.detailsCardName}`,
|
|
1139
|
+
);
|
|
1140
|
+
const resources: SidebarCardProps = latestProps<SidebarCardProps>(
|
|
1141
|
+
"CardModelDetail:Affected Resources",
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
for (const card of [details, resources]) {
|
|
1145
|
+
expect(card.cardProps.headerLayout).toBe("stacked");
|
|
1146
|
+
expect(card.editButtonText).toBe("Edit");
|
|
1147
|
+
// Still editable: the short label changes the words, not the gating.
|
|
1148
|
+
expect(card.isEditable).toBe(true);
|
|
1149
|
+
expect(card.cardProps.description).toBeTruthy();
|
|
1150
|
+
expect((card.cardProps.description || "").length).toBeLessThan(70);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
expect(details.cardProps.title).toBe(pageCase.detailsCardName);
|
|
1154
|
+
expect(details.cardProps.description).toBe(
|
|
1155
|
+
`Key facts about this ${pageCase.noun}.`,
|
|
1156
|
+
);
|
|
1157
|
+
expect(resources.cardProps.title).toBe("Affected Resources");
|
|
1158
|
+
|
|
1159
|
+
const displayField: DetailFieldProps | undefined =
|
|
1160
|
+
resources.modelDetailProps.fields.find((field: DetailFieldProps) => {
|
|
1161
|
+
return Boolean(field.getElement);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
expect(displayField).toBeDefined();
|
|
1165
|
+
|
|
1166
|
+
const display: ReactElement = displayField!.getElement!(
|
|
1167
|
+
new pageCase.eventModel(),
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
expect((display.props as { columns?: number }).columns).toBe(1);
|
|
1171
|
+
|
|
1172
|
+
expect(
|
|
1173
|
+
latestProps<{ headerLayout?: string }>("CustomFields").headerLayout,
|
|
1174
|
+
).toBe("stacked");
|
|
1175
|
+
|
|
1176
|
+
if (pageCase.noun === "incident") {
|
|
1177
|
+
expect(
|
|
1178
|
+
latestProps<{ headerLayout?: string }>("Roles").headerLayout,
|
|
1179
|
+
).toBe("stacked");
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("hands the series labels it already read to the resource card", async () => {
|
|
1184
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1185
|
+
|
|
1186
|
+
pageCase.renderPage();
|
|
1187
|
+
await waitForPage();
|
|
1188
|
+
|
|
1189
|
+
expect(
|
|
1190
|
+
latestProps<{ seriesLabels: unknown }>("SeriesResource").seriesLabels,
|
|
1191
|
+
).toEqual({ "host.name": "prod-01" });
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
describe("background refresh", () => {
|
|
1196
|
+
test("an action refreshes the page without unmounting a single card", async () => {
|
|
1197
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1198
|
+
|
|
1199
|
+
pageCase.renderPage();
|
|
1200
|
+
await waitForPage();
|
|
1201
|
+
|
|
1202
|
+
const skeletonSeen: Array<boolean> = [];
|
|
1203
|
+
const observer: MutationObserver = new MutationObserver(() => {
|
|
1204
|
+
if (document.body.textContent?.includes(`Loading ${pageCase.noun}`)) {
|
|
1205
|
+
skeletonSeen.push(true);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
observer.observe(document.body, {
|
|
1210
|
+
childList: true,
|
|
1211
|
+
subtree: true,
|
|
1212
|
+
characterData: true,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const readsBefore: number = countItemReads();
|
|
1216
|
+
const feedTokenBefore: number = latestProps<{ refreshToken: number }>(
|
|
1217
|
+
"Feed",
|
|
1218
|
+
).refreshToken;
|
|
1219
|
+
|
|
1220
|
+
serve(pageCase, {
|
|
1221
|
+
timeline: [...REOPENED_TIMELINE, [RESOLVED_STATE_ID, 100]],
|
|
1222
|
+
title: "Checkout slow (resolved)",
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
act(() => {
|
|
1226
|
+
latestProps<ChangeStateProps>(
|
|
1227
|
+
pageCase.changeStateKey,
|
|
1228
|
+
).onActionComplete();
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
await waitFor(() => {
|
|
1232
|
+
expect(
|
|
1233
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1234
|
+
).toBe("Checkout slow (resolved)");
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
observer.disconnect();
|
|
1238
|
+
|
|
1239
|
+
expect(countItemReads()).toBe(readsBefore + 1);
|
|
1240
|
+
expect(skeletonSeen).toEqual([]);
|
|
1241
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1242
|
+
expect(mountCounts["Feed"]).toBe(1);
|
|
1243
|
+
expect(mountCounts[pageCase.changeStateKey]).toBe(1);
|
|
1244
|
+
// The state change is a new feed entry.
|
|
1245
|
+
expect(latestProps<{ refreshToken: number }>("Feed").refreshToken).toBe(
|
|
1246
|
+
feedTokenBefore + 1,
|
|
1247
|
+
);
|
|
1248
|
+
expect(
|
|
1249
|
+
screen.getByRole("group", { name: pageCase.statBarName }),
|
|
1250
|
+
).toHaveTextContent("Ended");
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
test("saving the details card refreshes the header data and the feed", async () => {
|
|
1254
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1255
|
+
|
|
1256
|
+
pageCase.renderPage();
|
|
1257
|
+
await waitForPage();
|
|
1258
|
+
|
|
1259
|
+
const feedTokenBefore: number = latestProps<{ refreshToken: number }>(
|
|
1260
|
+
"Feed",
|
|
1261
|
+
).refreshToken;
|
|
1262
|
+
|
|
1263
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Renamed" });
|
|
1264
|
+
|
|
1265
|
+
act(() => {
|
|
1266
|
+
latestProps<CardModelDetailProps>(
|
|
1267
|
+
`CardModelDetail:${pageCase.detailsCardName}`,
|
|
1268
|
+
).onSaveSuccess!();
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
await waitFor(() => {
|
|
1272
|
+
expect(
|
|
1273
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1274
|
+
).toBe("Renamed");
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
expect(latestProps<{ refreshToken: number }>("Feed").refreshToken).toBe(
|
|
1278
|
+
feedTokenBefore + 1,
|
|
1279
|
+
);
|
|
1280
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
test("a failed refresh keeps the page and says so, with a working retry", async () => {
|
|
1284
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1285
|
+
|
|
1286
|
+
pageCase.renderPage();
|
|
1287
|
+
await waitForPage();
|
|
1288
|
+
|
|
1289
|
+
getItemMock.mockImplementationOnce(() => {
|
|
1290
|
+
return Promise.reject(new Error("Gateway timeout"));
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
act(() => {
|
|
1294
|
+
latestProps<ChangeStateProps>(
|
|
1295
|
+
pageCase.changeStateKey,
|
|
1296
|
+
).onActionComplete();
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
const banner: HTMLElement = await screen.findByRole("alert");
|
|
1300
|
+
|
|
1301
|
+
expect(banner).toHaveTextContent(
|
|
1302
|
+
`Could not refresh this ${pageCase.noun}. Gateway timeout`,
|
|
1303
|
+
);
|
|
1304
|
+
expect(screen.getByTestId("stub-InvestigationPanel")).toBeInTheDocument();
|
|
1305
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1306
|
+
|
|
1307
|
+
fireEvent.click(
|
|
1308
|
+
within(banner).getByRole("button", { name: "Try again" }),
|
|
1309
|
+
);
|
|
1310
|
+
|
|
1311
|
+
await waitFor(() => {
|
|
1312
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
test("the refresh banner can be dismissed", async () => {
|
|
1317
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1318
|
+
|
|
1319
|
+
pageCase.renderPage();
|
|
1320
|
+
await waitForPage();
|
|
1321
|
+
|
|
1322
|
+
getItemMock.mockImplementationOnce(() => {
|
|
1323
|
+
return Promise.reject(new Error("Gateway timeout"));
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
act(() => {
|
|
1327
|
+
latestProps<ChangeStateProps>(
|
|
1328
|
+
pageCase.changeStateKey,
|
|
1329
|
+
).onActionComplete();
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
const banner: HTMLElement = await screen.findByRole("alert");
|
|
1333
|
+
|
|
1334
|
+
fireEvent.click(within(banner).getByRole("button", { name: "Dismiss" }));
|
|
1335
|
+
|
|
1336
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test("an older refresh that answers last cannot overwrite a newer one", async () => {
|
|
1340
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1341
|
+
|
|
1342
|
+
pageCase.renderPage();
|
|
1343
|
+
await waitForPage();
|
|
1344
|
+
|
|
1345
|
+
const older: Deferred<Incident | Alert> = createDeferred<
|
|
1346
|
+
Incident | Alert
|
|
1347
|
+
>();
|
|
1348
|
+
const newer: Deferred<Incident | Alert> = createDeferred<
|
|
1349
|
+
Incident | Alert
|
|
1350
|
+
>();
|
|
1351
|
+
|
|
1352
|
+
getItemMock
|
|
1353
|
+
.mockImplementationOnce(() => {
|
|
1354
|
+
return older.promise;
|
|
1355
|
+
})
|
|
1356
|
+
.mockImplementationOnce(() => {
|
|
1357
|
+
return newer.promise;
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
act(() => {
|
|
1361
|
+
latestProps<ChangeStateProps>(
|
|
1362
|
+
pageCase.changeStateKey,
|
|
1363
|
+
).onActionComplete();
|
|
1364
|
+
});
|
|
1365
|
+
act(() => {
|
|
1366
|
+
latestProps<ChangeStateProps>(
|
|
1367
|
+
pageCase.changeStateKey,
|
|
1368
|
+
).onActionComplete();
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
await act(async () => {
|
|
1372
|
+
newer.resolve(pageCase.buildEvent("Newest title"));
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
await waitFor(() => {
|
|
1376
|
+
expect(
|
|
1377
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1378
|
+
).toBe("Newest title");
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
await act(async () => {
|
|
1382
|
+
older.resolve(pageCase.buildEvent("Stale title"));
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
expect(latestProps<ChangeStateProps>(pageCase.changeStateKey).title).toBe(
|
|
1386
|
+
"Newest title",
|
|
1387
|
+
);
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
/*
|
|
1392
|
+
* The overview is an index route inside the event's layout, and the AI
|
|
1393
|
+
* report links one event to another on the same route, so following such a
|
|
1394
|
+
* link re-renders this page with a new id instead of remounting it. The
|
|
1395
|
+
* first render for the new id used to hand every card the new id over the
|
|
1396
|
+
* previous event's header and numbers (each card firing its requests) before
|
|
1397
|
+
* an effect flipped the page back to its skeleton and the cards fetched
|
|
1398
|
+
* again once the new event landed.
|
|
1399
|
+
*/
|
|
1400
|
+
describe("moving to another event on the same route", () => {
|
|
1401
|
+
type ServeOtherFunction = (
|
|
1402
|
+
otherItem: Promise<Incident | Alert>,
|
|
1403
|
+
fallbackTitle: string,
|
|
1404
|
+
) => void;
|
|
1405
|
+
|
|
1406
|
+
// The next event's row answers when the test says; the first one's at once.
|
|
1407
|
+
const serveOther: ServeOtherFunction = (
|
|
1408
|
+
otherItem: Promise<Incident | Alert>,
|
|
1409
|
+
fallbackTitle: string,
|
|
1410
|
+
): void => {
|
|
1411
|
+
getItemMock.mockImplementation((...args: Array<unknown>) => {
|
|
1412
|
+
if (itemRequestId(args) === OTHER_EVENT_ID) {
|
|
1413
|
+
return otherItem;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return Promise.resolve(pageCase.buildEvent(fallbackTitle));
|
|
1417
|
+
});
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
test("shows the skeleton at once and mounts no card for the new id before its data lands", async () => {
|
|
1421
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1422
|
+
|
|
1423
|
+
const view: RenderResult = pageCase.renderPage();
|
|
1424
|
+
await waitForPage();
|
|
1425
|
+
await flush();
|
|
1426
|
+
|
|
1427
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1428
|
+
|
|
1429
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
1430
|
+
Incident | Alert
|
|
1431
|
+
>();
|
|
1432
|
+
serveOther(otherItem.promise, "Checkout slow");
|
|
1433
|
+
|
|
1434
|
+
const readsBefore: number = countItemReads();
|
|
1435
|
+
|
|
1436
|
+
currentEventId = OTHER_EVENT_ID;
|
|
1437
|
+
view.rerender(pageCase.pageElement());
|
|
1438
|
+
|
|
1439
|
+
// Straight after the re-render: the skeleton, and not one card.
|
|
1440
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
1441
|
+
`Loading ${pageCase.noun}`,
|
|
1442
|
+
);
|
|
1443
|
+
expect(screen.queryByTestId("stub-InvestigationPanel")).toBeNull();
|
|
1444
|
+
expect(
|
|
1445
|
+
screen.queryByTestId(`stub-${pageCase.changeStateKey}`),
|
|
1446
|
+
).toBeNull();
|
|
1447
|
+
expect(
|
|
1448
|
+
screen.queryByRole("group", { name: pageCase.statBarName }),
|
|
1449
|
+
).toBeNull();
|
|
1450
|
+
|
|
1451
|
+
await waitFor(() => {
|
|
1452
|
+
expect(countItemReads()).toBe(readsBefore + 1);
|
|
1453
|
+
});
|
|
1454
|
+
await flush();
|
|
1455
|
+
|
|
1456
|
+
expect(itemRequestId(getItemMock.mock.calls[readsBefore]!)).toBe(
|
|
1457
|
+
OTHER_EVENT_ID,
|
|
1458
|
+
);
|
|
1459
|
+
// No card rendered (and so none fetched) for the new id yet.
|
|
1460
|
+
expect(stubsRenderedWithId(OTHER_EVENT_ID)).toEqual([]);
|
|
1461
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1462
|
+
|
|
1463
|
+
await act(async () => {
|
|
1464
|
+
otherItem.resolve(pageCase.buildEvent("Payments failing"));
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
await waitFor(() => {
|
|
1468
|
+
expect(
|
|
1469
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1470
|
+
).toBe("Payments failing");
|
|
1471
|
+
});
|
|
1472
|
+
// Let the mount effects of that render run before counting mounts.
|
|
1473
|
+
await flush();
|
|
1474
|
+
|
|
1475
|
+
// Exactly one mount per event: the first one's and the next one's.
|
|
1476
|
+
expect(mountCounts["InvestigationPanel"]).toBe(2);
|
|
1477
|
+
expect(mountCounts["Feed"]).toBe(2);
|
|
1478
|
+
expect(mountCounts[pageCase.changeStateKey]).toBe(2);
|
|
1479
|
+
expect(
|
|
1480
|
+
(
|
|
1481
|
+
latestProps<{ subjectId: ObjectID }>("InvestigationPanel")
|
|
1482
|
+
.subjectId as ObjectID
|
|
1483
|
+
).toString(),
|
|
1484
|
+
).toBe(OTHER_EVENT_ID);
|
|
1485
|
+
|
|
1486
|
+
// The header never showed the previous event's title under the new id.
|
|
1487
|
+
const headerTitlesForOther: Array<string | undefined> = propsHistory<
|
|
1488
|
+
ChangeStateProps & Record<string, unknown>
|
|
1489
|
+
>(pageCase.changeStateKey)
|
|
1490
|
+
.filter((props: ChangeStateProps & Record<string, unknown>) => {
|
|
1491
|
+
const id: unknown = props["incidentId"] || props["alertId"];
|
|
1492
|
+
return id instanceof ObjectID && id.toString() === OTHER_EVENT_ID;
|
|
1493
|
+
})
|
|
1494
|
+
.map((props: ChangeStateProps) => {
|
|
1495
|
+
return props.title;
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
expect(headerTitlesForOther.length).toBeGreaterThan(0);
|
|
1499
|
+
expect(new Set(headerTitlesForOther)).toEqual(
|
|
1500
|
+
new Set(["Payments failing"]),
|
|
1501
|
+
);
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
test("a background refresh of the previous event that answers late is ignored", async () => {
|
|
1505
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1506
|
+
|
|
1507
|
+
const view: RenderResult = pageCase.renderPage();
|
|
1508
|
+
await waitForPage();
|
|
1509
|
+
|
|
1510
|
+
const lateRefresh: Deferred<Incident | Alert> = createDeferred<
|
|
1511
|
+
Incident | Alert
|
|
1512
|
+
>();
|
|
1513
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
1514
|
+
Incident | Alert
|
|
1515
|
+
>();
|
|
1516
|
+
|
|
1517
|
+
getItemMock.mockImplementation((...args: Array<unknown>) => {
|
|
1518
|
+
return itemRequestId(args) === OTHER_EVENT_ID
|
|
1519
|
+
? otherItem.promise
|
|
1520
|
+
: lateRefresh.promise;
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
// A refresh of the first event is in flight when the reader moves on.
|
|
1524
|
+
act(() => {
|
|
1525
|
+
latestProps<ChangeStateProps>(
|
|
1526
|
+
pageCase.changeStateKey,
|
|
1527
|
+
).onActionComplete();
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
currentEventId = OTHER_EVENT_ID;
|
|
1531
|
+
view.rerender(pageCase.pageElement());
|
|
1532
|
+
await flush();
|
|
1533
|
+
|
|
1534
|
+
await act(async () => {
|
|
1535
|
+
lateRefresh.resolve(pageCase.buildEvent("Stale title"));
|
|
1536
|
+
});
|
|
1537
|
+
await flush();
|
|
1538
|
+
|
|
1539
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
1540
|
+
`Loading ${pageCase.noun}`,
|
|
1541
|
+
);
|
|
1542
|
+
expect(stubsRenderedWithId(OTHER_EVENT_ID)).toEqual([]);
|
|
1543
|
+
|
|
1544
|
+
await act(async () => {
|
|
1545
|
+
otherItem.resolve(pageCase.buildEvent("Payments failing"));
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
await waitFor(() => {
|
|
1549
|
+
expect(
|
|
1550
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1551
|
+
).toBe("Payments failing");
|
|
1552
|
+
});
|
|
1553
|
+
// Let the mount effects of that render run before counting mounts.
|
|
1554
|
+
await flush();
|
|
1555
|
+
|
|
1556
|
+
expect(
|
|
1557
|
+
propsHistory<ChangeStateProps>(pageCase.changeStateKey).map(
|
|
1558
|
+
(props: ChangeStateProps) => {
|
|
1559
|
+
return props.title;
|
|
1560
|
+
},
|
|
1561
|
+
),
|
|
1562
|
+
).not.toContain("Stale title");
|
|
1563
|
+
expect(mountCounts["InvestigationPanel"]).toBe(2);
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
test("a first load of the previous event that answers after the switch is ignored", async () => {
|
|
1567
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1568
|
+
|
|
1569
|
+
const firstItem: Deferred<Incident | Alert> = createDeferred<
|
|
1570
|
+
Incident | Alert
|
|
1571
|
+
>();
|
|
1572
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
1573
|
+
Incident | Alert
|
|
1574
|
+
>();
|
|
1575
|
+
|
|
1576
|
+
getItemMock.mockImplementation((...args: Array<unknown>) => {
|
|
1577
|
+
return itemRequestId(args) === OTHER_EVENT_ID
|
|
1578
|
+
? otherItem.promise
|
|
1579
|
+
: firstItem.promise;
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
const view: RenderResult = pageCase.renderPage();
|
|
1583
|
+
await flush();
|
|
1584
|
+
|
|
1585
|
+
currentEventId = OTHER_EVENT_ID;
|
|
1586
|
+
view.rerender(pageCase.pageElement());
|
|
1587
|
+
await flush();
|
|
1588
|
+
|
|
1589
|
+
await act(async () => {
|
|
1590
|
+
firstItem.resolve(pageCase.buildEvent("Left behind"));
|
|
1591
|
+
});
|
|
1592
|
+
await flush();
|
|
1593
|
+
|
|
1594
|
+
// Still loading the event the reader is on; nothing mounted at all.
|
|
1595
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
1596
|
+
`Loading ${pageCase.noun}`,
|
|
1597
|
+
);
|
|
1598
|
+
expect(mountCounts["InvestigationPanel"]).toBeUndefined();
|
|
1599
|
+
|
|
1600
|
+
await act(async () => {
|
|
1601
|
+
otherItem.resolve(pageCase.buildEvent("Payments failing"));
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
await waitFor(() => {
|
|
1605
|
+
expect(
|
|
1606
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1607
|
+
).toBe("Payments failing");
|
|
1608
|
+
});
|
|
1609
|
+
// Let the mount effects of that render run before counting mounts.
|
|
1610
|
+
await flush();
|
|
1611
|
+
|
|
1612
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1613
|
+
expect(
|
|
1614
|
+
propsHistory<ChangeStateProps>(pageCase.changeStateKey).map(
|
|
1615
|
+
(props: ChangeStateProps) => {
|
|
1616
|
+
return props.title;
|
|
1617
|
+
},
|
|
1618
|
+
),
|
|
1619
|
+
).not.toContain("Left behind");
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test("a failed first load of the next event shows its error, not the previous event", async () => {
|
|
1623
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1624
|
+
|
|
1625
|
+
const view: RenderResult = pageCase.renderPage();
|
|
1626
|
+
await waitForPage();
|
|
1627
|
+
|
|
1628
|
+
getItemMock.mockImplementation((...args: Array<unknown>) => {
|
|
1629
|
+
return itemRequestId(args) === OTHER_EVENT_ID
|
|
1630
|
+
? Promise.reject(new Error("The database is asleep"))
|
|
1631
|
+
: Promise.resolve(pageCase.buildEvent("Checkout slow"));
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
currentEventId = OTHER_EVENT_ID;
|
|
1635
|
+
view.rerender(pageCase.pageElement());
|
|
1636
|
+
|
|
1637
|
+
expect(
|
|
1638
|
+
await screen.findByText("The database is asleep"),
|
|
1639
|
+
).toBeInTheDocument();
|
|
1640
|
+
expect(screen.queryByTestId("stub-InvestigationPanel")).toBeNull();
|
|
1641
|
+
expect(stubsRenderedWithId(OTHER_EVENT_ID)).toEqual([]);
|
|
1642
|
+
|
|
1643
|
+
serve(pageCase, {
|
|
1644
|
+
timeline: REOPENED_TIMELINE,
|
|
1645
|
+
title: "Payments failing",
|
|
1646
|
+
});
|
|
1647
|
+
fireEvent.click(screen.getByTestId("refresh-button"));
|
|
1648
|
+
|
|
1649
|
+
await waitForPage();
|
|
1650
|
+
|
|
1651
|
+
expect(screen.queryByText("The database is asleep")).toBeNull();
|
|
1652
|
+
expect(latestProps<ChangeStateProps>(pageCase.changeStateKey).title).toBe(
|
|
1653
|
+
"Payments failing",
|
|
1654
|
+
);
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
test("a refresh fired late by a card of the previous event cannot cancel the next event's load", async () => {
|
|
1658
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1659
|
+
|
|
1660
|
+
const view: RenderResult = pageCase.renderPage();
|
|
1661
|
+
await waitForPage();
|
|
1662
|
+
|
|
1663
|
+
// Handed to the previous event's header; it can still call it later.
|
|
1664
|
+
const previousOnActionComplete: () => void =
|
|
1665
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).onActionComplete;
|
|
1666
|
+
|
|
1667
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
1668
|
+
Incident | Alert
|
|
1669
|
+
>();
|
|
1670
|
+
serveOther(otherItem.promise, "Stale title");
|
|
1671
|
+
|
|
1672
|
+
const readsBefore: number = countItemReads();
|
|
1673
|
+
|
|
1674
|
+
currentEventId = OTHER_EVENT_ID;
|
|
1675
|
+
view.rerender(pageCase.pageElement());
|
|
1676
|
+
|
|
1677
|
+
await waitFor(() => {
|
|
1678
|
+
expect(countItemReads()).toBe(readsBefore + 1);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
// e.g. a state change on the previous event whose save lands now.
|
|
1682
|
+
act(() => {
|
|
1683
|
+
previousOnActionComplete();
|
|
1684
|
+
});
|
|
1685
|
+
await flush();
|
|
1686
|
+
|
|
1687
|
+
// It read nothing: the previous event is not on screen any more.
|
|
1688
|
+
expect(countItemReads()).toBe(readsBefore + 1);
|
|
1689
|
+
|
|
1690
|
+
await act(async () => {
|
|
1691
|
+
otherItem.resolve(pageCase.buildEvent("Payments failing"));
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
await waitFor(() => {
|
|
1695
|
+
expect(
|
|
1696
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1697
|
+
).toBe("Payments failing");
|
|
1698
|
+
});
|
|
1699
|
+
// Let the mount effects of that render run before counting mounts.
|
|
1700
|
+
await flush();
|
|
1701
|
+
|
|
1702
|
+
expect(
|
|
1703
|
+
propsHistory<ChangeStateProps>(pageCase.changeStateKey).map(
|
|
1704
|
+
(props: ChangeStateProps) => {
|
|
1705
|
+
return props.title;
|
|
1706
|
+
},
|
|
1707
|
+
),
|
|
1708
|
+
).not.toContain("Stale title");
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
test("the AI status and summary of the previous event never reach the next one's header", async () => {
|
|
1712
|
+
serve(pageCase, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
1713
|
+
|
|
1714
|
+
const view: RenderResult = pageCase.renderPage();
|
|
1715
|
+
await waitForPage();
|
|
1716
|
+
|
|
1717
|
+
const previousPanel: InvestigationPanelProps =
|
|
1718
|
+
latestProps<InvestigationPanelProps>("InvestigationPanel");
|
|
1719
|
+
|
|
1720
|
+
act(() => {
|
|
1721
|
+
previousPanel.onStatusChange(AIRunStatus.Completed);
|
|
1722
|
+
previousPanel.onReportSummaryChange("Pool exhaustion in checkout-api.");
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
1726
|
+
Incident | Alert
|
|
1727
|
+
>();
|
|
1728
|
+
serveOther(otherItem.promise, "Checkout slow");
|
|
1729
|
+
|
|
1730
|
+
currentEventId = OTHER_EVENT_ID;
|
|
1731
|
+
view.rerender(pageCase.pageElement());
|
|
1732
|
+
await flush();
|
|
1733
|
+
|
|
1734
|
+
await act(async () => {
|
|
1735
|
+
otherItem.resolve(pageCase.buildEvent("Payments failing"));
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
await waitFor(() => {
|
|
1739
|
+
expect(
|
|
1740
|
+
latestProps<ChangeStateProps>(pageCase.changeStateKey).title,
|
|
1741
|
+
).toBe("Payments failing");
|
|
1742
|
+
});
|
|
1743
|
+
// Let the mount effects of that render run before counting mounts.
|
|
1744
|
+
await flush();
|
|
1745
|
+
|
|
1746
|
+
const header: ChangeStateProps = latestProps<ChangeStateProps>(
|
|
1747
|
+
pageCase.changeStateKey,
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1750
|
+
expect(header.aiInvestigationStatus).toBeNull();
|
|
1751
|
+
expect(header.aiInvestigationSummary).toBeNull();
|
|
1752
|
+
});
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
describe("incident-only behaviour", () => {
|
|
1757
|
+
test("the header names who declared the incident once the details card has read it", async () => {
|
|
1758
|
+
serve(INCIDENT_CASE, {
|
|
1759
|
+
timeline: REOPENED_TIMELINE,
|
|
1760
|
+
title: "Checkout slow",
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
INCIDENT_CASE.renderPage();
|
|
1764
|
+
await waitForPage();
|
|
1765
|
+
|
|
1766
|
+
expect(factLabelsOf(INCIDENT_CASE)).not.toContain("Declared by");
|
|
1767
|
+
|
|
1768
|
+
const incident: Incident = new Incident();
|
|
1769
|
+
const probe: Probe = new Probe();
|
|
1770
|
+
probe.name = "Probe US";
|
|
1771
|
+
incident.createdByProbe = probe;
|
|
1772
|
+
|
|
1773
|
+
act(() => {
|
|
1774
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Incident Details")
|
|
1775
|
+
.modelDetailProps.onItemLoaded!(incident);
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
const declaredBy: EventStatusFact | undefined = factsOf(INCIDENT_CASE).find(
|
|
1779
|
+
(fact: EventStatusFact) => {
|
|
1780
|
+
return fact.label === "Declared by";
|
|
1781
|
+
},
|
|
1782
|
+
);
|
|
1783
|
+
|
|
1784
|
+
expect(declaredBy?.value).toBe("Probe US");
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
test("the header lists affected monitors as links, with a count past two", async () => {
|
|
1788
|
+
serve(INCIDENT_CASE, {
|
|
1789
|
+
timeline: REOPENED_TIMELINE,
|
|
1790
|
+
title: "Checkout slow",
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
INCIDENT_CASE.renderPage();
|
|
1794
|
+
await waitForPage();
|
|
1795
|
+
|
|
1796
|
+
const buildMonitor: (id: string, name: string) => Monitor = (
|
|
1797
|
+
id: string,
|
|
1798
|
+
name: string,
|
|
1799
|
+
): Monitor => {
|
|
1800
|
+
const monitor: Monitor = new Monitor();
|
|
1801
|
+
monitor.id = new ObjectID(id);
|
|
1802
|
+
monitor.name = name;
|
|
1803
|
+
return monitor;
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
const incident: Incident = new Incident();
|
|
1807
|
+
incident.monitors = [buildMonitor(MONITOR_ID, "checkout-api")];
|
|
1808
|
+
|
|
1809
|
+
act(() => {
|
|
1810
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Affected Resources")
|
|
1811
|
+
.modelDetailProps.onItemLoaded!(incident);
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
let monitorFact: EventStatusFact | undefined = factsOf(INCIDENT_CASE).find(
|
|
1815
|
+
(fact: EventStatusFact) => {
|
|
1816
|
+
return fact.label === "Monitor";
|
|
1817
|
+
},
|
|
1818
|
+
);
|
|
1819
|
+
|
|
1820
|
+
expect(monitorFact).toBeDefined();
|
|
1821
|
+
|
|
1822
|
+
const single: ReturnType<typeof render> = render(<>{monitorFact!.value}</>);
|
|
1823
|
+
const link: HTMLElement = within(single.container).getByRole("link", {
|
|
1824
|
+
name: "checkout-api",
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
expect(link.getAttribute("href")).toContain(MONITOR_ID);
|
|
1828
|
+
single.unmount();
|
|
1829
|
+
|
|
1830
|
+
incident.monitors = [
|
|
1831
|
+
buildMonitor(MONITOR_ID, "checkout-api"),
|
|
1832
|
+
buildMonitor("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", "payments-api"),
|
|
1833
|
+
buildMonitor("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb3", "search-api"),
|
|
1834
|
+
buildMonitor("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb4", "cart-api"),
|
|
1835
|
+
];
|
|
1836
|
+
|
|
1837
|
+
act(() => {
|
|
1838
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Affected Resources")
|
|
1839
|
+
.modelDetailProps.onItemLoaded!(incident);
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
monitorFact = factsOf(INCIDENT_CASE).find((fact: EventStatusFact) => {
|
|
1843
|
+
return fact.label === "Monitors";
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
const several: ReturnType<typeof render> = render(
|
|
1847
|
+
<>{monitorFact!.value}</>,
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
expect(within(several.container).getAllByRole("link")).toHaveLength(2);
|
|
1851
|
+
expect(several.container).toHaveTextContent(
|
|
1852
|
+
"checkout-api, payments-api +2 more",
|
|
1853
|
+
);
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
test("an incident with no monitors gets no monitor fact", async () => {
|
|
1857
|
+
serve(INCIDENT_CASE, {
|
|
1858
|
+
timeline: REOPENED_TIMELINE,
|
|
1859
|
+
title: "Checkout slow",
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
INCIDENT_CASE.renderPage();
|
|
1863
|
+
await waitForPage();
|
|
1864
|
+
|
|
1865
|
+
act(() => {
|
|
1866
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Affected Resources")
|
|
1867
|
+
.modelDetailProps.onItemLoaded!(new Incident());
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
expect(factLabelsOf(INCIDENT_CASE)).not.toContain("Monitor");
|
|
1871
|
+
expect(factLabelsOf(INCIDENT_CASE)).not.toContain("Monitors");
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
test("a role change refreshes only the feed", async () => {
|
|
1875
|
+
serve(INCIDENT_CASE, {
|
|
1876
|
+
timeline: REOPENED_TIMELINE,
|
|
1877
|
+
title: "Checkout slow",
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
INCIDENT_CASE.renderPage();
|
|
1881
|
+
await waitForPage();
|
|
1882
|
+
|
|
1883
|
+
const readsBefore: number = countItemReads();
|
|
1884
|
+
const feedTokenBefore: number = latestProps<{ refreshToken: number }>(
|
|
1885
|
+
"Feed",
|
|
1886
|
+
).refreshToken;
|
|
1887
|
+
|
|
1888
|
+
await act(async () => {
|
|
1889
|
+
await latestProps<{ onMemberChange: () => Promise<void> }>(
|
|
1890
|
+
"Roles",
|
|
1891
|
+
).onMemberChange();
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
expect(countItemReads()).toBe(readsBefore);
|
|
1895
|
+
expect(latestProps<{ refreshToken: number }>("Feed").refreshToken).toBe(
|
|
1896
|
+
feedTokenBefore + 1,
|
|
1897
|
+
);
|
|
1898
|
+
expect(mountCounts["Roles"]).toBe(1);
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
test("resending subscriber notifications re-reads only the details card", async () => {
|
|
1902
|
+
serve(INCIDENT_CASE, {
|
|
1903
|
+
timeline: REOPENED_TIMELINE,
|
|
1904
|
+
title: "Checkout slow",
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
INCIDENT_CASE.renderPage();
|
|
1908
|
+
await waitForPage();
|
|
1909
|
+
|
|
1910
|
+
const details: CardModelDetailProps = latestProps<CardModelDetailProps>(
|
|
1911
|
+
"CardModelDetail:Incident Details",
|
|
1912
|
+
);
|
|
1913
|
+
const refresherBefore: boolean | undefined = details.refresher;
|
|
1914
|
+
const statusField: DetailFieldProps | undefined =
|
|
1915
|
+
details.modelDetailProps.fields.find((field: DetailFieldProps) => {
|
|
1916
|
+
return field.title === "Subscriber Notification Status";
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
const incident: Incident = new Incident();
|
|
1920
|
+
incident.subscriberNotificationStatusOnIncidentCreated =
|
|
1921
|
+
StatusPageSubscriberNotificationStatus.Failed;
|
|
1922
|
+
|
|
1923
|
+
render(statusField!.getElement!(incident));
|
|
1924
|
+
|
|
1925
|
+
const readsBefore: number = countItemReads();
|
|
1926
|
+
|
|
1927
|
+
await act(async () => {
|
|
1928
|
+
latestProps<{ onResendNotification: () => void }>(
|
|
1929
|
+
"SubscriberNotificationStatus",
|
|
1930
|
+
).onResendNotification();
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
await waitFor(() => {
|
|
1934
|
+
expect(
|
|
1935
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Incident Details")
|
|
1936
|
+
.refresher,
|
|
1937
|
+
).toBe(!refresherBefore);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
expect(updateByIdMock).toHaveBeenCalledTimes(1);
|
|
1941
|
+
expect(
|
|
1942
|
+
(updateByIdMock.mock.calls[0]![0] as { data: Record<string, unknown> })
|
|
1943
|
+
.data,
|
|
1944
|
+
).toEqual({
|
|
1945
|
+
subscriberNotificationStatusOnIncidentCreated:
|
|
1946
|
+
StatusPageSubscriberNotificationStatus.Pending,
|
|
1947
|
+
subscriberNotificationStatusMessage: "Notification queued for resending",
|
|
1948
|
+
});
|
|
1949
|
+
expect(countItemReads()).toBe(readsBefore);
|
|
1950
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
/*
|
|
1954
|
+
* The details card is stubbed, so the status row is rendered here from the
|
|
1955
|
+
* card's latest props, the way the real card renders it on every page
|
|
1956
|
+
* render.
|
|
1957
|
+
*/
|
|
1958
|
+
type RenderStatusFieldFunction = () => RenderResult;
|
|
1959
|
+
|
|
1960
|
+
const renderStatusField: RenderStatusFieldFunction = (): RenderResult => {
|
|
1961
|
+
const statusField: DetailFieldProps | undefined =
|
|
1962
|
+
latestProps<CardModelDetailProps>(
|
|
1963
|
+
"CardModelDetail:Incident Details",
|
|
1964
|
+
).modelDetailProps.fields.find((field: DetailFieldProps) => {
|
|
1965
|
+
return field.title === "Subscriber Notification Status";
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
const incident: Incident = new Incident();
|
|
1969
|
+
incident.subscriberNotificationStatusOnIncidentCreated =
|
|
1970
|
+
StatusPageSubscriberNotificationStatus.Failed;
|
|
1971
|
+
|
|
1972
|
+
return render(statusField!.getElement!(incident));
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
type ResendFunction = () => Promise<void>;
|
|
1976
|
+
|
|
1977
|
+
const resend: ResendFunction = async (): Promise<void> => {
|
|
1978
|
+
await act(async () => {
|
|
1979
|
+
latestProps<{ onResendNotification: () => void }>(
|
|
1980
|
+
"SubscriberNotificationStatus",
|
|
1981
|
+
).onResendNotification();
|
|
1982
|
+
});
|
|
1983
|
+
await flush();
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
test("a failed resend shows under the notification status, not as a refresh failure", async () => {
|
|
1987
|
+
serve(INCIDENT_CASE, {
|
|
1988
|
+
timeline: REOPENED_TIMELINE,
|
|
1989
|
+
title: "Checkout slow",
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
INCIDENT_CASE.renderPage();
|
|
1993
|
+
await waitForPage();
|
|
1994
|
+
|
|
1995
|
+
let status: RenderResult = renderStatusField();
|
|
1996
|
+
|
|
1997
|
+
updateByIdMock.mockImplementationOnce(() => {
|
|
1998
|
+
return Promise.reject(new Error("Not allowed"));
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
await resend();
|
|
2002
|
+
|
|
2003
|
+
status.unmount();
|
|
2004
|
+
status = renderStatusField();
|
|
2005
|
+
|
|
2006
|
+
expect(within(status.container).getByRole("alert")).toHaveTextContent(
|
|
2007
|
+
"Could not resend notifications: Not allowed",
|
|
2008
|
+
);
|
|
2009
|
+
// It is the only alert: the page's refresh banner did not claim it.
|
|
2010
|
+
expect(screen.getAllByRole("alert")).toHaveLength(1);
|
|
2011
|
+
expect(screen.queryByText(/Could not refresh this incident/)).toBeNull();
|
|
2012
|
+
expect(screen.getByTestId("stub-InvestigationPanel")).toBeInTheDocument();
|
|
2013
|
+
expect(mountCounts["InvestigationPanel"]).toBe(1);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
test("a successful page refresh keeps the resend error, and a successful resend clears it", async () => {
|
|
2017
|
+
serve(INCIDENT_CASE, {
|
|
2018
|
+
timeline: REOPENED_TIMELINE,
|
|
2019
|
+
title: "Checkout slow",
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
INCIDENT_CASE.renderPage();
|
|
2023
|
+
await waitForPage();
|
|
2024
|
+
|
|
2025
|
+
let status: RenderResult = renderStatusField();
|
|
2026
|
+
|
|
2027
|
+
updateByIdMock.mockImplementationOnce(() => {
|
|
2028
|
+
return Promise.reject(new Error("Not allowed"));
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
await resend();
|
|
2032
|
+
|
|
2033
|
+
// A refresh does not retry the resend, so it must not hide that it failed.
|
|
2034
|
+
const readsBefore: number = countItemReads();
|
|
2035
|
+
|
|
2036
|
+
act(() => {
|
|
2037
|
+
latestProps<ChangeStateProps>(
|
|
2038
|
+
INCIDENT_CASE.changeStateKey,
|
|
2039
|
+
).onActionComplete();
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
await waitFor(() => {
|
|
2043
|
+
expect(countItemReads()).toBe(readsBefore + 1);
|
|
2044
|
+
});
|
|
2045
|
+
await flush();
|
|
2046
|
+
|
|
2047
|
+
status.unmount();
|
|
2048
|
+
status = renderStatusField();
|
|
2049
|
+
|
|
2050
|
+
expect(within(status.container).getByRole("alert")).toHaveTextContent(
|
|
2051
|
+
"Could not resend notifications: Not allowed",
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
// Resending again, successfully, clears it.
|
|
2055
|
+
await resend();
|
|
2056
|
+
|
|
2057
|
+
status.unmount();
|
|
2058
|
+
status = renderStatusField();
|
|
2059
|
+
|
|
2060
|
+
expect(updateByIdMock).toHaveBeenCalledTimes(2);
|
|
2061
|
+
expect(within(status.container).queryByRole("alert")).toBeNull();
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
test("a failed resend belongs to its incident and is not shown on the next one", async () => {
|
|
2065
|
+
serve(INCIDENT_CASE, {
|
|
2066
|
+
timeline: REOPENED_TIMELINE,
|
|
2067
|
+
title: "Checkout slow",
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
const view: RenderResult = INCIDENT_CASE.renderPage();
|
|
2071
|
+
await waitForPage();
|
|
2072
|
+
|
|
2073
|
+
let status: RenderResult = renderStatusField();
|
|
2074
|
+
|
|
2075
|
+
updateByIdMock.mockImplementationOnce(() => {
|
|
2076
|
+
return Promise.reject(new Error("Not allowed"));
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
await resend();
|
|
2080
|
+
|
|
2081
|
+
status.unmount();
|
|
2082
|
+
status = renderStatusField();
|
|
2083
|
+
|
|
2084
|
+
// Shown for the incident it failed on...
|
|
2085
|
+
expect(within(status.container).getByRole("alert")).toHaveTextContent(
|
|
2086
|
+
"Could not resend notifications: Not allowed",
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2089
|
+
status.unmount();
|
|
2090
|
+
|
|
2091
|
+
// ...and gone once the reader is on another incident.
|
|
2092
|
+
currentEventId = OTHER_EVENT_ID;
|
|
2093
|
+
view.rerender(INCIDENT_CASE.pageElement());
|
|
2094
|
+
|
|
2095
|
+
await waitFor(() => {
|
|
2096
|
+
expect(
|
|
2097
|
+
stubsRenderedWithId(OTHER_EVENT_ID).includes("InvestigationPanel"),
|
|
2098
|
+
).toBe(true);
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
status = renderStatusField();
|
|
2102
|
+
|
|
2103
|
+
expect(within(status.container).queryByRole("alert")).toBeNull();
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
test("header facts reported for the previous incident never show on the next", async () => {
|
|
2107
|
+
serve(INCIDENT_CASE, {
|
|
2108
|
+
timeline: REOPENED_TIMELINE,
|
|
2109
|
+
title: "Checkout slow",
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
const view: RenderResult = INCIDENT_CASE.renderPage();
|
|
2113
|
+
await waitForPage();
|
|
2114
|
+
|
|
2115
|
+
const previousDetails: CardModelDetailProps =
|
|
2116
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Incident Details");
|
|
2117
|
+
const previousResources: CardModelDetailProps =
|
|
2118
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Affected Resources");
|
|
2119
|
+
|
|
2120
|
+
const declaredByProbe: (name: string) => Incident = (
|
|
2121
|
+
name: string,
|
|
2122
|
+
): Incident => {
|
|
2123
|
+
const incident: Incident = new Incident();
|
|
2124
|
+
const probe: Probe = new Probe();
|
|
2125
|
+
probe.name = name;
|
|
2126
|
+
incident.createdByProbe = probe;
|
|
2127
|
+
return incident;
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
const withMonitor: Incident = new Incident();
|
|
2131
|
+
const monitor: Monitor = new Monitor();
|
|
2132
|
+
monitor.id = new ObjectID(MONITOR_ID);
|
|
2133
|
+
monitor.name = "checkout-api";
|
|
2134
|
+
withMonitor.monitors = [monitor];
|
|
2135
|
+
|
|
2136
|
+
act(() => {
|
|
2137
|
+
previousDetails.modelDetailProps.onItemLoaded!(
|
|
2138
|
+
declaredByProbe("Probe US"),
|
|
2139
|
+
);
|
|
2140
|
+
previousResources.modelDetailProps.onItemLoaded!(withMonitor);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
expect(factLabelsOf(INCIDENT_CASE)).toEqual([
|
|
2144
|
+
"Declared",
|
|
2145
|
+
"Declared by",
|
|
2146
|
+
"Monitor",
|
|
2147
|
+
]);
|
|
2148
|
+
|
|
2149
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
2150
|
+
Incident | Alert
|
|
2151
|
+
>();
|
|
2152
|
+
|
|
2153
|
+
getItemMock.mockImplementation((...args: Array<unknown>) => {
|
|
2154
|
+
return itemRequestId(args) === OTHER_EVENT_ID
|
|
2155
|
+
? otherItem.promise
|
|
2156
|
+
: Promise.resolve(INCIDENT_CASE.buildEvent("Checkout slow"));
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
currentEventId = OTHER_EVENT_ID;
|
|
2160
|
+
view.rerender(INCIDENT_CASE.pageElement());
|
|
2161
|
+
await flush();
|
|
2162
|
+
|
|
2163
|
+
// The previous incident's cards report in late, after the switch.
|
|
2164
|
+
act(() => {
|
|
2165
|
+
previousDetails.modelDetailProps.onItemLoaded!(
|
|
2166
|
+
declaredByProbe("Probe EU"),
|
|
2167
|
+
);
|
|
2168
|
+
previousResources.modelDetailProps.onItemLoaded!(withMonitor);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
await act(async () => {
|
|
2172
|
+
otherItem.resolve(INCIDENT_CASE.buildEvent("Payments failing"));
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
await waitFor(() => {
|
|
2176
|
+
expect(
|
|
2177
|
+
latestProps<ChangeStateProps>(INCIDENT_CASE.changeStateKey).title,
|
|
2178
|
+
).toBe("Payments failing");
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
// Every header render for the new incident carried only its own facts.
|
|
2182
|
+
for (const props of propsHistory<
|
|
2183
|
+
ChangeStateProps & Record<string, unknown>
|
|
2184
|
+
>(INCIDENT_CASE.changeStateKey)) {
|
|
2185
|
+
if ((props["incidentId"] as ObjectID).toString() !== OTHER_EVENT_ID) {
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
expect(
|
|
2190
|
+
(props.facts || []).map((fact: EventStatusFact) => {
|
|
2191
|
+
return fact.label;
|
|
2192
|
+
}),
|
|
2193
|
+
).toEqual(["Declared"]);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Its own cards still report in as usual.
|
|
2197
|
+
act(() => {
|
|
2198
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Incident Details")
|
|
2199
|
+
.modelDetailProps.onItemLoaded!(declaredByProbe("Probe AP"));
|
|
2200
|
+
});
|
|
2201
|
+
|
|
2202
|
+
expect(
|
|
2203
|
+
factsOf(INCIDENT_CASE).find((fact: EventStatusFact) => {
|
|
2204
|
+
return fact.label === "Declared by";
|
|
2205
|
+
})?.value,
|
|
2206
|
+
).toBe("Probe AP");
|
|
2207
|
+
});
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
describe("alert-only behaviour", () => {
|
|
2211
|
+
test("the header shows the monitor and episode once the details card has read them", async () => {
|
|
2212
|
+
serve(ALERT_CASE, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
2213
|
+
|
|
2214
|
+
ALERT_CASE.renderPage();
|
|
2215
|
+
await waitForPage();
|
|
2216
|
+
|
|
2217
|
+
expect(factLabelsOf(ALERT_CASE)).toEqual(["Created"]);
|
|
2218
|
+
|
|
2219
|
+
const alert: Alert = new Alert();
|
|
2220
|
+
const monitor: Monitor = new Monitor();
|
|
2221
|
+
monitor.id = new ObjectID(MONITOR_ID);
|
|
2222
|
+
monitor.name = "checkout-api";
|
|
2223
|
+
alert.monitor = monitor;
|
|
2224
|
+
|
|
2225
|
+
const episode: AlertEpisode = new AlertEpisode();
|
|
2226
|
+
episode.id = new ObjectID("cccccccc-cccc-4ccc-8ccc-ccccccccccc1");
|
|
2227
|
+
episode.title = "Checkout errors across regions";
|
|
2228
|
+
alert.alertEpisode = episode;
|
|
2229
|
+
|
|
2230
|
+
act(() => {
|
|
2231
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Alert Details")
|
|
2232
|
+
.modelDetailProps.onItemLoaded!(alert);
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
expect(factLabelsOf(ALERT_CASE)).toEqual(["Created", "Monitor", "Episode"]);
|
|
2236
|
+
|
|
2237
|
+
const facts: Array<EventStatusFact> = factsOf(ALERT_CASE);
|
|
2238
|
+
const rendered: ReturnType<typeof render> = render(
|
|
2239
|
+
<>
|
|
2240
|
+
{facts[1]!.value}
|
|
2241
|
+
{facts[2]!.value}
|
|
2242
|
+
</>,
|
|
2243
|
+
);
|
|
2244
|
+
|
|
2245
|
+
expect(
|
|
2246
|
+
within(rendered.container).getByTestId("monitor-element"),
|
|
2247
|
+
).toHaveTextContent("checkout-api");
|
|
2248
|
+
expect(
|
|
2249
|
+
within(rendered.container).getByTestId("episode-element"),
|
|
2250
|
+
).toHaveTextContent("Checkout errors across regions");
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
test("an alert outside any episode gets no episode fact", async () => {
|
|
2254
|
+
serve(ALERT_CASE, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
2255
|
+
|
|
2256
|
+
ALERT_CASE.renderPage();
|
|
2257
|
+
await waitForPage();
|
|
2258
|
+
|
|
2259
|
+
act(() => {
|
|
2260
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Alert Details")
|
|
2261
|
+
.modelDetailProps.onItemLoaded!(new Alert());
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
expect(factLabelsOf(ALERT_CASE)).toEqual(["Created"]);
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
test("the episode row never selects columns a relation query cannot read", async () => {
|
|
2268
|
+
serve(ALERT_CASE, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
2269
|
+
|
|
2270
|
+
ALERT_CASE.renderPage();
|
|
2271
|
+
await waitForPage();
|
|
2272
|
+
|
|
2273
|
+
const episodeField: DetailFieldProps | undefined =
|
|
2274
|
+
latestProps<CardModelDetailProps>(
|
|
2275
|
+
"CardModelDetail:Alert Details",
|
|
2276
|
+
).modelDetailProps.fields.find((field: DetailFieldProps) => {
|
|
2277
|
+
return field.title === "Episode";
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
expect(episodeField?.field).toEqual({
|
|
2281
|
+
alertEpisode: { title: true, _id: true },
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
test("the monitor and episode reported for the previous alert never show on the next", async () => {
|
|
2286
|
+
serve(ALERT_CASE, { timeline: REOPENED_TIMELINE, title: "Checkout slow" });
|
|
2287
|
+
|
|
2288
|
+
const view: RenderResult = ALERT_CASE.renderPage();
|
|
2289
|
+
await waitForPage();
|
|
2290
|
+
|
|
2291
|
+
const previousDetails: CardModelDetailProps =
|
|
2292
|
+
latestProps<CardModelDetailProps>("CardModelDetail:Alert Details");
|
|
2293
|
+
|
|
2294
|
+
const previousAlert: Alert = new Alert();
|
|
2295
|
+
const monitor: Monitor = new Monitor();
|
|
2296
|
+
monitor.id = new ObjectID(MONITOR_ID);
|
|
2297
|
+
monitor.name = "checkout-api";
|
|
2298
|
+
previousAlert.monitor = monitor;
|
|
2299
|
+
|
|
2300
|
+
const episode: AlertEpisode = new AlertEpisode();
|
|
2301
|
+
episode.id = new ObjectID("cccccccc-cccc-4ccc-8ccc-ccccccccccc1");
|
|
2302
|
+
episode.title = "Checkout errors across regions";
|
|
2303
|
+
previousAlert.alertEpisode = episode;
|
|
2304
|
+
|
|
2305
|
+
act(() => {
|
|
2306
|
+
previousDetails.modelDetailProps.onItemLoaded!(previousAlert);
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
expect(factLabelsOf(ALERT_CASE)).toEqual(["Created", "Monitor", "Episode"]);
|
|
2310
|
+
|
|
2311
|
+
const otherItem: Deferred<Incident | Alert> = createDeferred<
|
|
2312
|
+
Incident | Alert
|
|
2313
|
+
>();
|
|
2314
|
+
|
|
2315
|
+
getItemMock.mockImplementation((...args: Array<unknown>) => {
|
|
2316
|
+
return itemRequestId(args) === OTHER_EVENT_ID
|
|
2317
|
+
? otherItem.promise
|
|
2318
|
+
: Promise.resolve(ALERT_CASE.buildEvent("Checkout slow"));
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
currentEventId = OTHER_EVENT_ID;
|
|
2322
|
+
view.rerender(ALERT_CASE.pageElement());
|
|
2323
|
+
await flush();
|
|
2324
|
+
|
|
2325
|
+
// The previous alert's details card reports in late, after the switch.
|
|
2326
|
+
act(() => {
|
|
2327
|
+
previousDetails.modelDetailProps.onItemLoaded!(previousAlert);
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
await act(async () => {
|
|
2331
|
+
otherItem.resolve(ALERT_CASE.buildEvent("Payments failing"));
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
await waitFor(() => {
|
|
2335
|
+
expect(
|
|
2336
|
+
latestProps<ChangeStateProps>(ALERT_CASE.changeStateKey).title,
|
|
2337
|
+
).toBe("Payments failing");
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
for (const props of propsHistory<
|
|
2341
|
+
ChangeStateProps & Record<string, unknown>
|
|
2342
|
+
>(ALERT_CASE.changeStateKey)) {
|
|
2343
|
+
if ((props["alertId"] as ObjectID).toString() !== OTHER_EVENT_ID) {
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
expect(
|
|
2348
|
+
(props.facts || []).map((fact: EventStatusFact) => {
|
|
2349
|
+
return fact.label;
|
|
2350
|
+
}),
|
|
2351
|
+
).toEqual(["Created"]);
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
});
|