@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.
Files changed (556) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  6. package/Models/DatabaseModels/StatusPage.ts +55 -0
  7. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  8. package/Server/API/IncidentAPI.ts +162 -0
  9. package/Server/API/StatusPageAPI.ts +487 -171
  10. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
  20. package/Server/Infrastructure/Semaphore.ts +10 -0
  21. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  22. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  23. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  24. package/Server/Services/AlertService.ts +32 -36
  25. package/Server/Services/DatabaseService.ts +93 -10
  26. package/Server/Services/IncidentAlertService.ts +609 -2
  27. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  28. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  29. package/Server/Services/IncidentEpisodeService.ts +15 -0
  30. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  31. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  32. package/Server/Services/IncidentService.ts +1663 -103
  33. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  34. package/Server/Services/IncidentTemplateService.ts +109 -0
  35. package/Server/Services/InventoryItemService.ts +38 -0
  36. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  37. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  38. package/Server/Services/ScheduledMaintenanceService.ts +118 -64
  39. package/Server/Services/SmsService.ts +10 -0
  40. package/Server/Services/StatusPageService.ts +65 -8
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  42. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  43. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  44. package/Server/Services/UserNotificationRuleService.ts +55 -53
  45. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  46. package/Server/Types/Markdown.ts +153 -6
  47. package/Server/Types/Workflow/Components/Email.ts +61 -2
  48. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  49. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  50. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  51. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  52. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  53. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  54. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  55. package/Server/Utils/Response.ts +31 -0
  56. package/Server/Utils/SSRFProtection.ts +76 -16
  57. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  58. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  59. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  60. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  61. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  62. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  63. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  64. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  65. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  66. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  67. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  68. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  69. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  70. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  71. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  72. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  73. package/Server/Utils/StatusPageResource.ts +51 -20
  74. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  75. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  76. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  77. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  78. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  79. package/Server/Utils/VM/VMAPI.ts +1 -0
  80. package/Server/Utils/VM/VMRunner.ts +10 -0
  81. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  82. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  83. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  84. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  85. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  86. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  87. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  88. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  89. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  90. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  91. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  92. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  93. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  94. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  95. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  96. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  97. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  98. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  99. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  100. package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
  101. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  102. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  103. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +534 -6
  104. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  105. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  106. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  107. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  108. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  109. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  110. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  111. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  112. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  113. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  114. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  115. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  116. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  117. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  118. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  119. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  120. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  121. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  122. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  123. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  124. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  125. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  126. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  127. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  128. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  129. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  130. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  131. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  132. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  133. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  134. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  135. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  136. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  137. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  138. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  139. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  140. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  141. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  142. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  143. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  144. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  145. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  146. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  147. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  148. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  149. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  150. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  151. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  152. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  153. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  154. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  155. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  156. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  157. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  158. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  159. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  160. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  161. package/Tests/Server/Services/DomainService.test.ts +525 -0
  162. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  163. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  164. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  165. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  166. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  167. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  168. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  169. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  170. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  171. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  172. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  173. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  174. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  175. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  176. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  177. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  178. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  179. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  180. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  181. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  182. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  183. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  184. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  185. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  186. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  187. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  188. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  189. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  190. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  191. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  192. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  193. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  194. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  195. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  196. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  197. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  198. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  199. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  200. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  201. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  202. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  203. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  204. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  205. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  206. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  207. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  208. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  209. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  210. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  211. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  212. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  213. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  214. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  215. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  216. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  217. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  218. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  219. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  220. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  221. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  222. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  223. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  224. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  225. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  226. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  227. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  228. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  229. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  230. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  231. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  232. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  233. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  234. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  235. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  236. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  237. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  238. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  239. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  240. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  241. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  242. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  243. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  244. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  245. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  246. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  247. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  248. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  249. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  250. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  251. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  252. package/Tests/Types/SafeHtml.test.ts +102 -0
  253. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  254. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  255. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  256. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  257. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  258. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  259. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  260. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  261. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  262. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  263. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  264. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  265. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  266. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  267. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  268. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  269. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  270. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  271. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  272. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  273. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  274. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  275. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  276. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  277. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  278. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  279. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  280. package/Types/CustomField/CustomFieldType.ts +14 -0
  281. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  282. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  283. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  284. package/Types/Incident/IncidentAlertLink.ts +19 -0
  285. package/Types/SafeHtml.ts +93 -0
  286. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  287. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  288. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  289. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  290. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  291. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  292. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  293. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  294. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  295. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  296. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  297. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  298. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  299. package/Types/Topology/TopologyApi.ts +428 -0
  300. package/Types/Topology/TopologyTypeRules.ts +191 -0
  301. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  302. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  303. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  304. package/UI/Components/Forms/ModelForm.tsx +70 -8
  305. package/UI/Components/Forms/Validation.ts +1 -1
  306. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  307. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  308. package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
  309. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  310. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  311. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  312. package/Utils/API.ts +247 -2
  313. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  314. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  315. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  316. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  317. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  318. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  319. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  320. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  321. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  322. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  323. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  324. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  325. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  326. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  327. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  328. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  329. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  330. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  331. package/build/dist/Server/API/IncidentAPI.js +100 -1
  332. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  333. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  334. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  335. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  336. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  337. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  338. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  339. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  340. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  341. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  342. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  343. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  344. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  345. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  346. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  347. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  348. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  349. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  350. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  351. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  352. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  353. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
  354. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  355. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  356. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  357. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  358. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  359. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  360. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  361. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  362. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  363. package/build/dist/Server/Services/AlertService.js +21 -33
  364. package/build/dist/Server/Services/AlertService.js.map +1 -1
  365. package/build/dist/Server/Services/DatabaseService.js +56 -7
  366. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  367. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  368. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  369. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  370. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  371. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  372. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  373. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  374. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  375. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  376. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  377. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  378. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  379. package/build/dist/Server/Services/IncidentService.js +1148 -42
  380. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  381. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  382. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  383. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  384. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  385. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  386. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  387. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  388. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  389. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  390. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  391. package/build/dist/Server/Services/ScheduledMaintenanceService.js +84 -57
  392. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  393. package/build/dist/Server/Services/SmsService.js +2 -22
  394. package/build/dist/Server/Services/SmsService.js.map +1 -1
  395. package/build/dist/Server/Services/StatusPageService.js +47 -9
  396. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  397. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  398. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  399. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  400. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  401. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  402. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  403. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  404. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  405. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  406. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  407. package/build/dist/Server/Types/Markdown.js +110 -6
  408. package/build/dist/Server/Types/Markdown.js.map +1 -1
  409. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  410. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  411. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  412. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  413. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  414. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  415. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  416. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  417. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  418. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  419. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  420. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  421. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  422. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  423. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  424. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  425. package/build/dist/Server/Utils/Response.js +30 -0
  426. package/build/dist/Server/Utils/Response.js.map +1 -1
  427. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  428. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  429. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  430. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  431. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  432. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  433. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  434. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  435. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  436. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  437. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  438. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  439. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  440. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  441. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  442. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  443. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  444. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  445. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  446. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  447. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  448. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  449. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  450. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  451. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  452. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  453. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  454. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  455. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  456. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  457. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  458. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  459. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  460. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  461. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  462. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  463. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  464. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  465. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  466. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  467. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  468. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  469. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  470. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  471. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  472. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  473. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  474. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  475. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  476. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  477. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  478. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  479. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  480. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  481. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  482. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  483. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  484. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  485. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  486. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  487. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  488. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  489. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  490. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  491. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  492. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  493. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  494. package/build/dist/Types/SafeHtml.js +84 -0
  495. package/build/dist/Types/SafeHtml.js.map +1 -0
  496. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  497. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  498. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  499. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  500. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  501. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  502. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  503. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  504. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  505. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  506. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  507. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  508. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  509. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  510. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  511. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  512. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  513. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  514. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  515. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  516. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  517. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  518. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  519. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  520. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  521. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  522. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  523. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  524. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  525. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  526. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  527. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  528. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  529. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  530. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  531. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  532. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  533. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  534. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  535. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  536. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  537. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  538. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
  539. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  540. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  541. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  542. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  543. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  544. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  545. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  546. package/build/dist/Utils/API.js +177 -1
  547. package/build/dist/Utils/API.js.map +1 -1
  548. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  549. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  550. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  551. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  552. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  553. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  554. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  555. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  556. package/package.json +1 -1
@@ -3,8 +3,6 @@ import { afterEach, describe, expect, jest, test } from "@jest/globals";
3
3
  import { cleanup, fireEvent, render, screen } from "@testing-library/react";
4
4
  import React from "react";
5
5
  import { Edge, Node } from "reactflow";
6
- import InventoryItem from "../../../Models/DatabaseModels/InventoryItem";
7
- import InventoryItemRelationship from "../../../Models/DatabaseModels/InventoryItemRelationship";
8
6
  import EntityType from "../../../Types/Telemetry/EntityType";
9
7
  import EntityRelationshipType from "../../../Types/Telemetry/EntityRelationshipType";
10
8
  import EntitySource from "../../../Types/Telemetry/EntitySource";
@@ -12,14 +10,28 @@ import getJestMockFunction, { MockFunction } from "../../MockType";
12
10
  import InfrastructureGraph, {
13
11
  InfrastructureMapLayout,
14
12
  MAX_MAP_CARDS,
13
+ TRAFFIC_EDGE_PREFIX,
14
+ TRAFFIC_ROUTE_EDGE_TYPE,
15
+ TrafficEdgeData,
15
16
  cardForNode,
17
+ describeTrafficLink,
16
18
  layoutInfrastructureMap,
19
+ trafficEdges,
20
+ trafficLanePath,
21
+ trafficMetricLabel,
22
+ trafficRoutePath,
17
23
  } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/InfrastructureGraph";
18
24
  import {
19
25
  InfrastructureTopologyModel,
26
+ InfrastructureTrafficLink,
20
27
  buildInfrastructureTopologyModel,
21
28
  collectMapCards,
22
29
  } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/InfrastructureTopologyModel";
30
+ import { HEALTH_COLORS } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyMeta";
31
+ import {
32
+ TopologyEntity,
33
+ TopologyRelationship,
34
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyData";
23
35
 
24
36
  jest.mock("react-i18next", () => {
25
37
  return {
@@ -46,6 +58,9 @@ jest.mock("reactflow", () => {
46
58
  nodes: Array<Node>;
47
59
  edges: Array<Edge>;
48
60
  onNodeClick: (event: React.MouseEvent, node: Node) => void;
61
+ onEdgeClick?: (event: React.MouseEvent, edge: Edge) => void;
62
+ onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
63
+ onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
49
64
  }): React.ReactElement => {
50
65
  return (
51
66
  <div data-testid="infrastructure-canvas">
@@ -63,6 +78,27 @@ jest.mock("reactflow", () => {
63
78
  </button>
64
79
  );
65
80
  })}
81
+ {props.edges.map((edge: Edge): React.ReactElement => {
82
+ return (
83
+ <button
84
+ type="button"
85
+ key={edge.id}
86
+ data-testid={`edge-${edge.id}`}
87
+ aria-label={edge.ariaLabel}
88
+ onClick={(event: React.MouseEvent): void => {
89
+ props.onEdgeClick?.(event, edge);
90
+ }}
91
+ onMouseEnter={(event: React.MouseEvent): void => {
92
+ props.onEdgeMouseEnter?.(event, edge);
93
+ }}
94
+ onMouseLeave={(event: React.MouseEvent): void => {
95
+ props.onEdgeMouseLeave?.(event, edge);
96
+ }}
97
+ >
98
+ {typeof edge.label === "string" ? edge.label : ""}
99
+ </button>
100
+ );
101
+ })}
66
102
  <span data-testid="edge-count">{props.edges.length}</span>
67
103
  </div>
68
104
  );
@@ -84,30 +120,27 @@ jest.mock("reactflow", () => {
84
120
 
85
121
  const NOW: Date = new Date("2026-09-07T10:00:00Z");
86
122
 
123
+ /* The lean rows the Topology API sends. */
87
124
  function entity(
88
125
  key: string,
89
126
  type: EntityType,
90
127
  name: string = key,
91
128
  lastSeenAt: Date = NOW,
92
- ): InventoryItem {
93
- const item: InventoryItem = new InventoryItem();
94
- item.entityKey = key;
95
- item.displayName = name;
96
- item.entityType = type;
97
- item.lastSeenAt = lastSeenAt;
98
- item.source = EntitySource.Discovered;
99
- return item;
129
+ ): TopologyEntity {
130
+ return {
131
+ entityKey: key,
132
+ displayName: name,
133
+ entityType: type,
134
+ lastSeenAt,
135
+ source: EntitySource.Discovered,
136
+ };
100
137
  }
101
138
  function edge(
102
139
  from: string,
103
140
  to: string,
104
141
  type: EntityRelationshipType,
105
- ): InventoryItemRelationship {
106
- const item: InventoryItemRelationship = new InventoryItemRelationship();
107
- item.fromEntityKey = from;
108
- item.toEntityKey = to;
109
- item.relationshipType = type;
110
- return item;
142
+ ): TopologyRelationship {
143
+ return { fromEntityKey: from, toEntityKey: to, relationshipType: type };
111
144
  }
112
145
 
113
146
  function kubernetesModel(): InfrastructureTopologyModel {
@@ -208,7 +241,7 @@ describe("layoutInfrastructureMap", () => {
208
241
  });
209
242
 
210
243
  test("a huge scope is capped with an overflow card", () => {
211
- const hosts: Array<InventoryItem> = Array.from(
244
+ const hosts: Array<TopologyEntity> = Array.from(
212
245
  { length: MAX_MAP_CARDS + 10 },
213
246
  (_value: unknown, index: number) => {
214
247
  return entity(`h${index}`, EntityType.Host, `server-${index}x`);
@@ -220,7 +253,7 @@ describe("layoutInfrastructureMap", () => {
220
253
  );
221
254
  const layout: InfrastructureMapLayout = layoutInfrastructureMap({
222
255
  model,
223
- nodeIds: hosts.map((host: InventoryItem) => {
256
+ nodeIds: hosts.map((host: TopologyEntity) => {
224
257
  return host.entityKey!;
225
258
  }),
226
259
  now: NOW,
@@ -266,7 +299,7 @@ describe("layoutInfrastructureMap", () => {
266
299
  });
267
300
 
268
301
  test("a long column of running cards wraps instead of shrinking the map", () => {
269
- const hosts: Array<InventoryItem> = Array.from(
302
+ const hosts: Array<TopologyEntity> = Array.from(
270
303
  { length: 12 },
271
304
  (_value: unknown, index: number) => {
272
305
  return entity(`h${index}`, EntityType.Host, `machine-${index}x`);
@@ -274,13 +307,13 @@ describe("layoutInfrastructureMap", () => {
274
307
  );
275
308
  const model: InfrastructureTopologyModel = buildInfrastructureTopologyModel(
276
309
  [entity("svc", EntityType.Service), ...hosts],
277
- hosts.map((host: InventoryItem) => {
310
+ hosts.map((host: TopologyEntity) => {
278
311
  return edge("svc", host.entityKey!, EntityRelationshipType.HostedOn);
279
312
  }),
280
313
  );
281
314
  const layout: InfrastructureMapLayout = layoutInfrastructureMap({
282
315
  model,
283
- nodeIds: hosts.map((host: InventoryItem) => {
316
+ nodeIds: hosts.map((host: TopologyEntity) => {
284
317
  return host.entityKey!;
285
318
  }),
286
319
  now: NOW,
@@ -348,9 +381,117 @@ describe("cardForNode", () => {
348
381
  });
349
382
  });
350
383
 
384
+ /*
385
+ * A collection — a flat type with more items than the map ships, like
386
+ * thousands of IoT devices — is one card with its count, never a card per
387
+ * item, and opening it opens the collection.
388
+ */
389
+ function collectionModel(): InfrastructureTopologyModel {
390
+ return buildInfrastructureTopologyModel(
391
+ [
392
+ entity("svc", EntityType.Service),
393
+ entity("switch", EntityType.NetworkDevice, "core-switch"),
394
+ ],
395
+ [edge("svc", "switch", EntityRelationshipType.RunsOn)],
396
+ {
397
+ rangeStart: new Date("2026-09-06T10:00:00Z"),
398
+ collections: [
399
+ {
400
+ entityType: EntityType.IoTDevice,
401
+ total: 5000,
402
+ active: 4200,
403
+ lastSeenAt: NOW,
404
+ activeLastSeenAt: NOW,
405
+ },
406
+ ],
407
+ },
408
+ );
409
+ }
410
+
411
+ describe("collections on the map", () => {
412
+ test("a collection is one stacked card that carries its count", () => {
413
+ const model: InfrastructureTopologyModel = collectionModel();
414
+ const card: ReturnType<typeof cardForNode> = cardForNode(
415
+ model,
416
+ model.nodes.get("collection:iot.device")!,
417
+ NOW,
418
+ );
419
+ expect(card.title).toBe("IoT Devices");
420
+ expect(card.subtitle).toBe("Collection");
421
+ expect(card.statusLabel).toBe("Active · 4,200 IoT devices");
422
+ expect(card.stacked).toBe(true);
423
+ expect(card.dimmed).toBe(false);
424
+ expect(card.footer).toBeUndefined();
425
+ });
426
+
427
+ test("the overview draws the collection as a single card beside the devices", () => {
428
+ const model: InfrastructureTopologyModel = collectionModel();
429
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
430
+ model,
431
+ nodeIds: collectMapCards(model, null),
432
+ now: NOW,
433
+ });
434
+ expect(
435
+ layout.nodes.map((node: Node): string => {
436
+ return node.id;
437
+ }),
438
+ ).toEqual(["service:svc", "switch", "collection:iot.device"]);
439
+ expect(
440
+ layout.edges.map((item: Edge): string => {
441
+ return item.target;
442
+ }),
443
+ ).toEqual(["switch"]);
444
+ });
445
+
446
+ test("opening the card opens the collection", () => {
447
+ const model: InfrastructureTopologyModel = collectionModel();
448
+ const onOpenNode: MockFunction = getJestMockFunction();
449
+ render(
450
+ <InfrastructureGraph
451
+ model={model}
452
+ nodeIds={collectMapCards(model, null)}
453
+ onOpenNode={onOpenNode}
454
+ now={NOW}
455
+ />,
456
+ );
457
+ expect(screen.getByTestId("node-collection:iot.device")).toHaveTextContent(
458
+ "IoT Devices",
459
+ );
460
+ fireEvent.click(screen.getByTestId("node-collection:iot.device"));
461
+ expect(onOpenNode).toHaveBeenCalledWith("collection:iot.device");
462
+ });
463
+
464
+ test("a collection whose items all went quiet is dimmed", () => {
465
+ const model: InfrastructureTopologyModel = buildInfrastructureTopologyModel(
466
+ [],
467
+ [],
468
+ {
469
+ rangeStart: new Date("2026-09-06T10:00:00Z"),
470
+ includeInactive: true,
471
+ collections: [
472
+ {
473
+ entityType: EntityType.CloudResource,
474
+ total: 1500,
475
+ active: 0,
476
+ lastSeenAt: new Date("2026-08-10T10:00:00Z"),
477
+ activeLastSeenAt: null,
478
+ },
479
+ ],
480
+ },
481
+ );
482
+ const card: ReturnType<typeof cardForNode> = cardForNode(
483
+ model,
484
+ model.nodes.get("collection:cloud.resource")!,
485
+ NOW,
486
+ );
487
+ expect(card.dimmed).toBe(true);
488
+ expect(card.statusLabel).toBe("Inactive · 1,500 cloud resources");
489
+ });
490
+ });
491
+
351
492
  describe("InfrastructureGraph", () => {
352
493
  test("opens cards, services and the overflow through their own callbacks", () => {
353
- const hosts: Array<InventoryItem> = Array.from(
494
+ const hosts: Array<TopologyEntity> = Array.from(
354
495
  { length: MAX_MAP_CARDS + 1 },
355
496
  (_value: unknown, index: number) => {
356
497
  return entity(`h${index}`, EntityType.Host, `server-${index}x`);
@@ -366,7 +507,7 @@ describe("InfrastructureGraph", () => {
366
507
  render(
367
508
  <InfrastructureGraph
368
509
  model={model}
369
- nodeIds={hosts.map((host: InventoryItem) => {
510
+ nodeIds={hosts.map((host: TopologyEntity) => {
370
511
  return host.entityKey!;
371
512
  })}
372
513
  onOpenNode={onOpenNode}
@@ -398,3 +539,803 @@ describe("InfrastructureGraph", () => {
398
539
  );
399
540
  });
400
541
  });
542
+
543
+ /*
544
+ * Issue #3972: the map drew a node's pods as standalone cards, because
545
+ * nothing on it ever joined two resources. When services on one card call
546
+ * services on another, a line now joins the two cards and carries the calls'
547
+ * traffic, like the Service Map's connections.
548
+ */
549
+ function call(
550
+ from: string,
551
+ to: string,
552
+ callCount: number,
553
+ errorCount: number,
554
+ avgDurationMs: number,
555
+ ): TopologyRelationship {
556
+ return {
557
+ ...edge(from, to, EntityRelationshipType.DependsOn),
558
+ callCount,
559
+ errorCount,
560
+ avgDurationMs,
561
+ };
562
+ }
563
+
564
+ /* The node in the issue's screenshot: four pods of four workloads. */
565
+ function aksNodeModel(
566
+ calls: Array<TopologyRelationship> = [
567
+ call("svc-mcp", "svc-backend", 90, 0, 15),
568
+ call("svc-backend", "svc-blob", 1200, 6, 45),
569
+ call("svc-backend", "svc-edh", 300, 30, 120),
570
+ call("svc-edh", "svc-blob", 60, 0, 20),
571
+ ],
572
+ ): InfrastructureTopologyModel {
573
+ return buildInfrastructureTopologyModel(
574
+ [
575
+ entity("svc-backend", EntityType.Service, "wb-ims-backend"),
576
+ entity("svc-blob", EntityType.Service, "wb-ims-blob"),
577
+ entity("svc-edh", EntityType.Service, "wb-ims-integration-edh"),
578
+ entity("svc-mcp", EntityType.Service, "wb-ims-mcp-remote"),
579
+ entity(
580
+ "node-k",
581
+ EntityType.KubernetesNode,
582
+ "aks-agentpool-14451756-vmss00001k",
583
+ ),
584
+ entity(
585
+ "pod-backend",
586
+ EntityType.KubernetesPod,
587
+ "wb-ims-backend-76c6f6c8d9-x2k9p",
588
+ ),
589
+ entity(
590
+ "pod-blob",
591
+ EntityType.KubernetesPod,
592
+ "wb-ims-blob-56d969fb98-q8zwm",
593
+ ),
594
+ entity(
595
+ "pod-edh",
596
+ EntityType.KubernetesPod,
597
+ "wb-ims-integration-edh-6b7c8d9f5-zz2wm",
598
+ ),
599
+ entity(
600
+ "pod-mcp",
601
+ EntityType.KubernetesPod,
602
+ "wb-ims-mcp-remote-567bc8d9f4-bq2zt",
603
+ ),
604
+ entity("pod-quiet", EntityType.KubernetesPod, "cilium-x2k9p"),
605
+ ],
606
+ [
607
+ edge("pod-backend", "node-k", EntityRelationshipType.RunsOn),
608
+ edge("pod-blob", "node-k", EntityRelationshipType.RunsOn),
609
+ edge("pod-edh", "node-k", EntityRelationshipType.RunsOn),
610
+ edge("pod-mcp", "node-k", EntityRelationshipType.RunsOn),
611
+ edge("pod-quiet", "node-k", EntityRelationshipType.RunsOn),
612
+ edge("svc-backend", "pod-backend", EntityRelationshipType.RunsOn),
613
+ edge("svc-blob", "pod-blob", EntityRelationshipType.RunsOn),
614
+ edge("svc-edh", "pod-edh", EntityRelationshipType.RunsOn),
615
+ edge("svc-mcp", "pod-mcp", EntityRelationshipType.RunsOn),
616
+ ...calls,
617
+ ],
618
+ { rangeStart: new Date("2026-09-06T10:00:00Z") },
619
+ );
620
+ }
621
+
622
+ const WINDOW_SECONDS: number = 15 * 60;
623
+
624
+ function positionOf(
625
+ layout: InfrastructureMapLayout,
626
+ id: string,
627
+ ): {
628
+ x: number;
629
+ y: number;
630
+ } {
631
+ return layout.nodes.find((node: Node) => {
632
+ return node.id === id;
633
+ })!.position;
634
+ }
635
+
636
+ function linkFor(
637
+ layout: InfrastructureMapLayout,
638
+ from: string,
639
+ to: string,
640
+ ): InfrastructureTrafficLink {
641
+ return layout.traffic.links.find((link: InfrastructureTrafficLink) => {
642
+ return link.from === from && link.to === to;
643
+ })!;
644
+ }
645
+
646
+ /* One service per host, "a" on h-a and so on, with the given calls. */
647
+ function hostsModel(
648
+ calls: Array<[string, string, number]>,
649
+ ): InfrastructureTopologyModel {
650
+ const names: Set<string> = new Set<string>();
651
+ for (const [from, to] of calls) {
652
+ names.add(from);
653
+ names.add(to);
654
+ }
655
+ const entities: Array<TopologyEntity> = [];
656
+ const relationships: Array<TopologyRelationship> = [];
657
+ for (const name of names) {
658
+ entities.push(entity(name, EntityType.Service, name));
659
+ entities.push(entity(`h-${name}`, EntityType.Host, `${name}-box`));
660
+ relationships.push(
661
+ edge(name, `h-${name}`, EntityRelationshipType.HostedOn),
662
+ );
663
+ }
664
+ for (const [from, to, count] of calls) {
665
+ relationships.push(call(from, to, count, 0, 10));
666
+ }
667
+ return buildInfrastructureTopologyModel(entities, relationships);
668
+ }
669
+
670
+ describe("traffic between the cards (issue #3972)", () => {
671
+ test("a node's pods are joined by the calls between the services they run", () => {
672
+ const model: InfrastructureTopologyModel = aksNodeModel();
673
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
674
+ model,
675
+ nodeIds: collectMapCards(model, "node-k"),
676
+ now: NOW,
677
+ });
678
+ expect(
679
+ layout.traffic.links
680
+ .map((link: InfrastructureTrafficLink): string => {
681
+ return `${link.from}->${link.to}`;
682
+ })
683
+ .sort(),
684
+ ).toEqual([
685
+ "pod-backend->pod-blob",
686
+ "pod-backend->pod-edh",
687
+ "pod-edh->pod-blob",
688
+ "pod-mcp->pod-backend",
689
+ ]);
690
+ /*
691
+ * The traffic is what this map is about now: no service column and no
692
+ * arrows into every card — each card names what it runs.
693
+ */
694
+ expect(layout.edges).toEqual([]);
695
+ expect(
696
+ layout.nodes.filter((node: Node): boolean => {
697
+ return node.id.startsWith("service:");
698
+ }),
699
+ ).toEqual([]);
700
+ expect(
701
+ layout.nodes.find((node: Node): boolean => {
702
+ return node.id === "pod-backend";
703
+ })!.data.footer,
704
+ ).toBe("Runs wb-ims-backend");
705
+ });
706
+
707
+ test("cards that talk are laid out in call order, callers first", () => {
708
+ const model: InfrastructureTopologyModel = aksNodeModel();
709
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
710
+ model,
711
+ nodeIds: collectMapCards(model, "node-k"),
712
+ now: NOW,
713
+ });
714
+ const x: (id: string) => number = (id: string): number => {
715
+ return positionOf(layout, id).x;
716
+ };
717
+ expect(x("pod-mcp")).toBeLessThan(x("pod-backend"));
718
+ expect(x("pod-backend")).toBeLessThan(x("pod-edh"));
719
+ expect(x("pod-edh")).toBeLessThan(x("pod-blob"));
720
+ // The first callers start the drawing.
721
+ expect(x("pod-mcp")).toBe(0);
722
+ // A card nothing talks to follows the ones that do.
723
+ expect(x("pod-quiet")).toBeGreaterThan(x("pod-blob"));
724
+ });
725
+
726
+ test("cards never overlap when traffic places them", () => {
727
+ const model: InfrastructureTopologyModel = aksNodeModel([
728
+ call("svc-mcp", "svc-backend", 90, 0, 15),
729
+ call("svc-mcp", "svc-blob", 90, 0, 15),
730
+ call("svc-mcp", "svc-edh", 90, 0, 15),
731
+ ]);
732
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
733
+ model,
734
+ nodeIds: collectMapCards(model, "node-k"),
735
+ now: NOW,
736
+ });
737
+ const seen: Set<string> = new Set<string>();
738
+ for (const node of layout.nodes) {
739
+ const key: string = `${node.position.x},${node.position.y}`;
740
+ expect(seen.has(key)).toBe(false);
741
+ seen.add(key);
742
+ }
743
+ // One caller, three callees stacked in the next column.
744
+ const callees: Set<number> = new Set<number>(
745
+ ["pod-backend", "pod-blob", "pod-edh"].map((id: string): number => {
746
+ return positionOf(layout, id).x;
747
+ }),
748
+ );
749
+ expect(callees.size).toBe(1);
750
+ });
751
+
752
+ test("a line that skips a layer is routed through a slot of its own, clear of the cards", () => {
753
+ const model: InfrastructureTopologyModel = aksNodeModel();
754
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
755
+ model,
756
+ nodeIds: collectMapCards(model, "node-k"),
757
+ now: NOW,
758
+ });
759
+ /*
760
+ * backend -> blob crosses the integration's layer; the other three
761
+ * lines join neighbouring layers and need no route.
762
+ */
763
+ const backendToBlob: InfrastructureTrafficLink = linkFor(
764
+ layout,
765
+ "pod-backend",
766
+ "pod-blob",
767
+ );
768
+ expect(Array.from(layout.routes.keys())).toEqual([backendToBlob.id]);
769
+ const [slot] = layout.routes.get(backendToBlob.id)!;
770
+ // In the integration's layer, but not where the integration's card is.
771
+ expect(slot!.x).toBe(positionOf(layout, "pod-edh").x);
772
+ expect(slot!.y).not.toBe(positionOf(layout, "pod-edh").y);
773
+ for (const node of layout.nodes) {
774
+ expect(`${node.position.x},${node.position.y}`).not.toBe(
775
+ `${slot!.x},${slot!.y}`,
776
+ );
777
+ }
778
+
779
+ const edges: Array<Edge<TrafficEdgeData>> = trafficEdges({
780
+ model,
781
+ links: layout.traffic.links,
782
+ label: "calls",
783
+ metricsWindowSeconds: WINDOW_SECONDS,
784
+ routes: layout.routes,
785
+ cardTops: new Map<string, number>([["pod-backend", 40]]),
786
+ });
787
+ const routed: Array<Edge<TrafficEdgeData>> = edges.filter(
788
+ (item: Edge<TrafficEdgeData>): boolean => {
789
+ return item.type === TRAFFIC_ROUTE_EDGE_TYPE;
790
+ },
791
+ );
792
+ expect(routed).toHaveLength(1);
793
+ expect(routed[0]!.data).toEqual({
794
+ link: backendToBlob,
795
+ waypoints: [slot],
796
+ sourceTop: 40,
797
+ });
798
+ expect(
799
+ edges
800
+ .filter((item: Edge<TrafficEdgeData>): boolean => {
801
+ return item.type === "default";
802
+ })
803
+ .map((item: Edge<TrafficEdgeData>): string => {
804
+ return `${item.source}->${item.target}`;
805
+ })
806
+ .sort(),
807
+ ).toEqual([
808
+ "pod-backend->pod-edh",
809
+ "pod-edh->pod-blob",
810
+ "pod-mcp->pod-backend",
811
+ ]);
812
+ });
813
+
814
+ test("a routed line runs straight through each slot and curves between them", () => {
815
+ const route: { path: string; labelX: number; labelY: number } =
816
+ trafficRoutePath({
817
+ sourceX: 256,
818
+ sourceY: 50,
819
+ targetX: 1000,
820
+ targetY: 50,
821
+ waypoints: [
822
+ { x: 356, y: 152 },
823
+ { x: 712, y: 152 },
824
+ ],
825
+ handleOffset: 50,
826
+ });
827
+ expect(route.path).toBe(
828
+ "M 256,50 C 306,50 306,202 356,202 L 612,202 L 712,202 L 968,202 C 984,202 984,50 1000,50",
829
+ );
830
+ // The label sits in the first slot, where no card is.
831
+ expect(route.labelX).toBe(356 + 128);
832
+ expect(route.labelY).toBe(202);
833
+ });
834
+
835
+ test("a chain of calls routes a long line through every layer it crosses", () => {
836
+ const model: InfrastructureTopologyModel = aksNodeModel([
837
+ call("svc-mcp", "svc-backend", 90, 0, 15),
838
+ call("svc-backend", "svc-edh", 300, 0, 120),
839
+ call("svc-edh", "svc-blob", 60, 0, 20),
840
+ call("svc-mcp", "svc-blob", 10, 0, 5),
841
+ ]);
842
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
843
+ model,
844
+ nodeIds: collectMapCards(model, "node-k"),
845
+ now: NOW,
846
+ });
847
+ const long: InfrastructureTrafficLink = linkFor(
848
+ layout,
849
+ "pod-mcp",
850
+ "pod-blob",
851
+ );
852
+ const slots: Array<{ x: number; y: number }> = layout.routes.get(long.id)!;
853
+ expect(
854
+ slots.map((slot: { x: number; y: number }): number => {
855
+ return slot.x;
856
+ }),
857
+ ).toEqual([
858
+ positionOf(layout, "pod-backend").x,
859
+ positionOf(layout, "pod-edh").x,
860
+ ]);
861
+ });
862
+
863
+ test("a call against the flow takes a lane above the cards, the forward call stays direct", () => {
864
+ const model: InfrastructureTopologyModel = hostsModel([
865
+ ["a", "b", 100],
866
+ ["b", "a", 10],
867
+ ]);
868
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
869
+ model,
870
+ nodeIds: ["h-a", "h-b"],
871
+ now: NOW,
872
+ });
873
+ const back: InfrastructureTrafficLink = linkFor(layout, "h-b", "h-a");
874
+ expect(Array.from(layout.lanes.keys())).toEqual([back.id]);
875
+ const laneY: number = layout.lanes.get(back.id)!;
876
+ expect(laneY).toBeLessThan(
877
+ Math.min(positionOf(layout, "h-a").y, positionOf(layout, "h-b").y),
878
+ );
879
+ const edges: Array<Edge<TrafficEdgeData>> = trafficEdges({
880
+ model,
881
+ links: layout.traffic.links,
882
+ label: "calls",
883
+ metricsWindowSeconds: WINDOW_SECONDS,
884
+ routes: layout.routes,
885
+ lanes: layout.lanes,
886
+ });
887
+ const byLink: (link: InfrastructureTrafficLink) => Edge<TrafficEdgeData> = (
888
+ link: InfrastructureTrafficLink,
889
+ ): Edge<TrafficEdgeData> => {
890
+ return edges.find((item: Edge<TrafficEdgeData>) => {
891
+ return item.data!.link === link;
892
+ })!;
893
+ };
894
+ expect(byLink(back).type).toBe(TRAFFIC_ROUTE_EDGE_TYPE);
895
+ expect(byLink(back).data).toEqual({ link: back, laneY });
896
+ expect(byLink(linkFor(layout, "h-a", "h-b")).type).toBe("default");
897
+ });
898
+
899
+ test("each line against the flow has a lane of its own", () => {
900
+ const model: InfrastructureTopologyModel = hostsModel([
901
+ ["a", "b", 100],
902
+ ["b", "c", 100],
903
+ ["c", "b", 10],
904
+ ["b", "a", 10],
905
+ ["c", "a", 10],
906
+ ]);
907
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
908
+ model,
909
+ nodeIds: ["h-a", "h-b", "h-c"],
910
+ now: NOW,
911
+ });
912
+ expect(layout.lanes.size).toBe(3);
913
+ expect(new Set<number>(layout.lanes.values()).size).toBe(3);
914
+ });
915
+
916
+ test("a line against the flow leaves right, runs over the cards and enters its target from the left", () => {
917
+ const route: { path: string; labelX: number; labelY: number } =
918
+ trafficLanePath({
919
+ sourceX: 868,
920
+ sourceY: 50,
921
+ targetX: 256,
922
+ targetY: 60,
923
+ laneY: -40,
924
+ });
925
+ expect(route.path).toBe(
926
+ "M 868,50 C 916,50 916,-40 868,-40 L 256,-40 C 208,-40 208,60 256,60",
927
+ );
928
+ // Its label sits above the middle of the source card.
929
+ expect(route.labelX).toBe(868 - 128);
930
+ expect(route.labelY).toBe(-40);
931
+ });
932
+
933
+ test("calls running both ways between two cards need no route", () => {
934
+ const model: InfrastructureTopologyModel = aksNodeModel([
935
+ call("svc-blob", "svc-edh", 40, 0, 5),
936
+ call("svc-edh", "svc-blob", 60, 0, 20),
937
+ ]);
938
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
939
+ model,
940
+ nodeIds: collectMapCards(model, "node-k"),
941
+ now: NOW,
942
+ });
943
+ expect(layout.traffic.links).toHaveLength(2);
944
+ expect(layout.routes.size).toBe(0);
945
+ });
946
+
947
+ test("with no calls the map is laid out exactly as before", () => {
948
+ const withCalls: InfrastructureTopologyModel = aksNodeModel([]);
949
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
950
+ model: withCalls,
951
+ nodeIds: collectMapCards(withCalls, "node-k"),
952
+ now: NOW,
953
+ });
954
+ expect(layout.traffic).toEqual({ links: [], isPartial: false });
955
+ expect(layout.routes.size).toBe(0);
956
+ // Without traffic, the service column and its arrows are back.
957
+ expect(layout.edges).toHaveLength(4);
958
+ // Cards that run something share the column next to the services.
959
+ const runningX: Set<number> = new Set<number>(
960
+ ["pod-backend", "pod-blob", "pod-edh", "pod-mcp"].map(
961
+ (id: string): number => {
962
+ return positionOf(layout, id).x;
963
+ },
964
+ ),
965
+ );
966
+ expect(runningX.size).toBe(1);
967
+ expect(positionOf(layout, "pod-quiet").x).toBeGreaterThan(
968
+ [...runningX][0]!,
969
+ );
970
+ });
971
+
972
+ test("lines are labeled with the chosen metric, colored by error rate", () => {
973
+ const model: InfrastructureTopologyModel = aksNodeModel();
974
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
975
+ model,
976
+ nodeIds: collectMapCards(model, "node-k"),
977
+ now: NOW,
978
+ });
979
+ const backendToBlob: InfrastructureTrafficLink = linkFor(
980
+ layout,
981
+ "pod-backend",
982
+ "pod-blob",
983
+ );
984
+ expect(trafficMetricLabel(backendToBlob, "calls", WINDOW_SECONDS)).toBe(
985
+ "80/min",
986
+ );
987
+ expect(trafficMetricLabel(backendToBlob, "errors", WINDOW_SECONDS)).toBe(
988
+ "0.5% errors",
989
+ );
990
+ expect(trafficMetricLabel(backendToBlob, "latency", WINDOW_SECONDS)).toBe(
991
+ "45ms",
992
+ );
993
+
994
+ for (const [label, expected] of [
995
+ ["calls", "80/min"],
996
+ ["errors", "0.5% errors"],
997
+ ["latency", "45ms"],
998
+ ] as const) {
999
+ const edges: Array<Edge<TrafficEdgeData>> = trafficEdges({
1000
+ model,
1001
+ links: layout.traffic.links,
1002
+ label,
1003
+ metricsWindowSeconds: WINDOW_SECONDS,
1004
+ });
1005
+ const drawn: Edge<TrafficEdgeData> = edges.find(
1006
+ (item: Edge<TrafficEdgeData>) => {
1007
+ return item.data!.link === backendToBlob;
1008
+ },
1009
+ )!;
1010
+ expect(drawn.id.startsWith(TRAFFIC_EDGE_PREFIX)).toBe(true);
1011
+ expect(drawn.source).toBe("pod-backend");
1012
+ expect(drawn.target).toBe("pod-blob");
1013
+ expect(drawn.label).toBe(expected);
1014
+ }
1015
+
1016
+ const edges: Array<Edge<TrafficEdgeData>> = trafficEdges({
1017
+ model,
1018
+ links: layout.traffic.links,
1019
+ label: "calls",
1020
+ metricsWindowSeconds: WINDOW_SECONDS,
1021
+ });
1022
+ const colorOf: (link: InfrastructureTrafficLink) => unknown = (
1023
+ link: InfrastructureTrafficLink,
1024
+ ): unknown => {
1025
+ return edges.find((item: Edge<TrafficEdgeData>) => {
1026
+ return item.data!.link === link;
1027
+ })!.style!.stroke;
1028
+ };
1029
+ expect(colorOf(backendToBlob)).toBe(HEALTH_COLORS.healthy);
1030
+ expect(colorOf(linkFor(layout, "pod-backend", "pod-edh"))).toBe(
1031
+ HEALTH_COLORS.critical,
1032
+ );
1033
+ });
1034
+
1035
+ test("on hover, only the hovered line is labeled", () => {
1036
+ const model: InfrastructureTopologyModel = aksNodeModel();
1037
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
1038
+ model,
1039
+ nodeIds: collectMapCards(model, "node-k"),
1040
+ now: NOW,
1041
+ });
1042
+ const quiet: Array<Edge<TrafficEdgeData>> = trafficEdges({
1043
+ model,
1044
+ links: layout.traffic.links,
1045
+ label: "hover",
1046
+ metricsWindowSeconds: WINDOW_SECONDS,
1047
+ });
1048
+ expect(
1049
+ quiet.every((item: Edge<TrafficEdgeData>): boolean => {
1050
+ return item.label === undefined;
1051
+ }),
1052
+ ).toBe(true);
1053
+ const hoveredId: string = quiet[0]!.id;
1054
+ const hovered: Array<Edge<TrafficEdgeData>> = trafficEdges({
1055
+ model,
1056
+ links: layout.traffic.links,
1057
+ label: "hover",
1058
+ hoveredId,
1059
+ metricsWindowSeconds: WINDOW_SECONDS,
1060
+ });
1061
+ expect(
1062
+ hovered
1063
+ .filter((item: Edge<TrafficEdgeData>): boolean => {
1064
+ return item.label !== undefined;
1065
+ })
1066
+ .map((item: Edge<TrafficEdgeData>): string => {
1067
+ return item.id;
1068
+ }),
1069
+ ).toEqual([hoveredId]);
1070
+ });
1071
+
1072
+ test("a line whose calls reported no count is drawn gray and unlabeled", () => {
1073
+ const model: InfrastructureTopologyModel = aksNodeModel([
1074
+ { ...edge("svc-backend", "svc-blob", EntityRelationshipType.DependsOn) },
1075
+ ]);
1076
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
1077
+ model,
1078
+ nodeIds: collectMapCards(model, "node-k"),
1079
+ now: NOW,
1080
+ });
1081
+ const [drawn] = trafficEdges({
1082
+ model,
1083
+ links: layout.traffic.links,
1084
+ label: "calls",
1085
+ metricsWindowSeconds: WINDOW_SECONDS,
1086
+ });
1087
+ expect(drawn!.label).toBeUndefined();
1088
+ expect(drawn!.style!.stroke).toBe(HEALTH_COLORS.unknown);
1089
+ expect(drawn!.ariaLabel).toBe(
1090
+ "wb-ims-backend → wb-ims-blob: no calls counted",
1091
+ );
1092
+ });
1093
+
1094
+ test("a line says which calls it stands for", () => {
1095
+ const model: InfrastructureTopologyModel = aksNodeModel();
1096
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
1097
+ model,
1098
+ nodeIds: collectMapCards(model, "node-k"),
1099
+ now: NOW,
1100
+ });
1101
+ expect(
1102
+ describeTrafficLink(
1103
+ model,
1104
+ linkFor(layout, "pod-backend", "pod-blob"),
1105
+ WINDOW_SECONDS,
1106
+ ),
1107
+ ).toBe("wb-ims-backend → wb-ims-blob: 80/min · 0.5% errors · 45ms avg");
1108
+ });
1109
+
1110
+ test("a line standing for many calls names the busiest three", () => {
1111
+ const model: InfrastructureTopologyModel = buildInfrastructureTopologyModel(
1112
+ [
1113
+ entity("a", EntityType.Service, "alpha"),
1114
+ entity("b", EntityType.Service, "bravo"),
1115
+ entity("c", EntityType.Service, "charlie"),
1116
+ entity("d", EntityType.Service, "delta"),
1117
+ entity("e", EntityType.Service, "echo"),
1118
+ entity("left", EntityType.Host, "left-box"),
1119
+ entity("right", EntityType.Host, "right-box"),
1120
+ ],
1121
+ [
1122
+ edge("a", "left", EntityRelationshipType.HostedOn),
1123
+ edge("b", "right", EntityRelationshipType.HostedOn),
1124
+ edge("c", "right", EntityRelationshipType.HostedOn),
1125
+ edge("d", "right", EntityRelationshipType.HostedOn),
1126
+ edge("e", "right", EntityRelationshipType.HostedOn),
1127
+ call("a", "b", 10, 0, 1),
1128
+ call("a", "c", 40, 0, 1),
1129
+ call("a", "d", 30, 0, 1),
1130
+ call("a", "e", 20, 0, 1),
1131
+ ],
1132
+ );
1133
+ const layout: InfrastructureMapLayout = layoutInfrastructureMap({
1134
+ model,
1135
+ nodeIds: ["left", "right"],
1136
+ now: NOW,
1137
+ });
1138
+ expect(
1139
+ describeTrafficLink(
1140
+ model,
1141
+ linkFor(layout, "left", "right"),
1142
+ WINDOW_SECONDS,
1143
+ ),
1144
+ ).toBe(
1145
+ "alpha → charlie, alpha → delta, alpha → echo +1: 6.7/min · 0% errors · 1ms avg",
1146
+ );
1147
+ });
1148
+ });
1149
+
1150
+ describe("InfrastructureGraph traffic", () => {
1151
+ function renderNode(
1152
+ model: InfrastructureTopologyModel,
1153
+ onOpenTraffic?: (link: InfrastructureTrafficLink) => void,
1154
+ ): void {
1155
+ render(
1156
+ <InfrastructureGraph
1157
+ model={model}
1158
+ nodeIds={collectMapCards(model, "node-k")}
1159
+ onOpenNode={() => {}}
1160
+ onOpenTraffic={onOpenTraffic}
1161
+ metricsWindowSeconds={WINDOW_SECONDS}
1162
+ now={NOW}
1163
+ />,
1164
+ );
1165
+ }
1166
+
1167
+ function trafficButtons(): Array<HTMLElement> {
1168
+ return screen.getAllByTestId(/^edge-traffic:/);
1169
+ }
1170
+
1171
+ test("draws the lines with request rates, and says what they are", () => {
1172
+ renderNode(aksNodeModel());
1173
+ expect(trafficButtons()).toHaveLength(4);
1174
+ expect(
1175
+ trafficButtons()
1176
+ .map((button: HTMLElement): string => {
1177
+ return button.textContent || "";
1178
+ })
1179
+ .sort(),
1180
+ ).toEqual(["20/min", "4.0/min", "6.0/min", "80/min"]);
1181
+ expect(
1182
+ screen.getByTestId("infrastructure-traffic-status"),
1183
+ ).toHaveTextContent(
1184
+ "4 connections · Lines are calls between the services on these cards, measured per service.",
1185
+ );
1186
+ // Only the traffic: no placement arrows beside it.
1187
+ expect(screen.getByTestId("edge-count")).toHaveTextContent("4");
1188
+ expect(
1189
+ screen.getByRole("button", {
1190
+ name: "wb-ims-backend → wb-ims-blob: 80/min · 0.5% errors · 45ms avg",
1191
+ }),
1192
+ ).toBeInTheDocument();
1193
+ });
1194
+
1195
+ test("the label picker switches every line to another metric", () => {
1196
+ renderNode(aksNodeModel());
1197
+ fireEvent.change(screen.getByLabelText("Line labels"), {
1198
+ target: { value: "errors" },
1199
+ });
1200
+ expect(
1201
+ trafficButtons()
1202
+ .map((button: HTMLElement): string => {
1203
+ return button.textContent || "";
1204
+ })
1205
+ .sort(),
1206
+ ).toEqual(["0% errors", "0% errors", "0.5% errors", "10.0% errors"]);
1207
+ fireEvent.change(screen.getByLabelText("Line labels"), {
1208
+ target: { value: "hover" },
1209
+ });
1210
+ expect(
1211
+ trafficButtons().every((button: HTMLElement): boolean => {
1212
+ return button.textContent === "";
1213
+ }),
1214
+ ).toBe(true);
1215
+ });
1216
+
1217
+ test("hovering a line says which calls it carries", () => {
1218
+ renderNode(aksNodeModel());
1219
+ expect(
1220
+ screen.queryByTestId("infrastructure-traffic-hover"),
1221
+ ).not.toBeInTheDocument();
1222
+ const line: HTMLElement = screen.getByRole("button", {
1223
+ name: "wb-ims-backend → wb-ims-integration-edh: 20/min · 10.0% errors · 120ms avg",
1224
+ });
1225
+ fireEvent.mouseEnter(line);
1226
+ expect(
1227
+ screen.getByTestId("infrastructure-traffic-hover"),
1228
+ ).toHaveTextContent(
1229
+ "wb-ims-backend → wb-ims-integration-edh: 20/min · 10.0% errors · 120ms avg",
1230
+ );
1231
+ fireEvent.mouseLeave(line);
1232
+ expect(
1233
+ screen.queryByTestId("infrastructure-traffic-hover"),
1234
+ ).not.toBeInTheDocument();
1235
+ });
1236
+
1237
+ test("clicking a line opens it; clicking a card opens the card", () => {
1238
+ const onOpenTraffic: MockFunction = getJestMockFunction();
1239
+ renderNode(aksNodeModel(), onOpenTraffic);
1240
+ fireEvent.click(
1241
+ screen.getByRole("button", {
1242
+ name: "wb-ims-backend → wb-ims-blob: 80/min · 0.5% errors · 45ms avg",
1243
+ }),
1244
+ );
1245
+ expect(onOpenTraffic).toHaveBeenCalledTimes(1);
1246
+ const link: InfrastructureTrafficLink = onOpenTraffic.mock
1247
+ .calls[0]![0] as InfrastructureTrafficLink;
1248
+ expect(link.from).toBe("pod-backend");
1249
+ expect(link.to).toBe("pod-blob");
1250
+ expect(link.serviceCalls[0]!.from).toBe("svc-backend");
1251
+ fireEvent.click(screen.getByTestId("node-pod-blob"));
1252
+ expect(onOpenTraffic).toHaveBeenCalledTimes(1);
1253
+ });
1254
+
1255
+ test("the invisible slots a routed line runs through open nothing", () => {
1256
+ const onOpenTraffic: MockFunction = getJestMockFunction();
1257
+ const onOpenNode: MockFunction = getJestMockFunction();
1258
+ const model: InfrastructureTopologyModel = aksNodeModel();
1259
+ render(
1260
+ <InfrastructureGraph
1261
+ model={model}
1262
+ nodeIds={collectMapCards(model, "node-k")}
1263
+ onOpenNode={onOpenNode}
1264
+ onOpenTraffic={onOpenTraffic}
1265
+ metricsWindowSeconds={WINDOW_SECONDS}
1266
+ now={NOW}
1267
+ />,
1268
+ );
1269
+ const slots: Array<HTMLElement> =
1270
+ screen.getAllByTestId(/^node-route-slot:/);
1271
+ expect(slots).toHaveLength(1);
1272
+ fireEvent.click(slots[0]!);
1273
+ expect(onOpenNode).not.toHaveBeenCalled();
1274
+ expect(onOpenTraffic).not.toHaveBeenCalled();
1275
+ });
1276
+
1277
+ test("past a few lines, labels wait for the pointer until the user picks a metric", () => {
1278
+ const calls: Array<[string, string, number]> = [];
1279
+ for (const caller of ["a", "b", "c", "d"]) {
1280
+ for (const callee of ["x", "y"]) {
1281
+ calls.push([caller, callee, 900]);
1282
+ }
1283
+ }
1284
+ const model: InfrastructureTopologyModel = hostsModel(calls);
1285
+ render(
1286
+ <InfrastructureGraph
1287
+ model={model}
1288
+ nodeIds={collectMapCards(model, null)}
1289
+ onOpenNode={() => {}}
1290
+ metricsWindowSeconds={WINDOW_SECONDS}
1291
+ now={NOW}
1292
+ />,
1293
+ );
1294
+ expect(trafficButtons()).toHaveLength(8);
1295
+ expect(screen.getByLabelText("Line labels")).toHaveValue("hover");
1296
+ expect(
1297
+ trafficButtons().every((button: HTMLElement): boolean => {
1298
+ return button.textContent === "";
1299
+ }),
1300
+ ).toBe(true);
1301
+ fireEvent.change(screen.getByLabelText("Line labels"), {
1302
+ target: { value: "calls" },
1303
+ });
1304
+ expect(
1305
+ trafficButtons().every((button: HTMLElement): boolean => {
1306
+ return button.textContent === "60/min";
1307
+ }),
1308
+ ).toBe(true);
1309
+ });
1310
+
1311
+ test("services that never called each other say so instead of drawing nothing silently", () => {
1312
+ renderNode(aksNodeModel([]));
1313
+ expect(screen.queryAllByTestId(/^edge-traffic:/)).toHaveLength(0);
1314
+ expect(
1315
+ screen.getByTestId("infrastructure-traffic-status"),
1316
+ ).toHaveTextContent(
1317
+ "The services on these cards were not seen calling each other in this time range.",
1318
+ );
1319
+ expect(screen.queryByLabelText("Line labels")).not.toBeInTheDocument();
1320
+ });
1321
+
1322
+ test("a scope where no service is known to run explains why nothing is joined", () => {
1323
+ const model: InfrastructureTopologyModel = buildInfrastructureTopologyModel(
1324
+ [entity("a", EntityType.Host), entity("b", EntityType.Host)],
1325
+ [],
1326
+ );
1327
+ render(
1328
+ <InfrastructureGraph
1329
+ model={model}
1330
+ nodeIds={["a", "b"]}
1331
+ onOpenNode={() => {}}
1332
+ now={NOW}
1333
+ />,
1334
+ );
1335
+ expect(
1336
+ screen.getByTestId("infrastructure-traffic-status"),
1337
+ ).toHaveTextContent(
1338
+ "No services are known to run on these cards, so there are no calls to draw between them.",
1339
+ );
1340
+ });
1341
+ });