@flamingo-stack/openframe-frontend-core 0.0.740 → 0.0.742
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/dist/build/export-map.json +1 -1
- package/dist/{chunk-BV5WZVOP.cjs → chunk-32Q7K5OP.cjs} +254 -254
- package/dist/{chunk-ZLTCQKTD.cjs → chunk-3LW43GIJ.cjs} +6 -6
- package/dist/{chunk-VA7OMDZO.cjs → chunk-4FWURR3W.cjs} +50 -50
- package/dist/{chunk-EKGL5K5K.cjs → chunk-4LDU32OB.cjs} +17 -17
- package/dist/{chunk-VC4IV3AY.cjs → chunk-4PKORVOX.cjs} +2804 -954
- package/dist/{chunk-3ZIQOYNX.cjs → chunk-54EA4RLX.cjs} +8 -8
- package/dist/{chunk-YXKB5WRE.cjs → chunk-5MH6BRPY.cjs} +36 -36
- package/dist/{chunk-PQ2M7P2U.cjs → chunk-5TCIRWHL.cjs} +9 -9
- package/dist/{chunk-WFPCCBOM.cjs → chunk-7AM3AFM4.cjs} +10 -10
- package/dist/{chunk-7ZFCR7UY.cjs → chunk-ABLUMGCI.cjs} +3 -3
- package/dist/{chunk-PX7FY655.cjs → chunk-ASMNNSUQ.cjs} +3 -3
- package/dist/{chunk-HF7Z2E4W.cjs → chunk-BI3XBCQX.cjs} +2 -2
- package/dist/{chunk-SA7LDEVJ.cjs → chunk-CSJO7ZGN.cjs} +3 -3
- package/dist/{chunk-VOX4E6AX.cjs → chunk-DGFDBPO7.cjs} +7 -5
- package/dist/{chunk-KHXZFLIN.cjs → chunk-DKOLQPE7.cjs} +8 -2
- package/dist/{chunk-BDA5F6RM.cjs → chunk-DNFSMVXJ.cjs} +16 -16
- package/dist/{chunk-B3XBJCOD.cjs → chunk-H3UA7UY5.cjs} +7 -7
- package/dist/{chunk-2Y5Y4FLM.cjs → chunk-HDBY2URA.cjs} +64 -64
- package/dist/{chunk-UW3YXDC4.cjs → chunk-HIRVNBX5.cjs} +20 -20
- package/dist/{chunk-LLAC2UZ3.cjs → chunk-ISJXKKYW.cjs} +28 -28
- package/dist/{chunk-FICJS5OA.cjs → chunk-KTPR66EE.cjs} +4 -4
- package/dist/{chunk-7UIQPMBJ.cjs → chunk-L34TKFTS.cjs} +21 -164
- package/dist/{chunk-AAFW4STC.cjs → chunk-LU7TD6QS.cjs} +5 -5
- package/dist/{chunk-3HPB4H4C.cjs → chunk-NTGIQP7O.cjs} +7 -7
- package/dist/{chunk-WBANGORJ.cjs → chunk-OBODY7F3.cjs} +3 -3
- package/dist/{chunk-GABDE3FM.cjs → chunk-OWGLRE74.cjs} +19 -19
- package/dist/{chunk-CWRVTQUO.cjs → chunk-PS6T65KA.cjs} +101 -101
- package/dist/{chunk-IFS4AH77.cjs → chunk-QYLIHZKZ.cjs} +10 -10
- package/dist/{chunk-GVTDVS5U.cjs → chunk-R34JD6NP.cjs} +3 -3
- package/dist/{chunk-VU4CKUJW.cjs → chunk-R6SCSV2I.cjs} +103 -103
- package/dist/{chunk-II4QMH26.cjs → chunk-STEJZPIO.cjs} +3 -3
- package/dist/{chunk-4RLGLB45.cjs → chunk-T433HSL6.cjs} +47 -47
- package/dist/{chunk-DSVNFRY5.cjs → chunk-U2O5YQRP.cjs} +38 -38
- package/dist/{chunk-O67V3HJR.cjs → chunk-URVIPSAJ.cjs} +24 -24
- package/dist/{chunk-HYBMADYO.cjs → chunk-UWHCRIQQ.cjs} +22 -22
- package/dist/{chunk-GSWQZX23.cjs → chunk-UY6A6TEY.cjs} +2 -2
- package/dist/{chunk-D5KEWWLW.cjs → chunk-UZLRSFGG.cjs} +32 -32
- package/dist/{chunk-DUVOQHQI.cjs → chunk-W2RM764D.cjs} +7 -7
- package/dist/{chunk-NTJE6SM6.cjs → chunk-WI2OTGKC.cjs} +2 -2
- package/dist/{chunk-SFY4UYPY.cjs → chunk-WIHHA54J.cjs} +44 -44
- package/dist/{chunk-YRE3O65E.cjs → chunk-XAA5EZ5T.cjs} +12 -12
- package/dist/{chunk-KLI4OSRA.cjs → chunk-YQGDXH3M.cjs} +2 -2
- package/dist/{chunk-SWR22DOZ.cjs → chunk-Z2DQYKZA.cjs} +89 -89
- package/dist/{chunk-5SVOGLNK.cjs → chunk-ZIFYSLIM.cjs} +6 -6
- package/dist/{cloud-tenants-PVXDI5MU.cjs → cloud-tenants-6ICAL3HO.cjs} +34 -34
- package/dist/components/case-studies/index.cjs +37 -37
- package/dist/components/chat/index.cjs +17 -17
- package/dist/components/contact/index.cjs +25 -25
- package/dist/components/docs/index.cjs +28 -28
- package/dist/components/embeds/index.cjs +25 -25
- package/dist/components/faq/index.cjs +26 -26
- package/dist/components/features/capability-explorer.d.ts +9 -2
- package/dist/components/features/capability-explorer.d.ts.map +1 -1
- package/dist/components/features/capability-explorer.js +113 -6
- package/dist/components/features/index.cjs +162 -24
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +1 -0
- package/dist/components/features/scripts/execution-cells.d.ts +65 -0
- package/dist/components/features/scripts/execution-cells.d.ts.map +1 -0
- package/dist/components/features/scripts/execution-cells.js +75 -0
- package/dist/components/features/scripts/execution-status.d.ts +48 -0
- package/dist/components/features/scripts/execution-status.d.ts.map +1 -0
- package/dist/components/features/scripts/execution-status.js +15 -0
- package/dist/components/features/scripts/index.d.ts +20 -0
- package/dist/components/features/scripts/index.d.ts.map +1 -0
- package/dist/components/features/scripts/index.js +59 -0
- package/dist/components/features/scripts/schedule-form/index.d.ts +18 -0
- package/dist/components/features/scripts/schedule-form/index.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/index.js +84 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts +24 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.js +18 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts +57 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.js +169 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts +138 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.js +72 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts +54 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.js +60 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts +29 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.js +124 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts +52 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.js +244 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts +35 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.js +58 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts +25 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.js +66 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts +77 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.js +262 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts +288 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.js +165 -0
- package/dist/components/features/scripts/script-cells.d.ts +11 -0
- package/dist/components/features/scripts/script-cells.d.ts.map +1 -0
- package/dist/components/features/scripts/script-cells.js +19 -0
- package/dist/components/features/scripts/script-executions-table.d.ts +58 -0
- package/dist/components/features/scripts/script-executions-table.d.ts.map +1 -0
- package/dist/components/features/scripts/script-executions-table.js +88 -0
- package/dist/components/features/scripts/script-schedules-table.d.ts +38 -0
- package/dist/components/features/scripts/script-schedules-table.d.ts.map +1 -0
- package/dist/components/features/scripts/script-schedules-table.js +71 -0
- package/dist/components/features/scripts/script-shell-badge.d.ts +9 -0
- package/dist/components/features/scripts/script-shell-badge.d.ts.map +1 -0
- package/dist/components/features/scripts/script-shell-badge.js +15 -0
- package/dist/components/features/scripts/scripts-table-columns.d.ts +162 -0
- package/dist/components/features/scripts/scripts-table-columns.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table-columns.js +69 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts +19 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table-shared.js +35 -0
- package/dist/components/features/scripts/scripts-table.d.ts +40 -0
- package/dist/components/features/scripts/scripts-table.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table.js +84 -0
- package/dist/components/features/scripts/scripts-tabs.d.ts +5 -0
- package/dist/components/features/scripts/scripts-tabs.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-tabs.js +10 -0
- package/dist/components/features/scripts/shell-types.d.ts +13 -0
- package/dist/components/features/scripts/shell-types.d.ts.map +1 -0
- package/dist/components/features/scripts/shell-types.js +21 -0
- package/dist/components/help-center-pages/index.cjs +144 -144
- package/dist/components/index.cjs +224 -86
- package/dist/components/meeting-scheduler/index.cjs +74 -74
- package/dist/components/navigation/index.cjs +27 -27
- package/dist/components/onboarding-guides/index.cjs +27 -27
- package/dist/components/product-demo/fixtures/scripts.d.ts +34 -0
- package/dist/components/product-demo/fixtures/scripts.d.ts.map +1 -0
- package/dist/components/product-demo/fixtures/scripts.js +123 -0
- package/dist/components/product-demo/fixtures/shared.d.ts +8 -0
- package/dist/components/product-demo/fixtures/shared.d.ts.map +1 -1
- package/dist/components/product-demo/fixtures/shared.js +7 -1
- package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
- package/dist/components/product-demo/fixtures/software-update.js +1 -5
- package/dist/components/product-demo/index.cjs +16 -13
- package/dist/components/product-demo/product-screen.d.ts.map +1 -1
- package/dist/components/product-demo/product-screen.js +2 -1
- package/dist/components/product-demo/registry.d.ts.map +1 -1
- package/dist/components/product-demo/registry.js +2 -1
- package/dist/components/product-demo/screen-keys.cjs +2 -1
- package/dist/components/product-demo/screen-keys.d.ts +1 -0
- package/dist/components/product-demo/screen-keys.d.ts.map +1 -1
- package/dist/components/product-demo/screen-keys.js +2 -1
- package/dist/components/product-demo/screens/scripts.d.ts +11 -0
- package/dist/components/product-demo/screens/scripts.d.ts.map +1 -0
- package/dist/components/product-demo/screens/scripts.js +130 -0
- package/dist/components/related-content/index.cjs +12 -12
- package/dist/components/tickets/index.cjs +30 -30
- package/dist/components/ui/index.cjs +24 -24
- package/dist/{devices-TWEA7XOR.cjs → devices-OFP2IBKN.cjs} +30 -30
- package/dist/{embeddable-chat-6JKKFQEB.cjs → embeddable-chat-4C22MGNC.cjs} +92 -92
- package/dist/hooks/index.cjs +10 -10
- package/dist/index.cjs +169 -31
- package/dist/{knowledge-4GQDBNQB.cjs → knowledge-KKBCSNUA.cjs} +29 -29
- package/dist/{logs-EVU6GJHZ.cjs → logs-X2GRNO57.cjs} +46 -46
- package/dist/{policies-OZUEA5YS.cjs → policies-35BXKB6P.cjs} +31 -31
- package/dist/{remote-session-DNFZ5OLY.cjs → remote-session-N4PJB5FW.cjs} +15 -15
- package/dist/scripts-KQYVQOLU.cjs +2112 -0
- package/dist/{software-update-BLIVYOY2.cjs → software-update-ED33WSKT.cjs} +36 -40
- package/dist/{tickets-board-CKZRB6YV.cjs → tickets-board-O753S4ZB.cjs} +31 -31
- package/dist/utils/embed-authed-fetch.d.ts +33 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.js +3 -1
- package/dist/utils/index.cjs +3 -1
- package/dist/{video-file-player-ZH2ILAPO.cjs → video-file-player-QZ3YOLHO.cjs} +2 -2
- package/package.json +1 -1
- package/src/components/features/capability-explorer.tsx +134 -7
- package/src/components/features/index.ts +1 -0
- package/src/components/features/scripts/execution-cells.tsx +149 -0
- package/src/components/features/scripts/execution-status.ts +42 -0
- package/src/components/features/scripts/index.ts +42 -0
- package/src/components/features/scripts/schedule-form/index.ts +59 -0
- package/src/components/features/scripts/schedule-form/schedule-enums.ts +26 -0
- package/src/components/features/scripts/schedule-form/schedule-form-fields.tsx +249 -0
- package/src/components/features/scripts/schedule-form/schedule-form-types.ts +189 -0
- package/src/components/features/scripts/schedule-form/schedule-interval-input.tsx +121 -0
- package/src/components/features/scripts/schedule-form/schedule-offline-fields.tsx +218 -0
- package/src/components/features/scripts/schedule-form/schedule-script-picker-card.tsx +321 -0
- package/src/components/features/scripts/schedule-form/schedule-script-search-field.tsx +115 -0
- package/src/components/features/scripts/schedule-form/schedule-scripts-field.tsx +97 -0
- package/src/components/features/scripts/schedule-form/schedule-timing-fields.tsx +482 -0
- package/src/components/features/scripts/schedule-form/schedule-timing.ts +484 -0
- package/src/components/features/scripts/script-cells.tsx +29 -0
- package/src/components/features/scripts/script-executions-table.tsx +145 -0
- package/src/components/features/scripts/script-schedules-table.tsx +113 -0
- package/src/components/features/scripts/script-shell-badge.tsx +25 -0
- package/src/components/features/scripts/scripts-table-columns.ts +95 -0
- package/src/components/features/scripts/scripts-table-shared.tsx +43 -0
- package/src/components/features/scripts/scripts-table.tsx +124 -0
- package/src/components/features/scripts/scripts-tabs.ts +11 -0
- package/src/components/features/scripts/shell-types.ts +26 -0
- package/src/components/product-demo/fixtures/scripts.ts +186 -0
- package/src/components/product-demo/fixtures/shared.ts +7 -0
- package/src/components/product-demo/fixtures/software-update.ts +1 -7
- package/src/components/product-demo/product-screen.tsx +3 -1
- package/src/components/product-demo/registry.ts +1 -0
- package/src/components/product-demo/screen-keys.ts +1 -0
- package/src/components/product-demo/screens/scripts.tsx +192 -0
- package/src/utils/embed-authed-fetch.ts +42 -1
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { formatTime } from '../../../../utils/format-date';
|
|
2
|
+
import {
|
|
3
|
+
SCHEDULE_OFFLINE_BEHAVIOR,
|
|
4
|
+
SCHEDULE_TIME_REFERENCE,
|
|
5
|
+
type ScheduleOfflineBehavior,
|
|
6
|
+
type ScheduleTimeReference,
|
|
7
|
+
SCRIPT_SCHEDULE_TRIGGER,
|
|
8
|
+
type ScriptScheduleTrigger,
|
|
9
|
+
} from './schedule-enums';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Timing model helpers for Script Schedules v2.
|
|
13
|
+
*
|
|
14
|
+
* The backend models a schedule's timing as two fields on `ScriptSchedule`:
|
|
15
|
+
* - `startAt: Instant` — the first scheduled run as an absolute **UTC** instant,
|
|
16
|
+
* pinned to a 30-minute boundary (`xx:00` / `xx:30`).
|
|
17
|
+
* - `repeat: Long` — the recurrence interval in **seconds**, a whole number of
|
|
18
|
+
* 30-minute slots (1800, 3600, …). Null / 0 means a one-shot that fires once
|
|
19
|
+
* at `startAt`.
|
|
20
|
+
*
|
|
21
|
+
* The UI offers repeat units that are all clean multiples of 1800s (hour, day,
|
|
22
|
+
* week, month=30d), so anything produced here is always a valid interval on the
|
|
23
|
+
* backend's 30-minute grid. (The backend also accepts a bare 30-minute repeat;
|
|
24
|
+
* the UI simply doesn't surface a sub-hour unit.)
|
|
25
|
+
*
|
|
26
|
+
* Two more fields say what happens when the instant arrives and the device is
|
|
27
|
+
* NOT there — `offlineBehavior` (`SKIP` | `RETRY_ON_RECONNECT`) and, for the
|
|
28
|
+
* latter, `reconnectWindowSeconds`, the deadline past which a queued run is
|
|
29
|
+
* abandoned. They live here because they only mean anything against a scheduled
|
|
30
|
+
* time: a `DEVICE_ONLINE` schedule already fires on reconnect, so it has no
|
|
31
|
+
* offline case to decide and is stored as `SKIP`.
|
|
32
|
+
*
|
|
33
|
+
* **Everything the user sees or picks is their LOCAL wall clock**; UTC exists
|
|
34
|
+
* only on the wire. See `toScheduleInstant` (the host's write side).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** True when the schedule is event-driven and has no date/time/repeat. */
|
|
38
|
+
export function isEventTrigger(trigger: ScriptScheduleTrigger | string | null | undefined): boolean {
|
|
39
|
+
return trigger === SCRIPT_SCHEDULE_TRIGGER.DEVICE_ONLINE;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which clock the picked date + time mean — `timeReference`, the second half of
|
|
44
|
+
* what `startAt` says.
|
|
45
|
+
*
|
|
46
|
+
* - `SERVER` (the default, and what every schedule authored before the field
|
|
47
|
+
* existed carries): `startAt` is an absolute instant. One moment, worldwide.
|
|
48
|
+
* - `DEVICE_LOCAL`: `startAt` carries the picked WALL CLOCK with no zone applied
|
|
49
|
+
* — local 09:00 is stored as `…T09:00:00Z` — and the runner re-bases it into
|
|
50
|
+
* each device's own timezone. One reading, many moments; a fleet across three
|
|
51
|
+
* zones runs at 9 AM three times.
|
|
52
|
+
*
|
|
53
|
+
* The distinction reaches almost every function below, because it decides
|
|
54
|
+
* whether a stored instant is converted through the viewer's offset or read
|
|
55
|
+
* digit for digit: `toScheduleInstant`, {@link fromScheduleInstant},
|
|
56
|
+
* {@link getTimeSlotOptions} (the 30-minute grid is UTC's, and only the SERVER
|
|
57
|
+
* reading has to be shifted onto it) and `formatScheduleStartAt`.
|
|
58
|
+
*
|
|
59
|
+
* A predicate rather than `=== ENUM.X` at the call sites, for the same reason
|
|
60
|
+
* {@link isEventTrigger} is one: Relay types every schema enum with a
|
|
61
|
+
* `%future added value` member, so asking "is it the non-default arm" is what
|
|
62
|
+
* lets an unknown value fall back to SERVER — which is what the write side
|
|
63
|
+
* documents null as meaning.
|
|
64
|
+
*/
|
|
65
|
+
export function isDeviceLocalTime(timeReference: ScheduleTimeReference | string | null | undefined): boolean {
|
|
66
|
+
return timeReference === SCHEDULE_TIME_REFERENCE.DEVICE_LOCAL;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The units an `[interval] [unit]` pair is built from, finest first — one list,
|
|
71
|
+
* so the dropdowns, the form schema and the seconds table cannot drift apart.
|
|
72
|
+
*
|
|
73
|
+
* Two different settings are spelled in this vocabulary, and they are NOT the
|
|
74
|
+
* same quantity: the repeat cadence (`repeat`, how often the schedule fires) and
|
|
75
|
+
* the reconnect window (`reconnectWindowSeconds`, how long an offline device's
|
|
76
|
+
* queued run is still worth running). They share the units and the seconds
|
|
77
|
+
* table; nothing else. In particular {@link MIN_REPEAT_MINUTES} is a property of
|
|
78
|
+
* the RUNNER'S grid and constrains the cadence only — a reconnect window is a
|
|
79
|
+
* plain deadline and may sit anywhere.
|
|
80
|
+
*/
|
|
81
|
+
export const DURATION_UNIT_VALUES = ['minute', 'hour', 'day', 'week', 'month'] as const;
|
|
82
|
+
|
|
83
|
+
export type DurationUnit = (typeof DURATION_UNIT_VALUES)[number];
|
|
84
|
+
|
|
85
|
+
// `Record<DurationUnit, …>` on both maps: adding a unit above is then a compile
|
|
86
|
+
// error until its seconds and its label exist.
|
|
87
|
+
export const DURATION_UNIT_SECONDS: Record<DurationUnit, number> = {
|
|
88
|
+
minute: 60,
|
|
89
|
+
hour: 3600,
|
|
90
|
+
day: 86400,
|
|
91
|
+
week: 604800,
|
|
92
|
+
month: 2592000, // 30 days — the only "calendar" unit the seconds model can approximate
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const DURATION_UNIT_LABELS: Record<DurationUnit, string> = {
|
|
96
|
+
minute: 'Minute',
|
|
97
|
+
hour: 'Hour',
|
|
98
|
+
day: 'Day',
|
|
99
|
+
week: 'Week',
|
|
100
|
+
month: 'Month',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The smallest cadence the backend's grid allows: `repeat` must be a whole
|
|
105
|
+
* number of 30-minute slots, so 30 minutes is one slot and there is nothing
|
|
106
|
+
* below it. Every other unit clears this by construction (an hour is two slots),
|
|
107
|
+
* which is why the rule only ever has to be enforced on minutes.
|
|
108
|
+
*/
|
|
109
|
+
export const MIN_REPEAT_MINUTES = 30;
|
|
110
|
+
|
|
111
|
+
export const DURATION_UNIT_OPTIONS: { label: string; value: DurationUnit }[] = DURATION_UNIT_VALUES.map(value => ({
|
|
112
|
+
value,
|
|
113
|
+
label: DURATION_UNIT_LABELS[value],
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Snaps a repeat interval onto what its unit can legally express. Only minutes
|
|
118
|
+
* can be illegal — see {@link MIN_REPEAT_MINUTES} — so every other unit is
|
|
119
|
+
* returned untouched.
|
|
120
|
+
*
|
|
121
|
+
* Used when the unit CHANGES: switching "1 Day" to minutes would otherwise leave
|
|
122
|
+
* "1 Minute" sitting in the form, a value the user never typed and cannot save.
|
|
123
|
+
*/
|
|
124
|
+
export function snapRepeatInterval(interval: number | null, unit: DurationUnit): number | null {
|
|
125
|
+
if (unit !== 'minute' || interval === null) return interval;
|
|
126
|
+
if (!Number.isFinite(interval) || interval < MIN_REPEAT_MINUTES) return MIN_REPEAT_MINUTES;
|
|
127
|
+
return Math.ceil(interval / MIN_REPEAT_MINUTES) * MIN_REPEAT_MINUTES;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* True when a schedule QUEUES a run for a device that was offline at its
|
|
132
|
+
* scheduled time, instead of logging the miss and moving on.
|
|
133
|
+
*
|
|
134
|
+
* The counterpart of {@link isEventTrigger}, and the reason both are predicates
|
|
135
|
+
* rather than `=== ENUM.X` at the call sites: Relay types every schema enum with
|
|
136
|
+
* a `%future added value` member, so a stored value this client doesn't know
|
|
137
|
+
* reads as neither arm. Asking "is it the non-default one" answers that safely —
|
|
138
|
+
* an unknown behavior falls back to SKIP, which is also what the backend
|
|
139
|
+
* documents as the default.
|
|
140
|
+
*/
|
|
141
|
+
export function isRetryOnReconnect(behavior: ScheduleOfflineBehavior | string | null | undefined): boolean {
|
|
142
|
+
return behavior === SCHEDULE_OFFLINE_BEHAVIOR.RETRY_ON_RECONNECT;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The floor on the reconnect window, in minutes.
|
|
147
|
+
*
|
|
148
|
+
* NOT the runner's grid — this field sits on none, the backend accepts any
|
|
149
|
+
* number of seconds. It is the same 30 as {@link MIN_REPEAT_MINUTES} and a
|
|
150
|
+
* separate constant on purpose: that one follows the runner, this one follows
|
|
151
|
+
* the shortest cadence a schedule can have, and there is no reason the two must
|
|
152
|
+
* move together.
|
|
153
|
+
*
|
|
154
|
+
* Unlike the repeat floor it is a MINIMUM only — no multiple-of-30 rule, because
|
|
155
|
+
* there is no grid for a value to land on. A 45-minute window is legal.
|
|
156
|
+
*/
|
|
157
|
+
export const MIN_RECONNECT_MINUTES = 30;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Raises a reconnect interval to its floor when the unit switches to minutes —
|
|
161
|
+
* the counterpart of {@link snapRepeatInterval}, minus the multiple half.
|
|
162
|
+
*
|
|
163
|
+
* `null` (the field cleared while typing) passes through untouched: snapping it
|
|
164
|
+
* to a number would refill a box the user is in the middle of emptying, which is
|
|
165
|
+
* the behaviour this whole pair of fields was reported for.
|
|
166
|
+
*/
|
|
167
|
+
export function snapReconnectInterval(interval: number | null, unit: DurationUnit): number | null {
|
|
168
|
+
if (unit !== 'minute' || interval === null) return interval;
|
|
169
|
+
return Math.max(interval, MIN_RECONNECT_MINUTES);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* What the "Stop Retry after" pair starts at.
|
|
174
|
+
*
|
|
175
|
+
* The design shows 1 Week (node 460:63425); the product rule is the floor —
|
|
176
|
+
* minutes, at the minimum — so a new schedule starts at the smallest legal
|
|
177
|
+
* window and the user raises it, rather than starting at a week they may not
|
|
178
|
+
* want. It is only ever a SEED: the field is required whenever
|
|
179
|
+
* RETRY_ON_RECONNECT is picked, so every saved schedule carries a window the
|
|
180
|
+
* user could see, and `reconnectWindowSeconds` is never written blind.
|
|
181
|
+
*/
|
|
182
|
+
export const DEFAULT_RECONNECT_WINDOW: { interval: number; unit: DurationUnit } = {
|
|
183
|
+
interval: MIN_RECONNECT_MINUTES,
|
|
184
|
+
unit: 'minute',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const SLOTS_PER_DAY = 48;
|
|
188
|
+
const SLOT_MINUTES = 30;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The minute-of-hour the local time slots sit on.
|
|
192
|
+
*
|
|
193
|
+
* Almost everywhere this is 0 and the slots are the plain `xx:00` / `xx:30` of
|
|
194
|
+
* the local day. Zones offset by 45 minutes (Kathmandu, Chatham, Eucla) are the
|
|
195
|
+
* exception: there a local `xx:00` is `xx:15` in UTC, which the backend rejects,
|
|
196
|
+
* so their grid shifts to `xx:15` / `xx:45` — the local readings that DO land on
|
|
197
|
+
* the UTC grid. Those users pick a time that looks unusual but actually runs;
|
|
198
|
+
* the alternative is a save that fails with nothing they can do about it.
|
|
199
|
+
*
|
|
200
|
+
* Read per call, never once at module load: on the server this would be the
|
|
201
|
+
* *server's* timezone, and this module is imported during the prerender pass.
|
|
202
|
+
*
|
|
203
|
+
* The picked date doesn't enter into it. Every DST transition moves the offset
|
|
204
|
+
* by a whole hour (Lord Howe's 30 minutes is the outlier), so the remainder
|
|
205
|
+
* mod 30 holds all year and the grid never has to be rebuilt per date.
|
|
206
|
+
*
|
|
207
|
+
* All of which is the SERVER reading's problem alone. A DEVICE_LOCAL start is
|
|
208
|
+
* stored as its wall clock verbatim (see `toScheduleInstant`), so the
|
|
209
|
+
* boundary applies to the picked digits themselves and the grid is the plain
|
|
210
|
+
* `xx:00` / `xx:30` — in Kathmandu too, where the two grids genuinely differ.
|
|
211
|
+
*/
|
|
212
|
+
function slotBaseMinutes(timeReference: ScheduleTimeReference | string | null | undefined): number {
|
|
213
|
+
if (isDeviceLocalTime(timeReference)) return 0;
|
|
214
|
+
// getTimezoneOffset() is (UTC − local) in minutes, so UTC lands on a boundary
|
|
215
|
+
// exactly when (localMinutesOfDay + offset) % 30 === 0.
|
|
216
|
+
const offset = new Date().getTimezoneOffset();
|
|
217
|
+
return ((-offset % SLOT_MINUTES) + SLOT_MINUTES) % SLOT_MINUTES;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whether an `HH:mm` the form already holds is a slot the given reading offers.
|
|
222
|
+
*
|
|
223
|
+
* Only ever false where the two grids differ — the 45-minute zones — which is
|
|
224
|
+
* exactly where switching the Timezone control leaves a picked time the new
|
|
225
|
+
* dropdown cannot show. Asked about the GRID and not about the option list, so
|
|
226
|
+
* a stored start that has simply gone by (which the list withholds, and which
|
|
227
|
+
* the form is allowed to keep) is not mistaken for one the grid rejects.
|
|
228
|
+
*/
|
|
229
|
+
export function isSlotOnGrid(slot: string, timeReference: ScheduleTimeReference | string | null | undefined): boolean {
|
|
230
|
+
if (!slot) return true;
|
|
231
|
+
const [hours, minutes] = slot.split(':').map(Number);
|
|
232
|
+
const offGrid = (hours * 60 + minutes - slotBaseMinutes(timeReference)) % SLOT_MINUTES;
|
|
233
|
+
return ((offGrid % SLOT_MINUTES) + SLOT_MINUTES) % SLOT_MINUTES === 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Minutes since local midnight → the `HH:mm` the form stores. */
|
|
237
|
+
function slotValue(minutesOfDay: number): string {
|
|
238
|
+
const hours = Math.floor(minutesOfDay / 60);
|
|
239
|
+
const minutes = minutesOfDay % 60;
|
|
240
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Minutes since local midnight → the reading shown in the dropdown.
|
|
245
|
+
*
|
|
246
|
+
* Formatted through the app's date module like every other clock reading, so a
|
|
247
|
+
* 24-hour locale sees "14:15" rather than a hand-rolled AM/PM. The reference day
|
|
248
|
+
* is arbitrary and never leaves this function: a local `Date` formatted in the
|
|
249
|
+
* local zone returns the same wall clock it was built from.
|
|
250
|
+
*/
|
|
251
|
+
function slotLabel(minutesOfDay: number): string {
|
|
252
|
+
return formatTime(new Date(1970, 0, 1, Math.floor(minutesOfDay / 60), minutesOfDay % 60));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The Time dropdown's options — one per 30-minute slot of the **local** day,
|
|
257
|
+
* shifted onto the grid `startAt` must land on (see {@link slotBaseMinutes}).
|
|
258
|
+
* `value` is 24h `HH:mm` (what the form stores); `label` is the 12h form.
|
|
259
|
+
*
|
|
260
|
+
* A function rather than a const because the grid depends on the viewer's
|
|
261
|
+
* timezone, which a module evaluated during prerender does not know.
|
|
262
|
+
*
|
|
263
|
+
* `forDate` drops the slots that have already gone by: a schedule cannot start
|
|
264
|
+
* in the past, and offering "8:00 AM" at noon means offering a save the backend
|
|
265
|
+
* refuses. Only a `forDate` of TODAY narrows the list — a later day keeps the
|
|
266
|
+
* full 48, and so does a day not yet picked (the user may well be about to pick
|
|
267
|
+
* tomorrow; picking today afterwards is what clears a time that has gone by).
|
|
268
|
+
* A PAST day keeps them too: it can only be a stored start, and the value that
|
|
269
|
+
* schedule already holds has to stay readable.
|
|
270
|
+
*
|
|
271
|
+
* **None of that narrowing applies to DEVICE_LOCAL**, which keeps all 48 slots
|
|
272
|
+
* always. The reading is not the viewer's clock, so "already gone by" is not the
|
|
273
|
+
* viewer's to judge: the fleet's own clocks span 26 hours (UTC−12 … UTC+14), so
|
|
274
|
+
* 8:00 AM at the author's noon is still hours ahead for every device west of
|
|
275
|
+
* them. Withholding it would hide a perfectly good schedule behind the accident
|
|
276
|
+
* of where the admin sits. See {@link earliestDeviceLocalDay} for the one bound
|
|
277
|
+
* that IS meaningful, and note the backend agrees — it validates no start
|
|
278
|
+
* against the past, for either reading.
|
|
279
|
+
*/
|
|
280
|
+
export function getTimeSlotOptions(
|
|
281
|
+
forDate?: Date | null,
|
|
282
|
+
timeReference?: ScheduleTimeReference | string | null,
|
|
283
|
+
): { value: string; label: string }[] {
|
|
284
|
+
const base = slotBaseMinutes(timeReference);
|
|
285
|
+
const cutoff = !isDeviceLocalTime(timeReference) && forDate && isToday(forDate) ? nowMinutesOfDay() : -1;
|
|
286
|
+
return Array.from({ length: SLOTS_PER_DAY }, (_, slot) => {
|
|
287
|
+
const minutesOfDay = base + slot * SLOT_MINUTES;
|
|
288
|
+
return { value: slotValue(minutesOfDay), label: slotLabel(minutesOfDay), minutesOfDay };
|
|
289
|
+
})
|
|
290
|
+
.filter(slot => slot.minutesOfDay > cutoff)
|
|
291
|
+
.map(({ value, label }) => ({ value, label }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The dropdown reading for an `HH:mm` the option list no longer carries — a
|
|
296
|
+
* stored start whose slot has already gone by today. The form's value has to
|
|
297
|
+
* stay VISIBLE (it is what will be saved if nothing else changes), so the Select
|
|
298
|
+
* gets its option back with the same label the live slots use.
|
|
299
|
+
*/
|
|
300
|
+
export function slotToLabel(slot: string): string {
|
|
301
|
+
const [hours, minutes] = slot.split(':').map(Number);
|
|
302
|
+
return slotLabel(hours * 60 + minutes);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Minutes since local midnight, right now. */
|
|
306
|
+
function nowMinutesOfDay(): number {
|
|
307
|
+
const now = new Date();
|
|
308
|
+
return now.getHours() * 60 + now.getMinutes();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Same local calendar day as right now. */
|
|
312
|
+
function isToday(date: Date): boolean {
|
|
313
|
+
const now = new Date();
|
|
314
|
+
return (
|
|
315
|
+
date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Local midnight today — the earliest day a schedule may start, and what the
|
|
321
|
+
* date picker takes as its `fromDate`.
|
|
322
|
+
*
|
|
323
|
+
* Midnight rather than "now": the calendar selects whole days, so today has to
|
|
324
|
+
* stay pickable all day long. Which of today's SLOTS are still available is the
|
|
325
|
+
* time dropdown's half of the same rule ({@link getTimeSlotOptions}).
|
|
326
|
+
*
|
|
327
|
+
* Built per call — a module-level constant would freeze the boundary at import
|
|
328
|
+
* time and let a tab left open overnight pick yesterday.
|
|
329
|
+
*/
|
|
330
|
+
export function startOfToday(): Date {
|
|
331
|
+
const now = new Date();
|
|
332
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The earliest day a DEVICE_LOCAL schedule may start: today **as read on the
|
|
337
|
+
* westernmost clock on Earth** (UTC−12), returned as a local `Date` the calendar
|
|
338
|
+
* can take as its `fromDate`.
|
|
339
|
+
*
|
|
340
|
+
* The viewer's own midnight is the wrong bound here. A wall clock is not an
|
|
341
|
+
* instant, so a day is "gone" only once it has gone everywhere — and the fleet's
|
|
342
|
+
* clocks span 26 hours, so for a viewer in Kyiv or Auckland the calendar day
|
|
343
|
+
* that already ended for THEM is still running for a device in Baker Island.
|
|
344
|
+
* Bounding by the viewer would refuse a date that is genuinely in the future for
|
|
345
|
+
* part of the fleet; bounding by UTC−12 refuses only what no device anywhere can
|
|
346
|
+
* still reach.
|
|
347
|
+
*
|
|
348
|
+
* In practice this is today, or yesterday for viewers far enough east — which is
|
|
349
|
+
* the whole of the correction, and it is deliberately the only one: within a day
|
|
350
|
+
* that is still live somewhere, WHICH devices are still ahead of the picked time
|
|
351
|
+
* is the runner's business (it fires each device at its own local reading and
|
|
352
|
+
* marks the ones that have gone past their catch-up window MISSED), not a rule
|
|
353
|
+
* the form can usefully pre-empt.
|
|
354
|
+
*/
|
|
355
|
+
export function earliestDeviceLocalDay(): Date {
|
|
356
|
+
const westernmostNow = new Date(Date.now() - 12 * 60 * 60 * 1000);
|
|
357
|
+
return new Date(westernmostNow.getUTCFullYear(), westernmostNow.getUTCMonth(), westernmostNow.getUTCDate());
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The earliest day the picker may offer, for either reading — the calendar's
|
|
362
|
+
* `fromDate`, and the day the "start in the past" rules measure against.
|
|
363
|
+
*/
|
|
364
|
+
export function earliestScheduleDay(timeReference?: ScheduleTimeReference | string | null): Date {
|
|
365
|
+
return isDeviceLocalTime(timeReference) ? earliestDeviceLocalDay() : startOfToday();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Whether a picked day + slot has already gone by. Compared as instants, so a
|
|
370
|
+
* slot that is still minutes away passes.
|
|
371
|
+
*
|
|
372
|
+
* Always false for DEVICE_LOCAL: the pair is a wall clock every device reads on
|
|
373
|
+
* its own timezone, so there is no single instant to have gone by — see
|
|
374
|
+
* {@link getTimeSlotOptions}.
|
|
375
|
+
*/
|
|
376
|
+
export function isScheduleStartInPast(
|
|
377
|
+
date: Date | null | undefined,
|
|
378
|
+
slot: string,
|
|
379
|
+
timeReference?: ScheduleTimeReference | string | null,
|
|
380
|
+
): boolean {
|
|
381
|
+
if (!date || !slot || isDeviceLocalTime(timeReference)) return false;
|
|
382
|
+
return applyTimeSlot(date, slot).getTime() < Date.now();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** What both the field and the schema say about a start that has gone by. */
|
|
386
|
+
export const PAST_START_MESSAGE = 'Start time must be in the future';
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* What a picked TODAY says once its last 30-minute slot has gone by.
|
|
390
|
+
*
|
|
391
|
+
* The dead end this names: `getTimeSlotOptions` correctly withholds every slot,
|
|
392
|
+
* so the Time dropdown has nothing to offer — and an empty popup states no
|
|
393
|
+
* reason and offers no way on. It is the DATE that has to change, which is why
|
|
394
|
+
* the message says so and why the field showing it is the date's.
|
|
395
|
+
*
|
|
396
|
+
* Not a schema rule: a schedule with no time already fails the save on
|
|
397
|
+
* `scheduledTime` being required. This is the earlier, more useful half of the
|
|
398
|
+
* same fact, said while the user can still act on it.
|
|
399
|
+
*/
|
|
400
|
+
export const NO_SLOTS_TODAY_MESSAGE = 'No start times left today — pick another day';
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The "no start in the past" rule as the FORM applies it — stated once, and read
|
|
404
|
+
* by both the fields (which show it the moment it happens) and the schema (which
|
|
405
|
+
* refuses the save).
|
|
406
|
+
*
|
|
407
|
+
* The exemption is the whole reason it needs a name: a schedule already stored
|
|
408
|
+
* with a past start keeps it. Recurring schedules legitimately started long ago,
|
|
409
|
+
* and without this, renaming one would demand re-picking its date. So the stored
|
|
410
|
+
* instant stays legal for exactly as long as the form still shows it; moving
|
|
411
|
+
* either half makes the choice a new one, held to the same rule as any other.
|
|
412
|
+
*
|
|
413
|
+
* The rule does not exist at all for DEVICE_LOCAL — `isScheduleStartInPast`
|
|
414
|
+
* answers false there — which is why the day bound
|
|
415
|
+
* ({@link earliestDeviceLocalDay}) is the only thing left holding that reading
|
|
416
|
+
* to a future the fleet can still reach.
|
|
417
|
+
*/
|
|
418
|
+
export function isStartInPastAndChanged(
|
|
419
|
+
date: Date | null | undefined,
|
|
420
|
+
slot: string,
|
|
421
|
+
storedIso: string | null | undefined,
|
|
422
|
+
timeReference?: ScheduleTimeReference | string | null,
|
|
423
|
+
): boolean {
|
|
424
|
+
if (!isScheduleStartInPast(date, slot, timeReference) || !date) return false;
|
|
425
|
+
// Read under the schedule's own reading, or the stored start of a DEVICE_LOCAL
|
|
426
|
+
// schedule would come back shifted by the viewer's offset and never match the
|
|
427
|
+
// pair on screen — the exemption would lapse and an untouched schedule would
|
|
428
|
+
// become unsaveable.
|
|
429
|
+
const stored = storedIso ? fromScheduleInstant(storedIso, timeReference).getTime() : null;
|
|
430
|
+
return applyTimeSlot(date, slot).getTime() !== stored;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* A picked date → the local `HH:mm` slot it sits on (empty when no date yet).
|
|
435
|
+
*
|
|
436
|
+
* A stored `startAt` always lands on a slot exactly, since the backend enforces
|
|
437
|
+
* the same grid this builds from; the flooring is for values authored outside
|
|
438
|
+
* this UI, and it keeps them on a slot the dropdown can actually show.
|
|
439
|
+
*/
|
|
440
|
+
export function dateToTimeSlot(
|
|
441
|
+
date: Date | null | undefined,
|
|
442
|
+
timeReference?: ScheduleTimeReference | string | null,
|
|
443
|
+
): string {
|
|
444
|
+
if (!date) return '';
|
|
445
|
+
const base = slotBaseMinutes(timeReference);
|
|
446
|
+
const minutesOfDay = date.getHours() * 60 + date.getMinutes();
|
|
447
|
+
const slot = Math.min(SLOTS_PER_DAY - 1, Math.max(0, Math.floor((minutesOfDay - base) / SLOT_MINUTES)));
|
|
448
|
+
return slotValue(base + slot * SLOT_MINUTES);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Applies an `HH:mm` slot to the picked date. Choosing a time before a date
|
|
453
|
+
* anchors it to today — the same fallback the core date-time picker uses.
|
|
454
|
+
*/
|
|
455
|
+
export function applyTimeSlot(date: Date | null | undefined, slot: string): Date {
|
|
456
|
+
const [hours, minutes] = slot.split(':').map(Number);
|
|
457
|
+
const next = date ? new Date(date) : new Date();
|
|
458
|
+
next.setHours(hours, minutes, 0, 0);
|
|
459
|
+
return next;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Inverse of `toScheduleInstant`: a stored UTC instant → the `Date` for
|
|
464
|
+
* that moment, which the picker and every formatter below then read through the
|
|
465
|
+
* viewer's own clock.
|
|
466
|
+
*
|
|
467
|
+
* A DEVICE_LOCAL start carries no moment to convert — its digits are a wall
|
|
468
|
+
* clock — so they are lifted back out of UTC and rebuilt as LOCAL components.
|
|
469
|
+
* The `Date` that comes back is therefore not the instant the string names; it
|
|
470
|
+
* is the reading, in the only form the pickers and formatters here can hold one.
|
|
471
|
+
* Passing the reference through is what keeps "9 AM on every device" from
|
|
472
|
+
* displaying as noon to the viewer who typed it.
|
|
473
|
+
*/
|
|
474
|
+
export function fromScheduleInstant(iso: string, timeReference?: ScheduleTimeReference | string | null): Date {
|
|
475
|
+
const instant = new Date(iso);
|
|
476
|
+
if (!isDeviceLocalTime(timeReference)) return instant;
|
|
477
|
+
return new Date(
|
|
478
|
+
instant.getUTCFullYear(),
|
|
479
|
+
instant.getUTCMonth(),
|
|
480
|
+
instant.getUTCDate(),
|
|
481
|
+
instant.getUTCHours(),
|
|
482
|
+
instant.getUTCMinutes(),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { TruncateText } from '../../ui/truncate-text';
|
|
4
|
+
|
|
5
|
+
/** A name over its description: the first column of both Scripts tables. */
|
|
6
|
+
export function ScriptNameCell({ name, description }: { name: string; description?: string | null }) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex min-w-0 flex-col justify-center gap-1">
|
|
9
|
+
<TruncateText>{name}</TruncateText>
|
|
10
|
+
{description && (
|
|
11
|
+
<TruncateText variant="h6" tone="secondary">
|
|
12
|
+
{description}
|
|
13
|
+
</TruncateText>
|
|
14
|
+
)}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** When a schedule starts: the date over the time (and whose clock the time is read on). */
|
|
20
|
+
export function ScheduleDateTimeCell({ date, time }: { date: string; time: string }) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex min-w-0 flex-col justify-center gap-1">
|
|
23
|
+
<TruncateText>{date}</TruncateText>
|
|
24
|
+
<TruncateText variant="h6" tone="secondary">
|
|
25
|
+
{time}
|
|
26
|
+
</TruncateText>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useCallback, useMemo } from 'react';
|
|
4
|
+
import { type ColumnDef, DataTable, type Row, useDataTable } from '../../ui/data-table';
|
|
5
|
+
import { SquareAvatar } from '../../ui/square-avatar';
|
|
6
|
+
import {
|
|
7
|
+
ExecutionDeviceCell,
|
|
8
|
+
ExecutionInitiatorCell,
|
|
9
|
+
ExecutionResultCell,
|
|
10
|
+
ExecutionStatusCell,
|
|
11
|
+
ExecutionTimestampCell,
|
|
12
|
+
} from './execution-cells';
|
|
13
|
+
import { EXECUTION_COLUMNS, EXECUTIONS_TABLE_COLUMNS, type ScriptsTableColumnLayout } from './scripts-table-columns';
|
|
14
|
+
import { OpenInNewTabCell, scriptsColumnMeta } from './scripts-table-shared';
|
|
15
|
+
|
|
16
|
+
/** One execution, as the Execution History shows it. */
|
|
17
|
+
export interface ScriptExecutionTableRow {
|
|
18
|
+
id: string;
|
|
19
|
+
executionId: string;
|
|
20
|
+
/** When it was dispatched, already formatted. */
|
|
21
|
+
timestamp: string;
|
|
22
|
+
/** `'QUEUED'`, `'RUNNING'`, `'SUCCESS'` or `'FAILED'`. */
|
|
23
|
+
status: string;
|
|
24
|
+
machineName: string;
|
|
25
|
+
/** The device's customer. */
|
|
26
|
+
organization?: string | null;
|
|
27
|
+
/** Who it ran for. */
|
|
28
|
+
initiator: { name: string; initials: string; image?: string };
|
|
29
|
+
/** `'MANUAL'`, `'SCHEDULED'` or `'AI_ASSISTANT'`. */
|
|
30
|
+
source?: string | null;
|
|
31
|
+
/** Which script ran: only where a list runs several (a schedule's). */
|
|
32
|
+
scriptName?: string | null;
|
|
33
|
+
/** Everything the run printed. */
|
|
34
|
+
result?: string | null;
|
|
35
|
+
/** The execution's page: the row's link and the open-in-new-tab button. */
|
|
36
|
+
href?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScriptExecutionsTableProps {
|
|
40
|
+
rows: ScriptExecutionTableRow[];
|
|
41
|
+
/**
|
|
42
|
+
* The columns and their widths. Defaults to the product's
|
|
43
|
+
* (`EXECUTIONS_TABLE_COLUMNS`); a host narrower than the product's page
|
|
44
|
+
* passes its own arrangement of `EXECUTION_COLUMNS`.
|
|
45
|
+
*/
|
|
46
|
+
columns?: readonly ScriptsTableColumnLayout[];
|
|
47
|
+
/** Column ids to leave out (`'result'`, `'open'`). */
|
|
48
|
+
hideColumns?: readonly string[];
|
|
49
|
+
/** Content under a row, inside its card: an execution's output, for one. */
|
|
50
|
+
renderSubRow?: (row: ScriptExecutionTableRow) => ReactNode;
|
|
51
|
+
/** The table's dense rows, as tall as their two lines (the product's page uses the full row). */
|
|
52
|
+
compact?: boolean;
|
|
53
|
+
isLoading?: boolean;
|
|
54
|
+
emptyMessage?: string;
|
|
55
|
+
/** Draw the column header row; a picture short of height leaves it out. */
|
|
56
|
+
showHeader?: boolean;
|
|
57
|
+
/** Rendered at the right edge of the header (a row count). */
|
|
58
|
+
rightSlot?: ReactNode;
|
|
59
|
+
skeletonRows?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The Execution History of a script or a schedule, presentational: every
|
|
64
|
+
* run with its time, status, device and who it ran for. Rows are a view-model
|
|
65
|
+
* the host maps its executions into. The product's own page keeps its live
|
|
66
|
+
* table (filters, paging, the date header) over the same cells and layout.
|
|
67
|
+
*/
|
|
68
|
+
export function ScriptExecutionsTable({
|
|
69
|
+
rows,
|
|
70
|
+
columns: layout = EXECUTIONS_TABLE_COLUMNS,
|
|
71
|
+
hideColumns,
|
|
72
|
+
renderSubRow,
|
|
73
|
+
compact = false,
|
|
74
|
+
isLoading = false,
|
|
75
|
+
emptyMessage = 'No executions found.',
|
|
76
|
+
showHeader: withHeader = true,
|
|
77
|
+
rightSlot,
|
|
78
|
+
skeletonRows = 10,
|
|
79
|
+
}: ScriptExecutionsTableProps) {
|
|
80
|
+
const columns = useMemo<ColumnDef<ScriptExecutionTableRow>[]>(() => {
|
|
81
|
+
const renderers: Record<string, (row: ScriptExecutionTableRow) => ReactNode> = {
|
|
82
|
+
[EXECUTION_COLUMNS.executionId.id]: row => (
|
|
83
|
+
<ExecutionTimestampCell timestamp={row.timestamp} executionId={row.executionId} />
|
|
84
|
+
),
|
|
85
|
+
[EXECUTION_COLUMNS.status.id]: row => <ExecutionStatusCell status={row.status} />,
|
|
86
|
+
[EXECUTION_COLUMNS.machineId.id]: row => (
|
|
87
|
+
<ExecutionDeviceCell name={row.machineName} organization={row.organization} />
|
|
88
|
+
),
|
|
89
|
+
[EXECUTION_COLUMNS.initiatorId.id]: row => (
|
|
90
|
+
<ExecutionInitiatorCell
|
|
91
|
+
avatar={
|
|
92
|
+
<SquareAvatar
|
|
93
|
+
variant="round"
|
|
94
|
+
size="md"
|
|
95
|
+
src={row.initiator.image}
|
|
96
|
+
fallback={row.initiator.initials}
|
|
97
|
+
alt={row.initiator.name}
|
|
98
|
+
initialsClassName="text-ods-text-secondary"
|
|
99
|
+
/>
|
|
100
|
+
}
|
|
101
|
+
name={row.initiator.name}
|
|
102
|
+
source={row.source}
|
|
103
|
+
scriptName={row.scriptName}
|
|
104
|
+
/>
|
|
105
|
+
),
|
|
106
|
+
[EXECUTION_COLUMNS.result.id]: row => <ExecutionResultCell result={row.result} />,
|
|
107
|
+
[EXECUTION_COLUMNS.open.id]: row => (row.href ? <OpenInNewTabCell href={row.href} /> : null),
|
|
108
|
+
};
|
|
109
|
+
// No row menu here: its one action (copy the id) is the product's, so the actions column is left out.
|
|
110
|
+
return layout
|
|
111
|
+
.filter(column => renderers[column.id] && !hideColumns?.includes(column.id))
|
|
112
|
+
.map(column => ({
|
|
113
|
+
id: column.id,
|
|
114
|
+
header: column.header ?? '',
|
|
115
|
+
cell: ({ row }: { row: Row<ScriptExecutionTableRow> }) => renderers[column.id](row.original),
|
|
116
|
+
enableSorting: false,
|
|
117
|
+
meta: scriptsColumnMeta(column),
|
|
118
|
+
}));
|
|
119
|
+
}, [layout, hideColumns]);
|
|
120
|
+
|
|
121
|
+
const table = useDataTable<ScriptExecutionTableRow>({
|
|
122
|
+
data: rows,
|
|
123
|
+
columns,
|
|
124
|
+
getRowId: (row: ScriptExecutionTableRow) => row.id,
|
|
125
|
+
enableSorting: false,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const rowHref = useCallback((row: ScriptExecutionTableRow) => row.href, []);
|
|
129
|
+
const showHeader = withHeader && (isLoading || rows.length > 0);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<DataTable table={table}>
|
|
133
|
+
{showHeader && <DataTable.Header rightSlot={rightSlot} />}
|
|
134
|
+
<DataTable.Body
|
|
135
|
+
loading={isLoading}
|
|
136
|
+
skeletonRows={skeletonRows}
|
|
137
|
+
emptyMessage={emptyMessage}
|
|
138
|
+
rowClassName="mb-1"
|
|
139
|
+
compact={compact}
|
|
140
|
+
rowHref={rowHref}
|
|
141
|
+
renderSubRow={renderSubRow}
|
|
142
|
+
/>
|
|
143
|
+
</DataTable>
|
|
144
|
+
);
|
|
145
|
+
}
|