@oneuptime/common 14.0.6 → 14.0.8
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/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/InventoryItem.ts +23 -12
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1663 -103
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +118 -64
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +534 -6
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
- package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
- package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1148 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +84 -57
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -17,49 +17,72 @@ import {
|
|
|
17
17
|
} from "@testing-library/react";
|
|
18
18
|
import React from "react";
|
|
19
19
|
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
20
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
21
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
22
|
+
import URL from "../../../Types/API/URL";
|
|
23
23
|
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
24
|
-
import
|
|
24
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
25
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
25
26
|
import ObjectID from "../../../Types/ObjectID";
|
|
27
|
+
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
26
28
|
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
27
29
|
import TimeRange from "../../../Types/Time/TimeRange";
|
|
28
|
-
import
|
|
30
|
+
import {
|
|
31
|
+
TOPOLOGY_API_FORMAT_VERSION,
|
|
32
|
+
TopologyApiPath,
|
|
33
|
+
} from "../../../Types/Topology/TopologyApi";
|
|
34
|
+
import useTopologyData, {
|
|
35
|
+
TOPOLOGY_RANGE_REPIN_AFTER_MS,
|
|
36
|
+
TopologyData,
|
|
37
|
+
TopologyView,
|
|
38
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/UseTopologyData";
|
|
39
|
+
import { TopologyEntity } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyData";
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* useTopologyData decides WHEN the Topology page talks to the server: one
|
|
43
|
+
* POST per telemetry tab, the first time that tab is opened, pinned to one
|
|
44
|
+
* range start per generation. These tests drive it through the one seam the
|
|
45
|
+
* browser really has — API.post — with a fake server that records every
|
|
46
|
+
* request and answers when the test says so.
|
|
47
|
+
*/
|
|
29
48
|
|
|
30
|
-
const
|
|
49
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
31
50
|
const getProjectIdMock: MockFunction = getJestMockFunction();
|
|
32
51
|
|
|
33
|
-
jest.mock("../../../UI/Utils/
|
|
52
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
34
53
|
return {
|
|
35
54
|
__esModule: true,
|
|
36
55
|
default: {
|
|
37
|
-
|
|
38
|
-
return
|
|
56
|
+
post: (...args: Array<unknown>) => {
|
|
57
|
+
return postMock(...args);
|
|
58
|
+
},
|
|
59
|
+
getFriendlyMessage: (error: unknown): string => {
|
|
60
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
61
|
+
return String((error as { message: unknown }).message);
|
|
62
|
+
}
|
|
63
|
+
return "Unable to load topology";
|
|
39
64
|
},
|
|
40
65
|
},
|
|
41
66
|
};
|
|
42
67
|
});
|
|
43
68
|
|
|
44
|
-
jest.mock("../../../UI/Utils/
|
|
69
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
45
70
|
return {
|
|
46
71
|
__esModule: true,
|
|
47
72
|
default: {
|
|
48
|
-
|
|
49
|
-
return getProjectIdMock();
|
|
73
|
+
getCommonHeaders: (): Dictionary<string> => {
|
|
74
|
+
return { tenantid: String(getProjectIdMock()) };
|
|
50
75
|
},
|
|
51
76
|
},
|
|
52
77
|
};
|
|
53
78
|
});
|
|
54
79
|
|
|
55
|
-
jest.mock("../../../UI/Utils/
|
|
80
|
+
jest.mock("../../../UI/Utils/Project", () => {
|
|
56
81
|
return {
|
|
57
82
|
__esModule: true,
|
|
58
83
|
default: {
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
? error.message
|
|
62
|
-
: "Unable to load topology";
|
|
84
|
+
getCurrentProjectId: () => {
|
|
85
|
+
return getProjectIdMock();
|
|
63
86
|
},
|
|
64
87
|
},
|
|
65
88
|
};
|
|
@@ -71,7 +94,7 @@ const PROJECT_ID: ObjectID = new ObjectID(
|
|
|
71
94
|
const FIRST_RANGE: RangeStartAndEndDateTime = {
|
|
72
95
|
range: TimeRange.CUSTOM,
|
|
73
96
|
startAndEndDate: new InBetween<Date>(
|
|
74
|
-
new Date("2026-09-01T09:15:
|
|
97
|
+
new Date("2026-09-01T09:15:27.000Z"),
|
|
75
98
|
new Date("2026-09-01T10:15:00.000Z"),
|
|
76
99
|
),
|
|
77
100
|
};
|
|
@@ -82,297 +105,1038 @@ const SECOND_RANGE: RangeStartAndEndDateTime = {
|
|
|
82
105
|
new Date("2026-09-02T10:15:00.000Z"),
|
|
83
106
|
),
|
|
84
107
|
};
|
|
108
|
+
const LAST_HOUR: RangeStartAndEndDateTime = { range: TimeRange.PAST_ONE_HOUR };
|
|
85
109
|
|
|
86
|
-
|
|
110
|
+
interface PostOptions {
|
|
111
|
+
url: URL;
|
|
112
|
+
data: JSONObject;
|
|
113
|
+
headers: Dictionary<string>;
|
|
114
|
+
options?: { signal?: AbortSignal };
|
|
115
|
+
}
|
|
87
116
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
/* One request the fake server received, answerable by the test. */
|
|
118
|
+
interface RecordedRequest {
|
|
119
|
+
path: string;
|
|
120
|
+
rangeStart: string;
|
|
121
|
+
/* The whole body, to tell a `fresh` request from an ordinary one. */
|
|
122
|
+
body: JSONObject;
|
|
123
|
+
headers: Dictionary<string>;
|
|
124
|
+
signal: AbortSignal | undefined;
|
|
125
|
+
respond: (payload: JSONObject) => void;
|
|
126
|
+
fail: (statusCode: number, message: string) => void;
|
|
91
127
|
reject: (error: Error) => void;
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
|
|
130
|
+
let requests: Array<RecordedRequest> = [];
|
|
131
|
+
/* When true, every request is answered at once with defaultPayload. */
|
|
132
|
+
let autoRespond: boolean = true;
|
|
133
|
+
|
|
134
|
+
function pathOf(url: URL): string {
|
|
135
|
+
return url.toString().replace(/^.*\/api(?=\/)/, "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function requestsTo(path: TopologyApiPath): Array<RecordedRequest> {
|
|
139
|
+
return requests.filter((request: RecordedRequest): boolean => {
|
|
140
|
+
return request.path === path;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function lastRequestTo(path: TopologyApiPath): RecordedRequest {
|
|
145
|
+
const matching: Array<RecordedRequest> = requestsTo(path);
|
|
146
|
+
return matching[matching.length - 1]!;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
* The server floors the range start to the minute and echoes it; each answer
|
|
151
|
+
* names its own ordinal so a test can tell which response is on screen.
|
|
152
|
+
*/
|
|
153
|
+
function floorToMinute(iso: string): string {
|
|
154
|
+
const time: number = new Date(iso).getTime();
|
|
155
|
+
return new Date(time - (time % 60000)).toISOString();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function serviceMapPayload(
|
|
159
|
+
rangeStart: string,
|
|
160
|
+
serviceName: string,
|
|
161
|
+
extra: JSONObject = {},
|
|
162
|
+
): JSONObject {
|
|
163
|
+
return {
|
|
164
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
165
|
+
rangeStart: floorToMinute(rangeStart),
|
|
166
|
+
generatedAt: "2026-09-02T10:15:30.000Z",
|
|
167
|
+
entities: [
|
|
168
|
+
{
|
|
169
|
+
key: `service:${serviceName}`,
|
|
170
|
+
type: EntityType.Service,
|
|
171
|
+
name: serviceName,
|
|
172
|
+
source: "discovered",
|
|
173
|
+
lastSeenAt: null,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
dependencies: [],
|
|
177
|
+
runsOn: [],
|
|
178
|
+
entityTruncation: null,
|
|
179
|
+
dependencyTruncation: null,
|
|
180
|
+
...extra,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function infrastructurePayload(
|
|
185
|
+
rangeStart: string,
|
|
186
|
+
hostName: string,
|
|
187
|
+
): JSONObject {
|
|
188
|
+
return {
|
|
189
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
190
|
+
rangeStart: floorToMinute(rangeStart),
|
|
191
|
+
generatedAt: "2026-09-02T10:15:30.000Z",
|
|
192
|
+
nodes: [
|
|
193
|
+
{
|
|
194
|
+
key: `host:${hostName}`,
|
|
195
|
+
type: EntityType.Host,
|
|
196
|
+
name: hostName,
|
|
197
|
+
source: "discovered",
|
|
198
|
+
lastSeenAt: null,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
services: [],
|
|
202
|
+
placements: [],
|
|
203
|
+
collections: [],
|
|
204
|
+
totals: { resources: 1, activeResources: 1 },
|
|
205
|
+
truncation: null,
|
|
206
|
+
};
|
|
107
207
|
}
|
|
108
208
|
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
entity.entityKey = name;
|
|
114
|
-
entity.displayName = name;
|
|
115
|
-
data.push(entity);
|
|
209
|
+
function defaultPayload(request: RecordedRequest): JSONObject {
|
|
210
|
+
const ordinal: number = requestsTo(request.path as TopologyApiPath).length;
|
|
211
|
+
if (request.path === TopologyApiPath.ServiceMap) {
|
|
212
|
+
return serviceMapPayload(request.rangeStart, `service map #${ordinal}`);
|
|
116
213
|
}
|
|
117
|
-
return
|
|
214
|
+
return infrastructurePayload(
|
|
215
|
+
request.rangeStart,
|
|
216
|
+
`infrastructure #${ordinal}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function installFakeServer(): void {
|
|
221
|
+
postMock.mockImplementation((...args: Array<unknown>) => {
|
|
222
|
+
const options: PostOptions = args[0] as PostOptions;
|
|
223
|
+
return new Promise(
|
|
224
|
+
(
|
|
225
|
+
resolve: (value: HTTPResponse<JSONObject> | HTTPErrorResponse) => void,
|
|
226
|
+
reject: (error: Error) => void,
|
|
227
|
+
): void => {
|
|
228
|
+
const request: RecordedRequest = {
|
|
229
|
+
path: pathOf(options.url),
|
|
230
|
+
rangeStart: String(options.data["rangeStart"]),
|
|
231
|
+
body: options.data,
|
|
232
|
+
headers: options.headers,
|
|
233
|
+
signal: options.options?.signal,
|
|
234
|
+
respond: (payload: JSONObject): void => {
|
|
235
|
+
resolve(new HTTPResponse<JSONObject>(200, payload, {}));
|
|
236
|
+
},
|
|
237
|
+
fail: (statusCode: number, message: string): void => {
|
|
238
|
+
resolve(new HTTPErrorResponse(statusCode, { message }, {}));
|
|
239
|
+
},
|
|
240
|
+
reject: reject,
|
|
241
|
+
};
|
|
242
|
+
requests.push(request);
|
|
243
|
+
if (autoRespond) {
|
|
244
|
+
request.respond(defaultPayload(request));
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
* How each request asked: "fresh" when it told the server to bypass its
|
|
253
|
+
* response cache, "cached" when it sent the ordinary body (no `fresh` key at
|
|
254
|
+
* all — the flag is never sent as false).
|
|
255
|
+
*/
|
|
256
|
+
function freshness(list: Array<RecordedRequest>): Array<string> {
|
|
257
|
+
return list.map((request: RecordedRequest): string => {
|
|
258
|
+
if (request.body["fresh"] === true) {
|
|
259
|
+
return "fresh";
|
|
260
|
+
}
|
|
261
|
+
return "fresh" in request.body
|
|
262
|
+
? `unexpected fresh: ${String(request.body["fresh"])}`
|
|
263
|
+
: "cached";
|
|
264
|
+
});
|
|
118
265
|
}
|
|
119
266
|
|
|
120
|
-
function
|
|
121
|
-
|
|
267
|
+
function names(entities: Array<TopologyEntity> | undefined): string {
|
|
268
|
+
return (entities || [])
|
|
269
|
+
.map((entity: TopologyEntity): string => {
|
|
270
|
+
return entity.displayName || "";
|
|
271
|
+
})
|
|
272
|
+
.join(",");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function Probe(props: {
|
|
276
|
+
range: RangeStartAndEndDateTime;
|
|
277
|
+
view: TopologyView;
|
|
278
|
+
}): React.ReactElement {
|
|
279
|
+
const data: TopologyData = useTopologyData(props.range, props.view);
|
|
122
280
|
return (
|
|
123
281
|
<div>
|
|
124
|
-
<span data-testid="
|
|
125
|
-
<span data-testid="
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
282
|
+
<span data-testid="generation">{data.generation}</span>
|
|
283
|
+
<span data-testid="pinned">
|
|
284
|
+
{data.pinnedRangeStart?.toISOString() || ""}
|
|
285
|
+
</span>
|
|
286
|
+
<span data-testid="sm-status">{data.serviceMap.status}</span>
|
|
287
|
+
<span data-testid="sm-error">{data.serviceMap.error?.message || ""}</span>
|
|
288
|
+
<span data-testid="sm-outdated">
|
|
289
|
+
{String(Boolean(data.serviceMap.error?.isOutdated))}
|
|
290
|
+
</span>
|
|
291
|
+
<span data-testid="sm-entities">
|
|
292
|
+
{names(data.serviceMap.data?.entities)}
|
|
293
|
+
</span>
|
|
294
|
+
<span data-testid="sm-range-start">
|
|
295
|
+
{data.serviceMap.data?.rangeStart.toISOString() || ""}
|
|
132
296
|
</span>
|
|
133
|
-
<span data-testid="
|
|
134
|
-
|
|
135
|
-
<span data-testid="updated">
|
|
136
|
-
{data.lastUpdatedAt?.toISOString() || ""}
|
|
297
|
+
<span data-testid="sm-loaded-at">
|
|
298
|
+
{data.serviceMap.loadedAt?.toISOString() || ""}
|
|
137
299
|
</span>
|
|
138
|
-
<span data-testid="
|
|
139
|
-
|
|
300
|
+
<span data-testid="infra-status">{data.infrastructure.status}</span>
|
|
301
|
+
<span data-testid="infra-error">
|
|
302
|
+
{data.infrastructure.error?.message || ""}
|
|
303
|
+
</span>
|
|
304
|
+
<span data-testid="infra-outdated">
|
|
305
|
+
{String(Boolean(data.infrastructure.error?.isOutdated))}
|
|
306
|
+
</span>
|
|
307
|
+
<span data-testid="infra-entities">
|
|
308
|
+
{names(data.infrastructure.data?.entities)}
|
|
309
|
+
</span>
|
|
310
|
+
<span data-testid="infra-range-start">
|
|
311
|
+
{data.infrastructure.data?.rangeStart.toISOString() || ""}
|
|
140
312
|
</span>
|
|
141
313
|
<button type="button" onClick={data.reload}>
|
|
142
314
|
Reload topology
|
|
143
315
|
</button>
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
onClick={() => {
|
|
319
|
+
data.retry("Service Map");
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
Retry service map
|
|
323
|
+
</button>
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
onClick={() => {
|
|
327
|
+
data.retry("Infrastructure");
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
Retry infrastructure
|
|
331
|
+
</button>
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
onClick={() => {
|
|
335
|
+
data.retry("Network");
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
Retry network
|
|
339
|
+
</button>
|
|
144
340
|
</div>
|
|
145
341
|
);
|
|
146
342
|
}
|
|
147
343
|
|
|
344
|
+
function text(testId: string): string {
|
|
345
|
+
return screen.getByTestId(testId).textContent || "";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function flush(): Promise<void> {
|
|
349
|
+
await act(async () => {
|
|
350
|
+
await Promise.resolve();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
148
354
|
beforeEach(() => {
|
|
149
|
-
|
|
355
|
+
requests = [];
|
|
356
|
+
autoRespond = true;
|
|
357
|
+
postMock.mockReset();
|
|
150
358
|
getProjectIdMock.mockReset();
|
|
151
359
|
getProjectIdMock.mockReturnValue(PROJECT_ID);
|
|
152
|
-
|
|
360
|
+
installFakeServer();
|
|
153
361
|
});
|
|
154
362
|
|
|
155
363
|
afterEach(() => {
|
|
156
364
|
cleanup();
|
|
365
|
+
jest.useRealTimers();
|
|
157
366
|
jest.restoreAllMocks();
|
|
158
367
|
});
|
|
159
368
|
|
|
160
|
-
describe("
|
|
161
|
-
test("
|
|
162
|
-
render(<Probe range={FIRST_RANGE} />);
|
|
163
|
-
await waitFor(() => {
|
|
164
|
-
expect(
|
|
165
|
-
});
|
|
166
|
-
expect(
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expect(
|
|
172
|
-
expect(
|
|
173
|
-
expect(
|
|
174
|
-
|
|
175
|
-
expect(
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
|
|
369
|
+
describe("which tab loads, and when", () => {
|
|
370
|
+
test("the active tab loads with exactly one POST pinned to the range start", async () => {
|
|
371
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
372
|
+
await waitFor(() => {
|
|
373
|
+
expect(text("sm-status")).toBe("ready");
|
|
374
|
+
});
|
|
375
|
+
expect(requests).toHaveLength(1);
|
|
376
|
+
const request: RecordedRequest = requests[0]!;
|
|
377
|
+
expect(request.path).toBe(TopologyApiPath.ServiceMap);
|
|
378
|
+
/* The exact start, unfloored: flooring is the server's job. */
|
|
379
|
+
expect(request.rangeStart).toBe("2026-09-01T09:15:27.000Z");
|
|
380
|
+
expect(request.headers).toEqual({ tenantid: PROJECT_ID.toString() });
|
|
381
|
+
expect(text("pinned")).toBe("2026-09-01T09:15:27.000Z");
|
|
382
|
+
expect(text("sm-entities")).toBe("service map #1");
|
|
383
|
+
/* Views judge activity against the server's echo, not the request. */
|
|
384
|
+
expect(text("sm-range-start")).toBe("2026-09-01T09:15:00.000Z");
|
|
385
|
+
expect(text("sm-loaded-at")).toBe("2026-09-02T10:15:30.000Z");
|
|
386
|
+
expect(text("infra-status")).toBe("idle");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("the Network tab loads nothing at all", async () => {
|
|
390
|
+
render(<Probe range={FIRST_RANGE} view="Network" />);
|
|
391
|
+
await flush();
|
|
392
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
393
|
+
expect(text("sm-status")).toBe("idle");
|
|
394
|
+
expect(text("infra-status")).toBe("idle");
|
|
395
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry network" }));
|
|
396
|
+
await flush();
|
|
397
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
179
398
|
});
|
|
180
399
|
|
|
181
|
-
test("
|
|
182
|
-
render(<Probe range={FIRST_RANGE} />);
|
|
183
|
-
await waitFor(() => {
|
|
184
|
-
expect(
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
callCount: true,
|
|
200
|
-
lastSeenAt: true,
|
|
201
|
-
});
|
|
202
|
-
// The snapshot remembers the range start it was loaded for.
|
|
203
|
-
expect(screen.getByTestId("range-start")).toHaveTextContent(
|
|
204
|
-
FIRST_RANGE.startAndEndDate!.startValue.toISOString(),
|
|
400
|
+
test("a shared Infrastructure link loads only Infrastructure", async () => {
|
|
401
|
+
render(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
402
|
+
await waitFor(() => {
|
|
403
|
+
expect(text("infra-status")).toBe("ready");
|
|
404
|
+
});
|
|
405
|
+
expect(
|
|
406
|
+
requests.map((request: RecordedRequest) => {
|
|
407
|
+
return request.path;
|
|
408
|
+
}),
|
|
409
|
+
).toEqual([TopologyApiPath.Infrastructure]);
|
|
410
|
+
expect(text("infra-entities")).toBe("infrastructure #1");
|
|
411
|
+
expect(text("infra-range-start")).toBe("2026-09-01T09:15:00.000Z");
|
|
412
|
+
expect(text("sm-status")).toBe("idle");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("a tab loads on first activation only; switching back and forth never refetches", async () => {
|
|
416
|
+
const view: ReturnType<typeof render> = render(
|
|
417
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
205
418
|
);
|
|
419
|
+
await waitFor(() => {
|
|
420
|
+
expect(text("sm-status")).toBe("ready");
|
|
421
|
+
});
|
|
422
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
423
|
+
await waitFor(() => {
|
|
424
|
+
expect(text("infra-status")).toBe("ready");
|
|
425
|
+
});
|
|
426
|
+
for (const next of [
|
|
427
|
+
"Service Map",
|
|
428
|
+
"Network",
|
|
429
|
+
"Infrastructure",
|
|
430
|
+
"Service Map",
|
|
431
|
+
] as Array<TopologyView>) {
|
|
432
|
+
view.rerender(<Probe range={FIRST_RANGE} view={next} />);
|
|
433
|
+
await flush();
|
|
434
|
+
}
|
|
435
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
436
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(1);
|
|
437
|
+
expect(text("sm-entities")).toBe("service map #1");
|
|
438
|
+
expect(text("infra-entities")).toBe("infrastructure #1");
|
|
439
|
+
/* Both tabs describe the same moment. */
|
|
440
|
+
expect(requests[0]!.rangeStart).toBe(requests[1]!.rangeStart);
|
|
441
|
+
expect(text("generation")).toBe("1");
|
|
206
442
|
});
|
|
207
443
|
|
|
208
|
-
test("
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
444
|
+
test("switching away from a loading tab neither cancels nor repeats it", async () => {
|
|
445
|
+
autoRespond = false;
|
|
446
|
+
const view: ReturnType<typeof render> = render(
|
|
447
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
448
|
+
);
|
|
449
|
+
await flush();
|
|
450
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
451
|
+
await flush();
|
|
452
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Network" />);
|
|
453
|
+
await flush();
|
|
454
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
455
|
+
await flush();
|
|
456
|
+
expect(requests).toHaveLength(2);
|
|
457
|
+
expect(text("sm-status")).toBe("loading");
|
|
458
|
+
expect(text("infra-status")).toBe("loading");
|
|
459
|
+
expect(requests[0]!.signal?.aborted).toBe(false);
|
|
460
|
+
expect(requests[1]!.signal?.aborted).toBe(false);
|
|
213
461
|
await act(async () => {
|
|
214
|
-
|
|
462
|
+
requests[1]!.respond(
|
|
463
|
+
infrastructurePayload(requests[1]!.rangeStart, "late infrastructure"),
|
|
464
|
+
);
|
|
465
|
+
requests[0]!.respond(
|
|
466
|
+
serviceMapPayload(requests[0]!.rangeStart, "late service map"),
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
expect(text("sm-entities")).toBe("late service map");
|
|
470
|
+
expect(text("infra-entities")).toBe("late infrastructure");
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("generations: range, project and reload", () => {
|
|
475
|
+
test("a range change aborts in-flight work, drops both tabs and reloads only the active one", async () => {
|
|
476
|
+
const view: ReturnType<typeof render> = render(
|
|
477
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
478
|
+
);
|
|
479
|
+
await waitFor(() => {
|
|
480
|
+
expect(text("sm-status")).toBe("ready");
|
|
481
|
+
});
|
|
482
|
+
autoRespond = false;
|
|
483
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
484
|
+
await flush();
|
|
485
|
+
const obsoleteInfrastructure: RecordedRequest = lastRequestTo(
|
|
486
|
+
TopologyApiPath.Infrastructure,
|
|
487
|
+
);
|
|
488
|
+
expect(text("infra-status")).toBe("loading");
|
|
489
|
+
|
|
490
|
+
autoRespond = true;
|
|
491
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Infrastructure" />);
|
|
492
|
+
await waitFor(() => {
|
|
493
|
+
expect(text("infra-status")).toBe("ready");
|
|
215
494
|
});
|
|
216
|
-
expect(
|
|
217
|
-
expect(
|
|
495
|
+
expect(obsoleteInfrastructure.signal?.aborted).toBe(true);
|
|
496
|
+
expect(text("generation")).toBe("2");
|
|
497
|
+
expect(text("pinned")).toBe("2026-09-02T09:15:00.000Z");
|
|
498
|
+
expect(lastRequestTo(TopologyApiPath.Infrastructure).rangeStart).toBe(
|
|
499
|
+
"2026-09-02T09:15:00.000Z",
|
|
500
|
+
);
|
|
501
|
+
/* The Service Map's old-range data is gone and was NOT refetched. */
|
|
502
|
+
expect(text("sm-status")).toBe("idle");
|
|
503
|
+
expect(text("sm-entities")).toBe("");
|
|
504
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
505
|
+
|
|
506
|
+
/* The obsolete answer arriving now changes nothing. */
|
|
218
507
|
await act(async () => {
|
|
219
|
-
|
|
508
|
+
obsoleteInfrastructure.respond(
|
|
509
|
+
infrastructurePayload(
|
|
510
|
+
obsoleteInfrastructure.rangeStart,
|
|
511
|
+
"obsolete range",
|
|
512
|
+
),
|
|
513
|
+
);
|
|
220
514
|
});
|
|
221
|
-
expect(
|
|
222
|
-
|
|
515
|
+
expect(text("infra-entities")).toBe("infrastructure #2");
|
|
516
|
+
|
|
517
|
+
/* Opening the Service Map again loads it for the new range. */
|
|
518
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Service Map" />);
|
|
519
|
+
await waitFor(() => {
|
|
520
|
+
expect(text("sm-status")).toBe("ready");
|
|
521
|
+
});
|
|
522
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(2);
|
|
523
|
+
expect(lastRequestTo(TopologyApiPath.ServiceMap).rangeStart).toBe(
|
|
524
|
+
"2026-09-02T09:15:00.000Z",
|
|
525
|
+
);
|
|
223
526
|
});
|
|
224
527
|
|
|
225
528
|
test("an older range finishing last cannot replace the selected range", async () => {
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
529
|
+
autoRespond = false;
|
|
530
|
+
const view: ReturnType<typeof render> = render(
|
|
531
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
532
|
+
);
|
|
533
|
+
await flush();
|
|
534
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Service Map" />);
|
|
535
|
+
await flush();
|
|
536
|
+
expect(requests).toHaveLength(2);
|
|
537
|
+
await act(async () => {
|
|
538
|
+
requests[1]!.respond(
|
|
539
|
+
serviceMapPayload(requests[1]!.rangeStart, "latest range"),
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
expect(text("sm-entities")).toBe("latest range");
|
|
543
|
+
await act(async () => {
|
|
544
|
+
requests[0]!.respond(
|
|
545
|
+
serviceMapPayload(requests[0]!.rangeStart, "obsolete range", {
|
|
546
|
+
entityTruncation: { shown: 1, total: 100 },
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
expect(text("sm-entities")).toBe("latest range");
|
|
551
|
+
expect(text("sm-range-start")).toBe("2026-09-02T09:15:00.000Z");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("an obsolete failure cannot clear the loading state or show an error", async () => {
|
|
555
|
+
autoRespond = false;
|
|
230
556
|
const view: ReturnType<typeof render> = render(
|
|
231
|
-
<Probe range={FIRST_RANGE} />,
|
|
557
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
232
558
|
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
559
|
+
await flush();
|
|
560
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Service Map" />);
|
|
561
|
+
await flush();
|
|
562
|
+
await act(async () => {
|
|
563
|
+
requests[0]!.reject(new Error("An old request failed"));
|
|
564
|
+
});
|
|
565
|
+
expect(text("sm-status")).toBe("loading");
|
|
566
|
+
expect(text("sm-error")).toBe("");
|
|
567
|
+
await act(async () => {
|
|
568
|
+
requests[1]!.respond(
|
|
569
|
+
serviceMapPayload(requests[1]!.rangeStart, "current inventory"),
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
expect(text("sm-entities")).toBe("current inventory");
|
|
573
|
+
expect(text("sm-error")).toBe("");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("reload() starts a new generation and loads only the active tab", async () => {
|
|
577
|
+
const view: ReturnType<typeof render> = render(
|
|
578
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
579
|
+
);
|
|
580
|
+
await waitFor(() => {
|
|
581
|
+
expect(text("sm-status")).toBe("ready");
|
|
582
|
+
});
|
|
583
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
236
584
|
await waitFor(() => {
|
|
237
|
-
expect(
|
|
585
|
+
expect(text("infra-status")).toBe("ready");
|
|
586
|
+
});
|
|
587
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
588
|
+
await waitFor(() => {
|
|
589
|
+
expect(text("infra-entities")).toBe("infrastructure #2");
|
|
590
|
+
});
|
|
591
|
+
expect(text("generation")).toBe("2");
|
|
592
|
+
expect(text("sm-status")).toBe("idle");
|
|
593
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
594
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(2);
|
|
595
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
596
|
+
await waitFor(() => {
|
|
597
|
+
expect(text("sm-entities")).toBe("service map #2");
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("reload() while a request is pending aborts it and ignores its answer", async () => {
|
|
602
|
+
autoRespond = false;
|
|
603
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
604
|
+
await flush();
|
|
605
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
606
|
+
await flush();
|
|
607
|
+
expect(requests).toHaveLength(2);
|
|
608
|
+
expect(requests[0]!.signal?.aborted).toBe(true);
|
|
609
|
+
expect(requests[1]!.signal?.aborted).toBe(false);
|
|
610
|
+
await act(async () => {
|
|
611
|
+
requests[1]!.respond(
|
|
612
|
+
serviceMapPayload(requests[1]!.rangeStart, "reloaded"),
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
await act(async () => {
|
|
616
|
+
requests[0]!.reject(new Error("Request Canceled."));
|
|
617
|
+
});
|
|
618
|
+
expect(text("sm-entities")).toBe("reloaded");
|
|
619
|
+
expect(text("sm-error")).toBe("");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("reload() on the Network tab re-pins the range but requests nothing", async () => {
|
|
623
|
+
render(<Probe range={FIRST_RANGE} view="Network" />);
|
|
624
|
+
await flush();
|
|
625
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
626
|
+
await flush();
|
|
627
|
+
expect(text("generation")).toBe("2");
|
|
628
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("changing projects refetches and ignores the previous project's response", async () => {
|
|
632
|
+
autoRespond = false;
|
|
633
|
+
const view: ReturnType<typeof render> = render(
|
|
634
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
635
|
+
);
|
|
636
|
+
await flush();
|
|
637
|
+
const nextProject: ObjectID = new ObjectID(
|
|
638
|
+
"22222222-2222-4222-8222-222222222222",
|
|
639
|
+
);
|
|
640
|
+
getProjectIdMock.mockReturnValue(nextProject);
|
|
641
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
642
|
+
await flush();
|
|
643
|
+
expect(requests).toHaveLength(2);
|
|
644
|
+
expect(requests[0]!.signal?.aborted).toBe(true);
|
|
645
|
+
expect(requests[1]!.headers).toEqual({ tenantid: nextProject.toString() });
|
|
646
|
+
await act(async () => {
|
|
647
|
+
requests[1]!.respond(
|
|
648
|
+
serviceMapPayload(requests[1]!.rangeStart, "project two"),
|
|
649
|
+
);
|
|
238
650
|
});
|
|
239
651
|
await act(async () => {
|
|
240
|
-
|
|
241
|
-
|
|
652
|
+
requests[0]!.respond(
|
|
653
|
+
serviceMapPayload(requests[0]!.rangeStart, "project one"),
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
expect(text("sm-entities")).toBe("project two");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("never sends an unscoped request without a project", async () => {
|
|
660
|
+
getProjectIdMock.mockReturnValue(null);
|
|
661
|
+
const view: ReturnType<typeof render> = render(
|
|
662
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
663
|
+
);
|
|
664
|
+
await waitFor(() => {
|
|
665
|
+
expect(text("sm-status")).toBe("error");
|
|
242
666
|
});
|
|
243
|
-
expect(
|
|
244
|
-
|
|
667
|
+
expect(text("sm-error")).toContain("Select a project");
|
|
668
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
669
|
+
await waitFor(() => {
|
|
670
|
+
expect(text("infra-status")).toBe("error");
|
|
671
|
+
});
|
|
672
|
+
fireEvent.click(
|
|
673
|
+
screen.getByRole("button", { name: "Retry infrastructure" }),
|
|
674
|
+
);
|
|
675
|
+
await flush();
|
|
676
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
245
677
|
});
|
|
246
678
|
|
|
247
|
-
test("
|
|
248
|
-
|
|
249
|
-
const latestEntities: DeferredResult = deferred();
|
|
250
|
-
getListMock.mockReturnValueOnce(oldEntities.promise);
|
|
251
|
-
getListMock.mockResolvedValueOnce(result());
|
|
679
|
+
test("unmounting aborts in-flight requests", async () => {
|
|
680
|
+
autoRespond = false;
|
|
252
681
|
const view: ReturnType<typeof render> = render(
|
|
253
|
-
<Probe range={FIRST_RANGE} />,
|
|
682
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
254
683
|
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
684
|
+
await flush();
|
|
685
|
+
view.unmount();
|
|
686
|
+
expect(requests[0]!.signal?.aborted).toBe(true);
|
|
258
687
|
await act(async () => {
|
|
259
|
-
|
|
688
|
+
requests[0]!.respond(
|
|
689
|
+
serviceMapPayload(requests[0]!.rangeStart, "after unmount"),
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
describe("per-tab errors and retry", () => {
|
|
696
|
+
test("a failed tab reports its own error; retry reloads only that tab", async () => {
|
|
697
|
+
const view: ReturnType<typeof render> = render(
|
|
698
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
699
|
+
);
|
|
700
|
+
await waitFor(() => {
|
|
701
|
+
expect(text("sm-status")).toBe("ready");
|
|
260
702
|
});
|
|
261
|
-
|
|
262
|
-
|
|
703
|
+
autoRespond = false;
|
|
704
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
705
|
+
await flush();
|
|
263
706
|
await act(async () => {
|
|
264
|
-
|
|
707
|
+
lastRequestTo(TopologyApiPath.Infrastructure).fail(500, "Please retry");
|
|
265
708
|
});
|
|
266
|
-
expect(
|
|
267
|
-
|
|
709
|
+
expect(text("infra-status")).toBe("error");
|
|
710
|
+
expect(text("infra-error")).toBe("Please retry");
|
|
711
|
+
expect(text("infra-outdated")).toBe("false");
|
|
712
|
+
/* The other tab is untouched. */
|
|
713
|
+
expect(text("sm-status")).toBe("ready");
|
|
714
|
+
expect(text("sm-entities")).toBe("service map #1");
|
|
715
|
+
|
|
716
|
+
autoRespond = true;
|
|
717
|
+
fireEvent.click(
|
|
718
|
+
screen.getByRole("button", { name: "Retry infrastructure" }),
|
|
268
719
|
);
|
|
269
|
-
|
|
720
|
+
await waitFor(() => {
|
|
721
|
+
expect(text("infra-status")).toBe("ready");
|
|
722
|
+
});
|
|
723
|
+
expect(text("infra-error")).toBe("");
|
|
724
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(2);
|
|
725
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
726
|
+
/* Same generation, same pinned range as the Service Map. */
|
|
727
|
+
expect(text("generation")).toBe("1");
|
|
728
|
+
expect(lastRequestTo(TopologyApiPath.Infrastructure).rangeStart).toBe(
|
|
729
|
+
requests[0]!.rangeStart,
|
|
730
|
+
);
|
|
731
|
+
expect(text("sm-entities")).toBe("service map #1");
|
|
270
732
|
});
|
|
271
733
|
|
|
272
|
-
test.each([
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
734
|
+
test.each([
|
|
735
|
+
["a transport error", "reject"],
|
|
736
|
+
["an HTTP error", "fail"],
|
|
737
|
+
])(
|
|
738
|
+
"%s never presents partial data, and retry recovers",
|
|
739
|
+
async (_label: string, mode: string) => {
|
|
740
|
+
autoRespond = false;
|
|
741
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
742
|
+
await flush();
|
|
743
|
+
await act(async () => {
|
|
744
|
+
if (mode === "reject") {
|
|
745
|
+
requests[0]!.reject(new Error("Network is down"));
|
|
746
|
+
} else {
|
|
747
|
+
requests[0]!.fail(503, "Network is down");
|
|
748
|
+
}
|
|
285
749
|
});
|
|
286
|
-
expect(
|
|
287
|
-
expect(
|
|
288
|
-
expect(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
fireEvent.click(
|
|
750
|
+
expect(text("sm-status")).toBe("error");
|
|
751
|
+
expect(text("sm-error")).toBe("Network is down");
|
|
752
|
+
expect(text("sm-entities")).toBe("");
|
|
753
|
+
expect(text("sm-loaded-at")).toBe("");
|
|
754
|
+
autoRespond = true;
|
|
755
|
+
fireEvent.click(
|
|
756
|
+
screen.getByRole("button", { name: "Retry service map" }),
|
|
757
|
+
);
|
|
292
758
|
await waitFor(() => {
|
|
293
|
-
expect(
|
|
294
|
-
"Recovered inventory",
|
|
295
|
-
);
|
|
759
|
+
expect(text("sm-status")).toBe("ready");
|
|
296
760
|
});
|
|
297
|
-
expect(
|
|
298
|
-
expect(getListMock).toHaveBeenCalledTimes(4);
|
|
761
|
+
expect(text("sm-entities")).toBe("service map #2");
|
|
299
762
|
},
|
|
300
763
|
);
|
|
301
764
|
|
|
302
|
-
test
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
765
|
+
test("a retry supersedes a still-pending attempt of the same tab", async () => {
|
|
766
|
+
autoRespond = false;
|
|
767
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
768
|
+
await flush();
|
|
769
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry service map" }));
|
|
770
|
+
await flush();
|
|
771
|
+
expect(requests).toHaveLength(2);
|
|
772
|
+
expect(requests[0]!.signal?.aborted).toBe(true);
|
|
773
|
+
await act(async () => {
|
|
774
|
+
requests[1]!.respond(
|
|
775
|
+
serviceMapPayload(requests[1]!.rangeStart, "retry result"),
|
|
310
776
|
);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
777
|
+
});
|
|
778
|
+
await act(async () => {
|
|
779
|
+
requests[0]!.reject(new Error("Original attempt failed"));
|
|
780
|
+
});
|
|
781
|
+
expect(text("sm-entities")).toBe("retry result");
|
|
782
|
+
expect(text("sm-error")).toBe("");
|
|
783
|
+
expect(text("generation")).toBe("1");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test.each([
|
|
787
|
+
["a newer payload format", "version"],
|
|
788
|
+
["a server without the endpoint (404)", "404"],
|
|
789
|
+
])(
|
|
790
|
+
"%s is reported as an outdated page, not a transient failure",
|
|
791
|
+
async (_label: string, mode: string) => {
|
|
792
|
+
autoRespond = false;
|
|
793
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
794
|
+
await flush();
|
|
795
|
+
await act(async () => {
|
|
796
|
+
if (mode === "version") {
|
|
797
|
+
requests[0]!.respond(
|
|
798
|
+
serviceMapPayload(requests[0]!.rangeStart, "future", {
|
|
799
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION + 1,
|
|
800
|
+
}),
|
|
801
|
+
);
|
|
802
|
+
} else {
|
|
803
|
+
requests[0]!.fail(404, "Not found");
|
|
804
|
+
}
|
|
318
805
|
});
|
|
319
|
-
expect(
|
|
806
|
+
expect(text("sm-status")).toBe("error");
|
|
807
|
+
expect(text("sm-outdated")).toBe("true");
|
|
808
|
+
expect(text("sm-error")).toBe("Topology was updated. Reload the page.");
|
|
809
|
+
expect(text("sm-entities")).toBe("");
|
|
320
810
|
},
|
|
321
811
|
);
|
|
812
|
+
});
|
|
322
813
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
814
|
+
describe("an explicit refresh bypasses the server's response cache", () => {
|
|
815
|
+
/*
|
|
816
|
+
* The server keeps each map for a minute, keyed by the floored range
|
|
817
|
+
* start. A custom range (or a click within the same minute) would get the
|
|
818
|
+
* same payload back, so reload() — and only reload() — asks for a rebuild.
|
|
819
|
+
*/
|
|
820
|
+
test("ordinary loads, tab switches, range and project changes are never fresh", async () => {
|
|
327
821
|
const view: ReturnType<typeof render> = render(
|
|
328
|
-
<Probe range={FIRST_RANGE} />,
|
|
822
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
329
823
|
);
|
|
330
|
-
|
|
331
|
-
"
|
|
824
|
+
await waitFor(() => {
|
|
825
|
+
expect(text("sm-status")).toBe("ready");
|
|
826
|
+
});
|
|
827
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
828
|
+
await waitFor(() => {
|
|
829
|
+
expect(text("infra-status")).toBe("ready");
|
|
830
|
+
});
|
|
831
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Infrastructure" />);
|
|
832
|
+
await waitFor(() => {
|
|
833
|
+
expect(text("infra-entities")).toBe("infrastructure #2");
|
|
834
|
+
});
|
|
835
|
+
getProjectIdMock.mockReturnValue(
|
|
836
|
+
new ObjectID("22222222-2222-4222-8222-222222222222"),
|
|
332
837
|
);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
838
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Infrastructure" />);
|
|
839
|
+
await waitFor(() => {
|
|
840
|
+
expect(text("infra-entities")).toBe("infrastructure #3");
|
|
841
|
+
});
|
|
842
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Service Map" />);
|
|
843
|
+
await waitFor(() => {
|
|
844
|
+
expect(text("sm-status")).toBe("ready");
|
|
845
|
+
});
|
|
846
|
+
expect(requests).toHaveLength(5);
|
|
847
|
+
expect(freshness(requests)).toEqual([
|
|
848
|
+
"cached",
|
|
849
|
+
"cached",
|
|
850
|
+
"cached",
|
|
851
|
+
"cached",
|
|
852
|
+
"cached",
|
|
853
|
+
]);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test("reload() sends the active tab fresh, and the other tab's first load in that generation too", async () => {
|
|
857
|
+
const view: ReturnType<typeof render> = render(
|
|
858
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
859
|
+
);
|
|
860
|
+
await waitFor(() => {
|
|
861
|
+
expect(text("sm-status")).toBe("ready");
|
|
862
|
+
});
|
|
863
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
864
|
+
await waitFor(() => {
|
|
865
|
+
expect(text("sm-entities")).toBe("service map #2");
|
|
866
|
+
});
|
|
867
|
+
expect(text("generation")).toBe("2");
|
|
868
|
+
expect(freshness(requestsTo(TopologyApiPath.ServiceMap))).toEqual([
|
|
869
|
+
"cached",
|
|
870
|
+
"fresh",
|
|
871
|
+
]);
|
|
872
|
+
/* The refresh re-pinned the same custom start: only the flag differs. */
|
|
873
|
+
expect(requests[1]!.rangeStart).toBe(requests[0]!.rangeStart);
|
|
874
|
+
|
|
875
|
+
/*
|
|
876
|
+
* Infrastructure was never loaded in this generation: opened after the
|
|
877
|
+
* refresh, it must not be answered from the pre-refresh cache either.
|
|
878
|
+
*/
|
|
879
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
337
880
|
await waitFor(() => {
|
|
338
|
-
expect(
|
|
339
|
-
|
|
881
|
+
expect(text("infra-status")).toBe("ready");
|
|
882
|
+
});
|
|
883
|
+
expect(freshness(requestsTo(TopologyApiPath.Infrastructure))).toEqual([
|
|
884
|
+
"fresh",
|
|
885
|
+
]);
|
|
886
|
+
|
|
887
|
+
/* Switching back and forth loads nothing more. */
|
|
888
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
889
|
+
await flush();
|
|
890
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
891
|
+
await flush();
|
|
892
|
+
expect(requests).toHaveLength(3);
|
|
893
|
+
|
|
894
|
+
/* The next ordinary generation is back to the cache. */
|
|
895
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Infrastructure" />);
|
|
896
|
+
await waitFor(() => {
|
|
897
|
+
expect(text("infra-entities")).toBe("infrastructure #2");
|
|
898
|
+
});
|
|
899
|
+
view.rerender(<Probe range={SECOND_RANGE} view="Service Map" />);
|
|
900
|
+
await waitFor(() => {
|
|
901
|
+
expect(text("sm-entities")).toBe("service map #3");
|
|
902
|
+
});
|
|
903
|
+
expect(freshness(requests.slice(3))).toEqual(["cached", "cached"]);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
test("a refresh stays fresh until it succeeds: 'Try again' after a failed refresh is fresh too", async () => {
|
|
907
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
908
|
+
await waitFor(() => {
|
|
909
|
+
expect(text("sm-status")).toBe("ready");
|
|
910
|
+
});
|
|
911
|
+
autoRespond = false;
|
|
912
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
913
|
+
await flush();
|
|
914
|
+
await act(async () => {
|
|
915
|
+
lastRequestTo(TopologyApiPath.ServiceMap).fail(500, "Please retry");
|
|
916
|
+
});
|
|
917
|
+
expect(text("sm-status")).toBe("error");
|
|
918
|
+
|
|
919
|
+
autoRespond = true;
|
|
920
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry service map" }));
|
|
921
|
+
await waitFor(() => {
|
|
922
|
+
expect(text("sm-status")).toBe("ready");
|
|
923
|
+
});
|
|
924
|
+
expect(text("generation")).toBe("2");
|
|
925
|
+
/* A cached answer here would silently undo the refresh the user asked for. */
|
|
926
|
+
expect(freshness(requests)).toEqual(["cached", "fresh", "fresh"]);
|
|
927
|
+
|
|
928
|
+
/* Once it succeeded, further loads in the generation are ordinary. */
|
|
929
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry service map" }));
|
|
930
|
+
await waitFor(() => {
|
|
931
|
+
expect(requests).toHaveLength(4);
|
|
932
|
+
});
|
|
933
|
+
expect(freshness(requests)[3]).toBe("cached");
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test("a second reload is fresh again, and a reload that supersedes a pending one aborts it", async () => {
|
|
937
|
+
autoRespond = false;
|
|
938
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
939
|
+
await flush();
|
|
940
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
941
|
+
await flush();
|
|
942
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
943
|
+
await flush();
|
|
944
|
+
expect(freshness(requests)).toEqual(["cached", "fresh", "fresh"]);
|
|
945
|
+
expect(requests[1]!.signal?.aborted).toBe(true);
|
|
946
|
+
expect(requests[2]!.signal?.aborted).toBe(false);
|
|
947
|
+
await act(async () => {
|
|
948
|
+
requests[2]!.respond(
|
|
949
|
+
serviceMapPayload(requests[2]!.rangeStart, "rebuilt"),
|
|
340
950
|
);
|
|
341
951
|
});
|
|
342
|
-
expect(
|
|
952
|
+
expect(text("sm-entities")).toBe("rebuilt");
|
|
953
|
+
expect(text("generation")).toBe("3");
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test("a refresh on the Network tab makes the next opened tab fresh", async () => {
|
|
957
|
+
const view: ReturnType<typeof render> = render(
|
|
958
|
+
<Probe range={FIRST_RANGE} view="Network" />,
|
|
959
|
+
);
|
|
960
|
+
await flush();
|
|
961
|
+
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
962
|
+
await flush();
|
|
963
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
964
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
965
|
+
await waitFor(() => {
|
|
966
|
+
expect(text("infra-status")).toBe("ready");
|
|
967
|
+
});
|
|
968
|
+
expect(freshness(requests)).toEqual(["fresh"]);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
describe("a busy server", () => {
|
|
973
|
+
/*
|
|
974
|
+
* The server caps concurrent topology work and answers 429 when it is
|
|
975
|
+
* full. That passes: the tab says so in plain words and keeps "Try
|
|
976
|
+
* again", rather than the "Reload the page" path of an outdated bundle.
|
|
977
|
+
*/
|
|
978
|
+
test("a 429 is a friendly, retryable error, and retry recovers", async () => {
|
|
979
|
+
autoRespond = false;
|
|
980
|
+
render(<Probe range={FIRST_RANGE} view="Service Map" />);
|
|
981
|
+
await flush();
|
|
343
982
|
await act(async () => {
|
|
344
|
-
|
|
983
|
+
requests[0]!.fail(429, "Too many topology requests are running.");
|
|
345
984
|
});
|
|
346
|
-
expect(
|
|
347
|
-
|
|
985
|
+
expect(text("sm-status")).toBe("error");
|
|
986
|
+
expect(text("sm-error")).toBe(
|
|
987
|
+
"The topology service is busy. Try again in a moment.",
|
|
348
988
|
);
|
|
989
|
+
expect(text("sm-outdated")).toBe("false");
|
|
990
|
+
expect(text("sm-entities")).toBe("");
|
|
991
|
+
|
|
992
|
+
autoRespond = true;
|
|
993
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry service map" }));
|
|
994
|
+
await waitFor(() => {
|
|
995
|
+
expect(text("sm-status")).toBe("ready");
|
|
996
|
+
});
|
|
997
|
+
expect(text("sm-error")).toBe("");
|
|
998
|
+
expect(requests).toHaveLength(2);
|
|
349
999
|
});
|
|
1000
|
+
});
|
|
350
1001
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1002
|
+
describe("a relative range drifts while a tab waits to be opened", () => {
|
|
1003
|
+
/*
|
|
1004
|
+
* Only Date is faked: promises, timers and Testing Library's polling keep
|
|
1005
|
+
* running for real.
|
|
1006
|
+
*/
|
|
1007
|
+
function fakeClock(now: string): void {
|
|
1008
|
+
jest.useFakeTimers({
|
|
1009
|
+
now: new Date(now),
|
|
1010
|
+
doNotFake: [
|
|
1011
|
+
"hrtime",
|
|
1012
|
+
"nextTick",
|
|
1013
|
+
"performance",
|
|
1014
|
+
"queueMicrotask",
|
|
1015
|
+
"requestAnimationFrame",
|
|
1016
|
+
"cancelAnimationFrame",
|
|
1017
|
+
"requestIdleCallback",
|
|
1018
|
+
"cancelIdleCallback",
|
|
1019
|
+
"setImmediate",
|
|
1020
|
+
"clearImmediate",
|
|
1021
|
+
"setInterval",
|
|
1022
|
+
"clearInterval",
|
|
1023
|
+
"setTimeout",
|
|
1024
|
+
"clearTimeout",
|
|
1025
|
+
],
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
test("opening a tab more than 5 minutes after a relative pin re-pins both tabs", async () => {
|
|
1030
|
+
fakeClock("2026-09-02T12:00:00.000Z");
|
|
1031
|
+
const view: ReturnType<typeof render> = render(
|
|
1032
|
+
<Probe range={LAST_HOUR} view="Service Map" />,
|
|
1033
|
+
);
|
|
354
1034
|
await waitFor(() => {
|
|
355
|
-
expect(
|
|
1035
|
+
expect(text("sm-status")).toBe("ready");
|
|
356
1036
|
});
|
|
357
|
-
expect(
|
|
358
|
-
|
|
1037
|
+
expect(requests[0]!.rangeStart).toBe("2026-09-02T11:00:00.000Z");
|
|
1038
|
+
|
|
1039
|
+
jest.setSystemTime(
|
|
1040
|
+
new Date(
|
|
1041
|
+
new Date("2026-09-02T12:00:00.000Z").getTime() +
|
|
1042
|
+
TOPOLOGY_RANGE_REPIN_AFTER_MS +
|
|
1043
|
+
60 * 1000,
|
|
1044
|
+
),
|
|
1045
|
+
);
|
|
1046
|
+
view.rerender(<Probe range={LAST_HOUR} view="Infrastructure" />);
|
|
1047
|
+
await waitFor(() => {
|
|
1048
|
+
expect(text("infra-status")).toBe("ready");
|
|
1049
|
+
});
|
|
1050
|
+
expect(text("generation")).toBe("2");
|
|
1051
|
+
expect(lastRequestTo(TopologyApiPath.Infrastructure).rangeStart).toBe(
|
|
1052
|
+
"2026-09-02T11:06:00.000Z",
|
|
1053
|
+
);
|
|
1054
|
+
/* The Service Map was drawn for the old window: it is dropped too. */
|
|
1055
|
+
expect(text("sm-status")).toBe("idle");
|
|
1056
|
+
view.rerender(<Probe range={LAST_HOUR} view="Service Map" />);
|
|
1057
|
+
await waitFor(() => {
|
|
1058
|
+
expect(text("sm-status")).toBe("ready");
|
|
1059
|
+
});
|
|
1060
|
+
expect(lastRequestTo(TopologyApiPath.ServiceMap).rangeStart).toBe(
|
|
1061
|
+
"2026-09-02T11:06:00.000Z",
|
|
1062
|
+
);
|
|
1063
|
+
expect(text("generation")).toBe("2");
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
test("within 5 minutes the lazy tab reuses the pinned range", async () => {
|
|
1067
|
+
fakeClock("2026-09-02T12:00:00.000Z");
|
|
1068
|
+
const view: ReturnType<typeof render> = render(
|
|
1069
|
+
<Probe range={LAST_HOUR} view="Service Map" />,
|
|
1070
|
+
);
|
|
1071
|
+
await waitFor(() => {
|
|
1072
|
+
expect(text("sm-status")).toBe("ready");
|
|
1073
|
+
});
|
|
1074
|
+
jest.setSystemTime(new Date("2026-09-02T12:04:59.000Z"));
|
|
1075
|
+
view.rerender(<Probe range={LAST_HOUR} view="Infrastructure" />);
|
|
1076
|
+
await waitFor(() => {
|
|
1077
|
+
expect(text("infra-status")).toBe("ready");
|
|
1078
|
+
});
|
|
1079
|
+
expect(text("generation")).toBe("1");
|
|
1080
|
+
expect(lastRequestTo(TopologyApiPath.Infrastructure).rangeStart).toBe(
|
|
1081
|
+
"2026-09-02T11:00:00.000Z",
|
|
1082
|
+
);
|
|
1083
|
+
expect(text("sm-status")).toBe("ready");
|
|
359
1084
|
});
|
|
360
1085
|
|
|
361
|
-
test("a
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
1086
|
+
test("a custom range never drifts", async () => {
|
|
1087
|
+
fakeClock("2026-09-02T12:00:00.000Z");
|
|
1088
|
+
const view: ReturnType<typeof render> = render(
|
|
1089
|
+
<Probe range={FIRST_RANGE} view="Service Map" />,
|
|
1090
|
+
);
|
|
1091
|
+
await waitFor(() => {
|
|
1092
|
+
expect(text("sm-status")).toBe("ready");
|
|
1093
|
+
});
|
|
1094
|
+
jest.setSystemTime(new Date("2026-09-02T15:00:00.000Z"));
|
|
1095
|
+
view.rerender(<Probe range={FIRST_RANGE} view="Infrastructure" />);
|
|
1096
|
+
await waitFor(() => {
|
|
1097
|
+
expect(text("infra-status")).toBe("ready");
|
|
1098
|
+
});
|
|
1099
|
+
expect(text("generation")).toBe("1");
|
|
1100
|
+
expect(text("sm-status")).toBe("ready");
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test("a drifted lazy tab after a refresh re-pins as an ordinary load", async () => {
|
|
1104
|
+
fakeClock("2026-09-02T12:00:00.000Z");
|
|
1105
|
+
const view: ReturnType<typeof render> = render(
|
|
1106
|
+
<Probe range={LAST_HOUR} view="Service Map" />,
|
|
1107
|
+
);
|
|
1108
|
+
await waitFor(() => {
|
|
1109
|
+
expect(text("sm-status")).toBe("ready");
|
|
1110
|
+
});
|
|
368
1111
|
fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
|
|
369
1112
|
await waitFor(() => {
|
|
370
|
-
expect(
|
|
1113
|
+
expect(text("sm-entities")).toBe("service map #2");
|
|
1114
|
+
});
|
|
1115
|
+
jest.setSystemTime(new Date("2026-09-02T12:10:00.000Z"));
|
|
1116
|
+
view.rerender(<Probe range={LAST_HOUR} view="Infrastructure" />);
|
|
1117
|
+
await waitFor(() => {
|
|
1118
|
+
expect(text("infra-status")).toBe("ready");
|
|
371
1119
|
});
|
|
1120
|
+
expect(text("generation")).toBe("3");
|
|
1121
|
+
expect(freshness(requests)).toEqual(["cached", "fresh", "cached"]);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("a drifted retry re-pins instead of loading the old window", async () => {
|
|
1125
|
+
fakeClock("2026-09-02T12:00:00.000Z");
|
|
1126
|
+
autoRespond = false;
|
|
1127
|
+
render(<Probe range={LAST_HOUR} view="Service Map" />);
|
|
1128
|
+
await flush();
|
|
372
1129
|
await act(async () => {
|
|
373
|
-
|
|
1130
|
+
requests[0]!.fail(500, "Please retry");
|
|
1131
|
+
});
|
|
1132
|
+
expect(text("sm-status")).toBe("error");
|
|
1133
|
+
autoRespond = true;
|
|
1134
|
+
jest.setSystemTime(new Date("2026-09-02T12:10:00.000Z"));
|
|
1135
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry service map" }));
|
|
1136
|
+
await waitFor(() => {
|
|
1137
|
+
expect(text("sm-status")).toBe("ready");
|
|
374
1138
|
});
|
|
375
|
-
expect(
|
|
376
|
-
expect(
|
|
1139
|
+
expect(text("generation")).toBe("2");
|
|
1140
|
+
expect(requests[1]!.rangeStart).toBe("2026-09-02T11:10:00.000Z");
|
|
377
1141
|
});
|
|
378
1142
|
});
|