@oneuptime/common 11.7.0 → 11.7.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.
- package/Tests/Types/DateUserTimezone.test.ts +205 -0
- package/Tests/Types/DateUserTimezoneOverridesBrowser.test.ts +73 -0
- package/Tests/UI/Components/TimePicker/TimePicker.test.tsx +11 -0
- package/Types/Date.ts +119 -50
- package/UI/Components/Forms/Fields/FormField.tsx +13 -3
- package/UI/Components/Input/Input.tsx +6 -1
- package/UI/Components/TimePicker/TimePicker.tsx +8 -4
- package/UI/Utils/User.ts +19 -0
- package/build/dist/Types/Date.js +106 -35
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -3
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +6 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/TimePicker/TimePicker.js +4 -4
- package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
- package/build/dist/UI/Utils/User.js +19 -0
- package/build/dist/UI/Utils/User.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +0 -79
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +0 -41
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +0 -17
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +0 -34
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +0 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +0 -22
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +0 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +0 -12
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's profile timezone (User Settings > Timezone) — not the zone the
|
|
3
|
+
* browser reports — decides which wall clock OneUptimeDate reads and writes.
|
|
4
|
+
*
|
|
5
|
+
* The bug these lock in: a user in America/New_York whose browser reported
|
|
6
|
+
* America/Adak was told "your local timezone - HDT" on every date field, and
|
|
7
|
+
* had to enter Hawaii-Aleutian times for their events to land correctly. The
|
|
8
|
+
* profile timezone was stored but never consulted by the UI.
|
|
9
|
+
*
|
|
10
|
+
* Every assertion is expressed in explicit-zone wall-clock (or absolute
|
|
11
|
+
* milliseconds) so it holds under whatever TZ the suite runs in.
|
|
12
|
+
*/
|
|
13
|
+
import OneUptimeDate from "../../Types/Date";
|
|
14
|
+
import Timezone from "../../Types/Timezone";
|
|
15
|
+
import moment from "moment-timezone";
|
|
16
|
+
|
|
17
|
+
const NY: Timezone = Timezone.AmericaNew_York;
|
|
18
|
+
const ADAK: Timezone = Timezone.AmericaAdak;
|
|
19
|
+
const KOLKATA: Timezone = Timezone.AsiaKolkata;
|
|
20
|
+
|
|
21
|
+
describe("OneUptimeDate user timezone", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Never leak an override into the rest of the suite.
|
|
24
|
+
OneUptimeDate.setUserTimezone(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("setUserTimezone / getCurrentTimezone", () => {
|
|
28
|
+
it("falls back to the browser / process zone when no user timezone is set", () => {
|
|
29
|
+
expect(OneUptimeDate.getUserTimezone()).toBeNull();
|
|
30
|
+
expect(OneUptimeDate.getCurrentTimezone().toString()).toBe(
|
|
31
|
+
moment.tz.guess(),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("prefers the user timezone over the browser / process zone", () => {
|
|
36
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
37
|
+
|
|
38
|
+
expect(OneUptimeDate.getCurrentTimezone()).toBe(NY);
|
|
39
|
+
expect(OneUptimeDate.getUserTimezone()).toBe(NY);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("reports the abbreviation of the user timezone, DST aware", () => {
|
|
43
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
44
|
+
|
|
45
|
+
// The bug reported "HDT" (America/Adak) for a New York user.
|
|
46
|
+
expect(OneUptimeDate.getCurrentTimezoneString()).not.toBe("HDT");
|
|
47
|
+
expect(["EST", "EDT"]).toContain(
|
|
48
|
+
OneUptimeDate.getCurrentTimezoneString(),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("clears back to the browser / process zone when set to null", () => {
|
|
53
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
54
|
+
OneUptimeDate.setUserTimezone(null);
|
|
55
|
+
|
|
56
|
+
expect(OneUptimeDate.getUserTimezone()).toBeNull();
|
|
57
|
+
expect(OneUptimeDate.getCurrentTimezone().toString()).toBe(
|
|
58
|
+
moment.tz.guess(),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("ignores a value moment does not recognise as a zone rather than breaking every date", () => {
|
|
63
|
+
OneUptimeDate.setUserTimezone("Not/AZone" as Timezone);
|
|
64
|
+
|
|
65
|
+
expect(OneUptimeDate.getUserTimezone()).toBeNull();
|
|
66
|
+
expect(OneUptimeDate.getCurrentTimezone().toString()).toBe(
|
|
67
|
+
moment.tz.guess(),
|
|
68
|
+
);
|
|
69
|
+
// Formatting still works.
|
|
70
|
+
expect(
|
|
71
|
+
OneUptimeDate.toDateTimeLocalString(new Date("2026-07-09T13:00:00Z")),
|
|
72
|
+
).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("datetime-local round trip", () => {
|
|
77
|
+
it("renders a stored instant as its wall-clock in the user timezone", () => {
|
|
78
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
79
|
+
|
|
80
|
+
// 17:00 UTC on 2026-07-09 is 13:00 in New York (EDT).
|
|
81
|
+
expect(
|
|
82
|
+
OneUptimeDate.toDateTimeLocalString(new Date("2026-07-09T17:00:00Z")),
|
|
83
|
+
).toBe("2026-07-09T13:00:00");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("resolves a typed wall-clock in the user timezone, not the browser zone", () => {
|
|
87
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
88
|
+
|
|
89
|
+
const typed: Date =
|
|
90
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T13:00");
|
|
91
|
+
|
|
92
|
+
expect(typed.toISOString()).toBe("2026-07-09T17:00:00.000Z");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("round trips what the user typed back into the picker unchanged", () => {
|
|
96
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
97
|
+
|
|
98
|
+
const typed: Date =
|
|
99
|
+
OneUptimeDate.fromDateTimeLocalString("2026-01-15T09:30");
|
|
100
|
+
|
|
101
|
+
expect(OneUptimeDate.toDateTimeLocalString(typed)).toBe(
|
|
102
|
+
"2026-01-15T09:30:00",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stores different instants for the same wall-clock in different user timezones", () => {
|
|
107
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
108
|
+
const inNY: Date =
|
|
109
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T13:00");
|
|
110
|
+
|
|
111
|
+
OneUptimeDate.setUserTimezone(ADAK);
|
|
112
|
+
const inAdak: Date =
|
|
113
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T13:00");
|
|
114
|
+
|
|
115
|
+
// Adak is 5 hours behind New York in July.
|
|
116
|
+
expect(inAdak.getTime() - inNY.getTime()).toBe(5 * 60 * 60 * 1000);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("honours the user timezone's DST offset for a winter instant", () => {
|
|
120
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
121
|
+
|
|
122
|
+
// 09:00 EST is 14:00 UTC in January.
|
|
123
|
+
expect(
|
|
124
|
+
OneUptimeDate.fromDateTimeLocalString("2026-01-15T09:00").toISOString(),
|
|
125
|
+
).toBe("2026-01-15T14:00:00.000Z");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("display helpers", () => {
|
|
130
|
+
it("formats an instant in the user timezone with its abbreviation", () => {
|
|
131
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
132
|
+
|
|
133
|
+
const formatted: string = OneUptimeDate.getDateAsLocalFormattedString(
|
|
134
|
+
new Date("2026-07-09T17:00:00Z"),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(formatted).toContain("Jul 09 2026");
|
|
138
|
+
expect(formatted).toContain("13:00");
|
|
139
|
+
expect(formatted).toContain("EDT");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("reads the hour and minute of an instant in the user timezone", () => {
|
|
143
|
+
OneUptimeDate.setUserTimezone(KOLKATA);
|
|
144
|
+
|
|
145
|
+
const instant: Date = new Date("2026-07-09T17:00:00Z");
|
|
146
|
+
|
|
147
|
+
// 17:00 UTC is 22:30 in Kolkata (UTC+5:30).
|
|
148
|
+
expect(OneUptimeDate.getLocalHours(instant)).toBe(22);
|
|
149
|
+
expect(OneUptimeDate.getLocalMinutes(instant)).toBe(30);
|
|
150
|
+
expect(OneUptimeDate.getLocalTimeString(instant)).toBe("22:30");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rolls the calendar day when the user timezone puts the instant on another date", () => {
|
|
154
|
+
OneUptimeDate.setUserTimezone(KOLKATA);
|
|
155
|
+
|
|
156
|
+
// 23:00 UTC on Jul 9 is already 04:30 on Jul 10 in Kolkata.
|
|
157
|
+
const instant: Date = new Date("2026-07-09T23:00:00Z");
|
|
158
|
+
|
|
159
|
+
expect(OneUptimeDate.asDateForDatabaseQuery(instant)).toBe("2026-07-10");
|
|
160
|
+
expect(OneUptimeDate.getDateAsLocalDayMonthString(instant)).toBe(
|
|
161
|
+
"10 Jul",
|
|
162
|
+
);
|
|
163
|
+
expect(OneUptimeDate.getDateAsLocalMonthYearString(instant)).toBe(
|
|
164
|
+
"Jul 2026",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("schedule-zone wall-clock bridge", () => {
|
|
170
|
+
it("reads the entered wall-clock in the user timezone before anchoring it to the schedule zone", () => {
|
|
171
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
172
|
+
|
|
173
|
+
// The admin typed 09:00 while working in New York.
|
|
174
|
+
const typed: Date =
|
|
175
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T09:00");
|
|
176
|
+
|
|
177
|
+
const stored: Date = OneUptimeDate.getInstantFromLocalWallClockInTimezone(
|
|
178
|
+
typed,
|
|
179
|
+
KOLKATA.toString(),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// ...and the schedule enforces 09:00 in Kolkata.
|
|
183
|
+
expect(moment.tz(stored, KOLKATA.toString()).format("HH:mm")).toBe(
|
|
184
|
+
"09:00",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("redisplays a stored schedule-zone time as the same wall-clock in the picker", () => {
|
|
189
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
190
|
+
|
|
191
|
+
const stored: Date = moment
|
|
192
|
+
.tz("2026-07-09 09:00", KOLKATA.toString())
|
|
193
|
+
.toDate();
|
|
194
|
+
|
|
195
|
+
const forPicker: Date = OneUptimeDate.getLocalDateFromWallClockInTimezone(
|
|
196
|
+
stored,
|
|
197
|
+
KOLKATA.toString(),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(OneUptimeDate.toDateTimeLocalString(forPicker)).toBe(
|
|
201
|
+
"2026-07-09T09:00:00",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** @timezone America/Adak */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression test for the reported bug, reproduced exactly: the process (a
|
|
5
|
+
* stand-in for the customer's browser) reports America/Adak — abbreviated
|
|
6
|
+
* "HDT" — while the user has picked America/New_York in User Settings.
|
|
7
|
+
*
|
|
8
|
+
* Before the fix, every date field labelled itself "your local timezone - HDT"
|
|
9
|
+
* and resolved what the user typed in Adak's zone, so a New York user had to
|
|
10
|
+
* enter Hawaii-Aleutian times to get the right instant stored. The profile
|
|
11
|
+
* timezone must win over whatever the browser reports.
|
|
12
|
+
*/
|
|
13
|
+
import OneUptimeDate from "../../Types/Date";
|
|
14
|
+
import Timezone from "../../Types/Timezone";
|
|
15
|
+
import moment from "moment-timezone";
|
|
16
|
+
|
|
17
|
+
const NY: Timezone = Timezone.AmericaNew_York;
|
|
18
|
+
|
|
19
|
+
describe("user timezone overrides a browser reporting a different zone", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
OneUptimeDate.setUserTimezone(null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("confirms the process really is on the Adak (HDT) clock", () => {
|
|
25
|
+
// Guards the premise of every assertion below.
|
|
26
|
+
expect(moment.tz.guess()).toBe("America/Adak");
|
|
27
|
+
expect(OneUptimeDate.getCurrentTimezoneString()).toBe("HDT");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("labels date fields with the profile timezone, not HDT", () => {
|
|
31
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
32
|
+
|
|
33
|
+
expect(OneUptimeDate.getCurrentTimezoneString()).toBe("EDT");
|
|
34
|
+
expect(OneUptimeDate.getCurrentTimezone()).toBe(NY);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("stores the wall-clock the user typed as New York time", () => {
|
|
38
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* The user schedules maintenance for 14:00 on Jul 9. That is 18:00 UTC in
|
|
42
|
+
* New York; reading it on the Adak clock would have stored 23:00 UTC —
|
|
43
|
+
* five hours off, which is exactly what the customer worked around.
|
|
44
|
+
*/
|
|
45
|
+
const startsAt: Date =
|
|
46
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T14:00");
|
|
47
|
+
|
|
48
|
+
expect(startsAt.toISOString()).toBe("2026-07-09T18:00:00.000Z");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("reads a stored event back at the wall-clock the user typed", () => {
|
|
52
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
53
|
+
|
|
54
|
+
const startsAt: Date = new Date("2026-07-09T18:00:00.000Z");
|
|
55
|
+
|
|
56
|
+
expect(OneUptimeDate.toDateTimeLocalString(startsAt)).toBe(
|
|
57
|
+
"2026-07-09T14:00:00",
|
|
58
|
+
);
|
|
59
|
+
expect(OneUptimeDate.getDateAsLocalFormattedString(startsAt)).toContain(
|
|
60
|
+
"EDT",
|
|
61
|
+
);
|
|
62
|
+
expect(OneUptimeDate.getLocalTimeString(startsAt)).toBe("14:00");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("still follows the browser zone for a user who has not picked one", () => {
|
|
66
|
+
OneUptimeDate.setUserTimezone(null);
|
|
67
|
+
|
|
68
|
+
// 14:00 HDT (UTC-9) is 23:00 UTC the same day.
|
|
69
|
+
expect(
|
|
70
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T14:00").toISOString(),
|
|
71
|
+
).toBe("2026-07-09T23:00:00.000Z");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -89,6 +89,17 @@ jest.mock("../../../../Types/Date", () => {
|
|
|
89
89
|
return base;
|
|
90
90
|
},
|
|
91
91
|
),
|
|
92
|
+
/*
|
|
93
|
+
* The picker reads the wall-clock through OneUptimeDate so it resolves in
|
|
94
|
+
* the user's configured timezone; the stubbed dates above already carry
|
|
95
|
+
* the hour/minute the test intends.
|
|
96
|
+
*/
|
|
97
|
+
getLocalHours: jest.fn((d: HourMinuteMock) => {
|
|
98
|
+
return d.getHours();
|
|
99
|
+
}),
|
|
100
|
+
getLocalMinutes: jest.fn((d: HourMinuteMock) => {
|
|
101
|
+
return d.getMinutes();
|
|
102
|
+
}),
|
|
92
103
|
getCurrentTimezoneString: jest.fn(() => {
|
|
93
104
|
return "UTC";
|
|
94
105
|
}),
|
package/Types/Date.ts
CHANGED
|
@@ -12,6 +12,13 @@ export const Moment: typeof moment = moment;
|
|
|
12
12
|
export default class OneUptimeDate {
|
|
13
13
|
// get date time from unix timestamp
|
|
14
14
|
|
|
15
|
+
/*
|
|
16
|
+
* The timezone the signed-in user configured in User Settings, pushed in by
|
|
17
|
+
* the UI (see UserUtil.initializeUserTimezone). Null on the server and on
|
|
18
|
+
* signed-out pages, where the process / browser zone is used instead.
|
|
19
|
+
*/
|
|
20
|
+
private static userTimezone: Timezone | null = null;
|
|
21
|
+
|
|
15
22
|
private static padDatePart(value: number): string {
|
|
16
23
|
return value.toString().padStart(2, "0");
|
|
17
24
|
}
|
|
@@ -44,6 +51,7 @@ export default class OneUptimeDate {
|
|
|
44
51
|
private static getLocalShortMonthName(date: Date): string {
|
|
45
52
|
return date.toLocaleString("default", {
|
|
46
53
|
month: "short",
|
|
54
|
+
timeZone: this.getCurrentTimezone().toString(),
|
|
47
55
|
});
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -156,6 +164,19 @@ export default class OneUptimeDate {
|
|
|
156
164
|
return this.getLocalTimeString(date);
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The hour / minute `date` reads at in the current timezone. Time pickers use
|
|
169
|
+
* these instead of Date.getHours()/getMinutes() so the digits they show are
|
|
170
|
+
* the ones the user expects in the zone they configured.
|
|
171
|
+
*/
|
|
172
|
+
public static getLocalHours(date: Date | string): number {
|
|
173
|
+
return this.inCurrentTimezone(date).hours();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public static getLocalMinutes(date: Date | string): number {
|
|
177
|
+
return this.inCurrentTimezone(date).minutes();
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
public static getLocalShortMonthNameFromDate(date: Date | string): string {
|
|
160
181
|
date = this.fromString(date);
|
|
161
182
|
return this.getLocalShortMonthName(date);
|
|
@@ -172,26 +193,27 @@ export default class OneUptimeDate {
|
|
|
172
193
|
|
|
173
194
|
const includeMinutes: boolean = options?.includeMinutes ?? true;
|
|
174
195
|
const includeSeconds: boolean = options?.includeSeconds ?? false;
|
|
175
|
-
const
|
|
196
|
+
const localDate: moment.Moment = this.inCurrentTimezone(date);
|
|
197
|
+
const hours: string = this.padDatePart(localDate.hours());
|
|
176
198
|
|
|
177
199
|
if (!includeMinutes) {
|
|
178
200
|
return hours;
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
const minutes: string = this.padDatePart(
|
|
203
|
+
const minutes: string = this.padDatePart(localDate.minutes());
|
|
182
204
|
|
|
183
205
|
if (!includeSeconds) {
|
|
184
206
|
return `${hours}:${minutes}`;
|
|
185
207
|
}
|
|
186
208
|
|
|
187
|
-
const seconds: string = this.padDatePart(
|
|
209
|
+
const seconds: string = this.padDatePart(localDate.seconds());
|
|
188
210
|
return `${hours}:${minutes}:${seconds}`;
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
public static getDateAsLocalDayMonthString(date: Date | string): string {
|
|
192
214
|
date = this.fromString(date);
|
|
193
215
|
|
|
194
|
-
const day: string = this.padDatePart(date.
|
|
216
|
+
const day: string = this.padDatePart(this.inCurrentTimezone(date).date());
|
|
195
217
|
const month: string = this.getLocalShortMonthName(date);
|
|
196
218
|
|
|
197
219
|
return `${day} ${month}`;
|
|
@@ -207,7 +229,7 @@ export default class OneUptimeDate {
|
|
|
207
229
|
date = this.fromString(date);
|
|
208
230
|
|
|
209
231
|
const month: string = this.getLocalShortMonthName(date);
|
|
210
|
-
const year: string = date.
|
|
232
|
+
const year: string = this.inCurrentTimezone(date).year().toString();
|
|
211
233
|
|
|
212
234
|
return `${month} ${year}`;
|
|
213
235
|
}
|
|
@@ -316,7 +338,7 @@ export default class OneUptimeDate {
|
|
|
316
338
|
: `${datePart}, ${timePart}`;
|
|
317
339
|
|
|
318
340
|
if (!data.timezone) {
|
|
319
|
-
const local: moment.Moment =
|
|
341
|
+
const local: moment.Moment = this.inCurrentTimezone(date);
|
|
320
342
|
return (
|
|
321
343
|
local.format(formatString) +
|
|
322
344
|
(data.onlyShowDate ? "" : " " + this.getCurrentTimezoneString())
|
|
@@ -333,18 +355,18 @@ export default class OneUptimeDate {
|
|
|
333
355
|
}
|
|
334
356
|
|
|
335
357
|
/**
|
|
336
|
-
* Reinterpret the wall-clock components of `date` — read in the
|
|
337
|
-
*
|
|
338
|
-
* instant. Used to store a time the user typed
|
|
339
|
-
* even though the time picker captured it in the
|
|
340
|
-
* F1). Inverse of
|
|
358
|
+
* Reinterpret the wall-clock components of `date` — read in the CURRENT
|
|
359
|
+
* timezone, i.e. the one the pickers render in — as the SAME wall-clock in
|
|
360
|
+
* `timezone`, and return that instant. Used to store a time the user typed
|
|
361
|
+
* for the schedule's timezone even though the time picker captured it in the
|
|
362
|
+
* viewer's own zone (audit F1). Inverse of
|
|
363
|
+
* getLocalDateFromWallClockInTimezone.
|
|
341
364
|
*/
|
|
342
365
|
public static getInstantFromLocalWallClockInTimezone(
|
|
343
366
|
date: Date | string,
|
|
344
367
|
timezone: string,
|
|
345
368
|
): Date {
|
|
346
|
-
|
|
347
|
-
const local: moment.Moment = moment(date);
|
|
369
|
+
const local: moment.Moment = this.inCurrentTimezone(date);
|
|
348
370
|
return moment
|
|
349
371
|
.tz(
|
|
350
372
|
{
|
|
@@ -361,10 +383,10 @@ export default class OneUptimeDate {
|
|
|
361
383
|
}
|
|
362
384
|
|
|
363
385
|
/**
|
|
364
|
-
* Inverse of getInstantFromLocalWallClockInTimezone: return a
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
386
|
+
* Inverse of getInstantFromLocalWallClockInTimezone: return a Date whose
|
|
387
|
+
* wall-clock IN THE CURRENT TIMEZONE equals `date`'s wall-clock as seen in
|
|
388
|
+
* `timezone`. Used to display a stored schedule-timezone time inside a picker
|
|
389
|
+
* that renders in the viewer's own zone (audit F1).
|
|
368
390
|
*/
|
|
369
391
|
public static getLocalDateFromWallClockInTimezone(
|
|
370
392
|
date: Date | string,
|
|
@@ -372,14 +394,19 @@ export default class OneUptimeDate {
|
|
|
372
394
|
): Date {
|
|
373
395
|
date = this.fromString(date);
|
|
374
396
|
const zoned: moment.Moment = moment.tz(date, timezone);
|
|
375
|
-
return moment
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
return moment
|
|
398
|
+
.tz(
|
|
399
|
+
{
|
|
400
|
+
year: zoned.year(),
|
|
401
|
+
month: zoned.month(),
|
|
402
|
+
day: zoned.date(),
|
|
403
|
+
hour: zoned.hour(),
|
|
404
|
+
minute: zoned.minute(),
|
|
405
|
+
second: zoned.second(),
|
|
406
|
+
},
|
|
407
|
+
this.getCurrentTimezone().toString(),
|
|
408
|
+
)
|
|
409
|
+
.toDate();
|
|
383
410
|
}
|
|
384
411
|
|
|
385
412
|
public static isOverlapping(
|
|
@@ -709,22 +736,26 @@ export default class OneUptimeDate {
|
|
|
709
736
|
return this.addRemoveMinutes(date, date.getTimezoneOffset());
|
|
710
737
|
}
|
|
711
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Render an instant as the value of an `<input type="datetime-local">` — a
|
|
741
|
+
* bare wall-clock with no offset — read in the current timezone.
|
|
742
|
+
*/
|
|
712
743
|
public static toDateTimeLocalString(date: Date): string {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
type TenFunction = (i: number) => string;
|
|
716
|
-
|
|
717
|
-
const ten: TenFunction = (i: number): string => {
|
|
718
|
-
return (i < 10 ? "0" : "") + i;
|
|
719
|
-
},
|
|
720
|
-
YYYY: number = date.getFullYear(),
|
|
721
|
-
MM: string = ten(date.getMonth() + 1),
|
|
722
|
-
DD: string = ten(date.getDate()),
|
|
723
|
-
HH: string = ten(date.getHours()),
|
|
724
|
-
II: string = ten(date.getMinutes()),
|
|
725
|
-
SS: string = ten(date.getSeconds());
|
|
744
|
+
return this.inCurrentTimezone(date).format("YYYY-MM-DDTHH:mm:ss");
|
|
745
|
+
}
|
|
726
746
|
|
|
727
|
-
|
|
747
|
+
/**
|
|
748
|
+
* Inverse of toDateTimeLocalString: turn the offset-less wall-clock a user
|
|
749
|
+
* typed into a `<input type="date">` / `<input type="datetime-local">` into
|
|
750
|
+
* the instant it names in the current timezone. Without this the browser
|
|
751
|
+
* zone is used to resolve the wall-clock, so a user whose machine reports a
|
|
752
|
+
* different zone than the one they configured would have their times stored
|
|
753
|
+
* hours off.
|
|
754
|
+
*/
|
|
755
|
+
public static fromDateTimeLocalString(value: string): Date {
|
|
756
|
+
return moment
|
|
757
|
+
.tz(value.trim(), this.getCurrentTimezone().toString())
|
|
758
|
+
.toDate();
|
|
728
759
|
}
|
|
729
760
|
|
|
730
761
|
public static fromJSON(json: JSONObject): Date {
|
|
@@ -1713,7 +1744,7 @@ export default class OneUptimeDate {
|
|
|
1713
1744
|
formatstring = "MMM DD, YYYY";
|
|
1714
1745
|
}
|
|
1715
1746
|
|
|
1716
|
-
const momentDate: moment.Moment =
|
|
1747
|
+
const momentDate: moment.Moment = this.inCurrentTimezone(date);
|
|
1717
1748
|
|
|
1718
1749
|
return (
|
|
1719
1750
|
momentDate.format(formatstring) +
|
|
@@ -1752,8 +1783,41 @@ export default class OneUptimeDate {
|
|
|
1752
1783
|
return zoneAbbr;
|
|
1753
1784
|
}
|
|
1754
1785
|
|
|
1786
|
+
/**
|
|
1787
|
+
* Set the timezone the signed-in user picked in User Settings. Once set,
|
|
1788
|
+
* every "local" helper below resolves wall-clock times in THIS zone instead
|
|
1789
|
+
* of the zone the browser reports, so what the user reads and types matches
|
|
1790
|
+
* the timezone they configured — even when the machine, OS or VPN they are
|
|
1791
|
+
* on reports a different one. Pass null to fall back to the browser zone
|
|
1792
|
+
* (signed-out surfaces, or a user who has not picked a timezone).
|
|
1793
|
+
*/
|
|
1794
|
+
public static setUserTimezone(timezone: Timezone | null | undefined): void {
|
|
1795
|
+
/*
|
|
1796
|
+
* Ignore anything moment does not recognise as an IANA zone. A stale or
|
|
1797
|
+
* corrupt saved value must not take every date in the UI down with it.
|
|
1798
|
+
*/
|
|
1799
|
+
if (!timezone || !moment.tz.zone(timezone.toString())) {
|
|
1800
|
+
this.userTimezone = null;
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
this.userTimezone = timezone;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
public static getUserTimezone(): Timezone | null {
|
|
1808
|
+
return this.userTimezone;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1755
1811
|
public static getCurrentTimezone(): Timezone {
|
|
1756
|
-
return moment.tz.guess() as Timezone;
|
|
1812
|
+
return this.userTimezone || (moment.tz.guess() as Timezone);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* The instant `date` viewed through the current timezone — the single place
|
|
1817
|
+
* that decides which wall clock the "local" helpers below read from.
|
|
1818
|
+
*/
|
|
1819
|
+
private static inCurrentTimezone(date: Date | string): moment.Moment {
|
|
1820
|
+
return moment(this.fromString(date)).tz(this.getCurrentTimezone());
|
|
1757
1821
|
}
|
|
1758
1822
|
|
|
1759
1823
|
public static getDateString(date: Date): string {
|
|
@@ -1810,9 +1874,8 @@ export default class OneUptimeDate {
|
|
|
1810
1874
|
}
|
|
1811
1875
|
|
|
1812
1876
|
public static asDateForDatabaseQuery(date: string | Date): string {
|
|
1813
|
-
date = this.fromString(date);
|
|
1814
1877
|
const formatstring: string = "YYYY-MM-DD";
|
|
1815
|
-
return
|
|
1878
|
+
return this.inCurrentTimezone(date).format(formatstring);
|
|
1816
1879
|
}
|
|
1817
1880
|
|
|
1818
1881
|
public static asFilterDateForDatabaseQuery(
|
|
@@ -1850,13 +1913,19 @@ export default class OneUptimeDate {
|
|
|
1850
1913
|
throw new BadDataException("Invalid seconds");
|
|
1851
1914
|
}
|
|
1852
1915
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1916
|
+
/*
|
|
1917
|
+
* The hour/minute the user picked is a wall-clock reading in THEIR
|
|
1918
|
+
* timezone, so anchor it to today's date in that zone rather than setting
|
|
1919
|
+
* it on a browser-local Date.
|
|
1920
|
+
*/
|
|
1921
|
+
return this.inCurrentTimezone(OneUptimeDate.getCurrentDate())
|
|
1922
|
+
.set({
|
|
1923
|
+
hour: hour,
|
|
1924
|
+
minute: minutes,
|
|
1925
|
+
second: seconds,
|
|
1926
|
+
millisecond: 0,
|
|
1927
|
+
})
|
|
1928
|
+
.toDate();
|
|
1860
1929
|
}
|
|
1861
1930
|
|
|
1862
1931
|
public static toDatabaseDate(date: Date): string {
|
|
@@ -291,9 +291,19 @@ const FormField: <T extends GenericObject>(
|
|
|
291
291
|
fieldDescription = "";
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
/*
|
|
295
|
+
* Name the zone in full. The abbreviation on its own ("EDT") is not
|
|
296
|
+
* enough to tell whether the field is following the timezone picked in
|
|
297
|
+
* User Settings or the one the browser reports.
|
|
298
|
+
*/
|
|
299
|
+
fieldDescription = (
|
|
300
|
+
fieldDescription +
|
|
301
|
+
" This is in your timezone - " +
|
|
302
|
+
OneUptimeDate.getCurrentTimezoneString() +
|
|
303
|
+
" (" +
|
|
304
|
+
OneUptimeDate.getCurrentTimezone().toString() +
|
|
305
|
+
")."
|
|
306
|
+
).trim();
|
|
297
307
|
}
|
|
298
308
|
|
|
299
309
|
type GetFieldDescriptionFunction = () => ReactElement | string;
|
|
@@ -174,7 +174,12 @@ const Input: FunctionComponent<ComponentProps> = (
|
|
|
174
174
|
props.type === InputType.DATETIME_LOCAL) &&
|
|
175
175
|
value
|
|
176
176
|
) {
|
|
177
|
-
|
|
177
|
+
/*
|
|
178
|
+
* The input hands back a bare wall-clock with no offset. Resolve
|
|
179
|
+
* it in the user's configured timezone rather than letting the
|
|
180
|
+
* browser assume its own zone.
|
|
181
|
+
*/
|
|
182
|
+
const date: Date = OneUptimeDate.fromDateTimeLocalString(value);
|
|
178
183
|
const dateString: string = OneUptimeDate.toString(date);
|
|
179
184
|
setValue(dateString);
|
|
180
185
|
if (props.onChange) {
|
|
@@ -81,8 +81,12 @@ const TimePicker: FunctionComponent<ComponentProps> = (
|
|
|
81
81
|
return toDate(props.value) || OneUptimeDate.getCurrentDate();
|
|
82
82
|
}, [props.value]);
|
|
83
83
|
|
|
84
|
-
const [hours24, setHours24] = useState<number>(
|
|
85
|
-
|
|
84
|
+
const [hours24, setHours24] = useState<number>(
|
|
85
|
+
OneUptimeDate.getLocalHours(initialDate),
|
|
86
|
+
);
|
|
87
|
+
const [minutes, setMinutes] = useState<number>(
|
|
88
|
+
OneUptimeDate.getLocalMinutes(initialDate),
|
|
89
|
+
);
|
|
86
90
|
|
|
87
91
|
const hoursInputRef: React.MutableRefObject<HTMLInputElement | null> =
|
|
88
92
|
useRef<HTMLInputElement | null>(null);
|
|
@@ -99,8 +103,8 @@ const TimePicker: FunctionComponent<ComponentProps> = (
|
|
|
99
103
|
if (!d) {
|
|
100
104
|
return;
|
|
101
105
|
}
|
|
102
|
-
setHours24(
|
|
103
|
-
setMinutes(
|
|
106
|
+
setHours24(OneUptimeDate.getLocalHours(d));
|
|
107
|
+
setMinutes(OneUptimeDate.getLocalMinutes(d));
|
|
104
108
|
}, [props.value]);
|
|
105
109
|
|
|
106
110
|
const emitChange: (h24: number, m: number) => void = (
|