@oneuptime/common 11.5.13 → 11.6.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 (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -28,12 +28,22 @@ import {
28
28
  } from "../../Config";
29
29
  import Alert, { AlertType } from "../Alerts/Alert";
30
30
  import EnterpriseLicenseInstanceSummary from "../../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
31
+ import VersionUtil from "../../../Utils/VersionUtil";
31
32
 
32
33
  export interface ComponentProps {
33
34
  className?: string | undefined;
34
35
  }
35
36
 
36
37
  const ENTERPRISE_URL: string = "https://oneuptime.com/enterprise/demo";
38
+
39
+ /*
40
+ * Linked only for major upgrades. Points at GitHub rather than the docs site
41
+ * on this installation, because the guide for the version you are moving TO is
42
+ * the one you need, and this installation only ships the guide for the version
43
+ * it is already running.
44
+ */
45
+ const UPGRADE_GUIDE_URL: string =
46
+ "https://github.com/OneUptime/oneuptime/blob/master/App/FeatureSet/Docs/Content/en/installation/upgrading.md";
37
47
  const SALES_EMAIL: string = "sales@oneuptime.com";
38
48
  const SALES_MAILTO_URL: string = "mailto:sales@oneuptime.com";
39
49
 
@@ -85,6 +95,10 @@ const parseLicenseInstances: ParseLicenseInstancesFunction = (
85
95
  typeof instance["lastReportedAt"] === "string"
86
96
  ? instance["lastReportedAt"]
87
97
  : null,
98
+ version:
99
+ typeof instance["version"] === "string" && instance["version"]
100
+ ? instance["version"]
101
+ : null,
88
102
  });
89
103
  }
90
104
 
@@ -127,6 +141,28 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
127
141
  Array<EnterpriseLicenseInstanceSummary>
128
142
  >([]);
129
143
  const [thisInstanceId, setThisInstanceId] = useState<string>("");
144
+ /*
145
+ * Version state comes from the server rather than APP_VERSION in the
146
+ * browser bundle: the frontend env var is empty in every build that was
147
+ * not produced by the release pipeline, which would silently render a
148
+ * blank version instead of an honest one.
149
+ */
150
+ const [currentVersion, setCurrentVersion] = useState<string>("");
151
+ const [latestVersion, setLatestVersion] = useState<string>("");
152
+ const [latestVersionPublishedAt, setLatestVersionPublishedAt] =
153
+ useState<string>("");
154
+ const [latestVersionCheckedAt, setLatestVersionCheckedAt] =
155
+ useState<string>("");
156
+ const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false);
157
+ const [isUpdateCheckDisabled, setIsUpdateCheckDisabled] =
158
+ useState<boolean>(false);
159
+ /*
160
+ * True when the validated license was issued for evaluation/testing rather
161
+ * than production. Purely informational — it drives the evaluation notice in
162
+ * the modal and a small tag on the pill, and never gates any functionality.
163
+ */
164
+ const [isEvaluationLicense, setIsEvaluationLicense] =
165
+ useState<boolean>(false);
130
166
  /*
131
167
  * Validity as computed by the server. The server redacts the license
132
168
  * token for signed-out visitors (e.g. on the login page), so the client
@@ -142,12 +178,13 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
142
178
  return <></>;
143
179
  }
144
180
 
181
+ /*
182
+ * Runs on Community Edition too. The license half of the response is empty
183
+ * there, but the version and update-check half is not — a community
184
+ * installation still wants to be told a newer OneUptime has been released.
185
+ */
145
186
  const fetchGlobalConfig: () => Promise<void> =
146
187
  useCallback(async (): Promise<void> => {
147
- if (!IS_ENTERPRISE_EDITION) {
148
- return;
149
- }
150
-
151
188
  setIsConfigLoading(true);
152
189
  setConfigError("");
153
190
 
@@ -217,6 +254,30 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
217
254
  ? payload["licenseValid"]
218
255
  : null,
219
256
  );
257
+ setIsEvaluationLicense(payload["isEvaluationLicense"] === true);
258
+
259
+ setCurrentVersion(
260
+ typeof payload["currentVersion"] === "string"
261
+ ? payload["currentVersion"]
262
+ : "",
263
+ );
264
+ setLatestVersion(
265
+ typeof payload["latestVersion"] === "string"
266
+ ? payload["latestVersion"]
267
+ : "",
268
+ );
269
+ setLatestVersionPublishedAt(
270
+ typeof payload["latestVersionPublishedAt"] === "string"
271
+ ? payload["latestVersionPublishedAt"]
272
+ : "",
273
+ );
274
+ setLatestVersionCheckedAt(
275
+ typeof payload["latestVersionCheckedAt"] === "string"
276
+ ? payload["latestVersionCheckedAt"]
277
+ : "",
278
+ );
279
+ setIsUpdateAvailable(payload["isUpdateAvailable"] === true);
280
+ setIsUpdateCheckDisabled(payload["isUpdateCheckDisabled"] === true);
220
281
 
221
282
  if (!licenseInputEditedRef.current) {
222
283
  setLicenseKeyInput(configModel.enterpriseLicenseKey || "");
@@ -225,13 +286,30 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
225
286
  setGlobalConfig(null);
226
287
  setLicenseInstances([]);
227
288
  setServerLicenseValid(null);
289
+ setIsEvaluationLicense(false);
290
+ setCurrentVersion("");
291
+ setLatestVersion("");
292
+ setLatestVersionPublishedAt("");
293
+ setLatestVersionCheckedAt("");
294
+ setIsUpdateAvailable(false);
295
+ setIsUpdateCheckDisabled(false);
228
296
  setConfigError(API.getFriendlyMessage(err));
229
297
  } finally {
230
298
  setIsConfigLoading(false);
231
299
  }
232
300
  }, []);
233
301
 
302
+ /*
303
+ * Only Enterprise Edition needs this on mount — its pill reports license
304
+ * validity and seat pressure, which it cannot know without asking. The
305
+ * community pill is a static label, so it waits until the modal is actually
306
+ * opened rather than adding a request to every page load.
307
+ */
234
308
  useEffect(() => {
309
+ if (!IS_ENTERPRISE_EDITION) {
310
+ return;
311
+ }
312
+
235
313
  void fetchGlobalConfig();
236
314
  }, [fetchGlobalConfig]);
237
315
 
@@ -279,6 +357,105 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
279
357
  return expiresAt.toLocaleString();
280
358
  }, [globalConfig?.enterpriseLicenseExpiresAt]);
281
359
 
360
+ /*
361
+ * True only for a build that reports a real semantic version. Dev builds and
362
+ * images assembled without APP_VERSION report "unknown", which is worth
363
+ * showing verbatim but must never be compared against a release.
364
+ */
365
+ const hasComparableVersion: boolean = useMemo(() => {
366
+ return VersionUtil.isValid(currentVersion);
367
+ }, [currentVersion]);
368
+
369
+ const latestVersionPublishedAtText: string | null = useMemo(() => {
370
+ if (!latestVersionPublishedAt) {
371
+ return null;
372
+ }
373
+
374
+ const publishedAt: Date = OneUptimeDate.fromString(
375
+ latestVersionPublishedAt,
376
+ );
377
+
378
+ if (Number.isNaN(publishedAt.getTime())) {
379
+ return null;
380
+ }
381
+
382
+ return publishedAt.toLocaleDateString();
383
+ }, [latestVersionPublishedAt]);
384
+
385
+ const latestVersionCheckedAtText: string | null = useMemo(() => {
386
+ if (!latestVersionCheckedAt) {
387
+ return null;
388
+ }
389
+
390
+ const checkedAt: Date = OneUptimeDate.fromString(latestVersionCheckedAt);
391
+
392
+ if (Number.isNaN(checkedAt.getTime())) {
393
+ return null;
394
+ }
395
+
396
+ return checkedAt.toLocaleString();
397
+ }, [latestVersionCheckedAt]);
398
+
399
+ /*
400
+ * Only claim an installation is current once a release has actually been
401
+ * fetched to compare it against — an air-gapped install that has never
402
+ * reached GitHub knows nothing, and saying "up to date" there would be a
403
+ * guess dressed up as a fact.
404
+ */
405
+ const isUpToDate: boolean = useMemo(() => {
406
+ return (
407
+ hasComparableVersion &&
408
+ VersionUtil.isValid(latestVersion) &&
409
+ !isUpdateAvailable
410
+ );
411
+ }, [hasComparableVersion, latestVersion, isUpdateAvailable]);
412
+
413
+ /*
414
+ * Computed client-side rather than served, because the same comparison has
415
+ * to run per-instance in the list below, where the server has no single
416
+ * "current version" to compare against.
417
+ */
418
+ const isMajorUpgrade: boolean = useMemo(() => {
419
+ return VersionUtil.isMajorUpgrade({
420
+ currentVersion: currentVersion,
421
+ latestVersion: latestVersion,
422
+ });
423
+ }, [currentVersion, latestVersion]);
424
+
425
+ const updateAdvisoryText: string = useMemo(() => {
426
+ const released: string = latestVersionPublishedAtText
427
+ ? `Released on ${latestVersionPublishedAtText}. `
428
+ : "";
429
+
430
+ if (isMajorUpgrade) {
431
+ return `${released}Major versions carry breaking changes and have to be applied one at a time, so check the upgrade guide before you start.`;
432
+ }
433
+
434
+ return `${released}Upgrading picks up the latest fixes and improvements.`;
435
+ }, [isMajorUpgrade, latestVersionPublishedAtText]);
436
+
437
+ /*
438
+ * Null renders no footer at all. Every branch here has to be true of this
439
+ * installation specifically — a line promising a daily check would be a lie
440
+ * on a build that cannot be compared, and a worse one where the check has
441
+ * been turned off.
442
+ */
443
+ const updateFooterText: string | null = useMemo(() => {
444
+ if (isUpdateCheckDisabled) {
445
+ return "Update checks are turned off on this installation (DISABLE_UPDATE_CHECK), so it is not compared against OneUptime releases.";
446
+ }
447
+
448
+ if (!hasComparableVersion) {
449
+ return null;
450
+ }
451
+
452
+ if (!latestVersionCheckedAtText) {
453
+ return "This installation has not checked for updates yet. It checks OneUptime releases on GitHub once a day.";
454
+ }
455
+
456
+ return `Checked for updates on ${latestVersionCheckedAtText}.`;
457
+ }, [isUpdateCheckDisabled, hasComparableVersion, latestVersionCheckedAtText]);
458
+
282
459
  const userLimit: number | null = useMemo(() => {
283
460
  return typeof globalConfig?.enterpriseLicenseUserLimit === "number"
284
461
  ? globalConfig.enterpriseLicenseUserLimit
@@ -619,7 +796,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
619
796
  return "Validate your license key to activate Enterprise Edition.";
620
797
  }
621
798
 
622
- return "License, seat usage, and the instances covered by this key.";
799
+ return "License, version, seat usage, and the instances covered by this key.";
623
800
  }, [isConfigLoading, licenseValid]);
624
801
 
625
802
  /*
@@ -677,9 +854,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
677
854
  setValidationError("");
678
855
  setSuccessMessage("");
679
856
 
680
- if (IS_ENTERPRISE_EDITION) {
681
- void fetchGlobalConfig();
682
- }
857
+ void fetchGlobalConfig();
683
858
  };
684
859
 
685
860
  const closeDialog: () => void = () => {
@@ -825,9 +1000,34 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
825
1000
  const showLicenseDetails: boolean =
826
1001
  IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && licenseValid;
827
1002
 
1003
+ /*
1004
+ * The evaluation notice rides along with the license details: it is only
1005
+ * meaningful once there is a valid license to describe, and it sits at the
1006
+ * top of the body as the first thing an evaluating customer sees.
1007
+ */
1008
+ const showEvaluationNotice: boolean =
1009
+ showLicenseDetails && isEvaluationLicense;
1010
+
1011
+ /*
1012
+ * The pill tag is shown whenever a valid evaluation license is active, so an
1013
+ * admin sees "Evaluation" without opening the modal.
1014
+ */
1015
+ const showEvaluationTag: boolean =
1016
+ IS_ENTERPRISE_EDITION && licenseValid && isEvaluationLicense;
1017
+
828
1018
  const showEnterpriseFeatureList: boolean =
829
1019
  IS_ENTERPRISE_EDITION && !configError && !isConfigLoading && !licenseValid;
830
1020
 
1021
+ /*
1022
+ * Gated on neither edition nor licenseValid: which build you run, and
1023
+ * whether a newer one exists, is a property of the installation rather than
1024
+ * of the license, and is useful precisely when the license needs sorting
1025
+ * out. The server returns currentVersion only to a signed-in user, so an
1026
+ * anonymous visitor on the login page falls out here.
1027
+ */
1028
+ const showVersionCard: boolean =
1029
+ !configError && !isConfigLoading && Boolean(currentVersion);
1030
+
831
1031
  /*
832
1032
  * GlobalConfigAPI returns userLimit and currentUserCount to anonymous callers
833
1033
  * because the same route serves the signed-out login page, and gates only
@@ -913,6 +1113,176 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
913
1113
  ? "text-[11px] text-amber-600 group-hover:text-amber-700"
914
1114
  : "text-[11px] text-indigo-500 group-hover:text-indigo-600";
915
1115
 
1116
+ /*
1117
+ * Rendered in both the enterprise and the community branch of the modal:
1118
+ * the version and update state belong to the installation, not the license.
1119
+ */
1120
+ const versionCardElement: ReactElement | null = showVersionCard ? (
1121
+ <section
1122
+ aria-labelledby="edition-version-heading"
1123
+ className={`overflow-hidden rounded-xl border ${
1124
+ isUpdateAvailable ? "border-amber-200" : "border-gray-200"
1125
+ } bg-white`}
1126
+ >
1127
+ <div className="flex items-center justify-between gap-4 p-5">
1128
+ <div className="min-w-0">
1129
+ <h4
1130
+ id="edition-version-heading"
1131
+ className="text-sm font-semibold text-gray-900"
1132
+ >
1133
+ This installation
1134
+ </h4>
1135
+ <p className="mt-1.5 flex items-baseline gap-2">
1136
+ <span
1137
+ className={`text-2xl font-semibold leading-none tabular-nums ${
1138
+ hasComparableVersion ? "text-gray-900" : "text-gray-400"
1139
+ }`}
1140
+ >
1141
+ {hasComparableVersion ? `v${currentVersion}` : currentVersion}
1142
+ </span>
1143
+ </p>
1144
+ {!hasComparableVersion && (
1145
+ <p className="mt-1.5 text-xs leading-relaxed text-gray-500">
1146
+ This build does not report a version number, so it cannot be
1147
+ compared against the latest OneUptime release.
1148
+ </p>
1149
+ )}
1150
+ </div>
1151
+ {(isUpdateAvailable || isUpToDate) && (
1152
+ <span
1153
+ className={`inline-flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${
1154
+ isUpdateAvailable
1155
+ ? "bg-amber-100 text-amber-800"
1156
+ : "bg-emerald-50 text-emerald-700"
1157
+ }`}
1158
+ >
1159
+ {/*
1160
+ * Colour comes from className only — see the note on
1161
+ * the seat advisory icon below.
1162
+ */}
1163
+ <Icon
1164
+ icon={
1165
+ isUpdateAvailable
1166
+ ? IconProp.ArrowCircleUp
1167
+ : IconProp.CheckCircle
1168
+ }
1169
+ className={`h-3.5 w-3.5 shrink-0 ${
1170
+ isUpdateAvailable ? "text-amber-700" : "text-emerald-600"
1171
+ }`}
1172
+ />
1173
+ {isUpdateAvailable ? "Update available" : "Up to date"}
1174
+ </span>
1175
+ )}
1176
+ </div>
1177
+
1178
+ {isUpdateAvailable && (
1179
+ <div
1180
+ role="status"
1181
+ className="border-t border-amber-200 bg-amber-50 px-5 py-4"
1182
+ >
1183
+ <h5 className="text-sm font-semibold text-amber-900">
1184
+ {isMajorUpgrade
1185
+ ? `OneUptime v${latestVersion} is a major upgrade`
1186
+ : `OneUptime v${latestVersion} is available`}
1187
+ </h5>
1188
+ <p className="mt-1 text-xs leading-relaxed text-amber-800">
1189
+ {updateAdvisoryText}
1190
+ </p>
1191
+ {/*
1192
+ * Only a major upgrade gets a call to action. Minor and patch
1193
+ * releases are routine — a button on every one of them trains
1194
+ * administrators to dismiss the banner, and there is nothing to read
1195
+ * before taking them. A major has breaking changes and must be
1196
+ * applied one major at a time, so it earns the interruption.
1197
+ */}
1198
+ {isMajorUpgrade && (
1199
+ <div className="mt-3">
1200
+ <a
1201
+ href={UPGRADE_GUIDE_URL}
1202
+ target="_blank"
1203
+ rel="noopener noreferrer"
1204
+ /*
1205
+ * Indigo, not amber: the seat advisory directly
1206
+ * below is an amber panel with an indigo primary
1207
+ * button, and two different accent colours for
1208
+ * the same role would read as two conventions.
1209
+ */
1210
+ className="inline-flex items-center gap-1.5 rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition hover:bg-indigo-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2"
1211
+ >
1212
+ Read the upgrade guide
1213
+ <Icon
1214
+ icon={IconProp.ExternalLink}
1215
+ className="h-3 w-3 shrink-0 text-white"
1216
+ />
1217
+ </a>
1218
+ </div>
1219
+ )}
1220
+ </div>
1221
+ )}
1222
+
1223
+ {!isUpdateAvailable && updateFooterText && (
1224
+ <div className="border-t border-gray-100 px-5 py-3">
1225
+ <p className="text-xs text-gray-500">{updateFooterText}</p>
1226
+ </div>
1227
+ )}
1228
+ </section>
1229
+ ) : null;
1230
+
1231
+ /*
1232
+ * The evaluation notice. Violet rather than amber or red: this is not a
1233
+ * warning about something wrong, it is a statement of what the license is
1234
+ * for. Amber is spoken for by seat pressure and available updates, so a
1235
+ * distinct hue keeps the two from being read as the same kind of message.
1236
+ */
1237
+ const evaluationNoticeElement: ReactElement | null = showEvaluationNotice ? (
1238
+ <section
1239
+ aria-labelledby="edition-evaluation-heading"
1240
+ className="overflow-hidden rounded-xl border border-violet-200 bg-gradient-to-br from-violet-50 via-white to-white"
1241
+ >
1242
+ <div className="flex items-start gap-3.5 p-5">
1243
+ <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-violet-100 ring-4 ring-violet-50">
1244
+ {/*
1245
+ * Colour comes from className only — never add a type prop here, for
1246
+ * the reason spelled out on the seat advisory icon below.
1247
+ */}
1248
+ <Icon icon={IconProp.Beaker} className="h-5 w-5 text-violet-600" />
1249
+ </span>
1250
+ <div className="min-w-0 flex-1">
1251
+ <div className="flex flex-wrap items-center gap-2">
1252
+ <h4
1253
+ id="edition-evaluation-heading"
1254
+ className="text-sm font-semibold text-violet-900"
1255
+ >
1256
+ Evaluation license
1257
+ </h4>
1258
+ <span className="inline-flex items-center rounded-full bg-violet-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-700">
1259
+ Testing only
1260
+ </span>
1261
+ </div>
1262
+ <p className="mt-1.5 text-xs leading-relaxed text-violet-800">
1263
+ This key was issued for evaluation and testing. It is not licensed
1264
+ for production use — reach out whenever you are ready to go live and
1265
+ we will get a production license sorted for you.
1266
+ </p>
1267
+ <div className="mt-3">
1268
+ <a
1269
+ href={`${SALES_MAILTO_URL}?subject=${encodeURIComponent(
1270
+ "Moving our OneUptime evaluation to production",
1271
+ )}`}
1272
+ className="inline-flex items-center gap-1.5 rounded-md bg-violet-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition hover:bg-violet-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2"
1273
+ >
1274
+ <Icon
1275
+ icon={IconProp.Email}
1276
+ className="h-3 w-3 shrink-0 text-white"
1277
+ />
1278
+ Talk to sales about production
1279
+ </a>
1280
+ </div>
1281
+ </div>
1282
+ </div>
1283
+ </section>
1284
+ ) : null;
1285
+
916
1286
  return (
917
1287
  <>
918
1288
  <button
@@ -924,7 +1294,9 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
924
1294
  * Name) so voice-control users can activate it by the words they see
925
1295
  * ("{editionName}" and "{ctaLabel}", e.g. "Learn more").
926
1296
  */
927
- aria-label={`${editionName}, ${ctaLabel}`}
1297
+ aria-label={`${editionName}${
1298
+ showEvaluationTag ? ", Evaluation" : ""
1299
+ }, ${ctaLabel}`}
928
1300
  >
929
1301
  {pillTone !== "normal" && (
930
1302
  <Icon
@@ -942,6 +1314,11 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
942
1314
  className={`h-2 w-2 rounded-full transition group-hover:scale-110 ${indicatorColor}`}
943
1315
  ></span>
944
1316
  <span className="tracking-wide">{editionName}</span>
1317
+ {showEvaluationTag && (
1318
+ <span className="inline-flex items-center rounded-full bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-700">
1319
+ Evaluation
1320
+ </span>
1321
+ )}
945
1322
  <span className={pillCtaTextClassName}>{ctaLabel}</span>
946
1323
  </button>
947
1324
 
@@ -997,6 +1374,8 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
997
1374
  </div>
998
1375
  )}
999
1376
 
1377
+ {evaluationNoticeElement}
1378
+
1000
1379
  {showLicenseDetails && (
1001
1380
  <dl className="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-gray-200 bg-gray-200 sm:grid-cols-2">
1002
1381
  <div className="min-w-0 bg-white px-4 py-3">
@@ -1021,6 +1400,8 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1021
1400
  </dl>
1022
1401
  )}
1023
1402
 
1403
+ {versionCardElement}
1404
+
1024
1405
  {showLicenseDetails && (
1025
1406
  <section
1026
1407
  aria-labelledby="edition-seats-heading"
@@ -1240,7 +1621,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1240
1621
  <p className="mt-1 text-xs leading-relaxed text-gray-500">
1241
1622
  Use the same license key on every instance you deploy
1242
1623
  (staging, production, and so on). Each instance reports
1243
- its usage daily.
1624
+ its usage and the version it runs once a day.
1244
1625
  </p>
1245
1626
  {licenseInstances.length > 1 && (
1246
1627
  <p className="mt-1.5 text-xs leading-relaxed text-gray-500">
@@ -1257,15 +1638,47 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1257
1638
  Boolean(thisInstanceId) &&
1258
1639
  instance.instanceId === thisInstanceId;
1259
1640
 
1641
+ /*
1642
+ * The list is a snapshot of what each instance last
1643
+ * reported, refreshed at most once a day. For the
1644
+ * instance serving this page the server just told us
1645
+ * what it is running right now, so prefer that —
1646
+ * otherwise this row contradicts the "This
1647
+ * installation" card above it for up to a day after
1648
+ * an upgrade. hasComparableVersion keeps a dev build
1649
+ * from rendering "vunknown".
1650
+ */
1651
+ const displayVersion: string | null =
1652
+ isThisInstance && hasComparableVersion
1653
+ ? currentVersion
1654
+ : instance.version;
1655
+
1656
+ /*
1657
+ * Per-instance, so an admin can see which of their
1658
+ * deployments are lagging rather than only the one
1659
+ * they happen to be signed in to.
1660
+ */
1661
+ const isInstanceOutdated: boolean =
1662
+ VersionUtil.isUpdateAvailable({
1663
+ currentVersion: displayVersion,
1664
+ latestVersion: latestVersion,
1665
+ });
1666
+
1260
1667
  return (
1261
1668
  <li
1262
1669
  key={instance.instanceId || index}
1263
1670
  className="flex items-center justify-between gap-4 bg-white px-3 py-2.5 transition-colors hover:bg-gray-50"
1264
1671
  >
1265
1672
  <div className="min-w-0 flex-1">
1266
- <div className="flex min-w-0 items-center gap-2">
1673
+ {/*
1674
+ * Wraps rather than squeezing: both pills are
1675
+ * shrink-0, so without this a long hostname on
1676
+ * a narrow modal absorbs all the overflow and
1677
+ * truncates to a few characters.
1678
+ */}
1679
+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
1267
1680
  <span
1268
- className="truncate text-sm font-medium text-gray-900"
1681
+ className="min-w-0 max-w-full truncate text-sm font-medium text-gray-900"
1269
1682
  title={instance.host || "Unknown host"}
1270
1683
  >
1271
1684
  {instance.host || "Unknown host"}
@@ -1275,11 +1688,30 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1275
1688
  This instance
1276
1689
  </span>
1277
1690
  )}
1691
+ {displayVersion && (
1692
+ <span
1693
+ className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium tabular-nums ${
1694
+ isInstanceOutdated
1695
+ ? "bg-amber-50 text-amber-800"
1696
+ : "bg-gray-100 text-gray-600"
1697
+ }`}
1698
+ title={
1699
+ isInstanceOutdated
1700
+ ? `Running v${displayVersion}. OneUptime v${latestVersion} is available.`
1701
+ : `Running v${displayVersion}.`
1702
+ }
1703
+ >
1704
+ v{displayVersion}
1705
+ </span>
1706
+ )}
1278
1707
  </div>
1279
1708
  <p className="mt-0.5 truncate text-xs text-gray-500">
1280
1709
  {formatInstanceReportedAt(
1281
1710
  instance.lastReportedAt,
1282
1711
  )}
1712
+ {isInstanceOutdated
1713
+ ? " Update available."
1714
+ : ""}
1283
1715
  </p>
1284
1716
  </div>
1285
1717
  <div className="flex shrink-0 items-baseline justify-end gap-1">
@@ -1424,6 +1856,7 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
1424
1856
  </>
1425
1857
  ) : (
1426
1858
  <>
1859
+ {versionCardElement}
1427
1860
  <p>
1428
1861
  You are running the Community Edition of OneUptime. Here is a
1429
1862
  quick comparison to help you decide if Enterprise is the right