@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.
Files changed (206) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-BV5WZVOP.cjs → chunk-32Q7K5OP.cjs} +254 -254
  3. package/dist/{chunk-ZLTCQKTD.cjs → chunk-3LW43GIJ.cjs} +6 -6
  4. package/dist/{chunk-VA7OMDZO.cjs → chunk-4FWURR3W.cjs} +50 -50
  5. package/dist/{chunk-EKGL5K5K.cjs → chunk-4LDU32OB.cjs} +17 -17
  6. package/dist/{chunk-VC4IV3AY.cjs → chunk-4PKORVOX.cjs} +2804 -954
  7. package/dist/{chunk-3ZIQOYNX.cjs → chunk-54EA4RLX.cjs} +8 -8
  8. package/dist/{chunk-YXKB5WRE.cjs → chunk-5MH6BRPY.cjs} +36 -36
  9. package/dist/{chunk-PQ2M7P2U.cjs → chunk-5TCIRWHL.cjs} +9 -9
  10. package/dist/{chunk-WFPCCBOM.cjs → chunk-7AM3AFM4.cjs} +10 -10
  11. package/dist/{chunk-7ZFCR7UY.cjs → chunk-ABLUMGCI.cjs} +3 -3
  12. package/dist/{chunk-PX7FY655.cjs → chunk-ASMNNSUQ.cjs} +3 -3
  13. package/dist/{chunk-HF7Z2E4W.cjs → chunk-BI3XBCQX.cjs} +2 -2
  14. package/dist/{chunk-SA7LDEVJ.cjs → chunk-CSJO7ZGN.cjs} +3 -3
  15. package/dist/{chunk-VOX4E6AX.cjs → chunk-DGFDBPO7.cjs} +7 -5
  16. package/dist/{chunk-KHXZFLIN.cjs → chunk-DKOLQPE7.cjs} +8 -2
  17. package/dist/{chunk-BDA5F6RM.cjs → chunk-DNFSMVXJ.cjs} +16 -16
  18. package/dist/{chunk-B3XBJCOD.cjs → chunk-H3UA7UY5.cjs} +7 -7
  19. package/dist/{chunk-2Y5Y4FLM.cjs → chunk-HDBY2URA.cjs} +64 -64
  20. package/dist/{chunk-UW3YXDC4.cjs → chunk-HIRVNBX5.cjs} +20 -20
  21. package/dist/{chunk-LLAC2UZ3.cjs → chunk-ISJXKKYW.cjs} +28 -28
  22. package/dist/{chunk-FICJS5OA.cjs → chunk-KTPR66EE.cjs} +4 -4
  23. package/dist/{chunk-7UIQPMBJ.cjs → chunk-L34TKFTS.cjs} +21 -164
  24. package/dist/{chunk-AAFW4STC.cjs → chunk-LU7TD6QS.cjs} +5 -5
  25. package/dist/{chunk-3HPB4H4C.cjs → chunk-NTGIQP7O.cjs} +7 -7
  26. package/dist/{chunk-WBANGORJ.cjs → chunk-OBODY7F3.cjs} +3 -3
  27. package/dist/{chunk-GABDE3FM.cjs → chunk-OWGLRE74.cjs} +19 -19
  28. package/dist/{chunk-CWRVTQUO.cjs → chunk-PS6T65KA.cjs} +101 -101
  29. package/dist/{chunk-IFS4AH77.cjs → chunk-QYLIHZKZ.cjs} +10 -10
  30. package/dist/{chunk-GVTDVS5U.cjs → chunk-R34JD6NP.cjs} +3 -3
  31. package/dist/{chunk-VU4CKUJW.cjs → chunk-R6SCSV2I.cjs} +103 -103
  32. package/dist/{chunk-II4QMH26.cjs → chunk-STEJZPIO.cjs} +3 -3
  33. package/dist/{chunk-4RLGLB45.cjs → chunk-T433HSL6.cjs} +47 -47
  34. package/dist/{chunk-DSVNFRY5.cjs → chunk-U2O5YQRP.cjs} +38 -38
  35. package/dist/{chunk-O67V3HJR.cjs → chunk-URVIPSAJ.cjs} +24 -24
  36. package/dist/{chunk-HYBMADYO.cjs → chunk-UWHCRIQQ.cjs} +22 -22
  37. package/dist/{chunk-GSWQZX23.cjs → chunk-UY6A6TEY.cjs} +2 -2
  38. package/dist/{chunk-D5KEWWLW.cjs → chunk-UZLRSFGG.cjs} +32 -32
  39. package/dist/{chunk-DUVOQHQI.cjs → chunk-W2RM764D.cjs} +7 -7
  40. package/dist/{chunk-NTJE6SM6.cjs → chunk-WI2OTGKC.cjs} +2 -2
  41. package/dist/{chunk-SFY4UYPY.cjs → chunk-WIHHA54J.cjs} +44 -44
  42. package/dist/{chunk-YRE3O65E.cjs → chunk-XAA5EZ5T.cjs} +12 -12
  43. package/dist/{chunk-KLI4OSRA.cjs → chunk-YQGDXH3M.cjs} +2 -2
  44. package/dist/{chunk-SWR22DOZ.cjs → chunk-Z2DQYKZA.cjs} +89 -89
  45. package/dist/{chunk-5SVOGLNK.cjs → chunk-ZIFYSLIM.cjs} +6 -6
  46. package/dist/{cloud-tenants-PVXDI5MU.cjs → cloud-tenants-6ICAL3HO.cjs} +34 -34
  47. package/dist/components/case-studies/index.cjs +37 -37
  48. package/dist/components/chat/index.cjs +17 -17
  49. package/dist/components/contact/index.cjs +25 -25
  50. package/dist/components/docs/index.cjs +28 -28
  51. package/dist/components/embeds/index.cjs +25 -25
  52. package/dist/components/faq/index.cjs +26 -26
  53. package/dist/components/features/capability-explorer.d.ts +9 -2
  54. package/dist/components/features/capability-explorer.d.ts.map +1 -1
  55. package/dist/components/features/capability-explorer.js +113 -6
  56. package/dist/components/features/index.cjs +162 -24
  57. package/dist/components/features/index.d.ts +1 -0
  58. package/dist/components/features/index.d.ts.map +1 -1
  59. package/dist/components/features/index.js +1 -0
  60. package/dist/components/features/scripts/execution-cells.d.ts +65 -0
  61. package/dist/components/features/scripts/execution-cells.d.ts.map +1 -0
  62. package/dist/components/features/scripts/execution-cells.js +75 -0
  63. package/dist/components/features/scripts/execution-status.d.ts +48 -0
  64. package/dist/components/features/scripts/execution-status.d.ts.map +1 -0
  65. package/dist/components/features/scripts/execution-status.js +15 -0
  66. package/dist/components/features/scripts/index.d.ts +20 -0
  67. package/dist/components/features/scripts/index.d.ts.map +1 -0
  68. package/dist/components/features/scripts/index.js +59 -0
  69. package/dist/components/features/scripts/schedule-form/index.d.ts +18 -0
  70. package/dist/components/features/scripts/schedule-form/index.d.ts.map +1 -0
  71. package/dist/components/features/scripts/schedule-form/index.js +84 -0
  72. package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts +24 -0
  73. package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts.map +1 -0
  74. package/dist/components/features/scripts/schedule-form/schedule-enums.js +18 -0
  75. package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts +57 -0
  76. package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts.map +1 -0
  77. package/dist/components/features/scripts/schedule-form/schedule-form-fields.js +169 -0
  78. package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts +138 -0
  79. package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts.map +1 -0
  80. package/dist/components/features/scripts/schedule-form/schedule-form-types.js +72 -0
  81. package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts +54 -0
  82. package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts.map +1 -0
  83. package/dist/components/features/scripts/schedule-form/schedule-interval-input.js +60 -0
  84. package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts +29 -0
  85. package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts.map +1 -0
  86. package/dist/components/features/scripts/schedule-form/schedule-offline-fields.js +124 -0
  87. package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts +52 -0
  88. package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts.map +1 -0
  89. package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.js +244 -0
  90. package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts +35 -0
  91. package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts.map +1 -0
  92. package/dist/components/features/scripts/schedule-form/schedule-script-search-field.js +58 -0
  93. package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts +25 -0
  94. package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts.map +1 -0
  95. package/dist/components/features/scripts/schedule-form/schedule-scripts-field.js +66 -0
  96. package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts +77 -0
  97. package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts.map +1 -0
  98. package/dist/components/features/scripts/schedule-form/schedule-timing-fields.js +262 -0
  99. package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts +288 -0
  100. package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts.map +1 -0
  101. package/dist/components/features/scripts/schedule-form/schedule-timing.js +165 -0
  102. package/dist/components/features/scripts/script-cells.d.ts +11 -0
  103. package/dist/components/features/scripts/script-cells.d.ts.map +1 -0
  104. package/dist/components/features/scripts/script-cells.js +19 -0
  105. package/dist/components/features/scripts/script-executions-table.d.ts +58 -0
  106. package/dist/components/features/scripts/script-executions-table.d.ts.map +1 -0
  107. package/dist/components/features/scripts/script-executions-table.js +88 -0
  108. package/dist/components/features/scripts/script-schedules-table.d.ts +38 -0
  109. package/dist/components/features/scripts/script-schedules-table.d.ts.map +1 -0
  110. package/dist/components/features/scripts/script-schedules-table.js +71 -0
  111. package/dist/components/features/scripts/script-shell-badge.d.ts +9 -0
  112. package/dist/components/features/scripts/script-shell-badge.d.ts.map +1 -0
  113. package/dist/components/features/scripts/script-shell-badge.js +15 -0
  114. package/dist/components/features/scripts/scripts-table-columns.d.ts +162 -0
  115. package/dist/components/features/scripts/scripts-table-columns.d.ts.map +1 -0
  116. package/dist/components/features/scripts/scripts-table-columns.js +69 -0
  117. package/dist/components/features/scripts/scripts-table-shared.d.ts +19 -0
  118. package/dist/components/features/scripts/scripts-table-shared.d.ts.map +1 -0
  119. package/dist/components/features/scripts/scripts-table-shared.js +35 -0
  120. package/dist/components/features/scripts/scripts-table.d.ts +40 -0
  121. package/dist/components/features/scripts/scripts-table.d.ts.map +1 -0
  122. package/dist/components/features/scripts/scripts-table.js +84 -0
  123. package/dist/components/features/scripts/scripts-tabs.d.ts +5 -0
  124. package/dist/components/features/scripts/scripts-tabs.d.ts.map +1 -0
  125. package/dist/components/features/scripts/scripts-tabs.js +10 -0
  126. package/dist/components/features/scripts/shell-types.d.ts +13 -0
  127. package/dist/components/features/scripts/shell-types.d.ts.map +1 -0
  128. package/dist/components/features/scripts/shell-types.js +21 -0
  129. package/dist/components/help-center-pages/index.cjs +144 -144
  130. package/dist/components/index.cjs +224 -86
  131. package/dist/components/meeting-scheduler/index.cjs +74 -74
  132. package/dist/components/navigation/index.cjs +27 -27
  133. package/dist/components/onboarding-guides/index.cjs +27 -27
  134. package/dist/components/product-demo/fixtures/scripts.d.ts +34 -0
  135. package/dist/components/product-demo/fixtures/scripts.d.ts.map +1 -0
  136. package/dist/components/product-demo/fixtures/scripts.js +123 -0
  137. package/dist/components/product-demo/fixtures/shared.d.ts +8 -0
  138. package/dist/components/product-demo/fixtures/shared.d.ts.map +1 -1
  139. package/dist/components/product-demo/fixtures/shared.js +7 -1
  140. package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
  141. package/dist/components/product-demo/fixtures/software-update.js +1 -5
  142. package/dist/components/product-demo/index.cjs +16 -13
  143. package/dist/components/product-demo/product-screen.d.ts.map +1 -1
  144. package/dist/components/product-demo/product-screen.js +2 -1
  145. package/dist/components/product-demo/registry.d.ts.map +1 -1
  146. package/dist/components/product-demo/registry.js +2 -1
  147. package/dist/components/product-demo/screen-keys.cjs +2 -1
  148. package/dist/components/product-demo/screen-keys.d.ts +1 -0
  149. package/dist/components/product-demo/screen-keys.d.ts.map +1 -1
  150. package/dist/components/product-demo/screen-keys.js +2 -1
  151. package/dist/components/product-demo/screens/scripts.d.ts +11 -0
  152. package/dist/components/product-demo/screens/scripts.d.ts.map +1 -0
  153. package/dist/components/product-demo/screens/scripts.js +130 -0
  154. package/dist/components/related-content/index.cjs +12 -12
  155. package/dist/components/tickets/index.cjs +30 -30
  156. package/dist/components/ui/index.cjs +24 -24
  157. package/dist/{devices-TWEA7XOR.cjs → devices-OFP2IBKN.cjs} +30 -30
  158. package/dist/{embeddable-chat-6JKKFQEB.cjs → embeddable-chat-4C22MGNC.cjs} +92 -92
  159. package/dist/hooks/index.cjs +10 -10
  160. package/dist/index.cjs +169 -31
  161. package/dist/{knowledge-4GQDBNQB.cjs → knowledge-KKBCSNUA.cjs} +29 -29
  162. package/dist/{logs-EVU6GJHZ.cjs → logs-X2GRNO57.cjs} +46 -46
  163. package/dist/{policies-OZUEA5YS.cjs → policies-35BXKB6P.cjs} +31 -31
  164. package/dist/{remote-session-DNFZ5OLY.cjs → remote-session-N4PJB5FW.cjs} +15 -15
  165. package/dist/scripts-KQYVQOLU.cjs +2112 -0
  166. package/dist/{software-update-BLIVYOY2.cjs → software-update-ED33WSKT.cjs} +36 -40
  167. package/dist/{tickets-board-CKZRB6YV.cjs → tickets-board-O753S4ZB.cjs} +31 -31
  168. package/dist/utils/embed-authed-fetch.d.ts +33 -0
  169. package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
  170. package/dist/utils/embed-authed-fetch.js +3 -1
  171. package/dist/utils/index.cjs +3 -1
  172. package/dist/{video-file-player-ZH2ILAPO.cjs → video-file-player-QZ3YOLHO.cjs} +2 -2
  173. package/package.json +1 -1
  174. package/src/components/features/capability-explorer.tsx +134 -7
  175. package/src/components/features/index.ts +1 -0
  176. package/src/components/features/scripts/execution-cells.tsx +149 -0
  177. package/src/components/features/scripts/execution-status.ts +42 -0
  178. package/src/components/features/scripts/index.ts +42 -0
  179. package/src/components/features/scripts/schedule-form/index.ts +59 -0
  180. package/src/components/features/scripts/schedule-form/schedule-enums.ts +26 -0
  181. package/src/components/features/scripts/schedule-form/schedule-form-fields.tsx +249 -0
  182. package/src/components/features/scripts/schedule-form/schedule-form-types.ts +189 -0
  183. package/src/components/features/scripts/schedule-form/schedule-interval-input.tsx +121 -0
  184. package/src/components/features/scripts/schedule-form/schedule-offline-fields.tsx +218 -0
  185. package/src/components/features/scripts/schedule-form/schedule-script-picker-card.tsx +321 -0
  186. package/src/components/features/scripts/schedule-form/schedule-script-search-field.tsx +115 -0
  187. package/src/components/features/scripts/schedule-form/schedule-scripts-field.tsx +97 -0
  188. package/src/components/features/scripts/schedule-form/schedule-timing-fields.tsx +482 -0
  189. package/src/components/features/scripts/schedule-form/schedule-timing.ts +484 -0
  190. package/src/components/features/scripts/script-cells.tsx +29 -0
  191. package/src/components/features/scripts/script-executions-table.tsx +145 -0
  192. package/src/components/features/scripts/script-schedules-table.tsx +113 -0
  193. package/src/components/features/scripts/script-shell-badge.tsx +25 -0
  194. package/src/components/features/scripts/scripts-table-columns.ts +95 -0
  195. package/src/components/features/scripts/scripts-table-shared.tsx +43 -0
  196. package/src/components/features/scripts/scripts-table.tsx +124 -0
  197. package/src/components/features/scripts/scripts-tabs.ts +11 -0
  198. package/src/components/features/scripts/shell-types.ts +26 -0
  199. package/src/components/product-demo/fixtures/scripts.ts +186 -0
  200. package/src/components/product-demo/fixtures/shared.ts +7 -0
  201. package/src/components/product-demo/fixtures/software-update.ts +1 -7
  202. package/src/components/product-demo/product-screen.tsx +3 -1
  203. package/src/components/product-demo/registry.ts +1 -0
  204. package/src/components/product-demo/screen-keys.ts +1 -0
  205. package/src/components/product-demo/screens/scripts.tsx +192 -0
  206. 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
+ }