@oneuptime/common 13.0.8 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -1,15 +1,19 @@
1
1
  import Modal, { ModalWidth } from "../Modal/Modal";
2
2
  import Icon from "../Icon/Icon";
3
3
  import IconProp from "../../../Types/Icon/IconProp";
4
- import Input from "../Input/Input";
5
4
  import Button, { ButtonStyleType } from "../Button/Button";
5
+ import {
6
+ LicenseManagerComponent,
7
+ LicenseManagerDialogParts,
8
+ LicenseSnapshot,
9
+ SeatTone,
10
+ } from "./LicenseManager";
6
11
  import React, {
7
12
  FunctionComponent,
8
13
  ReactElement,
9
14
  useCallback,
10
15
  useEffect,
11
16
  useMemo,
12
- useRef,
13
17
  useState,
14
18
  } from "react";
15
19
  import GlobalConfig from "../../../Models/DatabaseModels/GlobalConfig";
@@ -25,14 +29,31 @@ import {
25
29
  APP_API_URL,
26
30
  BILLING_ENABLED,
27
31
  IS_ENTERPRISE_EDITION,
32
+ env,
28
33
  } from "../../Config";
29
- import Alert, { AlertType } from "../Alerts/Alert";
30
- import EnterpriseLicenseInstanceSummary from "../../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
31
34
  import VersionUtil from "../../../Utils/VersionUtil";
35
+ import {
36
+ ENTERPRISE_LICENSE_GRACE_PERIOD_IN_DAYS,
37
+ ENTERPRISE_LICENSE_TRIAL_PERIOD_IN_DAYS,
38
+ } from "../../../Types/EnterpriseLicense/EnterpriseLicensePeriods";
32
39
  import UserUtil from "../../Utils/User";
33
40
 
41
+ /*
42
+ * The edition pill and its dialog. This component only READS the license
43
+ * (GET /global-config/license): the pill, the status notices and the version
44
+ * card. Activating, refreshing and replacing a license, and the seat usage and
45
+ * instance tables, are the Enterprise Edition's license manager (see
46
+ * ./LicenseManager.ts), which the Dashboard and the Admin Dashboard pass in.
47
+ */
34
48
  export interface ComponentProps {
35
49
  className?: string | undefined;
50
+ /*
51
+ * The Enterprise plugin's LicenseManager, read from the plugin door inside
52
+ * the caller's render (never at module load). Undefined in the Community
53
+ * Edition and on the login page: the dialog then shows the license status
54
+ * only.
55
+ */
56
+ licenseManager?: LicenseManagerComponent | undefined;
36
57
  }
37
58
 
38
59
  const ENTERPRISE_URL: string = "https://oneuptime.com/enterprise/demo";
@@ -47,105 +68,83 @@ const ENTERPRISE_URL: string = "https://oneuptime.com/enterprise/demo";
47
68
  */
48
69
  const UPGRADE_GUIDE_URL: string =
49
70
  "https://oneuptime.com/docs/installation/upgrading";
50
- const SALES_EMAIL: string = "sales@oneuptime.com";
51
71
  const SALES_MAILTO_URL: string = "mailto:sales@oneuptime.com";
52
72
 
73
+ const DAY_IN_MS: number = 24 * 60 * 60 * 1000;
74
+
53
75
  /*
54
- * Seat usage at or above this percent asks the admin to talk to OneUptime about
55
- * expanding. Matches CRITICAL_CAPACITY_PERCENT in
56
- * App/FeatureSet/Workers/Jobs/InstanceHealth/EvaluateClickhouseCapacity.ts.
76
+ * What stops when the license lapses, in one sentence set, so every notice in
77
+ * the dialog says it the same way. It has to be said plainly and before it
78
+ * happens: after the trial or the grace period, single sign-on (SAML and
79
+ * OIDC) and SCIM provisioning stop, "Require SSO" is no longer enforced (so
80
+ * nobody is locked out: users sign in with their password), and audit logging
81
+ * stops recording - the Community Edition's behaviour - until a license is
82
+ * activated, when everything resumes without a restart. Core monitoring is
83
+ * never touched.
57
84
  */
58
- const SEAT_WARNING_PERCENT: number = 90;
59
-
60
- type SeatTone = "healthy" | "approaching" | "breached";
85
+ export const LICENSE_LAPSE_CONSEQUENCES: string =
86
+ 'single sign-on (SAML and OIDC) stops and "Require SSO" is no longer enforced, so users sign in with their password; SCIM provisioning stops; audit logging stops recording; enterprise configuration becomes read-only; and the enterprise admin dashboards are locked. Everything resumes as soon as a license is activated, and core monitoring is never affected.';
61
87
 
62
- type PillTone = "normal" | "warning" | "alerted";
63
-
64
- type ParseLicenseInstancesFunction = (
65
- value: unknown,
66
- ) => Array<EnterpriseLicenseInstanceSummary>;
88
+ /*
89
+ * The two periods have different names and different lengths: an unlicensed
90
+ * installation's first ENTERPRISE_LICENSE_TRIAL_PERIOD_IN_DAYS days are its
91
+ * trial, and only a license that expired has a grace period
92
+ * (ENTERPRISE_LICENSE_GRACE_PERIOD_IN_DAYS days after its expiry). The
93
+ * numbers come from the constants the license classifier uses.
94
+ */
95
+ export const TRIAL_ENFORCEMENT_SUMMARY: string = `Without a valid license (after the ${ENTERPRISE_LICENSE_TRIAL_PERIOD_IN_DAYS}-day trial), ${LICENSE_LAPSE_CONSEQUENCES}`;
67
96
 
68
- const parseLicenseInstances: ParseLicenseInstancesFunction = (
69
- value: unknown,
70
- ): Array<EnterpriseLicenseInstanceSummary> => {
71
- if (!Array.isArray(value)) {
72
- return [];
73
- }
97
+ export const GRACE_ENFORCEMENT_SUMMARY: string = `Without a valid license (after the ${ENTERPRISE_LICENSE_GRACE_PERIOD_IN_DAYS}-day grace period), ${LICENSE_LAPSE_CONSEQUENCES}`;
74
98
 
75
- const instances: Array<EnterpriseLicenseInstanceSummary> = [];
99
+ /*
100
+ * What has stopped once the license lapsed (expired past its grace period,
101
+ * missing after the trial, or invalid), after the reason the notice gives.
102
+ */
103
+ export const LICENSE_LAPSED_STATE: string =
104
+ 'Single sign-on (SAML and OIDC) and SCIM provisioning are off: "Require SSO" is not enforced, so users sign in with their password, and your identity provider\'s SCIM requests are refused. Audit logging is not recording. Enterprise configuration is read-only and the enterprise admin dashboards are locked. Everything resumes, without a restart, as soon as a valid license is added, and core monitoring is never affected.';
76
105
 
77
- for (const item of value) {
78
- if (!item || typeof item !== "object") {
79
- continue;
80
- }
106
+ type LicenseStatus = "valid" | "grace" | "expired" | "missing" | "invalid";
81
107
 
82
- const instance: JSONObject = item as JSONObject;
108
+ type ParseLicenseStatusFunction = (value: unknown) => LicenseStatus | null;
83
109
 
84
- instances.push({
85
- instanceId:
86
- typeof instance["instanceId"] === "string"
87
- ? instance["instanceId"]
88
- : "",
89
- host:
90
- typeof instance["host"] === "string" && instance["host"]
91
- ? instance["host"]
92
- : null,
93
- userCount:
94
- typeof instance["userCount"] === "number"
95
- ? instance["userCount"]
96
- : null,
97
- isCountedTowardsUsage:
98
- typeof instance["isCountedTowardsUsage"] === "boolean"
99
- ? instance["isCountedTowardsUsage"]
100
- : undefined,
101
- lastReportedAt:
102
- typeof instance["lastReportedAt"] === "string"
103
- ? instance["lastReportedAt"]
104
- : null,
105
- version:
106
- typeof instance["version"] === "string" && instance["version"]
107
- ? instance["version"]
108
- : null,
109
- });
110
+ const parseLicenseStatus: ParseLicenseStatusFunction = (
111
+ value: unknown,
112
+ ): LicenseStatus | null => {
113
+ if (
114
+ value === "valid" ||
115
+ value === "grace" ||
116
+ value === "expired" ||
117
+ value === "missing" ||
118
+ value === "invalid"
119
+ ) {
120
+ return value;
110
121
  }
111
122
 
112
- return instances;
123
+ return null;
113
124
  };
114
125
 
115
- type FormatInstanceReportedAtFunction = (
116
- lastReportedAt: string | null,
117
- ) => string;
118
-
119
- const formatInstanceReportedAt: FormatInstanceReportedAtFunction = (
120
- lastReportedAt: string | null,
121
- ): string => {
122
- if (!lastReportedAt) {
123
- return "No usage reported yet.";
124
- }
125
-
126
- const reportedAt: Date = OneUptimeDate.fromString(lastReportedAt);
127
-
128
- if (Number.isNaN(reportedAt.getTime())) {
129
- return "No usage reported yet.";
130
- }
126
+ /*
127
+ * Seat usage at or above this percent asks the admin to talk to OneUptime about
128
+ * expanding. Matches CRITICAL_CAPACITY_PERCENT in
129
+ * App/FeatureSet/Workers/Jobs/InstanceHealth/EvaluateClickhouseCapacity.ts.
130
+ */
131
+ const SEAT_WARNING_PERCENT: number = 90;
131
132
 
132
- return `Last reported ${reportedAt.toLocaleString()}.`;
133
- };
133
+ type PillTone = "normal" | "warning" | "alerted";
134
134
 
135
135
  const EditionLabel: FunctionComponent<ComponentProps> = (
136
136
  props: ComponentProps,
137
137
  ): ReactElement => {
138
138
  const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
139
139
  const [globalConfig, setGlobalConfig] = useState<GlobalConfig | null>(null);
140
+ /*
141
+ * The last successful license GET, as the server sent it. Handed to the
142
+ * license manager, which reads its own fields (the instances, the
143
+ * activation mode) from it.
144
+ */
145
+ const [licensePayload, setLicensePayload] = useState<JSONObject | null>(null);
140
146
  const [isConfigLoading, setIsConfigLoading] = useState<boolean>(false);
141
147
  const [configError, setConfigError] = useState<string>("");
142
- const [licenseKeyInput, setLicenseKeyInput] = useState<string>("");
143
- const [validationError, setValidationError] = useState<string>("");
144
- const [successMessage, setSuccessMessage] = useState<string>("");
145
- const [isValidating, setIsValidating] = useState<boolean>(false);
146
- const [isRefreshingLicense, setIsRefreshingLicense] =
147
- useState<boolean>(false);
148
- const [isChangingLicense, setIsChangingLicense] = useState<boolean>(false);
149
148
  /*
150
149
  * The seat limit as this installation actually enforces it, rather than as
151
150
  * oneuptime.com last reported it. The server computes it from the live User
@@ -161,10 +160,6 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
161
160
  null,
162
161
  );
163
162
  const [canAddMoreUsers, setCanAddMoreUsers] = useState<boolean>(true);
164
- const [licenseInstances, setLicenseInstances] = useState<
165
- Array<EnterpriseLicenseInstanceSummary>
166
- >([]);
167
- const [thisInstanceId, setThisInstanceId] = useState<string>("");
168
163
  /*
169
164
  * Version state comes from the server rather than APP_VERSION in the
170
165
  * browser bundle: the frontend env var is empty in every build that was
@@ -195,16 +190,65 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
195
190
  const [serverLicenseValid, setServerLicenseValid] = useState<boolean | null>(
196
191
  null,
197
192
  );
198
- const licenseInputEditedRef: React.MutableRefObject<boolean> =
199
- useRef<boolean>(false);
193
+ /*
194
+ * Where the license stands, as the Enterprise license client classified it:
195
+ * valid, grace (an expired license's grace period, or an unlicensed
196
+ * installation's trial counted from its first run), expired, missing or
197
+ * invalid - and whether it could be verified offline.
198
+ */
199
+ const [licenseStatus, setLicenseStatus] = useState<LicenseStatus | null>(
200
+ null,
201
+ );
202
+ const [licenseVerification, setLicenseVerification] = useState<string>("");
203
+ const [graceReason, setGraceReason] = useState<string>("");
204
+ const [graceEndsAt, setGraceEndsAt] = useState<string>("");
205
+ // Why the license is not valid, when the server says (master admins only).
206
+ const [licenseMessage, setLicenseMessage] = useState<string>("");
200
207
 
201
- if (BILLING_ENABLED) {
202
- return <></>;
203
- }
208
+ const isMasterAdmin: boolean = useMemo(() => {
209
+ return Boolean(UserUtil.isMasterAdmin());
210
+ }, []);
204
211
 
205
212
  /*
206
- * Both the license GET and the two license POSTs answer with the same seat
207
- * fields, so the three responses are unpacked in one place.
213
+ * The Enterprise Edition's license manager, when the caller passed one (the
214
+ * Dashboard and the Admin Dashboard of the Enterprise image do). Never on
215
+ * the Community Edition, whatever the bundle holds: there is no license to
216
+ * manage there.
217
+ */
218
+ const LicenseManager: LicenseManagerComponent | undefined =
219
+ IS_ENTERPRISE_EDITION ? props.licenseManager : undefined;
220
+
221
+ /*
222
+ * Activating, replacing and refreshing the license are master-admin actions:
223
+ * the license is instance-wide, and its seat limit is what UserService
224
+ * enforces against, so being able to rewrite it is being able to raise the
225
+ * ceiling on the whole installation. The server enforces this
226
+ * (MasterAdminAuthorization on both POSTs, and it answers everybody else
227
+ * with the edition pill's fields only); hiding the controls just stops
228
+ * everyone else being offered a button that can only fail.
229
+ *
230
+ * It also takes a license manager to manage the license with. The login
231
+ * page (where a signed-out visitor sees the same pill) and a Community
232
+ * build pass none, and telling somebody to "add a license below" there
233
+ * would point at nothing.
234
+ */
235
+ const canManageLicense: boolean = isMasterAdmin && Boolean(LicenseManager);
236
+
237
+ /*
238
+ * The server says it was asked for the Enterprise Edition
239
+ * (IS_ENTERPRISE_EDITION=true) but runs the Community Edition image, which
240
+ * has no enterprise code. Only a master admin can fix that, so only a
241
+ * master admin is told.
242
+ */
243
+ const showEditionMismatchNotice: boolean =
244
+ !IS_ENTERPRISE_EDITION &&
245
+ isMasterAdmin &&
246
+ env("ENTERPRISE_EDITION_REQUESTED_BUT_NOT_LOADED") === "true";
247
+
248
+ /*
249
+ * The license GET and the license manager's writes answer with the same
250
+ * seat fields, so every response is unpacked in one place (the manager
251
+ * calls this through applySeatEnforcement).
208
252
  * isSeatLimitEnforced is what tells "this installation does not enforce a
209
253
  * seat limit" apart from "this build did not report one" - without it, an
210
254
  * older server's silence would read as a limit of zero.
@@ -296,24 +340,36 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
296
340
  ] as number;
297
341
  }
298
342
 
299
- if (payload["userCountUpdatedAt"]) {
300
- configModel.enterpriseLicenseUserCountUpdatedAt =
301
- OneUptimeDate.fromString(payload["userCountUpdatedAt"] as string);
302
- }
303
-
304
343
  setGlobalConfig(configModel);
305
- setLicenseInstances(parseLicenseInstances(payload["instances"]));
306
- setThisInstanceId(
307
- typeof payload["instanceId"] === "string"
308
- ? payload["instanceId"]
309
- : "",
310
- );
344
+ setLicensePayload(payload);
311
345
  setServerLicenseValid(
312
346
  typeof payload["licenseValid"] === "boolean"
313
347
  ? payload["licenseValid"]
314
348
  : null,
315
349
  );
316
- setIsEvaluationLicense(payload["isEvaluationLicense"] === true);
350
+ setIsEvaluationLicense(
351
+ payload["isEvaluation"] === true ||
352
+ payload["isEvaluationLicense"] === true,
353
+ );
354
+ setLicenseStatus(parseLicenseStatus(payload["status"]));
355
+ setLicenseVerification(
356
+ typeof payload["verification"] === "string"
357
+ ? payload["verification"]
358
+ : "",
359
+ );
360
+ setGraceReason(
361
+ typeof payload["graceReason"] === "string"
362
+ ? payload["graceReason"]
363
+ : "",
364
+ );
365
+ setGraceEndsAt(
366
+ typeof payload["graceEndsAt"] === "string"
367
+ ? payload["graceEndsAt"]
368
+ : "",
369
+ );
370
+ setLicenseMessage(
371
+ typeof payload["message"] === "string" ? payload["message"] : "",
372
+ );
317
373
 
318
374
  setCurrentVersion(
319
375
  typeof payload["currentVersion"] === "string"
@@ -338,15 +394,16 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
338
394
  setIsUpdateAvailable(payload["isUpdateAvailable"] === true);
339
395
  setIsUpdateCheckDisabled(payload["isUpdateCheckDisabled"] === true);
340
396
  applySeatEnforcementPayload(payload);
341
-
342
- if (!licenseInputEditedRef.current) {
343
- setLicenseKeyInput(configModel.enterpriseLicenseKey || "");
344
- }
345
397
  } catch (err) {
346
398
  setGlobalConfig(null);
347
- setLicenseInstances([]);
399
+ setLicensePayload(null);
348
400
  setServerLicenseValid(null);
349
401
  setIsEvaluationLicense(false);
402
+ setLicenseStatus(null);
403
+ setLicenseVerification("");
404
+ setGraceReason("");
405
+ setGraceEndsAt("");
406
+ setLicenseMessage("");
350
407
  setCurrentVersion("");
351
408
  setLatestVersion("");
352
409
  setLatestVersionPublishedAt("");
@@ -369,7 +426,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
369
426
  * opened rather than adding a request to every page load.
370
427
  */
371
428
  useEffect(() => {
372
- if (!IS_ENTERPRISE_EDITION) {
429
+ if (BILLING_ENABLED || !IS_ENTERPRISE_EDITION) {
373
430
  return;
374
431
  }
375
432
 
@@ -404,6 +461,88 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
404
461
  serverLicenseValid,
405
462
  ]);
406
463
 
464
+ /*
465
+ * Whether a license token IS installed, whatever the license client made of
466
+ * it.
467
+ *
468
+ * It matters because "the unlicensed trial" is no longer only for an
469
+ * installation with no license: one whose token has no expiry recorded
470
+ * beside it falls back to the same trial and the same lapse (the license
471
+ * client's classifyUnverifiedWithoutExpiry), so the copy below must not tell
472
+ * a paying customer that no license is installed.
473
+ *
474
+ * Two signals, because the two audiences are told different things. A master
475
+ * admin is sent the token itself. Everybody else is sent only the
476
+ * verification, and the license client reports "none" there exactly when
477
+ * there was no token to judge - so an "unverified" or "verified" grace
478
+ * period is a grace period with a license behind it. Neither field is new:
479
+ * both already come back from GET /global-config/license.
480
+ */
481
+ const hasLicenseInstalled: boolean =
482
+ IS_ENTERPRISE_EDITION &&
483
+ (Boolean(globalConfig?.enterpriseLicenseToken) ||
484
+ licenseVerification === "unverified" ||
485
+ licenseVerification === "verified");
486
+
487
+ /*
488
+ * The two faces of "grace". An unlicensed installation's trial (counted from
489
+ * the first run of the Enterprise Edition) is not an expired license, and
490
+ * the dialog must not tell somebody who never had a license to renew one.
491
+ */
492
+ const isUnlicensedTrial: boolean =
493
+ IS_ENTERPRISE_EDITION &&
494
+ licenseStatus === "grace" &&
495
+ graceReason === "unlicensed";
496
+
497
+ const isExpiredGrace: boolean =
498
+ IS_ENTERPRISE_EDITION &&
499
+ licenseStatus === "grace" &&
500
+ graceReason !== "unlicensed";
501
+
502
+ /*
503
+ * A license issued before OneUptime signed its licenses: accepted, but it
504
+ * cannot be verified offline.
505
+ */
506
+ const isUnverifiedLegacyLicense: boolean =
507
+ IS_ENTERPRISE_EDITION &&
508
+ licenseValid &&
509
+ !isUnlicensedTrial &&
510
+ licenseVerification === "unverified";
511
+
512
+ const graceEndsAtText: string | null = useMemo(() => {
513
+ if (!graceEndsAt) {
514
+ return null;
515
+ }
516
+
517
+ const endsAt: Date = OneUptimeDate.fromString(graceEndsAt);
518
+
519
+ if (Number.isNaN(endsAt.getTime())) {
520
+ return null;
521
+ }
522
+
523
+ return endsAt.toLocaleDateString();
524
+ }, [graceEndsAt]);
525
+
526
+ // Whole days left in the trial or the grace period, rounded up; never negative.
527
+ const graceDaysLeft: number | null = useMemo(() => {
528
+ if (!graceEndsAt) {
529
+ return null;
530
+ }
531
+
532
+ const endsAt: Date = OneUptimeDate.fromString(graceEndsAt);
533
+
534
+ if (Number.isNaN(endsAt.getTime())) {
535
+ return null;
536
+ }
537
+
538
+ return Math.max(0, Math.ceil((endsAt.getTime() - Date.now()) / DAY_IN_MS));
539
+ }, [graceEndsAt]);
540
+
541
+ const graceDaysLeftText: string =
542
+ graceDaysLeft === null
543
+ ? ""
544
+ : `${graceDaysLeft} ${graceDaysLeft === 1 ? "day" : "days"} left`;
545
+
407
546
  const licenseExpiresAtText: string | null = useMemo(() => {
408
547
  if (!globalConfig?.enterpriseLicenseExpiresAt) {
409
548
  return null;
@@ -550,22 +689,6 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
550
689
  return currentUserCount;
551
690
  }, [isSeatLimitEnforced, enforcedSeatsInUse, currentUserCount]);
552
691
 
553
- const userCountUpdatedAtText: string | null = useMemo(() => {
554
- if (!globalConfig?.enterpriseLicenseUserCountUpdatedAt) {
555
- return null;
556
- }
557
-
558
- const reportedAt: Date = OneUptimeDate.fromString(
559
- globalConfig.enterpriseLicenseUserCountUpdatedAt,
560
- );
561
-
562
- if (Number.isNaN(reportedAt.getTime())) {
563
- return null;
564
- }
565
-
566
- return reportedAt.toLocaleString();
567
- }, [globalConfig?.enterpriseLicenseUserCountUpdatedAt]);
568
-
569
692
  const isUserLimitBreached: boolean = useMemo(() => {
570
693
  if (!licenseValid) {
571
694
  return false;
@@ -619,52 +742,6 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
619
742
  return rounded;
620
743
  }, [rawUserUsagePercent]);
621
744
 
622
- /*
623
- * The displayed percent clamped to 0-100. Drives the bar width and
624
- * aria-valuenow, so the fill can never overflow its track and aria-valuenow
625
- * can never exceed aria-valuemax. Derived from the displayed percent rather
626
- * than the raw ratio so the fill always agrees with the caption beneath it:
627
- * 599/600 reads 99% and fills to 99%, not to a visually full 100%. The true
628
- * figure for a breach goes in aria-valuetext.
629
- */
630
- const seatUsageBarPercent: number = useMemo(() => {
631
- if (seatUsageDisplayPercent === null) {
632
- return 0;
633
- }
634
-
635
- return Math.min(100, Math.max(0, seatUsageDisplayPercent));
636
- }, [seatUsageDisplayPercent]);
637
-
638
- const seatsRemaining: number | null = useMemo(() => {
639
- if (typeof userLimit !== "number" || userLimit <= 0) {
640
- return null;
641
- }
642
-
643
- if (typeof effectiveUserCount !== "number") {
644
- return null;
645
- }
646
-
647
- return userLimit - effectiveUserCount;
648
- }, [userLimit, effectiveUserCount]);
649
-
650
- const seatsRemainingText: string = useMemo(() => {
651
- if (typeof seatsRemaining !== "number") {
652
- return "";
653
- }
654
-
655
- if (seatsRemaining > 0) {
656
- return `${seatsRemaining.toLocaleString()} ${
657
- seatsRemaining === 1 ? "seat" : "seats"
658
- } remaining`;
659
- }
660
-
661
- if (seatsRemaining === 0) {
662
- return "No seats remaining";
663
- }
664
-
665
- return `${Math.abs(seatsRemaining).toLocaleString()} over limit`;
666
- }, [seatsRemaining]);
667
-
668
745
  /*
669
746
  * Total and mutually exclusive. isUserLimitBreached is tested first, so
670
747
  * reaching the >= 90 test structurally guarantees the limit is not exceeded
@@ -708,107 +785,47 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
708
785
  seatUsageDisplayPercent,
709
786
  ]);
710
787
 
711
- /*
712
- * Copy containing apostrophes lives in string literals rather than JSX text,
713
- * because react/no-unescaped-entities is enforced in this repo.
714
- */
715
- const seatAdvisoryTitle: string = useMemo(() => {
716
- if (seatTone === "breached") {
717
- const over: number = Math.abs(seatsRemaining || 0);
718
-
719
- /*
720
- * Reachable now that "every seat taken" is a breach in its own right:
721
- * an installation sitting exactly on its limit is not "0 users over".
722
- */
723
- if (over === 0) {
724
- return "Every licensed seat is in use";
725
- }
726
-
727
- return over === 1
728
- ? "1 user over your licensed seats"
729
- : `${over.toLocaleString()} users over your licensed seats`;
788
+ const editionName: string = useMemo(() => {
789
+ if (!IS_ENTERPRISE_EDITION) {
790
+ return "Community Edition";
730
791
  }
731
792
 
732
- if (seatTone === "approaching") {
733
- if (seatsRemaining === 0) {
734
- return "Every licensed seat is in use";
735
- }
736
-
737
- return seatsRemaining === 1
738
- ? "Only 1 seat left on your license"
739
- : `Only ${(seatsRemaining || 0).toLocaleString()} seats left on your license`;
793
+ if (isConfigLoading) {
794
+ return "Enterprise Edition (Checking...)";
740
795
  }
741
796
 
742
- return "";
743
- }, [seatTone, seatsRemaining]);
744
-
745
- const seatAdvisoryBody: string = useMemo(() => {
746
- const limitText: string = (userLimit || 0).toLocaleString();
747
- const inUse: string = `${(effectiveUserCount || 0).toLocaleString()} of ${limitText}`;
748
-
749
- if (seatTone === "breached") {
750
- /*
751
- * When the limit is being enforced this is no longer advice, it is a
752
- * description of what the installation is already doing to invitations
753
- * and signups - so it says so plainly rather than suggesting an upgrade
754
- * as though adding people were still an option.
755
- */
756
- if (isSeatLimitEnforced && !canAddMoreUsers) {
757
- return `This installation is using ${inUse} licensed seats. New users cannot be invited, signed up or provisioned until a seat is freed up or the license is expanded.`;
758
- }
759
-
760
- return `This installation is using ${inUse} licensed seats. Expand your license so it covers everyone on the platform.`;
797
+ if (isUnlicensedTrial) {
798
+ return graceDaysLeftText
799
+ ? `Enterprise Edition (Trial, ${graceDaysLeftText})`
800
+ : "Enterprise Edition (Trial)";
761
801
  }
762
802
 
763
- if (seatTone === "approaching") {
764
- return `You're using ${inUse} licensed seats. Anyone you add beyond ${limitText} puts this installation over its licensed limit — if more people are joining, it's worth talking to OneUptime about expanding now.`;
803
+ if (isExpiredGrace) {
804
+ return "Enterprise Edition (License Expired, Grace Period)";
765
805
  }
766
806
 
767
- return "";
768
- }, [
769
- seatTone,
770
- effectiveUserCount,
771
- userLimit,
772
- isSeatLimitEnforced,
773
- canAddMoreUsers,
774
- ]);
775
-
776
- const seatUsageAriaValueText: string = useMemo(() => {
777
- if (typeof seatUsageDisplayPercent !== "number") {
778
- return "";
807
+ if (licenseValid) {
808
+ return "Enterprise Edition";
779
809
  }
780
810
 
781
- if (seatTone === "breached") {
782
- return `${seatUsageDisplayPercent}% of licensed seats in use — ${seatAdvisoryTitle}`;
811
+ if (licenseStatus === "expired") {
812
+ return "Enterprise Edition (License Expired)";
783
813
  }
784
814
 
785
- if (typeof seatsRemaining === "number") {
786
- return `${seatUsageDisplayPercent}% of licensed seats in use — ${seatsRemainingText}`;
815
+ if (licenseStatus === "invalid") {
816
+ return "Enterprise Edition (License Invalid)";
787
817
  }
788
818
 
789
- return `${seatUsageDisplayPercent}% of licensed seats in use`;
819
+ return "Enterprise Edition (License Required)";
790
820
  }, [
791
- seatUsageDisplayPercent,
792
- seatTone,
793
- seatAdvisoryTitle,
794
- seatsRemaining,
795
- seatsRemainingText,
821
+ isConfigLoading,
822
+ licenseValid,
823
+ licenseStatus,
824
+ isUnlicensedTrial,
825
+ isExpiredGrace,
826
+ graceDaysLeftText,
796
827
  ]);
797
828
 
798
- const editionName: string = useMemo(() => {
799
- if (!IS_ENTERPRISE_EDITION) {
800
- return "Community Edition";
801
- }
802
-
803
- if (isConfigLoading) {
804
- return "Enterprise Edition (Checking...)";
805
- }
806
-
807
- return licenseValid
808
- ? "Enterprise Edition"
809
- : "Enterprise Edition (License Required)";
810
- }, [isConfigLoading, licenseValid]);
811
-
812
829
  const indicatorColor: string = useMemo(() => {
813
830
  if (!IS_ENTERPRISE_EDITION) {
814
831
  return "bg-indigo-400";
@@ -826,23 +843,39 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
826
843
  return "bg-red-500";
827
844
  }
828
845
 
829
- if (seatTone === "approaching") {
846
+ if (seatTone === "approaching" || licenseStatus === "grace") {
830
847
  return "bg-amber-500";
831
848
  }
832
849
 
833
850
  return "bg-emerald-500";
834
- }, [isConfigLoading, licenseValid, seatTone]);
851
+ }, [isConfigLoading, licenseValid, seatTone, licenseStatus]);
835
852
 
836
853
  const ctaLabel: string = useMemo(() => {
837
854
  if (!IS_ENTERPRISE_EDITION) {
838
- return "Learn more";
855
+ return showEditionMismatchNotice ? "Action needed" : "Learn more";
839
856
  }
840
857
 
841
858
  if (isConfigLoading) {
842
859
  return "Checking";
843
860
  }
844
861
 
862
+ if (isUnlicensedTrial) {
863
+ return "Add license";
864
+ }
865
+
866
+ if (isExpiredGrace) {
867
+ return "Renew license";
868
+ }
869
+
845
870
  if (!licenseValid) {
871
+ if (licenseStatus === "expired") {
872
+ return "Renew license";
873
+ }
874
+
875
+ if (licenseStatus === "invalid") {
876
+ return "Fix license";
877
+ }
878
+
846
879
  return "Validate license";
847
880
  }
848
881
 
@@ -855,21 +888,36 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
855
888
  }
856
889
 
857
890
  return "View details";
858
- }, [isConfigLoading, licenseValid, seatTone]);
891
+ }, [
892
+ isConfigLoading,
893
+ licenseValid,
894
+ licenseStatus,
895
+ seatTone,
896
+ isUnlicensedTrial,
897
+ isExpiredGrace,
898
+ showEditionMismatchNotice,
899
+ ]);
859
900
 
860
901
  const pillTone: PillTone = useMemo(() => {
861
- if (!IS_ENTERPRISE_EDITION || isConfigLoading) {
902
+ if (!IS_ENTERPRISE_EDITION) {
903
+ return showEditionMismatchNotice ? "warning" : "normal";
904
+ }
905
+
906
+ if (isConfigLoading) {
862
907
  return "normal";
863
908
  }
864
909
 
865
910
  if (
866
911
  seatTone === "breached" ||
867
- (!licenseValid && Boolean(globalConfig?.enterpriseLicenseKey))
912
+ (!licenseValid &&
913
+ (Boolean(globalConfig?.enterpriseLicenseKey) ||
914
+ licenseStatus === "expired" ||
915
+ licenseStatus === "invalid"))
868
916
  ) {
869
917
  return "alerted";
870
918
  }
871
919
 
872
- if (seatTone === "approaching") {
920
+ if (seatTone === "approaching" || licenseStatus === "grace") {
873
921
  return "warning";
874
922
  }
875
923
 
@@ -877,8 +925,10 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
877
925
  }, [
878
926
  isConfigLoading,
879
927
  licenseValid,
928
+ licenseStatus,
880
929
  seatTone,
881
930
  globalConfig?.enterpriseLicenseKey,
931
+ showEditionMismatchNotice,
882
932
  ]);
883
933
 
884
934
  const modalDescription: string = useMemo(() => {
@@ -890,41 +940,20 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
890
940
  return "Checking your license with OneUptime...";
891
941
  }
892
942
 
893
- if (!licenseValid) {
894
- return "Validate your license key to activate Enterprise Edition.";
943
+ if (isUnlicensedTrial) {
944
+ return "No license is installed yet. Enterprise features are available during the trial.";
895
945
  }
896
946
 
897
- return "License, version, seat usage, and the instances covered by this key.";
898
- }, [isConfigLoading, licenseValid]);
899
-
900
- /*
901
- * The per-instance counts only exceed the unique total when someone actually
902
- * appears on more than one instance, and the component cannot know whether
903
- * they do — it receives per-instance counts and the unique total separately.
904
- * So the summation is stated as a possibility, and is dropped entirely when
905
- * there is no unique total to compare against. It names the unique user count
906
- * rather than "the total above", which would be ambiguous next to a hero
907
- * reading "119 / 120 seats".
908
- */
909
- const instanceOverlapText: string = useMemo(() => {
910
- const countingRule: string = `Seats are counted uniquely across all ${licenseInstances.length} instances that share this license — the same person on multiple instances uses one seat.`;
911
-
912
- if (typeof currentUserCount !== "number") {
913
- return countingRule;
947
+ if (isExpiredGrace) {
948
+ return "Your license has expired. Renew it before the grace period ends.";
914
949
  }
915
950
 
916
- return `${countingRule} Per-instance counts can therefore add up to more than the ${currentUserCount.toLocaleString()} unique ${
917
- currentUserCount === 1 ? "user" : "users"
918
- } counted above.`;
919
- }, [licenseInstances.length, currentUserCount]);
920
-
921
- const licenseKeyHelperText: string = useMemo(() => {
922
- if (isChangingLicense) {
923
- return "Enter the new enterprise license key and validate it to replace the current one. Your existing license stays active until the new key is validated.";
951
+ if (!licenseValid) {
952
+ return "Add a valid license to turn single sign-on, SCIM and audit logging back on and make enterprise configuration editable.";
924
953
  }
925
954
 
926
- return "You have installed Enterprise Edition of OneUptime. You need to validate your license key. Need a license key? Contact our sales team at";
927
- }, [isChangingLicense]);
955
+ return "License, version, seat usage, and the instances covered by this key.";
956
+ }, [isConfigLoading, licenseValid, isUnlicensedTrial, isExpiredGrace]);
928
957
 
929
958
  const communityFeatures: Array<string> = useMemo(() => {
930
959
  return [
@@ -947,34 +976,18 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
947
976
  ];
948
977
  }, []);
949
978
 
979
+ /*
980
+ * The license manager clears its own messages (and abandons a license
981
+ * change in progress) when the dialog opens or closes.
982
+ */
950
983
  const openDialog: () => void = () => {
951
984
  setIsDialogOpen(true);
952
- setValidationError("");
953
- setSuccessMessage("");
954
985
 
955
986
  void fetchGlobalConfig();
956
987
  };
957
988
 
958
989
  const closeDialog: () => void = () => {
959
990
  setIsDialogOpen(false);
960
- setValidationError("");
961
- setSuccessMessage("");
962
- setIsChangingLicense(false);
963
- };
964
-
965
- const handleStartChangingLicense: () => void = () => {
966
- setIsChangingLicense(true);
967
- setValidationError("");
968
- setSuccessMessage("");
969
- setLicenseKeyInput("");
970
- licenseInputEditedRef.current = true;
971
- };
972
-
973
- const handleCancelChangingLicense: () => void = () => {
974
- setIsChangingLicense(false);
975
- setValidationError("");
976
- licenseInputEditedRef.current = false;
977
- setLicenseKeyInput(globalConfig?.enterpriseLicenseKey || "");
978
991
  };
979
992
 
980
993
  const handlePrimaryAction: () => void = () => {
@@ -985,181 +998,12 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
985
998
  closeDialog();
986
999
  };
987
1000
 
988
- type BuildSeatExpansionMailtoFunction = () => string;
989
-
990
- const buildSeatExpansionMailto: BuildSeatExpansionMailtoFunction =
991
- (): string => {
992
- const subject: string =
993
- seatTone === "breached"
994
- ? "Expand OneUptime license - over seat limit"
995
- : "Expand OneUptime license - nearly out of seats";
996
-
997
- const body: string = [
998
- "Hi OneUptime,",
999
- "",
1000
- "We would like to expand the seat count on our enterprise license.",
1001
- "",
1002
- `Company: ${globalConfig?.enterpriseCompanyName || "Not specified"}`,
1003
- `Seats in use: ${(effectiveUserCount || 0).toLocaleString()} of ${(
1004
- userLimit || 0
1005
- ).toLocaleString()}`,
1006
- `Instances on this license: ${licenseInstances.length}`,
1007
- ].join("\n");
1008
-
1009
- return `${SALES_MAILTO_URL}?subject=${encodeURIComponent(
1010
- subject,
1011
- )}&body=${encodeURIComponent(body)}`;
1012
- };
1013
-
1014
- const handleRequestMoreSeats: () => void = () => {
1015
- if (typeof window !== "undefined") {
1016
- window.location.href = buildSeatExpansionMailto();
1017
- }
1018
- };
1019
-
1020
- const runLicenseValidation: (
1021
- key: string,
1022
- setLoading: React.Dispatch<React.SetStateAction<boolean>>,
1023
- ) => Promise<void> = useCallback(
1024
- async (
1025
- key: string,
1026
- setLoading: React.Dispatch<React.SetStateAction<boolean>>,
1027
- ): Promise<void> => {
1028
- const trimmedKey: string = key.trim();
1029
-
1030
- if (!trimmedKey) {
1031
- setValidationError("Please enter a license key before validating.");
1032
- setSuccessMessage("");
1033
- return;
1034
- }
1035
-
1036
- setValidationError("");
1037
- setSuccessMessage("");
1038
- setLoading(true);
1039
-
1040
- try {
1041
- const validationUrl: URL = URL.fromURL(APP_API_URL).addRoute(
1042
- new Route("/global-config/license"),
1043
- );
1044
-
1045
- const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
1046
- await API.fetch<JSONObject>({
1047
- method: HTTPMethod.POST,
1048
- url: validationUrl,
1049
- data: {
1050
- licenseKey: trimmedKey,
1051
- },
1052
- });
1053
-
1054
- if (!response.isSuccess()) {
1055
- throw response;
1056
- }
1057
-
1058
- const payload: JSONObject = response.data as JSONObject;
1059
-
1060
- licenseInputEditedRef.current = false;
1061
- setLicenseKeyInput((payload["licenseKey"] as string) || trimmedKey);
1062
- setSuccessMessage("License validated successfully.");
1063
- setIsChangingLicense(false);
1064
-
1065
- await fetchGlobalConfig();
1066
- } catch (err) {
1067
- setValidationError(API.getFriendlyMessage(err));
1068
- } finally {
1069
- setLoading(false);
1070
- }
1071
- },
1072
- [fetchGlobalConfig],
1073
- );
1074
-
1075
- const handleValidateClick: () => void = () => {
1076
- if (isValidating) {
1077
- return;
1078
- }
1079
-
1080
- void runLicenseValidation(licenseKeyInput, setIsValidating);
1081
- };
1082
-
1083
- /*
1084
- * Re-ask oneuptime.com about the license this installation already holds.
1085
- *
1086
- * The seat limit and the expiry are changed on oneuptime.com, and the daily
1087
- * report job is what normally carries them here - so without this, an
1088
- * administrator who has just bought seats waits up to a day before the
1089
- * installation stops refusing invitations. Deliberately sends no key: the
1090
- * server refreshes the stored one.
1091
- */
1092
- const handleRefreshLicense: () => void = useCallback((): void => {
1093
- if (isRefreshingLicense) {
1094
- return;
1095
- }
1096
-
1097
- const refresh: () => Promise<void> = async (): Promise<void> => {
1098
- setValidationError("");
1099
- setSuccessMessage("");
1100
- setIsRefreshingLicense(true);
1101
-
1102
- try {
1103
- const refreshUrl: URL = URL.fromURL(APP_API_URL).addRoute(
1104
- new Route("/global-config/license/refresh"),
1105
- );
1106
-
1107
- const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
1108
- await API.fetch<JSONObject>({
1109
- method: HTTPMethod.POST,
1110
- url: refreshUrl,
1111
- });
1112
-
1113
- if (!response.isSuccess()) {
1114
- throw response;
1115
- }
1116
-
1117
- applySeatEnforcementPayload(response.data as JSONObject);
1118
-
1119
- setSuccessMessage("License refreshed from OneUptime.");
1120
-
1121
- /*
1122
- * The POST already returned the new terms, but the modal renders from
1123
- * what the GET reports - re-reading keeps one source of truth for the
1124
- * whole dialog instead of two that can disagree.
1125
- */
1126
- await fetchGlobalConfig();
1127
- } catch (err) {
1128
- setValidationError(API.getFriendlyMessage(err));
1129
- } finally {
1130
- setIsRefreshingLicense(false);
1131
- }
1132
- };
1133
-
1134
- void refresh();
1135
- }, [isRefreshingLicense, fetchGlobalConfig, applySeatEnforcementPayload]);
1136
-
1137
1001
  const handleRetryFetch: () => void = () => {
1138
1002
  if (!isConfigLoading) {
1139
1003
  void fetchGlobalConfig();
1140
1004
  }
1141
1005
  };
1142
1006
 
1143
- /*
1144
- * Activating, replacing and refreshing the license are master-admin actions:
1145
- * the license is instance-wide, and its seat limit is what UserService
1146
- * enforces against, so being able to rewrite it is being able to raise the
1147
- * ceiling on the whole installation. The server enforces this
1148
- * (MasterAdminAuthorization on both POSTs); hiding the controls here just
1149
- * stops everyone else being offered a button that can only fail.
1150
- *
1151
- * False for a signed-out visitor, which is what keeps the license key input
1152
- * off the login page where this same component renders the edition pill.
1153
- */
1154
- const canManageLicense: boolean = useMemo(() => {
1155
- return Boolean(UserUtil.isMasterAdmin());
1156
- }, []);
1157
-
1158
- const showLicenseKeyInput: boolean =
1159
- IS_ENTERPRISE_EDITION &&
1160
- canManageLicense &&
1161
- (!licenseValid || isChangingLicense);
1162
-
1163
1007
  /*
1164
1008
  * Someone who cannot fix the license still needs to be told why the
1165
1009
  * installation is behaving like an unlicensed one, and who can fix it.
@@ -1171,14 +1015,21 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1171
1015
  !licenseValid &&
1172
1016
  !canManageLicense;
1173
1017
 
1174
- const shouldShowEnterpriseValidationButton: boolean = showLicenseKeyInput;
1175
-
1176
1018
  /*
1177
1019
  * Collapses the !configError && !isConfigLoading && licenseValid chain that
1178
- * gates the stat strip, the seat card and the instances card.
1020
+ * gates the stat strip here, and the license manager's seat card, instances
1021
+ * card and footer actions.
1179
1022
  */
1180
1023
  const showLicenseDetails: boolean =
1181
- IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && licenseValid;
1024
+ IS_ENTERPRISE_EDITION &&
1025
+ !configError &&
1026
+ !isConfigLoading &&
1027
+ licenseValid &&
1028
+ !isUnlicensedTrial;
1029
+
1030
+ // Status notices describe the license, so they wait for it to load.
1031
+ const showLicenseStatusNotices: boolean =
1032
+ IS_ENTERPRISE_EDITION && !configError && !isConfigLoading;
1182
1033
 
1183
1034
  /*
1184
1035
  * The evaluation notice rides along with the license details: it is only
@@ -1196,7 +1047,10 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1196
1047
  IS_ENTERPRISE_EDITION && licenseValid && isEvaluationLicense;
1197
1048
 
1198
1049
  const showEnterpriseFeatureList: boolean =
1199
- IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && !licenseValid;
1050
+ IS_ENTERPRISE_EDITION &&
1051
+ !configError &&
1052
+ !isConfigLoading &&
1053
+ (!licenseValid || isUnlicensedTrial);
1200
1054
 
1201
1055
  /*
1202
1056
  * Gated on neither edition nor licenseValid: which build you run, and
@@ -1208,40 +1062,6 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1208
1062
  const showVersionCard: boolean =
1209
1063
  !configError && !isConfigLoading && Boolean(currentVersion);
1210
1064
 
1211
- /*
1212
- * GlobalConfigAPI returns userLimit and currentUserCount to anonymous callers
1213
- * because the same route serves the signed-out login page, and gates only
1214
- * licenseKey, token, instances and instanceId on isAuthenticatedUser. Gate
1215
- * the sales ask on a field the server does redact, so a signed-out visitor is
1216
- * never shown an administrator-targeted CTA.
1217
- */
1218
- const canSeeLicenseAdmin: boolean = Boolean(
1219
- globalConfig?.enterpriseLicenseKey,
1220
- );
1221
-
1222
- const modalSubmitButtonText: string | undefined = IS_ENTERPRISE_EDITION
1223
- ? shouldShowEnterpriseValidationButton
1224
- ? "Validate License"
1225
- : undefined
1226
- : "Talk to Sales";
1227
-
1228
- const modalOnSubmit: (() => void) | undefined = IS_ENTERPRISE_EDITION
1229
- ? shouldShowEnterpriseValidationButton
1230
- ? handleValidateClick
1231
- : undefined
1232
- : handlePrimaryAction;
1233
-
1234
- const modalIsLoading: boolean =
1235
- IS_ENTERPRISE_EDITION && shouldShowEnterpriseValidationButton
1236
- ? isValidating
1237
- : false;
1238
-
1239
- const modalDisableSubmitButton: boolean | undefined = IS_ENTERPRISE_EDITION
1240
- ? shouldShowEnterpriseValidationButton
1241
- ? !licenseKeyInput.trim() || isValidating || isConfigLoading
1242
- : undefined
1243
- : false;
1244
-
1245
1065
  const modalRightElement: ReactElement | undefined = showLicenseDetails ? (
1246
1066
  <span
1247
1067
  className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${
@@ -1265,41 +1085,12 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1265
1085
  ? "Seat limit exceeded"
1266
1086
  : seatTone === "approaching"
1267
1087
  ? "Seats nearly full"
1268
- : "License active"}
1088
+ : isExpiredGrace
1089
+ ? "Grace period"
1090
+ : "License active"}
1269
1091
  </span>
1270
1092
  ) : undefined;
1271
1093
 
1272
- const modalLeftFooterElement: ReactElement | undefined =
1273
- showLicenseDetails && !isChangingLicense && canManageLicense ? (
1274
- <div className="flex flex-wrap items-center gap-2">
1275
- {/*
1276
- * The seat limit and the expiry are changed on oneuptime.com and reach
1277
- * this installation through the daily report job. This is the button
1278
- * for the administrator who has just bought seats and would rather not
1279
- * wait a day for the installation to stop refusing invitations.
1280
- */}
1281
- <Button
1282
- title="Refresh license"
1283
- icon={IconProp.Refresh}
1284
- buttonStyle={ButtonStyleType.NORMAL}
1285
- onClick={handleRefreshLicense}
1286
- isLoading={isRefreshingLicense}
1287
- disabled={isRefreshingLicense || isConfigLoading}
1288
- tooltip="Fetch the latest seat limit and expiry for this license from OneUptime."
1289
- dataTestId="refresh-enterprise-license"
1290
- className="!mt-0 md:!ml-0"
1291
- />
1292
- <Button
1293
- title="Change license key"
1294
- icon={IconProp.Edit}
1295
- buttonStyle={ButtonStyleType.NORMAL}
1296
- onClick={handleStartChangingLicense}
1297
- disabled={isRefreshingLicense}
1298
- className="!mt-0 md:!ml-0"
1299
- />
1300
- </div>
1301
- ) : undefined;
1302
-
1303
1094
  const pillClassName: string =
1304
1095
  pillTone === "alerted"
1305
1096
  ? "group inline-flex items-center gap-2 rounded-full border border-red-200 bg-red-50 px-3 py-1 text-xs font-medium text-red-700 shadow-sm transition hover:border-red-300 hover:bg-red-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400"
@@ -1484,6 +1275,450 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1484
1275
  </section>
1485
1276
  ) : null;
1486
1277
 
1278
+ /*
1279
+ * What the license status means for this installation, in plain words.
1280
+ * Before the lapse (the trial, the grace period) every branch warns what
1281
+ * will stop and when (LICENSE_LAPSE_CONSEQUENCES); after it, what has
1282
+ * stopped (LICENSE_LAPSED_STATE).
1283
+ */
1284
+ const licenseStatusNoticeElement: ReactElement | null = (() => {
1285
+ if (!showLicenseStatusNotices) {
1286
+ return null;
1287
+ }
1288
+
1289
+ if (isUnlicensedTrial) {
1290
+ return (
1291
+ <section
1292
+ role="status"
1293
+ data-testid="enterprise-license-trial-notice"
1294
+ className="rounded-xl border border-amber-200 bg-amber-50 p-5"
1295
+ >
1296
+ <h4 className="text-sm font-semibold text-amber-900">
1297
+ {graceDaysLeftText
1298
+ ? `Enterprise Edition trial: ${graceDaysLeftText}`
1299
+ : "Enterprise Edition trial"}
1300
+ </h4>
1301
+ <p className="mt-1 text-xs leading-relaxed text-amber-800">
1302
+ {`${
1303
+ hasLicenseInstalled
1304
+ ? "A license is installed, but this installation cannot tell whether it is still current, so it is treating this as an unlicensed install."
1305
+ : "No Enterprise license is installed."
1306
+ } Enterprise features stay fully available${
1307
+ graceEndsAtText ? ` until ${graceEndsAtText}` : ""
1308
+ }, counted from the first time this installation ran the Enterprise Edition. ${TRIAL_ENFORCEMENT_SUMMARY}`}
1309
+ </p>
1310
+ {/*
1311
+ * What the license client actually said about the installed license.
1312
+ * Only a master admin is sent it, and only this state has one to
1313
+ * show: a genuinely unlicensed install's message is the sentence
1314
+ * above.
1315
+ */}
1316
+ {hasLicenseInstalled && licenseMessage && (
1317
+ <p
1318
+ data-testid="enterprise-license-status-message"
1319
+ className="mt-2 text-xs leading-relaxed text-amber-800"
1320
+ >
1321
+ {licenseMessage}
1322
+ </p>
1323
+ )}
1324
+ <p className="mt-2 text-xs leading-relaxed text-amber-800">
1325
+ {canManageLicense
1326
+ ? hasLicenseInstalled
1327
+ ? "Re-activate the license below before the trial ends, or let the daily license sync fetch its expiry from OneUptime, to keep single sign-on, SCIM and audit logging running and enterprise configuration editable."
1328
+ : "Add a license below before the trial ends to keep single sign-on, SCIM and audit logging running and enterprise configuration editable."
1329
+ : hasLicenseInstalled
1330
+ ? "Ask a master admin of this installation to re-activate the license."
1331
+ : "Ask a master admin of this installation to add a license."}
1332
+ </p>
1333
+ </section>
1334
+ );
1335
+ }
1336
+
1337
+ if (isExpiredGrace) {
1338
+ return (
1339
+ <section
1340
+ role="status"
1341
+ data-testid="enterprise-license-grace-notice"
1342
+ className="rounded-xl border border-amber-200 bg-amber-50 p-5"
1343
+ >
1344
+ <h4 className="text-sm font-semibold text-amber-900">
1345
+ {graceEndsAtText
1346
+ ? `License expired, grace period until ${graceEndsAtText}`
1347
+ : "License expired, grace period"}
1348
+ </h4>
1349
+ <p className="mt-1 text-xs leading-relaxed text-amber-800">
1350
+ {`The Enterprise license expired${
1351
+ licenseExpiresAtText ? ` on ${licenseExpiresAtText}` : ""
1352
+ }. Every enterprise feature keeps working until the grace period ends${
1353
+ graceDaysLeftText ? ` (${graceDaysLeftText})` : ""
1354
+ }. ${GRACE_ENFORCEMENT_SUMMARY}`}
1355
+ </p>
1356
+ <p className="mt-2 text-xs leading-relaxed text-amber-800">
1357
+ {canManageLicense
1358
+ ? "Renew the license with OneUptime, then refresh it here."
1359
+ : "Ask a master admin of this installation to renew the license."}
1360
+ </p>
1361
+ </section>
1362
+ );
1363
+ }
1364
+
1365
+ if (!licenseValid) {
1366
+ const title: string =
1367
+ licenseStatus === "expired"
1368
+ ? "License expired"
1369
+ : licenseStatus === "invalid"
1370
+ ? "License not valid"
1371
+ : "No valid license";
1372
+
1373
+ const reason: string =
1374
+ licenseStatus === "expired"
1375
+ ? `The Enterprise license expired${
1376
+ licenseExpiresAtText ? ` on ${licenseExpiresAtText}` : ""
1377
+ } and its grace period is over.`
1378
+ : licenseStatus === "invalid"
1379
+ ? licenseMessage ||
1380
+ "The stored license could not be verified. Validate the license again."
1381
+ : hasLicenseInstalled
1382
+ ? "A license is installed, but this installation cannot confirm that it is current, so nothing is licensed by it."
1383
+ : "This installation has no valid Enterprise license.";
1384
+
1385
+ /*
1386
+ * Whatever the license client said, for EVERY status and not only
1387
+ * "invalid".
1388
+ *
1389
+ * The reason above is this component's own reading of a status code, and
1390
+ * a status code cannot carry a diagnosis: "missing" is both "no license"
1391
+ * and "a license whose expiry was never recorded, re-activate it or let
1392
+ * the daily sync fetch it" (the license client's
1393
+ * classifyUnverifiedWithoutExpiry). The second one is the whole point of
1394
+ * the message field, and it used to be rendered nowhere but here in the
1395
+ * "invalid" branch - which that state deliberately no longer is.
1396
+ *
1397
+ * Skipped when the reason already IS the message ("invalid"), so a master
1398
+ * admin is never shown the same sentence twice.
1399
+ */
1400
+ const explanation: string =
1401
+ licenseMessage && licenseMessage !== reason ? licenseMessage : "";
1402
+
1403
+ return (
1404
+ <section
1405
+ role="alert"
1406
+ data-testid="enterprise-license-required-notice"
1407
+ className="rounded-xl border border-red-200 bg-red-50 p-5"
1408
+ >
1409
+ <h4 className="text-sm font-semibold text-red-900">{title}</h4>
1410
+ {explanation && (
1411
+ <p
1412
+ data-testid="enterprise-license-status-message"
1413
+ className="mt-1 text-xs leading-relaxed text-red-800"
1414
+ >
1415
+ {explanation}
1416
+ </p>
1417
+ )}
1418
+ <p className="mt-1 text-xs leading-relaxed text-red-800">
1419
+ {`${reason} ${LICENSE_LAPSED_STATE}`}
1420
+ </p>
1421
+ </section>
1422
+ );
1423
+ }
1424
+
1425
+ if (isUnverifiedLegacyLicense && canManageLicense) {
1426
+ return (
1427
+ <section
1428
+ data-testid="enterprise-license-unverified-notice"
1429
+ className="rounded-xl border border-gray-200 bg-gray-50 p-4"
1430
+ >
1431
+ <h4 className="text-sm font-semibold text-gray-900">
1432
+ Legacy license
1433
+ </h4>
1434
+ <p className="mt-1 text-xs leading-relaxed text-gray-600">
1435
+ This license was issued before OneUptime signed its licenses, so
1436
+ this installation cannot verify it offline. It keeps working;
1437
+ refreshing the license replaces it with a signed one once OneUptime
1438
+ issues them.
1439
+ </p>
1440
+ </section>
1441
+ );
1442
+ }
1443
+
1444
+ return null;
1445
+ })();
1446
+
1447
+ /*
1448
+ * What the license manager needs to know, all of it read from the license
1449
+ * GET (see LicenseManager.ts).
1450
+ */
1451
+ const licenseSnapshot: LicenseSnapshot = {
1452
+ payload: licensePayload,
1453
+ isLoading: isConfigLoading,
1454
+ loadError: configError,
1455
+ canManageLicense: canManageLicense,
1456
+ licenseValid: licenseValid,
1457
+ isUnlicensedTrial: isUnlicensedTrial,
1458
+ showLicenseDetails: showLicenseDetails,
1459
+ companyName: globalConfig?.enterpriseCompanyName || "",
1460
+ licenseKey: globalConfig?.enterpriseLicenseKey || "",
1461
+ userLimit: userLimit,
1462
+ currentUserCount: currentUserCount,
1463
+ effectiveUserCount: effectiveUserCount,
1464
+ isSeatLimitEnforced: isSeatLimitEnforced,
1465
+ canAddMoreUsers: canAddMoreUsers,
1466
+ seatTone: seatTone,
1467
+ seatUsageDisplayPercent: seatUsageDisplayPercent,
1468
+ currentVersion: currentVersion,
1469
+ latestVersion: latestVersion,
1470
+ hasComparableVersion: hasComparableVersion,
1471
+ };
1472
+
1473
+ /*
1474
+ * The dialog, with whatever the license manager adds to it: its messages,
1475
+ * the seat usage and instance cards, the activation input and the footer
1476
+ * actions. With no manager (the Community Edition, the login page) the
1477
+ * parts are empty and the dialog shows the license status only.
1478
+ */
1479
+ const renderDialog: (parts: LicenseManagerDialogParts) => ReactElement = (
1480
+ parts: LicenseManagerDialogParts,
1481
+ ): ReactElement => {
1482
+ if (!isDialogOpen) {
1483
+ return <></>;
1484
+ }
1485
+
1486
+ return (
1487
+ <Modal
1488
+ title={editionName}
1489
+ description={modalDescription}
1490
+ rightElement={modalRightElement}
1491
+ submitButtonText={
1492
+ IS_ENTERPRISE_EDITION ? parts.submitButtonText : "Talk to Sales"
1493
+ }
1494
+ closeButtonText="Close"
1495
+ onClose={closeDialog}
1496
+ onSubmit={IS_ENTERPRISE_EDITION ? parts.onSubmit : handlePrimaryAction}
1497
+ modalWidth={ModalWidth.Medium}
1498
+ isLoading={IS_ENTERPRISE_EDITION ? Boolean(parts.isSubmitting) : false}
1499
+ disableSubmitButton={
1500
+ IS_ENTERPRISE_EDITION ? parts.disableSubmitButton : false
1501
+ }
1502
+ isBodyLoading={IS_ENTERPRISE_EDITION ? isConfigLoading : false}
1503
+ leftFooterElement={
1504
+ IS_ENTERPRISE_EDITION ? parts.leftFooterElement : undefined
1505
+ }
1506
+ >
1507
+ <div className="space-y-4 text-sm text-gray-600">
1508
+ {IS_ENTERPRISE_EDITION ? (
1509
+ <>
1510
+ {parts.messages}
1511
+
1512
+ {configError && (
1513
+ <div className="rounded-xl border border-red-200 bg-red-50 p-4">
1514
+ <div className="flex items-start gap-2.5">
1515
+ <Icon
1516
+ icon={IconProp.Alert}
1517
+ className="mt-0.5 h-4 w-4 shrink-0 text-red-600"
1518
+ />
1519
+ <div className="min-w-0 flex-1">
1520
+ <p className="text-sm font-semibold text-red-900">
1521
+ Unable to load license details
1522
+ </p>
1523
+ <p className="mt-1 text-xs leading-relaxed text-red-800">
1524
+ {configError}
1525
+ </p>
1526
+ <div className="mt-3">
1527
+ <Button
1528
+ title="Try again"
1529
+ buttonStyle={ButtonStyleType.DANGER}
1530
+ onClick={handleRetryFetch}
1531
+ isLoading={isConfigLoading}
1532
+ className="!mt-0 md:!ml-0"
1533
+ />
1534
+ </div>
1535
+ </div>
1536
+ </div>
1537
+ </div>
1538
+ )}
1539
+
1540
+ {evaluationNoticeElement}
1541
+
1542
+ {licenseStatusNoticeElement}
1543
+
1544
+ {showLicenseDetails && (
1545
+ <dl className="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-gray-200 bg-gray-200 sm:grid-cols-2">
1546
+ <div className="min-w-0 bg-white px-4 py-3">
1547
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-gray-500">
1548
+ Licensed to
1549
+ </dt>
1550
+ <dd
1551
+ className="mt-1 truncate text-sm font-medium text-gray-900"
1552
+ title={globalConfig?.enterpriseCompanyName || undefined}
1553
+ >
1554
+ {globalConfig?.enterpriseCompanyName || "Not specified"}
1555
+ </dd>
1556
+ </div>
1557
+ <div className="bg-white px-4 py-3">
1558
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-gray-500">
1559
+ Expires
1560
+ </dt>
1561
+ <dd className="mt-1 text-sm font-medium tabular-nums text-gray-900">
1562
+ {licenseExpiresAtText || "—"}
1563
+ </dd>
1564
+ </div>
1565
+ </dl>
1566
+ )}
1567
+
1568
+ {versionCardElement}
1569
+
1570
+ {parts.usage}
1571
+
1572
+ {showLicenseAdminRequiredNotice && (
1573
+ <section className="rounded-xl border border-amber-200 bg-amber-50 p-5">
1574
+ <h4 className="text-sm font-semibold text-amber-900">
1575
+ A master admin has to activate this license
1576
+ </h4>
1577
+ <p className="mt-1 text-xs leading-relaxed text-amber-800">
1578
+ This installation does not have a valid enterprise license.
1579
+ Ask a master admin of this OneUptime installation to enter
1580
+ or refresh the license key from this dialog.
1581
+ </p>
1582
+ </section>
1583
+ )}
1584
+
1585
+ {parts.activation}
1586
+
1587
+ {showEnterpriseFeatureList && (
1588
+ <section
1589
+ aria-labelledby="edition-features-heading"
1590
+ className="rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-5"
1591
+ >
1592
+ <h4
1593
+ id="edition-features-heading"
1594
+ className="text-sm font-semibold text-indigo-900"
1595
+ >
1596
+ What your license unlocks
1597
+ </h4>
1598
+ <p className="mt-0.5 text-xs text-indigo-700">
1599
+ {canManageLicense
1600
+ ? "A valid license that includes them keeps single sign-on, SCIM provisioning and audit logging running, enterprise configuration (SSO, SCIM, team compliance, audit log settings) editable and the enterprise admin dashboards unlocked."
1601
+ : "A master admin can add the license to keep single sign-on, SCIM provisioning and audit logging running and enterprise configuration editable."}{" "}
1602
+ Core monitoring never depends on it.
1603
+ </p>
1604
+ <ul className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
1605
+ {enterpriseFeatures.map(
1606
+ (feature: string, index: number) => {
1607
+ return (
1608
+ <li
1609
+ key={index}
1610
+ className="flex items-start gap-2 rounded-lg border border-gray-100 bg-white px-3 py-2.5"
1611
+ >
1612
+ <span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-indigo-50">
1613
+ <Icon
1614
+ icon={IconProp.Check}
1615
+ className="h-3 w-3 text-indigo-600"
1616
+ />
1617
+ </span>
1618
+ <span className="text-xs leading-snug text-gray-700">
1619
+ {feature}
1620
+ </span>
1621
+ </li>
1622
+ );
1623
+ },
1624
+ )}
1625
+ </ul>
1626
+ </section>
1627
+ )}
1628
+ </>
1629
+ ) : (
1630
+ <>
1631
+ {showEditionMismatchNotice && (
1632
+ <section
1633
+ role="alert"
1634
+ data-testid="enterprise-edition-image-mismatch"
1635
+ className="rounded-xl border border-amber-200 bg-amber-50 p-5"
1636
+ >
1637
+ <h4 className="text-sm font-semibold text-amber-900">
1638
+ This is the Community Edition image
1639
+ </h4>
1640
+ <p className="mt-1 text-xs leading-relaxed text-amber-800">
1641
+ IS_ENTERPRISE_EDITION is set but this is the Community
1642
+ Edition image — switch to the enterprise image (the
1643
+ enterprise-* image tags) to use Enterprise Edition features.
1644
+ Until then they are off; everything else keeps working.
1645
+ </p>
1646
+ </section>
1647
+ )}
1648
+ {versionCardElement}
1649
+ <p>
1650
+ You are running the Community Edition of OneUptime. Here is a
1651
+ quick comparison to help you decide if Enterprise is the right
1652
+ fit for your team.
1653
+ </p>
1654
+ <div className="grid gap-4 md:grid-cols-2">
1655
+ <div className="rounded-xl border border-gray-200 bg-white p-4">
1656
+ <h4 className="text-sm font-semibold text-gray-900">
1657
+ Community Edition
1658
+ </h4>
1659
+ <ul className="mt-3 space-y-2 text-sm text-gray-600">
1660
+ {communityFeatures.map((feature: string, index: number) => {
1661
+ return (
1662
+ <li key={index} className="flex items-start gap-2">
1663
+ <Icon
1664
+ icon={IconProp.Check}
1665
+ className="mt-0.5 h-3 w-3 shrink-0 text-gray-400"
1666
+ />
1667
+ <span className="leading-snug">{feature}</span>
1668
+ </li>
1669
+ );
1670
+ })}
1671
+ </ul>
1672
+ <p className="mt-3 text-xs text-gray-500">
1673
+ Best for small teams experimenting with reliability
1674
+ workflows.
1675
+ </p>
1676
+ </div>
1677
+ <div className="rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-4">
1678
+ <h4 className="text-sm font-semibold text-indigo-900">
1679
+ Enterprise Edition
1680
+ </h4>
1681
+ <ul className="mt-3 space-y-2 text-sm text-indigo-900">
1682
+ {enterpriseFeatures.map(
1683
+ (feature: string, index: number) => {
1684
+ return (
1685
+ <li key={index} className="flex items-start gap-2">
1686
+ <Icon
1687
+ icon={IconProp.Check}
1688
+ className="mt-0.5 h-3 w-3 shrink-0 text-indigo-600"
1689
+ />
1690
+ <span className="leading-snug">{feature}</span>
1691
+ </li>
1692
+ );
1693
+ },
1694
+ )}
1695
+ </ul>
1696
+ <p className="mt-3 text-xs text-indigo-700">
1697
+ Everything in Community plus white-glove onboarding,
1698
+ enterprise SLAs, and a partner dedicated to your reliability
1699
+ goals.
1700
+ </p>
1701
+ </div>
1702
+ </div>
1703
+ <p className="text-xs text-gray-500">
1704
+ Ready to unlock enterprise capabilities? Click &quot;Talk to
1705
+ Sales&quot; to start the conversation.
1706
+ </p>
1707
+ </>
1708
+ )}
1709
+ </div>
1710
+ </Modal>
1711
+ );
1712
+ };
1713
+
1714
+ /*
1715
+ * Kept after every hook: oneuptime.com (billing enabled) bounds seats and
1716
+ * plans through subscriptions and shows no edition pill at all.
1717
+ */
1718
+ if (BILLING_ENABLED) {
1719
+ return <></>;
1720
+ }
1721
+
1487
1722
  return (
1488
1723
  <>
1489
1724
  <button
@@ -1523,650 +1758,16 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1523
1758
  <span className={pillCtaTextClassName}>{ctaLabel}</span>
1524
1759
  </button>
1525
1760
 
1526
- {isDialogOpen && (
1527
- <Modal
1528
- title={editionName}
1529
- description={modalDescription}
1530
- rightElement={modalRightElement}
1531
- submitButtonText={modalSubmitButtonText}
1532
- closeButtonText="Close"
1533
- onClose={closeDialog}
1534
- onSubmit={modalOnSubmit}
1535
- modalWidth={ModalWidth.Medium}
1536
- isLoading={modalIsLoading}
1537
- disableSubmitButton={modalDisableSubmitButton}
1538
- isBodyLoading={IS_ENTERPRISE_EDITION ? isConfigLoading : false}
1539
- leftFooterElement={modalLeftFooterElement}
1540
- >
1541
- <div className="space-y-4 text-sm text-gray-600">
1542
- {IS_ENTERPRISE_EDITION ? (
1543
- <>
1544
- {!configError && successMessage && (
1545
- <Alert type={AlertType.SUCCESS} title={successMessage} />
1546
- )}
1547
-
1548
- {/*
1549
- * A failed refresh has nowhere else to go: the license key
1550
- * section carries its own copy of this alert, but that
1551
- * section is not rendered at all while the license is valid,
1552
- * which is exactly when refreshing is worth doing.
1553
- */}
1554
- {!configError && validationError && !showLicenseKeyInput && (
1555
- <Alert type={AlertType.DANGER} title={validationError} />
1556
- )}
1557
-
1558
- {configError && (
1559
- <div className="rounded-xl border border-red-200 bg-red-50 p-4">
1560
- <div className="flex items-start gap-2.5">
1561
- <Icon
1562
- icon={IconProp.Alert}
1563
- className="mt-0.5 h-4 w-4 shrink-0 text-red-600"
1564
- />
1565
- <div className="min-w-0 flex-1">
1566
- <p className="text-sm font-semibold text-red-900">
1567
- Unable to load license details
1568
- </p>
1569
- <p className="mt-1 text-xs leading-relaxed text-red-800">
1570
- {configError}
1571
- </p>
1572
- <div className="mt-3">
1573
- <Button
1574
- title="Try again"
1575
- buttonStyle={ButtonStyleType.DANGER}
1576
- onClick={handleRetryFetch}
1577
- isLoading={isConfigLoading}
1578
- className="!mt-0 md:!ml-0"
1579
- />
1580
- </div>
1581
- </div>
1582
- </div>
1583
- </div>
1584
- )}
1585
-
1586
- {evaluationNoticeElement}
1587
-
1588
- {showLicenseDetails && (
1589
- <dl className="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-gray-200 bg-gray-200 sm:grid-cols-2">
1590
- <div className="min-w-0 bg-white px-4 py-3">
1591
- <dt className="text-[11px] font-medium uppercase tracking-wide text-gray-500">
1592
- Licensed to
1593
- </dt>
1594
- <dd
1595
- className="mt-1 truncate text-sm font-medium text-gray-900"
1596
- title={globalConfig?.enterpriseCompanyName || undefined}
1597
- >
1598
- {globalConfig?.enterpriseCompanyName || "Not specified"}
1599
- </dd>
1600
- </div>
1601
- <div className="bg-white px-4 py-3">
1602
- <dt className="text-[11px] font-medium uppercase tracking-wide text-gray-500">
1603
- Expires
1604
- </dt>
1605
- <dd className="mt-1 text-sm font-medium tabular-nums text-gray-900">
1606
- {licenseExpiresAtText || "—"}
1607
- </dd>
1608
- </div>
1609
- </dl>
1610
- )}
1611
-
1612
- {versionCardElement}
1613
-
1614
- {showLicenseDetails && (
1615
- <section
1616
- aria-labelledby="edition-seats-heading"
1617
- className="rounded-xl border border-gray-200 bg-white p-5"
1618
- >
1619
- <div className="flex items-start justify-between gap-4">
1620
- <div className="min-w-0">
1621
- <h4
1622
- id="edition-seats-heading"
1623
- className="text-sm font-semibold text-gray-900"
1624
- >
1625
- Licensed seats
1626
- </h4>
1627
- <p className="mt-0.5 text-xs text-gray-500">
1628
- Unique users across every instance on this license.
1629
- </p>
1630
- </div>
1631
- {seatTone !== "healthy" && (
1632
- <span
1633
- className={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${
1634
- seatTone === "breached"
1635
- ? "bg-red-100 text-red-700"
1636
- : "bg-amber-100 text-amber-800"
1637
- }`}
1638
- >
1639
- {seatTone === "breached"
1640
- ? "Limit exceeded"
1641
- : "Nearly full"}
1642
- </span>
1643
- )}
1644
- </div>
1645
-
1646
- <div className="mt-4 flex items-end justify-between gap-4">
1647
- <p className="flex items-baseline gap-1.5">
1648
- <span
1649
- className={`text-3xl font-semibold leading-none tabular-nums ${
1650
- typeof effectiveUserCount !== "number"
1651
- ? "text-gray-300"
1652
- : seatTone === "breached"
1653
- ? "text-red-700"
1654
- : "text-gray-900"
1655
- }`}
1656
- >
1657
- {typeof effectiveUserCount === "number"
1658
- ? effectiveUserCount.toLocaleString()
1659
- : "—"}
1660
- </span>
1661
- <span className="text-sm tabular-nums text-gray-500">
1662
- {" / "}
1663
- {typeof userLimit === "number" && userLimit > 0
1664
- ? `${userLimit.toLocaleString()} seats`
1665
- : "unlimited"}
1666
- </span>
1667
- </p>
1668
- {typeof seatsRemaining === "number" && (
1669
- <p
1670
- className={`text-xs font-medium tabular-nums ${
1671
- seatTone === "breached"
1672
- ? "text-red-700"
1673
- : seatTone === "approaching"
1674
- ? "text-amber-700"
1675
- : "text-gray-500"
1676
- }`}
1677
- >
1678
- {seatsRemainingText}
1679
- </p>
1680
- )}
1681
- </div>
1682
-
1683
- {typeof seatUsageDisplayPercent === "number" && (
1684
- <div className="mt-2.5">
1685
- <div
1686
- className="h-2.5 w-full overflow-hidden rounded-full bg-gray-100"
1687
- role="progressbar"
1688
- aria-label="Licensed seat usage"
1689
- aria-valuenow={seatUsageBarPercent}
1690
- aria-valuemin={0}
1691
- aria-valuemax={100}
1692
- aria-valuetext={seatUsageAriaValueText}
1693
- >
1694
- <div
1695
- className={`h-full rounded-full transition-all duration-300 ease-out ${
1696
- seatTone === "breached"
1697
- ? "bg-red-500"
1698
- : seatTone === "approaching"
1699
- ? "bg-amber-500"
1700
- : "bg-emerald-500"
1701
- }`}
1702
- style={{ width: `${seatUsageBarPercent}%` }}
1703
- />
1704
- </div>
1705
- <p className="mt-1.5 text-xs tabular-nums text-gray-500">
1706
- {seatUsageDisplayPercent}% of licensed seats in use
1707
- </p>
1708
- </div>
1709
- )}
1710
-
1711
- {seatTone !== "healthy" && canSeeLicenseAdmin && (
1712
- <div
1713
- role={seatTone === "breached" ? "alert" : "status"}
1714
- className={`mt-4 rounded-lg border p-3.5 ${
1715
- seatTone === "breached"
1716
- ? "border-red-200 bg-red-50"
1717
- : "border-amber-200 bg-amber-50"
1718
- }`}
1719
- >
1720
- <div className="flex items-start gap-2.5">
1721
- {/*
1722
- * Colour comes from className only. Never add a type
1723
- * prop here: Icon emits both the type colour and
1724
- * className into one class attribute, so two colour
1725
- * classes would land on one svg and Tailwind's
1726
- * emission order would pick the winner.
1727
- */}
1728
- <Icon
1729
- icon={
1730
- seatTone === "breached"
1731
- ? IconProp.Alert
1732
- : IconProp.ExclaimationCircle
1733
- }
1734
- className={`mt-0.5 h-4 w-4 shrink-0 ${
1735
- seatTone === "breached"
1736
- ? "text-red-600"
1737
- : "text-amber-600"
1738
- }`}
1739
- />
1740
- <div className="min-w-0 flex-1">
1741
- <h5
1742
- className={`text-sm font-semibold ${
1743
- seatTone === "breached"
1744
- ? "text-red-900"
1745
- : "text-amber-900"
1746
- }`}
1747
- >
1748
- {seatAdvisoryTitle}
1749
- </h5>
1750
- <p
1751
- className={`mt-1 text-xs leading-relaxed ${
1752
- seatTone === "breached"
1753
- ? "text-red-800"
1754
- : "text-amber-800"
1755
- }`}
1756
- >
1757
- {seatAdvisoryBody}
1758
- </p>
1759
- <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
1760
- <Button
1761
- title={
1762
- seatTone === "breached"
1763
- ? "Expand your license"
1764
- : "Request more seats"
1765
- }
1766
- icon={IconProp.Email}
1767
- buttonStyle={
1768
- seatTone === "breached"
1769
- ? ButtonStyleType.DANGER
1770
- : ButtonStyleType.PRIMARY
1771
- }
1772
- onClick={handleRequestMoreSeats}
1773
- className="!mt-0 md:!ml-0"
1774
- />
1775
- <span
1776
- className={`text-[11px] ${
1777
- seatTone === "breached"
1778
- ? "text-red-700"
1779
- : "text-amber-700"
1780
- }`}
1781
- >
1782
- Or email{" "}
1783
- <a
1784
- href={SALES_MAILTO_URL}
1785
- className={`font-medium underline ${
1786
- seatTone === "breached"
1787
- ? "text-red-800 hover:text-red-900"
1788
- : "text-amber-900 hover:text-amber-950"
1789
- }`}
1790
- >
1791
- {SALES_EMAIL}
1792
- </a>{" "}
1793
- directly.
1794
- </span>
1795
- </div>
1796
- </div>
1797
- </div>
1798
- </div>
1799
- )}
1800
-
1801
- <div className="mt-4 border-t border-gray-100 pt-3">
1802
- <p className="text-xs text-gray-500">
1803
- {userCountUpdatedAtText
1804
- ? `Last reported to OneUptime on ${userCountUpdatedAtText}.`
1805
- : "User count has not been reported to OneUptime yet. The first report will be sent within 24 hours."}
1806
- {isSeatLimitEnforced && (
1807
- <>
1808
- {" "}
1809
- This installation enforces the seat limit: users
1810
- above it cannot be invited, signed up or
1811
- provisioned.
1812
- </>
1813
- )}
1814
- {/*
1815
- * Only pointed at somebody who can actually press it.
1816
- * The refresh button is master-admin only, so telling
1817
- * everyone else to use it would be an instruction they
1818
- * cannot follow.
1819
- */}
1820
- {isSeatLimitEnforced && canManageLicense && (
1821
- <>
1822
- {" "}
1823
- Bought more seats? Use &quot;Refresh license&quot;
1824
- below to apply the new limit without waiting for the
1825
- next daily report.
1826
- </>
1827
- )}
1828
- </p>
1829
- </div>
1830
- </section>
1831
- )}
1832
-
1833
- {showLicenseDetails && licenseInstances.length > 0 && (
1834
- <section
1835
- aria-labelledby="edition-instances-heading"
1836
- className="rounded-xl border border-gray-200 bg-white p-5"
1837
- >
1838
- <div className="flex items-center justify-between gap-3">
1839
- <h4
1840
- id="edition-instances-heading"
1841
- className="text-sm font-semibold text-gray-900"
1842
- >
1843
- Instances on this license
1844
- </h4>
1845
- <span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium tabular-nums text-gray-600">
1846
- {licenseInstances.length}{" "}
1847
- {licenseInstances.length === 1
1848
- ? "instance"
1849
- : "instances"}
1850
- </span>
1851
- </div>
1852
- <p className="mt-1 text-xs leading-relaxed text-gray-500">
1853
- Use the same license key on every instance you deploy
1854
- (staging, production, and so on). Each instance reports
1855
- its usage and the version it runs once a day.
1856
- </p>
1857
- {licenseInstances.length > 1 && (
1858
- <p className="mt-1.5 text-xs leading-relaxed text-gray-500">
1859
- {instanceOverlapText}
1860
- </p>
1861
- )}
1862
- <ul className="mt-3 divide-y divide-gray-100 overflow-hidden rounded-lg border border-gray-200">
1863
- {licenseInstances.map(
1864
- (
1865
- instance: EnterpriseLicenseInstanceSummary,
1866
- index: number,
1867
- ) => {
1868
- const isThisInstance: boolean =
1869
- Boolean(thisInstanceId) &&
1870
- instance.instanceId === thisInstanceId;
1871
-
1872
- /*
1873
- * The list is a snapshot of what each instance last
1874
- * reported, refreshed at most once a day. For the
1875
- * instance serving this page the server just told us
1876
- * what it is running right now, so prefer that —
1877
- * otherwise this row contradicts the "This
1878
- * installation" card above it for up to a day after
1879
- * an upgrade. hasComparableVersion keeps a dev build
1880
- * from rendering "vunknown".
1881
- */
1882
- const displayVersion: string | null =
1883
- isThisInstance && hasComparableVersion
1884
- ? currentVersion
1885
- : instance.version;
1886
-
1887
- /*
1888
- * Per-instance, so an admin can see which of their
1889
- * deployments are lagging rather than only the one
1890
- * they happen to be signed in to.
1891
- */
1892
- const isInstanceOutdated: boolean =
1893
- VersionUtil.isUpdateAvailable({
1894
- currentVersion: displayVersion,
1895
- latestVersion: latestVersion,
1896
- });
1897
-
1898
- return (
1899
- <li
1900
- key={instance.instanceId || index}
1901
- className="flex items-center justify-between gap-4 bg-white px-3 py-2.5 transition-colors hover:bg-gray-50"
1902
- >
1903
- <div className="min-w-0 flex-1">
1904
- {/*
1905
- * Wraps rather than squeezing: both pills are
1906
- * shrink-0, so without this a long hostname on
1907
- * a narrow modal absorbs all the overflow and
1908
- * truncates to a few characters.
1909
- */}
1910
- <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
1911
- <span
1912
- className="min-w-0 max-w-full truncate text-sm font-medium text-gray-900"
1913
- title={instance.host || "Unknown host"}
1914
- >
1915
- {instance.host || "Unknown host"}
1916
- </span>
1917
- {isThisInstance && (
1918
- <span className="shrink-0 rounded-full bg-indigo-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-700">
1919
- This instance
1920
- </span>
1921
- )}
1922
- {displayVersion && (
1923
- <span
1924
- className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium tabular-nums ${
1925
- isInstanceOutdated
1926
- ? "bg-amber-50 text-amber-800"
1927
- : "bg-gray-100 text-gray-600"
1928
- }`}
1929
- title={
1930
- isInstanceOutdated
1931
- ? `Running v${displayVersion}. OneUptime v${latestVersion} is available.`
1932
- : `Running v${displayVersion}.`
1933
- }
1934
- >
1935
- v{displayVersion}
1936
- </span>
1937
- )}
1938
- </div>
1939
- <p className="mt-0.5 truncate text-xs text-gray-500">
1940
- {formatInstanceReportedAt(
1941
- instance.lastReportedAt,
1942
- )}
1943
- {isInstanceOutdated
1944
- ? " Update available."
1945
- : ""}
1946
- </p>
1947
- </div>
1948
- <div className="flex shrink-0 items-baseline justify-end gap-1">
1949
- {typeof instance.userCount === "number" ? (
1950
- <>
1951
- <span className="text-sm font-medium tabular-nums text-gray-900">
1952
- {instance.userCount.toLocaleString()}
1953
- </span>
1954
- <span className="text-xs text-gray-500">
1955
- {instance.userCount === 1
1956
- ? "user"
1957
- : "users"}
1958
- </span>
1959
- </>
1960
- ) : (
1961
- <span className="text-sm text-gray-400">
1962
- —
1963
- </span>
1964
- )}
1965
- </div>
1966
- </li>
1967
- );
1968
- },
1969
- )}
1970
- </ul>
1971
- </section>
1972
- )}
1973
-
1974
- {!configError &&
1975
- !isConfigLoading &&
1976
- !licenseValid &&
1977
- globalConfig?.enterpriseLicenseKey && (
1978
- <div className="rounded-xl border border-red-200 bg-red-50 p-4">
1979
- <p className="text-sm font-semibold text-red-900">
1980
- License validation required
1981
- </p>
1982
- <p className="mt-1 text-xs leading-relaxed text-red-800">
1983
- The stored license information could not be verified.
1984
- Please validate the license key again.
1985
- </p>
1986
- </div>
1987
- )}
1988
-
1989
- {showLicenseAdminRequiredNotice && (
1990
- <section className="rounded-xl border border-amber-200 bg-amber-50 p-5">
1991
- <h4 className="text-sm font-semibold text-amber-900">
1992
- A master admin has to activate this license
1993
- </h4>
1994
- <p className="mt-1 text-xs leading-relaxed text-amber-800">
1995
- This installation does not have a valid enterprise
1996
- license. Ask a master admin of this OneUptime installation
1997
- to enter or refresh the license key from this dialog.
1998
- </p>
1999
- </section>
2000
- )}
2001
-
2002
- {!configError && showLicenseKeyInput && (
2003
- <section className="rounded-xl border border-gray-200 bg-white p-5">
2004
- <div className="flex items-start justify-between gap-4">
2005
- <div className="min-w-0">
2006
- <label
2007
- htmlFor="enterprise-license-key"
2008
- className="text-sm font-semibold text-gray-900"
2009
- >
2010
- {isChangingLicense
2011
- ? "New license key"
2012
- : "License key"}
2013
- </label>
2014
- <p className="mt-0.5 text-xs leading-relaxed text-gray-500">
2015
- {licenseKeyHelperText}
2016
- {!isChangingLicense && (
2017
- <>
2018
- {" "}
2019
- <a
2020
- href={SALES_MAILTO_URL}
2021
- className="font-medium text-indigo-600 hover:text-indigo-700"
2022
- >
2023
- {SALES_EMAIL}
2024
- </a>
2025
- .
2026
- </>
2027
- )}
2028
- </p>
2029
- </div>
2030
- {isChangingLicense && (
2031
- <Button
2032
- title="Cancel"
2033
- buttonStyle={ButtonStyleType.NORMAL}
2034
- onClick={handleCancelChangingLicense}
2035
- className="!mt-0 shrink-0 md:!ml-0"
2036
- />
2037
- )}
2038
- </div>
2039
- <div className="mt-3">
2040
- <Input
2041
- id="enterprise-license-key"
2042
- value={licenseKeyInput}
2043
- onChange={(value: string) => {
2044
- setLicenseKeyInput(value);
2045
- licenseInputEditedRef.current = true;
2046
- }}
2047
- placeholder="Enter your enterprise license key"
2048
- disableSpellCheck={true}
2049
- />
2050
- </div>
2051
- {validationError && (
2052
- <div className="mt-3">
2053
- <Alert
2054
- type={AlertType.DANGER}
2055
- title={validationError}
2056
- />
2057
- </div>
2058
- )}
2059
- </section>
2060
- )}
2061
-
2062
- {showEnterpriseFeatureList && (
2063
- <section
2064
- aria-labelledby="edition-features-heading"
2065
- className="rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-5"
2066
- >
2067
- <h4
2068
- id="edition-features-heading"
2069
- className="text-sm font-semibold text-indigo-900"
2070
- >
2071
- What your license unlocks
2072
- </h4>
2073
- <p className="mt-0.5 text-xs text-indigo-700">
2074
- {canManageLicense
2075
- ? "Validate your key above to turn these on immediately."
2076
- : "A master admin can activate the license to turn these on."}
2077
- </p>
2078
- <ul className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
2079
- {enterpriseFeatures.map(
2080
- (feature: string, index: number) => {
2081
- return (
2082
- <li
2083
- key={index}
2084
- className="flex items-start gap-2 rounded-lg border border-gray-100 bg-white px-3 py-2.5"
2085
- >
2086
- <span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-indigo-50">
2087
- <Icon
2088
- icon={IconProp.Check}
2089
- className="h-3 w-3 text-indigo-600"
2090
- />
2091
- </span>
2092
- <span className="text-xs leading-snug text-gray-700">
2093
- {feature}
2094
- </span>
2095
- </li>
2096
- );
2097
- },
2098
- )}
2099
- </ul>
2100
- </section>
2101
- )}
2102
- </>
2103
- ) : (
2104
- <>
2105
- {versionCardElement}
2106
- <p>
2107
- You are running the Community Edition of OneUptime. Here is a
2108
- quick comparison to help you decide if Enterprise is the right
2109
- fit for your team.
2110
- </p>
2111
- <div className="grid gap-4 md:grid-cols-2">
2112
- <div className="rounded-xl border border-gray-200 bg-white p-4">
2113
- <h4 className="text-sm font-semibold text-gray-900">
2114
- Community Edition
2115
- </h4>
2116
- <ul className="mt-3 space-y-2 text-sm text-gray-600">
2117
- {communityFeatures.map(
2118
- (feature: string, index: number) => {
2119
- return (
2120
- <li key={index} className="flex items-start gap-2">
2121
- <Icon
2122
- icon={IconProp.Check}
2123
- className="mt-0.5 h-3 w-3 shrink-0 text-gray-400"
2124
- />
2125
- <span className="leading-snug">{feature}</span>
2126
- </li>
2127
- );
2128
- },
2129
- )}
2130
- </ul>
2131
- <p className="mt-3 text-xs text-gray-500">
2132
- Best for small teams experimenting with reliability
2133
- workflows.
2134
- </p>
2135
- </div>
2136
- <div className="rounded-xl border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-white p-4">
2137
- <h4 className="text-sm font-semibold text-indigo-900">
2138
- Enterprise Edition
2139
- </h4>
2140
- <ul className="mt-3 space-y-2 text-sm text-indigo-900">
2141
- {enterpriseFeatures.map(
2142
- (feature: string, index: number) => {
2143
- return (
2144
- <li key={index} className="flex items-start gap-2">
2145
- <Icon
2146
- icon={IconProp.Check}
2147
- className="mt-0.5 h-3 w-3 shrink-0 text-indigo-600"
2148
- />
2149
- <span className="leading-snug">{feature}</span>
2150
- </li>
2151
- );
2152
- },
2153
- )}
2154
- </ul>
2155
- <p className="mt-3 text-xs text-indigo-700">
2156
- Everything in Community plus white-glove onboarding,
2157
- enterprise SLAs, and a partner dedicated to your
2158
- reliability goals.
2159
- </p>
2160
- </div>
2161
- </div>
2162
- <p className="text-xs text-gray-500">
2163
- Ready to unlock enterprise capabilities? Click &quot;Talk to
2164
- Sales&quot; to start the conversation.
2165
- </p>
2166
- </>
2167
- )}
2168
- </div>
2169
- </Modal>
1761
+ {LicenseManager ? (
1762
+ <LicenseManager
1763
+ license={licenseSnapshot}
1764
+ isDialogOpen={isDialogOpen}
1765
+ reloadLicense={fetchGlobalConfig}
1766
+ applySeatEnforcement={applySeatEnforcementPayload}
1767
+ renderDialog={renderDialog}
1768
+ />
1769
+ ) : (
1770
+ renderDialog({})
2170
1771
  )}
2171
1772
  </>
2172
1773
  );