@oneuptime/common 8.0.5159 → 8.0.5163
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/OnCallDutyPolicy/LayerUtil.test.ts +479 -0
- package/Types/OnCallDutyPolicy/Layer.ts +73 -0
- package/UI/Components/JSONTable/JSONTable.tsx +3 -1
- package/Utils/API.ts +16 -13
- package/build/dist/Tests/Types/OnCallDutyPolicy/LayerUtil.test.js +350 -0
- package/build/dist/Tests/Types/OnCallDutyPolicy/LayerUtil.test.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +40 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/UI/Components/JSONTable/JSONTable.js +3 -1
- package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
- package/build/dist/Utils/API.js +16 -14
- package/build/dist/Utils/API.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
2
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
3
|
+
import RestrictionTimes, {
|
|
4
|
+
RestrictionType,
|
|
5
|
+
WeeklyResctriction,
|
|
6
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
7
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
8
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
9
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
10
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
11
|
+
import DayOfWeek, { DayOfWeekUtil } from "../../../Types/Day/DayOfWeek";
|
|
12
|
+
|
|
13
|
+
// Helper to create a user model with id only.
|
|
14
|
+
function user(id: string): User {
|
|
15
|
+
return {
|
|
16
|
+
id: {
|
|
17
|
+
toString: () => {
|
|
18
|
+
return id;
|
|
19
|
+
},
|
|
20
|
+
} as any,
|
|
21
|
+
} as User;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildLayerProps(data: {
|
|
25
|
+
users: string[];
|
|
26
|
+
start: Date;
|
|
27
|
+
handoff: Date;
|
|
28
|
+
restriction?: { type: RestrictionType; start?: string; end?: string };
|
|
29
|
+
weeklyRestrictions?: Array<{
|
|
30
|
+
startDay: DayOfWeek;
|
|
31
|
+
endDay: DayOfWeek;
|
|
32
|
+
start: string; // HH:mm
|
|
33
|
+
end: string; // HH:mm
|
|
34
|
+
}>;
|
|
35
|
+
rotation?: { intervalType: EventInterval; intervalCount: number };
|
|
36
|
+
}): LayerProps {
|
|
37
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
38
|
+
|
|
39
|
+
if (data.restriction) {
|
|
40
|
+
restrictionTimes.restictionType = data.restriction.type;
|
|
41
|
+
if (
|
|
42
|
+
data.restriction.type === RestrictionType.Daily &&
|
|
43
|
+
data.restriction.start &&
|
|
44
|
+
data.restriction.end
|
|
45
|
+
) {
|
|
46
|
+
restrictionTimes.dayRestrictionTimes = {
|
|
47
|
+
startTime: OneUptimeDate.getDateWithCustomTime({
|
|
48
|
+
hours: parseInt(data.restriction.start.split(":")[0] || "0"),
|
|
49
|
+
minutes: parseInt(data.restriction.start.split(":")[1] || "0"),
|
|
50
|
+
seconds: 0,
|
|
51
|
+
}),
|
|
52
|
+
endTime: OneUptimeDate.getDateWithCustomTime({
|
|
53
|
+
hours: parseInt(data.restriction.end.split(":")[0] || "0"),
|
|
54
|
+
minutes: parseInt(data.restriction.end.split(":")[1] || "0"),
|
|
55
|
+
seconds: 0,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
} else if (data.weeklyRestrictions && data.weeklyRestrictions.length > 0) {
|
|
60
|
+
restrictionTimes.restictionType = RestrictionType.Weekly;
|
|
61
|
+
const weekly: Array<WeeklyResctriction> = [];
|
|
62
|
+
// Base week anchor (start of week for provided start date)
|
|
63
|
+
const baseWeekStart: Date = OneUptimeDate.getStartOfTheWeek(data.start);
|
|
64
|
+
const baseWeekDay: DayOfWeek = OneUptimeDate.getDayOfWeek(baseWeekStart);
|
|
65
|
+
const baseWeekDayNumber: number =
|
|
66
|
+
DayOfWeekUtil.getNumberOfDayOfWeek(baseWeekDay);
|
|
67
|
+
|
|
68
|
+
for (const r of data.weeklyRestrictions) {
|
|
69
|
+
const desiredStartDayNum: number = DayOfWeekUtil.getNumberOfDayOfWeek(
|
|
70
|
+
r.startDay,
|
|
71
|
+
);
|
|
72
|
+
const desiredEndDayNum: number = DayOfWeekUtil.getNumberOfDayOfWeek(
|
|
73
|
+
r.endDay,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const startOffsetDays: number = desiredStartDayNum - baseWeekDayNumber;
|
|
77
|
+
const endOffsetDays: number = desiredEndDayNum - baseWeekDayNumber;
|
|
78
|
+
|
|
79
|
+
const startDate: Date = OneUptimeDate.addRemoveDays(
|
|
80
|
+
baseWeekStart,
|
|
81
|
+
startOffsetDays,
|
|
82
|
+
);
|
|
83
|
+
const endDate: Date = OneUptimeDate.addRemoveDays(
|
|
84
|
+
baseWeekStart,
|
|
85
|
+
endOffsetDays,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const startTime: Date = OneUptimeDate.keepTimeButMoveDay(
|
|
89
|
+
OneUptimeDate.getDateWithCustomTime({
|
|
90
|
+
hours: parseInt(r.start.split(":")[0] || "0"),
|
|
91
|
+
minutes: parseInt(r.start.split(":")[1] || "0"),
|
|
92
|
+
seconds: 0,
|
|
93
|
+
}),
|
|
94
|
+
startDate,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const endTime: Date = OneUptimeDate.keepTimeButMoveDay(
|
|
98
|
+
OneUptimeDate.getDateWithCustomTime({
|
|
99
|
+
hours: parseInt(r.end.split(":")[0] || "0"),
|
|
100
|
+
minutes: parseInt(r.end.split(":")[1] || "0"),
|
|
101
|
+
seconds: 0,
|
|
102
|
+
}),
|
|
103
|
+
endDate,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
weekly.push({
|
|
107
|
+
startDay: r.startDay,
|
|
108
|
+
endDay: r.endDay,
|
|
109
|
+
startTime,
|
|
110
|
+
endTime,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
restrictionTimes.weeklyRestrictionTimes = weekly;
|
|
115
|
+
} else {
|
|
116
|
+
restrictionTimes.restictionType = RestrictionType.None;
|
|
117
|
+
restrictionTimes.dayRestrictionTimes = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const rotation: Recurring = data.rotation
|
|
121
|
+
? Recurring.fromJSON({
|
|
122
|
+
_type: "Recurring",
|
|
123
|
+
value: {
|
|
124
|
+
intervalType: data.rotation.intervalType,
|
|
125
|
+
intervalCount: {
|
|
126
|
+
_type: "PositiveNumber",
|
|
127
|
+
value: data.rotation.intervalCount,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
} as any)
|
|
131
|
+
: Recurring.fromJSON({
|
|
132
|
+
_type: "Recurring",
|
|
133
|
+
value: {
|
|
134
|
+
intervalType: EventInterval.Day,
|
|
135
|
+
intervalCount: { _type: "PositiveNumber", value: 1 },
|
|
136
|
+
},
|
|
137
|
+
} as any);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
users: data.users.map(user),
|
|
141
|
+
startDateTimeOfLayer: data.start,
|
|
142
|
+
handOffTime: data.handoff,
|
|
143
|
+
restrictionTimes,
|
|
144
|
+
rotation,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
describe("LayerUtil getEvents - Daily Restrictions", () => {
|
|
149
|
+
test("Should return full-day events when no restriction", () => {
|
|
150
|
+
const util: LayerUtil = new LayerUtil();
|
|
151
|
+
const start: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
152
|
+
const end: Date = OneUptimeDate.addRemoveDays(start, 1); // one day calendar
|
|
153
|
+
|
|
154
|
+
const layer: LayerProps = buildLayerProps({
|
|
155
|
+
users: ["u1"],
|
|
156
|
+
start: start,
|
|
157
|
+
handoff: OneUptimeDate.addRemoveDays(start, 10),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
161
|
+
...layer,
|
|
162
|
+
calendarStartDate: start,
|
|
163
|
+
calendarEndDate: end,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(events.length).toBe(1);
|
|
167
|
+
const only: CalendarEvent = events[0]!;
|
|
168
|
+
expect(only.start.getTime()).toBe(start.getTime());
|
|
169
|
+
expect(only.end.getTime()).toBe(end.getTime());
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("Should trim to same-day restriction window (11:00-23:00)", () => {
|
|
173
|
+
const util: LayerUtil = new LayerUtil();
|
|
174
|
+
const start: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
175
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveDays(start, 1);
|
|
176
|
+
|
|
177
|
+
const layer: LayerProps = buildLayerProps({
|
|
178
|
+
users: ["u1"],
|
|
179
|
+
start: start,
|
|
180
|
+
handoff: OneUptimeDate.addRemoveDays(start, 2),
|
|
181
|
+
restriction: {
|
|
182
|
+
type: RestrictionType.Daily,
|
|
183
|
+
start: "11:00",
|
|
184
|
+
end: "23:00",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
189
|
+
...layer,
|
|
190
|
+
calendarStartDate: start,
|
|
191
|
+
calendarEndDate: calendarEnd,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(events.length).toBe(1);
|
|
195
|
+
const ev: CalendarEvent = events[0]!;
|
|
196
|
+
expect(OneUptimeDate.getLocalHourAndMinuteFromDate(ev.start)).toBe("11:00");
|
|
197
|
+
expect(OneUptimeDate.getLocalHourAndMinuteFromDate(ev.end)).toBe("23:00");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("Should produce two segments for overnight window (23:00-11:00 next day)", () => {
|
|
201
|
+
const util: LayerUtil = new LayerUtil();
|
|
202
|
+
const todayStart: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
203
|
+
// Extend calendar to cover next day morning (till at least 12:00) so both segments can appear.
|
|
204
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveHours(todayStart, 36); // 24h + 12h
|
|
205
|
+
|
|
206
|
+
const layer: LayerProps = buildLayerProps({
|
|
207
|
+
users: ["u1"],
|
|
208
|
+
start: todayStart,
|
|
209
|
+
handoff: OneUptimeDate.addRemoveDays(todayStart, 2),
|
|
210
|
+
restriction: {
|
|
211
|
+
type: RestrictionType.Daily,
|
|
212
|
+
start: "23:00",
|
|
213
|
+
end: "11:00",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
218
|
+
...layer,
|
|
219
|
+
calendarStartDate: todayStart,
|
|
220
|
+
calendarEndDate: calendarEnd,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Expect two events: 23:00 -> 23:59:59 (approx) and 00:00 -> 11:00 next day (depending on trimming logic)
|
|
224
|
+
// We simplify by checking presence of one starting at 23:00 and one ending at 11:00.
|
|
225
|
+
expect(events.length).toBeGreaterThanOrEqual(2); // Expect at least two distinct segments across midnight.
|
|
226
|
+
const has23Window: boolean = events.some((e: CalendarEvent) => {
|
|
227
|
+
return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "23:00";
|
|
228
|
+
});
|
|
229
|
+
// End might be 10:59 or 11:00 depending on second trimming; allow both 10 or 11 hour boundary.
|
|
230
|
+
const hasMorningCoverage: boolean = events.some((e: CalendarEvent) => {
|
|
231
|
+
const startHM: string = OneUptimeDate.getLocalHourAndMinuteFromDate(
|
|
232
|
+
e.start,
|
|
233
|
+
);
|
|
234
|
+
const endHM: string = OneUptimeDate.getLocalHourAndMinuteFromDate(e.end);
|
|
235
|
+
// Morning segment should end at or near 11:00 and start at or near 00:00
|
|
236
|
+
return (
|
|
237
|
+
(startHM === "00:00" || startHM === "00:01" || startHM === "23:59") &&
|
|
238
|
+
(endHM === "11:00" || endHM === "10:59" || endHM === "10:58")
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(has23Window).toBeTruthy();
|
|
243
|
+
expect(hasMorningCoverage).toBeTruthy();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("LayerUtil getEvents - Multi-day Daily Windows", () => {
|
|
248
|
+
test("Daily restriction (09:00-17:00) over 3 day calendar produces one window per day", () => {
|
|
249
|
+
const util: LayerUtil = new LayerUtil();
|
|
250
|
+
const day1: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
251
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveDays(day1, 3); // 3 days window
|
|
252
|
+
|
|
253
|
+
const layer: LayerProps = buildLayerProps({
|
|
254
|
+
users: ["u1"],
|
|
255
|
+
start: day1,
|
|
256
|
+
handoff: OneUptimeDate.addRemoveDays(day1, 1), // initial handoff end of day1
|
|
257
|
+
restriction: {
|
|
258
|
+
type: RestrictionType.Daily,
|
|
259
|
+
start: "09:00",
|
|
260
|
+
end: "17:00",
|
|
261
|
+
},
|
|
262
|
+
rotation: { intervalType: EventInterval.Day, intervalCount: 1 },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
266
|
+
...layer,
|
|
267
|
+
calendarStartDate: day1,
|
|
268
|
+
calendarEndDate: calendarEnd,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const windowsStartingAtNine: Array<CalendarEvent> = events.filter(
|
|
272
|
+
(e: CalendarEvent) => {
|
|
273
|
+
return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "09:00";
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
expect(windowsStartingAtNine.length).toBeGreaterThanOrEqual(2);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("LayerUtil getEvents - Weekly Restrictions", () => {
|
|
281
|
+
test("Simple weekly window Monday 09:00 to Wednesday 17:00 yields trimmed events", () => {
|
|
282
|
+
const util: LayerUtil = new LayerUtil();
|
|
283
|
+
const monday: Date = OneUptimeDate.getStartOfTheWeek(new Date());
|
|
284
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveDays(monday, 7);
|
|
285
|
+
|
|
286
|
+
const layer: LayerProps = buildLayerProps({
|
|
287
|
+
users: ["u1"],
|
|
288
|
+
start: monday,
|
|
289
|
+
handoff: OneUptimeDate.addRemoveWeeks(monday, 1),
|
|
290
|
+
weeklyRestrictions: [
|
|
291
|
+
{
|
|
292
|
+
startDay: DayOfWeek.Monday,
|
|
293
|
+
endDay: DayOfWeek.Wednesday,
|
|
294
|
+
start: "09:00",
|
|
295
|
+
end: "17:00",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
rotation: { intervalType: EventInterval.Week, intervalCount: 1 },
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
302
|
+
...layer,
|
|
303
|
+
calendarStartDate: monday,
|
|
304
|
+
calendarEndDate: calendarEnd,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const hasStartNine: boolean = events.some((e: CalendarEvent) => {
|
|
308
|
+
return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "09:00";
|
|
309
|
+
});
|
|
310
|
+
const hasEndSeventeen: boolean = events.some((e: CalendarEvent) => {
|
|
311
|
+
return OneUptimeDate.getLocalHourAndMinuteFromDate(e.end) === "17:00";
|
|
312
|
+
});
|
|
313
|
+
expect(hasStartNine).toBeTruthy();
|
|
314
|
+
expect(hasEndSeventeen).toBeTruthy();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("Weekly wrap-around Friday 22:00 to Monday 06:00 produces appropriate segments", () => {
|
|
318
|
+
const util: LayerUtil = new LayerUtil();
|
|
319
|
+
const monday: Date = OneUptimeDate.getStartOfTheWeek(new Date());
|
|
320
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveDays(monday, 7);
|
|
321
|
+
|
|
322
|
+
const layer: LayerProps = buildLayerProps({
|
|
323
|
+
users: ["u1"],
|
|
324
|
+
start: monday,
|
|
325
|
+
handoff: OneUptimeDate.addRemoveWeeks(monday, 1),
|
|
326
|
+
weeklyRestrictions: [
|
|
327
|
+
{
|
|
328
|
+
startDay: DayOfWeek.Friday,
|
|
329
|
+
endDay: DayOfWeek.Monday,
|
|
330
|
+
start: "22:00",
|
|
331
|
+
end: "06:00",
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
rotation: { intervalType: EventInterval.Week, intervalCount: 1 },
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
338
|
+
...layer,
|
|
339
|
+
calendarStartDate: monday,
|
|
340
|
+
calendarEndDate: calendarEnd,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const has22: boolean = events.some((e: CalendarEvent) => {
|
|
344
|
+
return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "22:00";
|
|
345
|
+
});
|
|
346
|
+
const has06: boolean = events.some((e: CalendarEvent) => {
|
|
347
|
+
return OneUptimeDate.getLocalHourAndMinuteFromDate(e.end) === "06:00";
|
|
348
|
+
});
|
|
349
|
+
expect(has22).toBeTruthy();
|
|
350
|
+
expect(has06).toBeTruthy();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("LayerUtil getEvents - Daily Rotation Across Users", () => {
|
|
355
|
+
test("Daily rotation cycles users", () => {
|
|
356
|
+
const util: LayerUtil = new LayerUtil();
|
|
357
|
+
const day1: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
358
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveDays(day1, 3); // 3 days
|
|
359
|
+
|
|
360
|
+
const layer: LayerProps = buildLayerProps({
|
|
361
|
+
users: ["a", "b"],
|
|
362
|
+
start: day1,
|
|
363
|
+
handoff: OneUptimeDate.addRemoveDays(day1, 1),
|
|
364
|
+
rotation: { intervalType: EventInterval.Day, intervalCount: 1 },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
368
|
+
...layer,
|
|
369
|
+
calendarStartDate: day1,
|
|
370
|
+
calendarEndDate: calendarEnd,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (events.length >= 2) {
|
|
374
|
+
expect(events[0]!.title).not.toBe(events[1]!.title);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("LayerUtil getMultiLayerEvents - Partial Overlap Trimming", () => {
|
|
380
|
+
test("Primary layer inside backup trims backup", () => {
|
|
381
|
+
const util: LayerUtil = new LayerUtil();
|
|
382
|
+
const start: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
383
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveHours(start, 6);
|
|
384
|
+
|
|
385
|
+
const primary: LayerProps = buildLayerProps({
|
|
386
|
+
users: ["primary"],
|
|
387
|
+
start: OneUptimeDate.addRemoveHours(start, 2),
|
|
388
|
+
handoff: OneUptimeDate.addRemoveHours(start, 4),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const backup: LayerProps = buildLayerProps({
|
|
392
|
+
users: ["backup"],
|
|
393
|
+
start: start,
|
|
394
|
+
handoff: OneUptimeDate.addRemoveHours(start, 6),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const events: Array<CalendarEvent> = util.getMultiLayerEvents({
|
|
398
|
+
layers: [primary, backup],
|
|
399
|
+
calendarStartDate: start,
|
|
400
|
+
calendarEndDate: calendarEnd,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const containsPrimary: boolean = events.some((e: CalendarEvent) => {
|
|
404
|
+
return e.title === "primary";
|
|
405
|
+
});
|
|
406
|
+
const containsBackup: boolean = events.some((e: CalendarEvent) => {
|
|
407
|
+
return e.title === "backup";
|
|
408
|
+
});
|
|
409
|
+
expect(containsPrimary).toBeTruthy();
|
|
410
|
+
expect(containsBackup).toBeTruthy();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("LayerUtil getEvents - Rotation Handoff", () => {
|
|
415
|
+
test("Hourly rotation changes user after each hour", () => {
|
|
416
|
+
const util: LayerUtil = new LayerUtil();
|
|
417
|
+
const start: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
418
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveHours(start, 5); // 5 hours window
|
|
419
|
+
|
|
420
|
+
const layer: LayerProps = buildLayerProps({
|
|
421
|
+
users: ["u1", "u2", "u3"],
|
|
422
|
+
start: start,
|
|
423
|
+
handoff: OneUptimeDate.addRemoveHours(start, 1), // first handoff at +1h
|
|
424
|
+
rotation: { intervalType: EventInterval.Hour, intervalCount: 1 },
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
428
|
+
...layer,
|
|
429
|
+
calendarStartDate: start,
|
|
430
|
+
calendarEndDate: calendarEnd,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Expect roughly 5 events (one per hour) and user IDs rotate in sequence.
|
|
434
|
+
expect(events.length).toBeGreaterThanOrEqual(4);
|
|
435
|
+
const userSequence: Array<string> = events.map((e: CalendarEvent) => {
|
|
436
|
+
return e.title;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Titles are user ids (strings we passed) according to implementation.
|
|
440
|
+
// Check that at least first three rotate u1 -> u2 -> u3
|
|
441
|
+
expect(userSequence.slice(0, 3)).toEqual(["u1", "u2", "u3"]);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("LayerUtil getMultiLayerEvents - Overlap Priority", () => {
|
|
446
|
+
test("Higher priority (lower index) layer should trim overlapping lower priority events", () => {
|
|
447
|
+
const util: LayerUtil = new LayerUtil();
|
|
448
|
+
const start: Date = OneUptimeDate.getStartOfDay(new Date());
|
|
449
|
+
const calendarEnd: Date = OneUptimeDate.addRemoveHours(start, 6);
|
|
450
|
+
|
|
451
|
+
const layer1: LayerProps = buildLayerProps({
|
|
452
|
+
users: ["primary"],
|
|
453
|
+
start: start,
|
|
454
|
+
handoff: OneUptimeDate.addRemoveHours(start, 6),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const layer2: LayerProps = buildLayerProps({
|
|
458
|
+
users: ["backup"],
|
|
459
|
+
start: start,
|
|
460
|
+
handoff: OneUptimeDate.addRemoveHours(start, 6),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const events: Array<CalendarEvent> = util.getMultiLayerEvents({
|
|
464
|
+
layers: [layer1, layer2],
|
|
465
|
+
calendarStartDate: start,
|
|
466
|
+
calendarEndDate: calendarEnd,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// All events should belong to primary (priority 1) with no backup overlapping intervals left.
|
|
470
|
+
const titles: Array<string> = events.map((e: CalendarEvent) => {
|
|
471
|
+
return e.title;
|
|
472
|
+
});
|
|
473
|
+
expect(
|
|
474
|
+
titles.every((t: string) => {
|
|
475
|
+
return t === "primary";
|
|
476
|
+
}),
|
|
477
|
+
).toBeTruthy();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
@@ -718,6 +718,79 @@ export default class LayerUtil {
|
|
|
718
718
|
let restrictionStartTime: Date = dayRestrictionTimes.startTime;
|
|
719
719
|
let restrictionEndTime: Date = dayRestrictionTimes.endTime;
|
|
720
720
|
|
|
721
|
+
// Special Case: Overnight (wrap-around) window where end time is logically on the next day.
|
|
722
|
+
// Example: 23:00 -> 11:00 (next day). Existing algorithm assumed end >= start within same day
|
|
723
|
+
// and returned no events. We explicitly expand such windows into two segments per day:
|
|
724
|
+
// 1) start -> endOfDay(start)
|
|
725
|
+
// 2) startOfNextDay -> end (moved to next day)
|
|
726
|
+
if (OneUptimeDate.isBefore(restrictionEndTime, restrictionStartTime)) {
|
|
727
|
+
const results: Array<StartAndEndTime> = [];
|
|
728
|
+
|
|
729
|
+
// We'll iterate day-by-day within the event range (max 31 iterations safeguard)
|
|
730
|
+
let currentDayStart: Date = OneUptimeDate.getStartOfDay(
|
|
731
|
+
data.eventStartTime,
|
|
732
|
+
);
|
|
733
|
+
const absoluteEventEnd: Date = data.eventEndTime;
|
|
734
|
+
let safetyCounter: number = 0;
|
|
735
|
+
const maxDays: number = 62; // generous safeguard
|
|
736
|
+
|
|
737
|
+
while (
|
|
738
|
+
OneUptimeDate.isOnOrBefore(currentDayStart, absoluteEventEnd) &&
|
|
739
|
+
safetyCounter < maxDays
|
|
740
|
+
) {
|
|
741
|
+
safetyCounter++;
|
|
742
|
+
|
|
743
|
+
const segmentNightStart: Date = OneUptimeDate.keepTimeButMoveDay(
|
|
744
|
+
restrictionStartTime,
|
|
745
|
+
currentDayStart,
|
|
746
|
+
);
|
|
747
|
+
const segmentNightEnd: Date =
|
|
748
|
+
OneUptimeDate.getEndOfDay(segmentNightStart);
|
|
749
|
+
|
|
750
|
+
const nextDayStart: Date = OneUptimeDate.addRemoveDays(
|
|
751
|
+
currentDayStart,
|
|
752
|
+
1,
|
|
753
|
+
);
|
|
754
|
+
const segmentMorningStart: Date =
|
|
755
|
+
OneUptimeDate.getStartOfDay(nextDayStart);
|
|
756
|
+
const segmentMorningEnd: Date = OneUptimeDate.keepTimeButMoveDay(
|
|
757
|
+
restrictionEndTime,
|
|
758
|
+
nextDayStart,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// helper to add intersection if it overlaps the event window
|
|
762
|
+
const addIntersection: (segStart: Date, segEnd: Date) => void = (
|
|
763
|
+
segStart: Date,
|
|
764
|
+
segEnd: Date,
|
|
765
|
+
): void => {
|
|
766
|
+
// normalize zero / invalid lengths
|
|
767
|
+
if (OneUptimeDate.isOnOrBefore(segEnd, segStart)) {
|
|
768
|
+
return; // no length
|
|
769
|
+
}
|
|
770
|
+
// intersect with [eventStart, eventEnd]
|
|
771
|
+
const start: Date = OneUptimeDate.getGreaterDate(
|
|
772
|
+
segStart,
|
|
773
|
+
data.eventStartTime,
|
|
774
|
+
);
|
|
775
|
+
const end: Date = OneUptimeDate.getLesserDate(
|
|
776
|
+
segEnd,
|
|
777
|
+
data.eventEndTime,
|
|
778
|
+
);
|
|
779
|
+
if (OneUptimeDate.isAfter(end, start)) {
|
|
780
|
+
results.push({ startTime: start, endTime: end });
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
addIntersection(segmentNightStart, segmentNightEnd);
|
|
785
|
+
addIntersection(segmentMorningStart, segmentMorningEnd);
|
|
786
|
+
|
|
787
|
+
// advance a day
|
|
788
|
+
currentDayStart = nextDayStart;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return results;
|
|
792
|
+
}
|
|
793
|
+
|
|
721
794
|
let currentStartTime: Date = data.eventStartTime;
|
|
722
795
|
const currentEndTime: Date = data.eventEndTime;
|
|
723
796
|
|
|
@@ -83,7 +83,9 @@ const JSONTable: FunctionComponent<JSONTableProps> = (
|
|
|
83
83
|
if (!groupMap[prefix]) {
|
|
84
84
|
groupMap[prefix] = [];
|
|
85
85
|
}
|
|
86
|
-
groupMap[prefix].
|
|
86
|
+
// At this point groupMap[prefix] is initialized. Use a local ref so TS can narrow.
|
|
87
|
+
const entries: Array<GroupEntry> = groupMap[prefix]!;
|
|
88
|
+
entries.push({ index, value: working[key] });
|
|
87
89
|
keysToRemove.add(key);
|
|
88
90
|
}
|
|
89
91
|
|
package/Utils/API.ts
CHANGED
|
@@ -504,55 +504,58 @@ export default class API {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
|
|
507
|
+
const status: number | undefined = (error as AxiosError)?.response?.status;
|
|
508
|
+
const lowerErr: string = errorString.toLocaleLowerCase();
|
|
509
|
+
|
|
510
|
+
if (status !== 400 && lowerErr.includes("network error")) {
|
|
508
511
|
return "Network Error.";
|
|
509
512
|
}
|
|
510
513
|
|
|
511
|
-
if (
|
|
514
|
+
if (status !== 400 && lowerErr.includes("timeout")) {
|
|
512
515
|
return "Timeout Error.";
|
|
513
516
|
}
|
|
514
517
|
|
|
515
|
-
if (
|
|
518
|
+
if (status !== 400 && lowerErr.includes("request aborted")) {
|
|
516
519
|
return "Request Aborted.";
|
|
517
520
|
}
|
|
518
521
|
|
|
519
|
-
if (
|
|
522
|
+
if (status !== 400 && lowerErr.includes("canceled")) {
|
|
520
523
|
return "Request Canceled.";
|
|
521
524
|
}
|
|
522
525
|
|
|
523
|
-
if (
|
|
526
|
+
if (status !== 400 && lowerErr.includes("connection refused")) {
|
|
524
527
|
return "Connection Refused.";
|
|
525
528
|
}
|
|
526
529
|
|
|
527
|
-
if (
|
|
530
|
+
if (status !== 400 && lowerErr.includes("connection reset")) {
|
|
528
531
|
return "Connection Reset.";
|
|
529
532
|
}
|
|
530
533
|
|
|
531
|
-
if (
|
|
534
|
+
if (status !== 400 && lowerErr.includes("connection closed")) {
|
|
532
535
|
return "Connection Closed.";
|
|
533
536
|
}
|
|
534
537
|
|
|
535
|
-
if (
|
|
538
|
+
if (status !== 400 && lowerErr.includes("connection failed")) {
|
|
536
539
|
return "Connection Failed.";
|
|
537
540
|
}
|
|
538
541
|
|
|
539
|
-
if (
|
|
542
|
+
if (status !== 400 && lowerErr.includes("enotfound")) {
|
|
540
543
|
return "Cannot Find Host.";
|
|
541
544
|
}
|
|
542
545
|
|
|
543
|
-
if (
|
|
546
|
+
if (status !== 400 && lowerErr.includes("econnreset")) {
|
|
544
547
|
return "Connection Reset.";
|
|
545
548
|
}
|
|
546
549
|
|
|
547
|
-
if (
|
|
550
|
+
if (status !== 400 && lowerErr.includes("econnrefused")) {
|
|
548
551
|
return "Connection Refused.";
|
|
549
552
|
}
|
|
550
553
|
|
|
551
|
-
if (
|
|
554
|
+
if (status !== 400 && lowerErr.includes("econnaborted")) {
|
|
552
555
|
return "Connection Aborted.";
|
|
553
556
|
}
|
|
554
557
|
|
|
555
|
-
if (
|
|
558
|
+
if (status !== 400 && lowerErr.includes("certificate has expired")) {
|
|
556
559
|
return "SSL Certificate Expired.";
|
|
557
560
|
}
|
|
558
561
|
|