@oneuptime/common 12.0.27 → 12.0.29

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 (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,422 @@
1
+ /** @timezone UTC */
2
+ /**
3
+ * The reported bug, exercised through the two pickers that actually sit above
4
+ * the Metrics, Traces and Logs explorers: pick a custom window, and the button
5
+ * summarising it must speak the clock and the timezone the person's computer is
6
+ * set to.
7
+ *
8
+ * It did not. The label was assembled by reading the digits off the Date
9
+ * (`date.getHours().toString().padStart(2, "0")`), which is a 24-hour clock in
10
+ * the browser process's zone and nothing else - so a user on an AM/PM machine
11
+ * picked 2:30 PM in the modal and got a button reading "Mar 1, 14:30", and a
12
+ * user who had set a timezone in User Settings got a button disagreeing with
13
+ * the modal they had just set the window in.
14
+ *
15
+ * These render the real components rather than calling the formatter, because
16
+ * the formatter being right is worth nothing if the button stops reading from
17
+ * it. The timezone is pinned to UTC by the docblock above and the 12/24-hour
18
+ * preference is pinned per test, so the assertions hold on any machine.
19
+ */
20
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
21
+ import OneUptimeDate from "../../../Types/Date";
22
+ import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
23
+ import TimeRange from "../../../Types/Time/TimeRange";
24
+ import Timezone from "../../../Types/Timezone";
25
+ import LogTimeRangePicker, {
26
+ LOG_TIME_RANGE_PICKER_TEST_ID_PREFIX,
27
+ } from "../../../UI/Components/LogsViewer/components/LogTimeRangePicker";
28
+ import TelemetryTimeRangePicker, {
29
+ TELEMETRY_TIME_RANGE_PICKER_TEST_ID_PREFIX,
30
+ } from "../../../UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker";
31
+ import "@testing-library/jest-dom";
32
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
33
+ import React from "react";
34
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
35
+ import fs from "fs";
36
+ import path from "path";
37
+
38
+ /*
39
+ * An afternoon window, so a 12-hour clock has to do real work: 14:30 becoming
40
+ * "2:30 PM" is the exact translation the bug was skipping.
41
+ */
42
+ const AFTERNOON_WINDOW: InBetween<Date> = new InBetween<Date>(
43
+ new Date("2024-03-01T14:30:00.000Z"),
44
+ new Date("2024-03-01T18:45:00.000Z"),
45
+ );
46
+
47
+ const CUSTOM_AFTERNOON: RangeStartAndEndDateTime = {
48
+ range: TimeRange.CUSTOM,
49
+ startAndEndDate: AFTERNOON_WINDOW,
50
+ };
51
+
52
+ interface Surface {
53
+ name: string;
54
+ prefix: string;
55
+ render: (value: RangeStartAndEndDateTime) => void;
56
+ }
57
+
58
+ const SURFACES: Array<Surface> = [
59
+ {
60
+ name: "metrics and traces",
61
+ prefix: TELEMETRY_TIME_RANGE_PICKER_TEST_ID_PREFIX,
62
+ render: (value: RangeStartAndEndDateTime): void => {
63
+ render(
64
+ <TelemetryTimeRangePicker
65
+ value={value}
66
+ onChange={() => {
67
+ // The label under test does not depend on the change handler.
68
+ }}
69
+ />,
70
+ );
71
+ },
72
+ },
73
+ {
74
+ name: "logs",
75
+ prefix: LOG_TIME_RANGE_PICKER_TEST_ID_PREFIX,
76
+ render: (value: RangeStartAndEndDateTime): void => {
77
+ render(
78
+ <LogTimeRangePicker
79
+ value={value}
80
+ onChange={() => {
81
+ // The label under test does not depend on the change handler.
82
+ }}
83
+ />,
84
+ );
85
+ },
86
+ },
87
+ ];
88
+
89
+ function pin(use12HourFormat: boolean): void {
90
+ jest
91
+ .spyOn(OneUptimeDate, "getUserPrefers12HourFormat")
92
+ .mockReturnValue(use12HourFormat);
93
+ }
94
+
95
+ function buttonText(prefix: string): string {
96
+ return (screen.getByTestId(`${prefix}-button`).textContent || "").trim();
97
+ }
98
+
99
+ afterEach(() => {
100
+ cleanup();
101
+ jest.restoreAllMocks();
102
+ OneUptimeDate.setUserTimezone(null);
103
+ });
104
+
105
+ describe.each(SURFACES)(
106
+ "the $name time range picker button",
107
+ (surface: Surface) => {
108
+ describe("on a machine set to a 12-hour clock", () => {
109
+ test("writes the custom window with AM/PM", () => {
110
+ pin(true);
111
+ surface.render(CUSTOM_AFTERNOON);
112
+
113
+ expect(buttonText(surface.prefix)).toBe(
114
+ "Mar 1, 2:30 PM – Mar 1, 6:45 PM",
115
+ );
116
+ });
117
+
118
+ test("never leaves a bare 24-hour reading on the button", () => {
119
+ pin(true);
120
+ surface.render(CUSTOM_AFTERNOON);
121
+
122
+ const label: string = buttonText(surface.prefix);
123
+
124
+ expect(label).toContain("PM");
125
+ expect(label).not.toContain("14:30");
126
+ expect(label).not.toContain("18:45");
127
+ });
128
+
129
+ test("marks a window that straddles noon AM then PM", () => {
130
+ pin(true);
131
+ surface.render({
132
+ range: TimeRange.CUSTOM,
133
+ startAndEndDate: new InBetween<Date>(
134
+ new Date("2024-03-01T08:00:00.000Z"),
135
+ new Date("2024-03-01T20:00:00.000Z"),
136
+ ),
137
+ });
138
+
139
+ expect(buttonText(surface.prefix)).toBe(
140
+ "Mar 1, 8:00 AM – Mar 1, 8:00 PM",
141
+ );
142
+ });
143
+
144
+ test("writes a whole day from 12 AM to 11:59 PM", () => {
145
+ pin(true);
146
+ surface.render({
147
+ range: TimeRange.CUSTOM,
148
+ startAndEndDate: new InBetween<Date>(
149
+ new Date("2024-03-01T00:00:00.000Z"),
150
+ new Date("2024-03-01T23:59:00.000Z"),
151
+ ),
152
+ });
153
+
154
+ expect(buttonText(surface.prefix)).toBe(
155
+ "Mar 1, 12:00 AM – Mar 1, 11:59 PM",
156
+ );
157
+ });
158
+ });
159
+
160
+ describe("on a machine set to a 24-hour clock", () => {
161
+ test("writes the custom window on a 24-hour clock", () => {
162
+ pin(false);
163
+ surface.render(CUSTOM_AFTERNOON);
164
+
165
+ expect(buttonText(surface.prefix)).toBe("Mar 1, 14:30 – Mar 1, 18:45");
166
+ });
167
+
168
+ test("puts no day period on the button", () => {
169
+ pin(false);
170
+ surface.render(CUSTOM_AFTERNOON);
171
+
172
+ expect(buttonText(surface.prefix)).not.toMatch(/AM|PM/);
173
+ });
174
+
175
+ test("pads a single-digit hour", () => {
176
+ pin(false);
177
+ surface.render({
178
+ range: TimeRange.CUSTOM,
179
+ startAndEndDate: new InBetween<Date>(
180
+ new Date("2024-03-01T08:00:00.000Z"),
181
+ new Date("2024-03-01T09:05:00.000Z"),
182
+ ),
183
+ });
184
+
185
+ expect(buttonText(surface.prefix)).toBe("Mar 1, 08:00 – Mar 1, 09:05");
186
+ });
187
+ });
188
+
189
+ describe("timezone", () => {
190
+ test("reads the window in the timezone set in User Settings", () => {
191
+ pin(true);
192
+ OneUptimeDate.setUserTimezone(Timezone.AsiaKolkata);
193
+ surface.render(CUSTOM_AFTERNOON);
194
+
195
+ // 14:30 and 18:45 UTC are 20:00 and 00:15 (next day) at +05:30.
196
+ expect(buttonText(surface.prefix)).toBe(
197
+ "Mar 1, 8:00 PM – Mar 2, 12:15 AM",
198
+ );
199
+ });
200
+
201
+ test("rolls the date back when the configured zone is behind UTC", () => {
202
+ pin(false);
203
+ OneUptimeDate.setUserTimezone(Timezone.AmericaLos_Angeles);
204
+ surface.render({
205
+ range: TimeRange.CUSTOM,
206
+ startAndEndDate: new InBetween<Date>(
207
+ new Date("2024-03-01T02:00:00.000Z"),
208
+ new Date("2024-03-01T10:00:00.000Z"),
209
+ ),
210
+ });
211
+
212
+ // 02:00 UTC is 18:00 the previous evening in Los Angeles (PST).
213
+ expect(buttonText(surface.prefix)).toBe("Feb 29, 18:00 – Mar 1, 02:00");
214
+ });
215
+
216
+ test("agrees with the summary inside the modal that set the window", () => {
217
+ /*
218
+ * The two used to disagree outright: the modal summary already went
219
+ * through the timezone- and clock-aware formatter while the button did
220
+ * not, so the same window read two different ways on one screen.
221
+ */
222
+ pin(true);
223
+ OneUptimeDate.setUserTimezone(Timezone.AsiaKolkata);
224
+ surface.render(CUSTOM_AFTERNOON);
225
+
226
+ fireEvent.click(screen.getByTestId(`${surface.prefix}-button`));
227
+ fireEvent.click(screen.getByTestId(`${surface.prefix}-custom-option`));
228
+
229
+ const summary: string =
230
+ screen.getByTestId("custom-time-range-summary").textContent || "";
231
+
232
+ /*
233
+ * The summary carries seconds and a year the compact button label
234
+ * drops, so compare the parts they both claim: the wall clock.
235
+ */
236
+ expect(summary).toContain("8:00:00 PM");
237
+ expect(summary).toContain("12:15:00 AM");
238
+ expect(buttonText(surface.prefix)).toContain("8:00 PM");
239
+ expect(buttonText(surface.prefix)).toContain("12:15 AM");
240
+ });
241
+ });
242
+
243
+ describe("presets are unaffected", () => {
244
+ test("shows the preset's own label on a 12-hour machine", () => {
245
+ pin(true);
246
+ surface.render({ range: TimeRange.PAST_ONE_HOUR });
247
+
248
+ expect(buttonText(surface.prefix)).toBe("Past 1 Hour");
249
+ });
250
+
251
+ test("shows the preset's own label on a 24-hour machine", () => {
252
+ pin(false);
253
+ surface.render({ range: TimeRange.PAST_ONE_HOUR });
254
+
255
+ expect(buttonText(surface.prefix)).toBe("Past 1 Hour");
256
+ });
257
+
258
+ test("shows no time at all for a preset", () => {
259
+ pin(true);
260
+ surface.render({ range: TimeRange.PAST_THIRTY_MINS });
261
+
262
+ expect(buttonText(surface.prefix)).not.toMatch(/\d{1,2}:\d{2}/);
263
+ });
264
+ });
265
+ },
266
+ );
267
+
268
+ describe("applying a window from the modal", () => {
269
+ test("re-labels the button on the machine's clock once applied", () => {
270
+ /*
271
+ * End to end: open the picker, pick the custom editor, type a window,
272
+ * apply, and read back what the button now says. This is the path the bug
273
+ * report walks.
274
+ */
275
+ pin(true);
276
+
277
+ const emitted: Array<RangeStartAndEndDateTime> = [];
278
+ const prefix: string = TELEMETRY_TIME_RANGE_PICKER_TEST_ID_PREFIX;
279
+
280
+ const view: { rerender: (ui: React.ReactElement) => void } = render(
281
+ <TelemetryTimeRangePicker
282
+ value={{ range: TimeRange.PAST_ONE_HOUR }}
283
+ onChange={(next: RangeStartAndEndDateTime) => {
284
+ emitted.push(next);
285
+ }}
286
+ />,
287
+ );
288
+
289
+ fireEvent.click(screen.getByTestId(`${prefix}-button`));
290
+ fireEvent.click(screen.getByTestId(`${prefix}-custom-option`));
291
+
292
+ fireEvent.change(screen.getByTestId("custom-time-range-start"), {
293
+ target: { value: "2024-03-01T14:30:00" },
294
+ });
295
+ fireEvent.change(screen.getByTestId("custom-time-range-end"), {
296
+ target: { value: "2024-03-01T18:45:00" },
297
+ });
298
+
299
+ fireEvent.click(screen.getByText("Apply"));
300
+
301
+ expect(emitted).toHaveLength(1);
302
+ expect(emitted[0]?.range).toBe(TimeRange.CUSTOM);
303
+
304
+ // The parent re-renders the picker with the window it was handed.
305
+ view.rerender(
306
+ <TelemetryTimeRangePicker
307
+ value={emitted[0] as RangeStartAndEndDateTime}
308
+ onChange={() => {
309
+ // Not exercised again.
310
+ }}
311
+ />,
312
+ );
313
+
314
+ expect(buttonText(prefix)).toBe("Mar 1, 2:30 PM – Mar 1, 6:45 PM");
315
+ });
316
+
317
+ test("re-labels the same applied window on a 24-hour machine", () => {
318
+ pin(false);
319
+
320
+ const emitted: Array<RangeStartAndEndDateTime> = [];
321
+ const prefix: string = LOG_TIME_RANGE_PICKER_TEST_ID_PREFIX;
322
+
323
+ const view: { rerender: (ui: React.ReactElement) => void } = render(
324
+ <LogTimeRangePicker
325
+ value={{ range: TimeRange.PAST_ONE_HOUR }}
326
+ onChange={(next: RangeStartAndEndDateTime) => {
327
+ emitted.push(next);
328
+ }}
329
+ />,
330
+ );
331
+
332
+ fireEvent.click(screen.getByTestId(`${prefix}-button`));
333
+ fireEvent.click(screen.getByTestId(`${prefix}-custom-option`));
334
+
335
+ fireEvent.change(screen.getByTestId("custom-time-range-start"), {
336
+ target: { value: "2024-03-01T14:30:00" },
337
+ });
338
+ fireEvent.change(screen.getByTestId("custom-time-range-end"), {
339
+ target: { value: "2024-03-01T18:45:00" },
340
+ });
341
+
342
+ fireEvent.click(screen.getByText("Apply"));
343
+
344
+ view.rerender(
345
+ <LogTimeRangePicker
346
+ value={emitted[0] as RangeStartAndEndDateTime}
347
+ onChange={() => {
348
+ // Not exercised again.
349
+ }}
350
+ />,
351
+ );
352
+
353
+ expect(buttonText(prefix)).toBe("Mar 1, 14:30 – Mar 1, 18:45");
354
+ });
355
+ });
356
+
357
+ /*
358
+ * The defect was not a wrong format string - it was a formatter hand-rolled out
359
+ * of Date's own getters, which cannot see either the configured timezone or the
360
+ * machine's clock preference. A rendering test cannot catch that coming back on
361
+ * some future surface, so this reads the source and refuses the idiom outright.
362
+ */
363
+ describe("the picker never hand-rolls a date format again", () => {
364
+ /*
365
+ * Comments are stripped first: the banned tokens are exactly the words this
366
+ * file's own comments use to explain what went wrong, so a guard that reads
367
+ * them would fail on a prose edit that changes no behaviour.
368
+ */
369
+ const PICKER_SOURCE: string = fs
370
+ .readFileSync(
371
+ path.join(
372
+ __dirname,
373
+ "..",
374
+ "..",
375
+ "..",
376
+ "UI",
377
+ "Components",
378
+ "Date",
379
+ "TimeRangePickerDropdown.tsx",
380
+ ),
381
+ "utf-8",
382
+ )
383
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
384
+ .replace(/\/\/.*$/gm, " ");
385
+
386
+ test("reads no wall-clock field off the Date itself", () => {
387
+ /*
388
+ * getHours/getMinutes/getDate report the browser process's zone, never the
389
+ * one the user configured, so none of them belongs in a rendered label.
390
+ */
391
+ for (const banned of [
392
+ ".getHours()",
393
+ ".getMinutes()",
394
+ ".getDate()",
395
+ ".getMonth()",
396
+ ]) {
397
+ expect(PICKER_SOURCE).not.toContain(banned);
398
+ }
399
+ });
400
+
401
+ test("pins no locale of its own", () => {
402
+ /*
403
+ * Match the date-shaped calls only. A bare "toLocaleString" ban would also
404
+ * catch Number.prototype.toLocaleString, which formats counts and has
405
+ * nothing to do with this bug.
406
+ */
407
+ expect(PICKER_SOURCE).not.toContain('"en-US"');
408
+ expect(PICKER_SOURCE).not.toMatch(
409
+ /toLocale(Date|Time)?String\s*\(\s*["'`[]/,
410
+ );
411
+ });
412
+
413
+ test("hardcodes no hour cycle", () => {
414
+ expect(PICKER_SOURCE).not.toContain("hour12");
415
+ });
416
+
417
+ test("goes through the shared timezone- and clock-aware formatter", () => {
418
+ expect(PICKER_SOURCE).toContain(
419
+ "OneUptimeDate.getDateAsLocalShortDateTimeString",
420
+ );
421
+ });
422
+ });
@@ -8,10 +8,11 @@ import InBetween from "../../../Types/BaseDatabase/InBetween";
8
8
  import OneUptimeDate from "../../../Types/Date";
9
9
  import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
10
10
  import TimeRange from "../../../Types/Time/TimeRange";
11
+ import Timezone from "../../../Types/Timezone";
11
12
  import "@testing-library/jest-dom";
12
13
  import { cleanup, fireEvent, render, screen } from "@testing-library/react";
13
14
  import React from "react";
14
- import { afterEach, describe, expect, test } from "@jest/globals";
15
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
15
16
 
16
17
  const PREFIX: string = "test-range-picker";
17
18
  const DROPDOWN_WIDTH_IN_PX: number = 288;
@@ -71,8 +72,31 @@ function minutesBetweenFields(): number {
71
72
 
72
73
  afterEach(() => {
73
74
  cleanup();
75
+ jest.restoreAllMocks();
76
+ OneUptimeDate.setUserTimezone(null);
74
77
  });
75
78
 
79
+ /*
80
+ * The label writes times the way the machine running the browser writes them,
81
+ * so a test that does not pin the preference reads "Mar 1, 10:00" on a European
82
+ * laptop and "Mar 1, 10:00 AM" on a US one. Pinning it is what lets these
83
+ * assert the digits rather than a regex loose enough to accept the time going
84
+ * missing altogether.
85
+ */
86
+ function pin(use12HourFormat: boolean): void {
87
+ jest
88
+ .spyOn(OneUptimeDate, "getUserPrefers12HourFormat")
89
+ .mockReturnValue(use12HourFormat);
90
+ }
91
+
92
+ const CUSTOM_RANGE: RangeStartAndEndDateTime = {
93
+ range: TimeRange.CUSTOM,
94
+ startAndEndDate: new InBetween<Date>(
95
+ new Date("2024-03-01T10:00:00.000Z"),
96
+ new Date("2024-03-02T13:30:00.000Z"),
97
+ ),
98
+ };
99
+
76
100
  describe("getTimeRangeButtonLabel", () => {
77
101
  test("uses the preset label for a preset range", () => {
78
102
  expect(getTimeRangeButtonLabel({ range: TimeRange.PAST_THIRTY_MINS })).toBe(
@@ -87,15 +111,129 @@ describe("getTimeRangeButtonLabel", () => {
87
111
  });
88
112
 
89
113
  test("summarises a custom range as a start–end pair", () => {
90
- const label: string = getTimeRangeButtonLabel({
91
- range: TimeRange.CUSTOM,
92
- startAndEndDate: new InBetween<Date>(
93
- new Date("2024-03-01T10:00:00.000Z"),
94
- new Date("2024-03-02T13:30:00.000Z"),
95
- ),
114
+ pin(false);
115
+
116
+ expect(getTimeRangeButtonLabel(CUSTOM_RANGE)).toBe(
117
+ "Mar 1, 10:00 – Mar 2, 13:30",
118
+ );
119
+ });
120
+
121
+ /*
122
+ * The bug these cover: the label was built by reading the digits off the
123
+ * Date - `date.getHours().toString().padStart(2, "0")` - so it was always a
124
+ * 24-hour clock in the browser process's own zone. A user whose computer is
125
+ * set to AM/PM picked an afternoon window and got "Mar 1, 14:30" back, and a
126
+ * user with a timezone set in User Settings got a label that disagreed with
127
+ * the modal they had just picked the window in.
128
+ */
129
+ describe("clock format", () => {
130
+ test("writes both ends with AM/PM on a 12-hour machine", () => {
131
+ pin(true);
132
+
133
+ expect(getTimeRangeButtonLabel(CUSTOM_RANGE)).toBe(
134
+ "Mar 1, 10:00 AM – Mar 2, 1:30 PM",
135
+ );
136
+ });
137
+
138
+ test("writes both ends on a 24-hour clock on a 24-hour machine", () => {
139
+ pin(false);
140
+
141
+ const label: string = getTimeRangeButtonLabel(CUSTOM_RANGE);
142
+
143
+ expect(label).toBe("Mar 1, 10:00 – Mar 2, 13:30");
144
+ expect(label).not.toMatch(/AM|PM/);
145
+ });
146
+
147
+ test("marks a morning start AM and an afternoon end PM", () => {
148
+ pin(true);
149
+
150
+ expect(
151
+ getTimeRangeButtonLabel({
152
+ range: TimeRange.CUSTOM,
153
+ startAndEndDate: new InBetween<Date>(
154
+ new Date("2024-03-01T09:15:00.000Z"),
155
+ new Date("2024-03-01T17:45:00.000Z"),
156
+ ),
157
+ }),
158
+ ).toBe("Mar 1, 9:15 AM – Mar 1, 5:45 PM");
159
+ });
160
+
161
+ test("writes a midnight-to-noon window as 12 AM to 12 PM", () => {
162
+ pin(true);
163
+
164
+ expect(
165
+ getTimeRangeButtonLabel({
166
+ range: TimeRange.CUSTOM,
167
+ startAndEndDate: new InBetween<Date>(
168
+ new Date("2024-03-01T00:00:00.000Z"),
169
+ new Date("2024-03-01T12:00:00.000Z"),
170
+ ),
171
+ }),
172
+ ).toBe("Mar 1, 12:00 AM – Mar 1, 12:00 PM");
96
173
  });
97
174
 
98
- expect(label).toBe("Mar 1, 10:00 Mar 2, 13:30");
175
+ test("keeps the en dash separator on both clocks", () => {
176
+ pin(true);
177
+ expect(getTimeRangeButtonLabel(CUSTOM_RANGE)).toContain(" – ");
178
+
179
+ pin(false);
180
+ expect(getTimeRangeButtonLabel(CUSTOM_RANGE)).toContain(" – ");
181
+ });
182
+
183
+ test("leaves preset labels untouched by the clock preference", () => {
184
+ pin(true);
185
+ const on12Hour: string = getTimeRangeButtonLabel({
186
+ range: TimeRange.PAST_ONE_HOUR,
187
+ });
188
+
189
+ pin(false);
190
+ const on24Hour: string = getTimeRangeButtonLabel({
191
+ range: TimeRange.PAST_ONE_HOUR,
192
+ });
193
+
194
+ expect(on12Hour).toBe("Past 1 Hour");
195
+ expect(on24Hour).toBe("Past 1 Hour");
196
+ });
197
+ });
198
+
199
+ describe("timezone", () => {
200
+ test("reads the window in the timezone configured in User Settings", () => {
201
+ pin(true);
202
+ OneUptimeDate.setUserTimezone(Timezone.AsiaKolkata);
203
+
204
+ // 10:00 and 13:30 UTC are 15:30 and 19:00 at +05:30.
205
+ expect(getTimeRangeButtonLabel(CUSTOM_RANGE)).toBe(
206
+ "Mar 1, 3:30 PM – Mar 2, 7:00 PM",
207
+ );
208
+ });
209
+
210
+ test("moves the date too when the configured zone crosses midnight", () => {
211
+ pin(false);
212
+ OneUptimeDate.setUserTimezone(Timezone.AmericaNew_York);
213
+
214
+ expect(
215
+ getTimeRangeButtonLabel({
216
+ range: TimeRange.CUSTOM,
217
+ startAndEndDate: new InBetween<Date>(
218
+ new Date("2024-03-01T02:00:00.000Z"),
219
+ new Date("2024-03-01T06:00:00.000Z"),
220
+ ),
221
+ }),
222
+ ).toBe("Feb 29, 21:00 – Mar 1, 01:00");
223
+ });
224
+
225
+ test("relabels the same window when the configured zone changes", () => {
226
+ pin(false);
227
+
228
+ OneUptimeDate.setUserTimezone(Timezone.UTC);
229
+ const inUtc: string = getTimeRangeButtonLabel(CUSTOM_RANGE);
230
+
231
+ OneUptimeDate.setUserTimezone(Timezone.AsiaKolkata);
232
+ const inKolkata: string = getTimeRangeButtonLabel(CUSTOM_RANGE);
233
+
234
+ expect(inUtc).toBe("Mar 1, 10:00 – Mar 2, 13:30");
235
+ expect(inKolkata).toBe("Mar 1, 15:30 – Mar 2, 19:00");
236
+ });
99
237
  });
100
238
  });
101
239