railwatch 0.6.1 → 0.7.0

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 (345) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/app/frontend/components/app-content.tsx +26 -0
  4. data/app/frontend/components/app-logo-icon.tsx +23 -0
  5. data/app/frontend/components/app-logo.tsx +14 -0
  6. data/app/frontend/components/app-shell.tsx +38 -0
  7. data/app/frontend/components/app-sidebar-header.tsx +54 -0
  8. data/app/frontend/components/app-sidebar.tsx +94 -0
  9. data/app/frontend/components/app-wordmark.tsx +29 -0
  10. data/app/frontend/components/breadcrumbs.tsx +46 -0
  11. data/app/frontend/components/icon.tsx +16 -0
  12. data/app/frontend/components/input-error.tsx +18 -0
  13. data/app/frontend/components/nav-footer.tsx +46 -0
  14. data/app/frontend/components/nav-main.tsx +41 -0
  15. data/app/frontend/components/nav-user.tsx +50 -0
  16. data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
  17. data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
  18. data/app/frontend/components/railwatch/code.tsx +28 -0
  19. data/app/frontend/components/railwatch/command-palette.tsx +132 -0
  20. data/app/frontend/components/railwatch/copy-block.tsx +52 -0
  21. data/app/frontend/components/railwatch/copy-id.tsx +49 -0
  22. data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
  23. data/app/frontend/components/railwatch/data-table.tsx +195 -0
  24. data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
  25. data/app/frontend/components/railwatch/empty-state.tsx +159 -0
  26. data/app/frontend/components/railwatch/env-nav.tsx +130 -0
  27. data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
  28. data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
  29. data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
  30. data/app/frontend/components/railwatch/frames.tsx +295 -0
  31. data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
  32. data/app/frontend/components/railwatch/live-dot.tsx +78 -0
  33. data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
  34. data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
  35. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
  36. data/app/frontend/components/railwatch/onboarding.tsx +91 -0
  37. data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
  38. data/app/frontend/components/railwatch/page-header.tsx +42 -0
  39. data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
  40. data/app/frontend/components/railwatch/relative-time.tsx +18 -0
  41. data/app/frontend/components/railwatch/saved-views.tsx +232 -0
  42. data/app/frontend/components/railwatch/segmented.tsx +64 -0
  43. data/app/frontend/components/railwatch/series-chart.tsx +400 -0
  44. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
  45. data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
  46. data/app/frontend/components/railwatch/sort-header.tsx +37 -0
  47. data/app/frontend/components/railwatch/source-link.tsx +57 -0
  48. data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
  49. data/app/frontend/components/railwatch/sql-block.tsx +116 -0
  50. data/app/frontend/components/railwatch/stat.tsx +173 -0
  51. data/app/frontend/components/railwatch/status-badge.tsx +79 -0
  52. data/app/frontend/components/railwatch/step-picker.tsx +57 -0
  53. data/app/frontend/components/railwatch/timeline.tsx +405 -0
  54. data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
  55. data/app/frontend/components/railwatch/window-picker.tsx +147 -0
  56. data/app/frontend/components/ui/avatar.tsx +51 -0
  57. data/app/frontend/components/ui/badge.tsx +46 -0
  58. data/app/frontend/components/ui/breadcrumb.tsx +109 -0
  59. data/app/frontend/components/ui/button.tsx +60 -0
  60. data/app/frontend/components/ui/card.tsx +92 -0
  61. data/app/frontend/components/ui/chart.tsx +374 -0
  62. data/app/frontend/components/ui/checkbox.tsx +32 -0
  63. data/app/frontend/components/ui/collapsible.tsx +31 -0
  64. data/app/frontend/components/ui/command.tsx +182 -0
  65. data/app/frontend/components/ui/dialog.tsx +158 -0
  66. data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
  67. data/app/frontend/components/ui/hover-card.tsx +42 -0
  68. data/app/frontend/components/ui/input.tsx +21 -0
  69. data/app/frontend/components/ui/label.tsx +24 -0
  70. data/app/frontend/components/ui/popover.tsx +87 -0
  71. data/app/frontend/components/ui/select.tsx +185 -0
  72. data/app/frontend/components/ui/separator.tsx +28 -0
  73. data/app/frontend/components/ui/sheet.tsx +137 -0
  74. data/app/frontend/components/ui/sidebar.tsx +726 -0
  75. data/app/frontend/components/ui/skeleton.tsx +13 -0
  76. data/app/frontend/components/ui/sonner.tsx +41 -0
  77. data/app/frontend/components/ui/table.tsx +114 -0
  78. data/app/frontend/components/ui/tabs.tsx +91 -0
  79. data/app/frontend/components/ui/textarea.tsx +18 -0
  80. data/app/frontend/components/ui/tooltip.tsx +61 -0
  81. data/app/frontend/components/user-info.tsx +32 -0
  82. data/app/frontend/components/user-menu-content.tsx +94 -0
  83. data/app/frontend/entrypoints/application.css +244 -0
  84. data/app/frontend/entrypoints/inertia.tsx +95 -0
  85. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  86. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  87. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  88. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  89. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  90. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  91. data/app/frontend/hooks/use-appearance.tsx +67 -0
  92. data/app/frontend/hooks/use-clipboard.ts +32 -0
  93. data/app/frontend/hooks/use-flash.tsx +23 -0
  94. data/app/frontend/hooks/use-hotkeys.ts +63 -0
  95. data/app/frontend/hooks/use-initials.tsx +15 -0
  96. data/app/frontend/hooks/use-live.ts +179 -0
  97. data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
  98. data/app/frontend/hooks/use-mobile.ts +21 -0
  99. data/app/frontend/hooks/use-percentile.ts +24 -0
  100. data/app/frontend/hooks/use-reduced-motion.ts +25 -0
  101. data/app/frontend/hooks/use-row-nav.ts +106 -0
  102. data/app/frontend/hooks/use-step.ts +27 -0
  103. data/app/frontend/hooks/use-ticker.ts +12 -0
  104. data/app/frontend/hooks/use-window.ts +58 -0
  105. data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
  106. data/app/frontend/layouts/app-layout.tsx +21 -0
  107. data/app/frontend/layouts/env-layout.tsx +53 -0
  108. data/app/frontend/layouts/persistent-layout.tsx +18 -0
  109. data/app/frontend/lib/cable.ts +10 -0
  110. data/app/frontend/lib/env-pages.ts +236 -0
  111. data/app/frontend/lib/execution-path.ts +55 -0
  112. data/app/frontend/lib/explain.ts +50 -0
  113. data/app/frontend/lib/filter.ts +25 -0
  114. data/app/frontend/lib/flamegraph.ts +155 -0
  115. data/app/frontend/lib/format.ts +71 -0
  116. data/app/frontend/lib/keyboard.ts +18 -0
  117. data/app/frontend/lib/source-link.ts +217 -0
  118. data/app/frontend/lib/utils.ts +6 -0
  119. data/app/frontend/pages/alerts/index.tsx +171 -0
  120. data/app/frontend/pages/broadcasts/index.tsx +171 -0
  121. data/app/frontend/pages/cache_events/index.tsx +127 -0
  122. data/app/frontend/pages/commands/index.tsx +143 -0
  123. data/app/frontend/pages/deploys/index.tsx +94 -0
  124. data/app/frontend/pages/deploys/show.tsx +309 -0
  125. data/app/frontend/pages/deprecations/index.tsx +176 -0
  126. data/app/frontend/pages/exceptions/index.tsx +271 -0
  127. data/app/frontend/pages/executions/show.tsx +821 -0
  128. data/app/frontend/pages/issues/index.tsx +399 -0
  129. data/app/frontend/pages/issues/show.tsx +1367 -0
  130. data/app/frontend/pages/jobs/index.tsx +381 -0
  131. data/app/frontend/pages/jobs/klass.tsx +215 -0
  132. data/app/frontend/pages/llm_calls/index.tsx +664 -0
  133. data/app/frontend/pages/logs/index.tsx +303 -0
  134. data/app/frontend/pages/mails/index.tsx +218 -0
  135. data/app/frontend/pages/notifications/index.tsx +161 -0
  136. data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
  137. data/app/frontend/pages/overview/show.tsx +333 -0
  138. data/app/frontend/pages/people/index.tsx +102 -0
  139. data/app/frontend/pages/people/show.tsx +149 -0
  140. data/app/frontend/pages/processes/index.tsx +335 -0
  141. data/app/frontend/pages/profiles/index.tsx +110 -0
  142. data/app/frontend/pages/profiles/show.tsx +105 -0
  143. data/app/frontend/pages/queries/index.tsx +492 -0
  144. data/app/frontend/pages/queries/show.tsx +276 -0
  145. data/app/frontend/pages/releases/index.tsx +190 -0
  146. data/app/frontend/pages/releases/release-health.tsx +68 -0
  147. data/app/frontend/pages/releases/release-path.ts +25 -0
  148. data/app/frontend/pages/releases/show.tsx +262 -0
  149. data/app/frontend/pages/requests/index.tsx +235 -0
  150. data/app/frontend/pages/requests/route.tsx +159 -0
  151. data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
  152. data/app/frontend/pages/spans/index.tsx +229 -0
  153. data/app/frontend/pages/spans/show.tsx +220 -0
  154. data/app/frontend/pages/storage_ops/index.tsx +150 -0
  155. data/app/frontend/pages/tenants/index.tsx +236 -0
  156. data/app/frontend/pages/tenants/show.tsx +477 -0
  157. data/app/frontend/pages/tenants/tenant-path.ts +26 -0
  158. data/app/frontend/pages/thresholds/index.tsx +414 -0
  159. data/app/frontend/pages/traces/show.tsx +208 -0
  160. data/app/frontend/pages/transactions/index.tsx +220 -0
  161. data/app/frontend/pages/view_renders/index.tsx +241 -0
  162. data/app/frontend/pages/visits/index.tsx +321 -0
  163. data/app/frontend/public/icon.png +0 -0
  164. data/app/frontend/public/icon.svg +5 -0
  165. data/app/frontend/routes/index.d.ts +2136 -0
  166. data/app/frontend/routes/index.js +2112 -0
  167. data/app/frontend/types/globals.d.ts +9 -0
  168. data/app/frontend/types/index.ts +288 -0
  169. data/app/frontend/types/vite-env.d.ts +9 -0
  170. data/lib/railwatch/version.rb +1 -1
  171. data/lib/railwatch.rb +8 -3
  172. data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
  173. data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
  174. data/public/railwatch/assets/{arrow-up-CLQ-7heQ.js → arrow-up-DG01k9AL.js} +1 -1
  175. data/public/railwatch/assets/{braces-rSYydpLY.js → braces-C3w7pfxS.js} +1 -1
  176. data/public/railwatch/assets/{card-DPjFKfen.js → card-Dpwlr0uY.js} +1 -1
  177. data/public/railwatch/assets/{chart-DWh7l8yM.js → chart-DJHv3R7W.js} +1 -1
  178. data/public/railwatch/assets/{chart-hover-CfoZUY4J.js → chart-hover-CEH-DtXO.js} +1 -1
  179. data/public/railwatch/assets/{chart-panel-CC49WTQL.js → chart-panel-CUdIzJvk.js} +1 -1
  180. data/public/railwatch/assets/code-BRZprwun.js +1 -0
  181. data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
  182. data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
  183. data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
  184. data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
  185. data/public/railwatch/assets/{execution-path-FAuIzOXB.js → execution-path-CElXXFO_.js} +1 -1
  186. data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
  187. data/public/railwatch/assets/{flamegraph-EaGkP2NT.js → flamegraph-SKD9yxW9.js} +2 -2
  188. data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
  189. data/public/railwatch/assets/index-2W-ppwap.js +1 -0
  190. data/public/railwatch/assets/index-74ocuU5d.js +1 -0
  191. data/public/railwatch/assets/{index-C8Cggnbw.js → index-B8NA62Lz.js} +2 -2
  192. data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
  193. data/public/railwatch/assets/index-BDcglJzX.js +1 -0
  194. data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
  195. data/public/railwatch/assets/index-BqK_VKom.js +1 -0
  196. data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
  197. data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
  198. data/public/railwatch/assets/index-C7zREldp.js +1 -0
  199. data/public/railwatch/assets/index-CEetm4oC.js +1 -0
  200. data/public/railwatch/assets/index-CIaZkokF.js +1 -0
  201. data/public/railwatch/assets/index-CNH85TZp.js +1 -0
  202. data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
  203. data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
  204. data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
  205. data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
  206. data/public/railwatch/assets/index-CteZ6fop.js +1 -0
  207. data/public/railwatch/assets/index-CvqfEExO.js +1 -0
  208. data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
  209. data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
  210. data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
  211. data/public/railwatch/assets/index-DYK_dps4.js +1 -0
  212. data/public/railwatch/assets/index-DrayARrI.js +1 -0
  213. data/public/railwatch/assets/index-HRXGir-0.js +1 -0
  214. data/public/railwatch/assets/index-UJOUALr_.js +1 -0
  215. data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
  216. data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
  217. data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
  218. data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
  219. data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
  220. data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
  221. data/public/railwatch/assets/{percentile-picker-CeQgllxD.js → percentile-picker-E2aUdLxx.js} +1 -1
  222. data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
  223. data/public/railwatch/assets/{release-health-uP-GF3_V.js → release-health-C8WSCDKD.js} +1 -1
  224. data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
  225. data/public/railwatch/assets/{segmented-BeIe4uqk.js → segmented-BWee8uc9.js} +1 -1
  226. data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
  227. data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
  228. data/public/railwatch/assets/show-BEWisLQw.js +1 -0
  229. data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
  230. data/public/railwatch/assets/{show-YsNpqbcI.js → show-BiT_Aloc.js} +3 -3
  231. data/public/railwatch/assets/show-BpCO49d3.js +1 -0
  232. data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
  233. data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
  234. data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
  235. data/public/railwatch/assets/show-DyzHM527.js +1 -0
  236. data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
  237. data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
  238. data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
  239. data/public/railwatch/assets/{sort-header-mEJUM3Av.js → sort-header-uOq5VlrB.js} +1 -1
  240. data/public/railwatch/assets/{sparkline-cell-Dwg7awwh.js → sparkline-cell-DgGij0cu.js} +1 -1
  241. data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
  242. data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
  243. data/public/railwatch/assets/{tenant-path-CQoP-BeF.js → tenant-path-DU0ezKSU.js} +1 -1
  244. data/public/railwatch/assets/{use-clipboard-DNefo-ky.js → use-clipboard-DoENmlWY.js} +1 -1
  245. data/public/railwatch/assets/{use-live-DMTuhKfB.js → use-live-CE_fN5_6.js} +1 -1
  246. data/public/railwatch/manifest.json +612 -1591
  247. metadata +242 -117
  248. data/public/railwatch/assets/app-layout-Zc0v-hYh.js +0 -1
  249. data/public/railwatch/assets/app-wordmark-BA_60AVb.js +0 -1
  250. data/public/railwatch/assets/appearance-CcfP9tZ7.js +0 -1
  251. data/public/railwatch/assets/application-BrN3Sz94.css +0 -1
  252. data/public/railwatch/assets/auth-layout-C05gEBIQ.js +0 -1
  253. data/public/railwatch/assets/badge-DRae8XwK.js +0 -1
  254. data/public/railwatch/assets/checkbox-DPkLUiwM.js +0 -1
  255. data/public/railwatch/assets/code-CLmYS6FU.js +0 -1
  256. data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
  257. data/public/railwatch/assets/copy-block-CGcXxp8J.js +0 -1
  258. data/public/railwatch/assets/copy-id-vBYHQwxg.js +0 -1
  259. data/public/railwatch/assets/cursor-load-more-Ddwxe5dZ.js +0 -1
  260. data/public/railwatch/assets/data-table-CzKTEE-O.js +0 -1
  261. data/public/railwatch/assets/edit-B1kmWkzd.js +0 -8
  262. data/public/railwatch/assets/edit-D9cx4pbG.js +0 -1
  263. data/public/railwatch/assets/edit-yv6j9p-V.js +0 -1
  264. data/public/railwatch/assets/empty-state-CW4wclK_.js +0 -1
  265. data/public/railwatch/assets/env-layout-Kz7wks1x.js +0 -1
  266. data/public/railwatch/assets/filter-bar-CJDjWFib.js +0 -1
  267. data/public/railwatch/assets/frames-zZuIaNH9.js +0 -1
  268. data/public/railwatch/assets/google-sign-in-button-2_zgbgVy.js +0 -1
  269. data/public/railwatch/assets/index-B49SWz7K.js +0 -1
  270. data/public/railwatch/assets/index-BHlY4wKe.js +0 -1
  271. data/public/railwatch/assets/index-BZpPtyFY.js +0 -1
  272. data/public/railwatch/assets/index-BeVK6jCL.js +0 -1
  273. data/public/railwatch/assets/index-BfbSo01U.js +0 -1
  274. data/public/railwatch/assets/index-BfgncAv6.js +0 -1
  275. data/public/railwatch/assets/index-BhNszK1k.js +0 -1
  276. data/public/railwatch/assets/index-Bu01uWvw.js +0 -1
  277. data/public/railwatch/assets/index-C1s_hK3p.js +0 -1
  278. data/public/railwatch/assets/index-CBip6V4z.js +0 -1
  279. data/public/railwatch/assets/index-CMJGss5R.js +0 -1
  280. data/public/railwatch/assets/index-CRo3yK20.js +0 -1
  281. data/public/railwatch/assets/index-CWB_p2J8.js +0 -1
  282. data/public/railwatch/assets/index-CWHpndGc.js +0 -1
  283. data/public/railwatch/assets/index-Ca_S4Sc3.js +0 -1
  284. data/public/railwatch/assets/index-CanPDDOa.js +0 -1
  285. data/public/railwatch/assets/index-Cie90Yat.js +0 -1
  286. data/public/railwatch/assets/index-CiepQ_pR.js +0 -1
  287. data/public/railwatch/assets/index-Cm1uCIGN.js +0 -1
  288. data/public/railwatch/assets/index-CzitnnSC.js +0 -1
  289. data/public/railwatch/assets/index-DEUfClv3.js +0 -1
  290. data/public/railwatch/assets/index-DJKwo-mI.js +0 -1
  291. data/public/railwatch/assets/index-DK6y0YHp.js +0 -1
  292. data/public/railwatch/assets/index-DMNPpLH9.js +0 -1
  293. data/public/railwatch/assets/index-DZO1mSfX.js +0 -1
  294. data/public/railwatch/assets/index-Db5wj3M5.js +0 -1
  295. data/public/railwatch/assets/index-Dcy5WktB.js +0 -1
  296. data/public/railwatch/assets/index-DvG0-7Lx.js +0 -1
  297. data/public/railwatch/assets/index-Dvj1wuka.js +0 -1
  298. data/public/railwatch/assets/index-KyZX46qX.js +0 -1
  299. data/public/railwatch/assets/index-Ze-KP-sl.js +0 -1
  300. data/public/railwatch/assets/index-mhRbWLBM.js +0 -1
  301. data/public/railwatch/assets/index-x099JL5f.js +0 -1
  302. data/public/railwatch/assets/index-x28zb_nf.js +0 -1
  303. data/public/railwatch/assets/inertia-Cuyz2ZHO.js +0 -97
  304. data/public/railwatch/assets/input-error-hog6gGxg.js +0 -1
  305. data/public/railwatch/assets/json-viewer-DH2W9HXf.js +0 -1
  306. data/public/railwatch/assets/klass-DHbDelLk.js +0 -1
  307. data/public/railwatch/assets/label-DdCBgiUn.js +0 -1
  308. data/public/railwatch/assets/layout-Cueyl7c5.js +0 -1
  309. data/public/railwatch/assets/live-dot-BZgYTYdt.js +0 -41
  310. data/public/railwatch/assets/nav-BSSGObDZ.js +0 -1
  311. data/public/railwatch/assets/new-B8FSb8Bl.js +0 -1
  312. data/public/railwatch/assets/new-C39_v2Ll.js +0 -1
  313. data/public/railwatch/assets/new-DVOPwaC5.js +0 -1
  314. data/public/railwatch/assets/new-Dd40nvJR.js +0 -1
  315. data/public/railwatch/assets/new-SxnYe1SE.js +0 -1
  316. data/public/railwatch/assets/new-eP5vKD3Y.js +0 -1
  317. data/public/railwatch/assets/onboarding-CUpZl5KB.js +0 -1
  318. data/public/railwatch/assets/origin-identity-BYt2uiuo.js +0 -1
  319. data/public/railwatch/assets/relative-time-D5UbF4oO.js +0 -1
  320. data/public/railwatch/assets/route-DQAY8JEr.js +0 -1
  321. data/public/railwatch/assets/select-2R16457Z.js +0 -1
  322. data/public/railwatch/assets/separator-D5I0UCB5.js +0 -1
  323. data/public/railwatch/assets/series-chart-gnrzhmM6.js +0 -1
  324. data/public/railwatch/assets/show-2BkeNRUC.js +0 -2
  325. data/public/railwatch/assets/show-B0X1hRQH.js +0 -1
  326. data/public/railwatch/assets/show-BKUV5l5q.js +0 -1
  327. data/public/railwatch/assets/show-BQJD_CsF.js +0 -6
  328. data/public/railwatch/assets/show-BhmD6Sxp.js +0 -1
  329. data/public/railwatch/assets/show-C3KcnVo2.js +0 -1
  330. data/public/railwatch/assets/show-C95cHd06.js +0 -2
  331. data/public/railwatch/assets/show-CdB4uVQS.js +0 -1
  332. data/public/railwatch/assets/show-CoCqcVIp.js +0 -1
  333. data/public/railwatch/assets/show-D2LqjEsU.js +0 -1
  334. data/public/railwatch/assets/show-DNpnKyTh.js +0 -1
  335. data/public/railwatch/assets/show-DOlTxbig.js +0 -1
  336. data/public/railwatch/assets/show-DQCkttL8.js +0 -1
  337. data/public/railwatch/assets/show-DcxesipC.js +0 -1
  338. data/public/railwatch/assets/show-IGJoc_X0.js +0 -1
  339. data/public/railwatch/assets/show-qNV6H8SH.js +0 -2
  340. data/public/railwatch/assets/stat-ZtxGU8lE.js +0 -1
  341. data/public/railwatch/assets/status-badge-CH5P-Xkj.js +0 -1
  342. data/public/railwatch/assets/text-link-DHJ8BXx5.js +0 -1
  343. data/public/railwatch/assets/textarea-BeQtQyl5.js +0 -1
  344. data/public/railwatch/assets/timeline-CaKQVu48.js +0 -1
  345. data/public/railwatch/assets/transition-ksDpqhKJ.js +0 -5
@@ -0,0 +1,179 @@
1
+ import { router } from "@inertiajs/react"
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react"
3
+
4
+ import { getConsumer } from "@/lib/cable"
5
+
6
+ const RELOAD_THROTTLE_MS = 5_000
7
+ const PAUSED_STORAGE_KEY = "railwatch:live-paused"
8
+
9
+ export interface LiveEvent {
10
+ event: string
11
+ at: string
12
+ counts: Record<string, number>
13
+ }
14
+
15
+ // Whether live updates are actually flowing right now: subscribed and not
16
+ // paused. The page's LiveToggle owns the one `useLive` subscription, so
17
+ // anything else that needs the answer -- a number that only animates while
18
+ // the data behind it is moving -- reads it from here rather than opening a
19
+ // second WebSocket subscription of its own.
20
+ let live = false
21
+ const liveWatchers = new Set<() => void>()
22
+
23
+ function publishLive(value: boolean) {
24
+ if (live === value) return
25
+ live = value
26
+ for (const watcher of liveWatchers) watcher()
27
+ }
28
+
29
+ function subscribeLive(onChange: () => void) {
30
+ liveWatchers.add(onChange)
31
+ return () => {
32
+ liveWatchers.delete(onChange)
33
+ }
34
+ }
35
+
36
+ export function useIsLive() {
37
+ return useSyncExternalStore(
38
+ subscribeLive,
39
+ () => live,
40
+ () => false,
41
+ )
42
+ }
43
+
44
+ // Subscribes to an environment's EnvironmentChannel stream. On each message,
45
+ // reloads the page's Inertia data props in the background (a partial
46
+ // `router.reload`, not a navigation), throttled to at most once every 5s and
47
+ // only while the tab is visible; one reload fires on return to the tab if an
48
+ // event arrived while it was hidden. Pass `only` to limit the reload to
49
+ // specific props, matching whatever `router.reload({ only })` accepts;
50
+ // omit it to refresh every prop the current page was rendered with.
51
+ export function useLive(
52
+ environmentId: number,
53
+ {
54
+ only,
55
+ onEvent,
56
+ }: { only?: string[]; onEvent?: (event: LiveEvent) => void } = {},
57
+ ) {
58
+ const [connected, setConnected] = useState(false)
59
+ const [lastEventAt, setLastEventAt] = useState<number | null>(null)
60
+ const [lastRefreshedAt, setLastRefreshedAt] = useState<number | null>(null)
61
+ const [subscribedEnvironment, setSubscribedEnvironment] =
62
+ useState(environmentId)
63
+ if (subscribedEnvironment !== environmentId) {
64
+ setSubscribedEnvironment(environmentId)
65
+ setConnected(false)
66
+ setLastEventAt(null)
67
+ setLastRefreshedAt(null)
68
+ }
69
+ const [paused, setPausedState] = useState(() => {
70
+ if (typeof window === "undefined") return false
71
+ return localStorage.getItem(PAUSED_STORAGE_KEY) === "1"
72
+ })
73
+
74
+ const onlyRef = useRef(only)
75
+ const onEventRef = useRef(onEvent)
76
+ const pausedRef = useRef(paused)
77
+ useEffect(() => {
78
+ onlyRef.current = only
79
+ onEventRef.current = onEvent
80
+ pausedRef.current = paused
81
+ })
82
+
83
+ const scheduleRef = useRef<() => void>(() => undefined)
84
+
85
+ useEffect(() => {
86
+ publishLive(connected && !paused)
87
+ return () => publishLive(false)
88
+ }, [connected, paused])
89
+
90
+ function setPaused(value: boolean) {
91
+ pausedRef.current = value
92
+ localStorage.setItem(PAUSED_STORAGE_KEY, value ? "1" : "0")
93
+ setPausedState(value)
94
+ scheduleRef.current()
95
+ }
96
+
97
+ useEffect(() => {
98
+ let disposed = false
99
+ let pending = false
100
+ let inFlight = false
101
+ let lastReloadAt = -Infinity
102
+ let timer: ReturnType<typeof setTimeout> | undefined
103
+ let cancelReload: (() => void) | undefined
104
+
105
+ function reload() {
106
+ pending = false
107
+ inFlight = true
108
+ lastReloadAt = Date.now()
109
+ // router.reload always preserves scroll and state; that's why
110
+ // ReloadOptions omits those keys (unlike router.visit).
111
+ router.reload({
112
+ only: onlyRef.current,
113
+ onCancelToken: (token) => {
114
+ cancelReload = () => token.cancel()
115
+ },
116
+ onSuccess: () => {
117
+ if (!disposed) setLastRefreshedAt(Date.now())
118
+ },
119
+ onFinish: () => {
120
+ cancelReload = undefined
121
+ inFlight = false
122
+ maybeReload()
123
+ },
124
+ })
125
+ }
126
+
127
+ function maybeReload() {
128
+ clearTimeout(timer)
129
+ timer = undefined
130
+ if (
131
+ disposed ||
132
+ !pending ||
133
+ inFlight ||
134
+ pausedRef.current ||
135
+ document.visibilityState !== "visible"
136
+ )
137
+ return
138
+ const delay = RELOAD_THROTTLE_MS - (Date.now() - lastReloadAt)
139
+ if (delay > 0) timer = setTimeout(maybeReload, delay)
140
+ else reload()
141
+ }
142
+
143
+ function onVisibilityChange() {
144
+ maybeReload()
145
+ }
146
+
147
+ document.addEventListener("visibilitychange", onVisibilityChange)
148
+ scheduleRef.current = maybeReload
149
+ const subscription = getConsumer().subscriptions.create(
150
+ { channel: "EnvironmentChannel", id: environmentId },
151
+ {
152
+ connected: () => {
153
+ if (!disposed) setConnected(true)
154
+ },
155
+ disconnected: () => {
156
+ if (!disposed) setConnected(false)
157
+ },
158
+ received: (data: LiveEvent) => {
159
+ if (disposed) return
160
+ setLastEventAt(Date.now())
161
+ onEventRef.current?.(data)
162
+ pending = true
163
+ maybeReload()
164
+ },
165
+ },
166
+ )
167
+
168
+ return () => {
169
+ disposed = true
170
+ cancelReload?.()
171
+ clearTimeout(timer)
172
+ scheduleRef.current = () => undefined
173
+ document.removeEventListener("visibilitychange", onVisibilityChange)
174
+ subscription.unsubscribe()
175
+ }
176
+ }, [environmentId])
177
+
178
+ return { connected, lastEventAt, lastRefreshedAt, paused, setPaused }
179
+ }
@@ -0,0 +1,8 @@
1
+ import { useCallback } from "react"
2
+
3
+ export function useMobileNavigation() {
4
+ return useCallback(() => {
5
+ // Remove pointer-events style from body...
6
+ document.body.style.removeProperty("pointer-events")
7
+ }, [])
8
+ }
@@ -0,0 +1,21 @@
1
+ import { useSyncExternalStore } from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
6
+
7
+ function mediaQueryListener(callback: (event: MediaQueryListEvent) => void) {
8
+ mql.addEventListener("change", callback)
9
+
10
+ return () => {
11
+ mql.removeEventListener("change", callback)
12
+ }
13
+ }
14
+
15
+ function isSmallerThanBreakpoint() {
16
+ return mql.matches
17
+ }
18
+
19
+ export function useIsMobile() {
20
+ return useSyncExternalStore(mediaQueryListener, isSmallerThanBreakpoint)
21
+ }
@@ -0,0 +1,24 @@
1
+ import { router } from "@inertiajs/react"
2
+
3
+ import type { Percentile } from "@/types"
4
+
5
+ const PERCENTILES: Percentile[] = ["p50", "p95", "p99"]
6
+
7
+ // Client-only URL param (?pct=), not inertia_share'd like window: every
8
+ // list page reads/writes it the same way the count/p95 sort toggle already
9
+ // does with router.visit + preserveState.
10
+ export function usePercentile() {
11
+ const url = new URL(globalThis.location.href)
12
+ const raw = url.searchParams.get("pct")
13
+ const percentile: Percentile = PERCENTILES.includes(raw as Percentile)
14
+ ? (raw as Percentile)
15
+ : "p95"
16
+
17
+ const set = (value: Percentile) => {
18
+ const next = new URL(globalThis.location.href)
19
+ next.searchParams.set("pct", value)
20
+ router.visit(next.toString(), { preserveScroll: true, preserveState: true })
21
+ }
22
+
23
+ return { percentile, set }
24
+ }
@@ -0,0 +1,25 @@
1
+ import { useSyncExternalStore } from "react"
2
+
3
+ const QUERY = "(prefers-reduced-motion: reduce)"
4
+
5
+ // Whether the user has asked their OS for less motion. Read in JavaScript
6
+ // rather than left to CSS: a global `animation: none` does not stop a Web
7
+ // Animations API animation, so a component that animates has to decide not
8
+ // to. Follows the media query live, so turning the setting on stops the
9
+ // motion without a reload.
10
+ function subscribe(onChange: () => void) {
11
+ const query = window.matchMedia(QUERY)
12
+ query.addEventListener("change", onChange)
13
+ return () => {
14
+ query.removeEventListener("change", onChange)
15
+ }
16
+ }
17
+
18
+ function snapshot() {
19
+ return window.matchMedia(QUERY).matches
20
+ }
21
+
22
+ export function useReducedMotion() {
23
+ // Server-rendered markup carries no motion, so it renders as if reduced.
24
+ return useSyncExternalStore(subscribe, snapshot, () => true)
25
+ }
@@ -0,0 +1,106 @@
1
+ import { useEffect, useRef, useState } from "react"
2
+
3
+ import { ignorePageShortcut } from "@/lib/keyboard"
4
+
5
+ const navigators = new Map<symbol, () => void>()
6
+ let activeNavigator: symbol | undefined
7
+
8
+ export interface RowNavOptions<T> {
9
+ rows: T[]
10
+ rowKey: (row: T) => string | number
11
+ onOpen: (row: T, opts?: { newTab?: boolean }) => void
12
+ enabled?: boolean
13
+ }
14
+
15
+ // j/k highlight a row, Enter opens it (onOpen), "o" opens it in a new tab
16
+ // (onOpen with newTab: true), Esc clears the highlight, and "/" focuses the
17
+ // page's FilterBar input (matched by `[data-filter-bar-input]`) if present.
18
+ // Ignored while focus is in a form field or a modifier key is held. Backs
19
+ // `DataTable`'s `keyboardNav` prop; extracted here so every list page shares
20
+ // one implementation.
21
+ export function useRowNav<T>({
22
+ rows,
23
+ rowKey,
24
+ onOpen,
25
+ enabled = true,
26
+ }: RowNavOptions<T>) {
27
+ const [highlighted, setHighlighted] = useState<string | number | null>(null)
28
+ const [identity] = useState(() => Symbol("row-nav"))
29
+ function activate() {
30
+ if (!enabled || activeNavigator === identity) return
31
+ if (activeNavigator) navigators.get(activeNavigator)?.()
32
+ activeNavigator = identity
33
+ }
34
+
35
+ useEffect(() => {
36
+ if (!enabled) return
37
+ navigators.set(identity, () => setHighlighted(null))
38
+ activeNavigator ??= identity
39
+ return () => {
40
+ navigators.delete(identity)
41
+ if (activeNavigator === identity)
42
+ activeNavigator = navigators.keys().next().value
43
+ }
44
+ }, [enabled, identity])
45
+
46
+ const rowsRef = useRef(rows)
47
+ const rowKeyRef = useRef(rowKey)
48
+ const onOpenRef = useRef(onOpen)
49
+ useEffect(() => {
50
+ rowsRef.current = rows
51
+ rowKeyRef.current = rowKey
52
+ onOpenRef.current = onOpen
53
+ })
54
+
55
+ useEffect(() => {
56
+ if (!enabled) return
57
+
58
+ function onKeyDown(event: KeyboardEvent) {
59
+ if (activeNavigator !== identity || ignorePageShortcut(event)) return
60
+
61
+ if (event.key === "/") {
62
+ const input = document.querySelector<HTMLElement>(
63
+ "[data-filter-bar-input]",
64
+ )
65
+ if (input) {
66
+ event.preventDefault()
67
+ input.focus()
68
+ }
69
+ return
70
+ }
71
+
72
+ const currentRows = rowsRef.current
73
+ const key = rowKeyRef.current
74
+ const index = currentRows.findIndex((r) => key(r) === highlighted)
75
+
76
+ if (event.key === "j") {
77
+ if (currentRows.length === 0) return
78
+ event.preventDefault()
79
+ const next = Math.min(index + 1, currentRows.length - 1)
80
+ setHighlighted(key(currentRows[next]))
81
+ } else if (event.key === "k") {
82
+ if (currentRows.length === 0) return
83
+ event.preventDefault()
84
+ const prev = index < 0 ? 0 : Math.max(index - 1, 0)
85
+ setHighlighted(key(currentRows[prev]))
86
+ } else if (event.key === "Enter") {
87
+ if (index >= 0) {
88
+ event.preventDefault()
89
+ onOpenRef.current(currentRows[index])
90
+ }
91
+ } else if (event.key === "o") {
92
+ if (index >= 0) {
93
+ event.preventDefault()
94
+ onOpenRef.current(currentRows[index], { newTab: true })
95
+ }
96
+ } else if (event.key === "Escape") {
97
+ setHighlighted(null)
98
+ }
99
+ }
100
+
101
+ document.addEventListener("keydown", onKeyDown)
102
+ return () => document.removeEventListener("keydown", onKeyDown)
103
+ }, [highlighted, enabled, identity])
104
+
105
+ return { highlighted, setHighlighted, activate }
106
+ }
@@ -0,0 +1,27 @@
1
+ import { router, usePage } from "@inertiajs/react"
2
+
3
+ import type { SharedProps, Step } from "@/types"
4
+
5
+ export const STEP_LABELS: Record<Step, string> = {
6
+ "1m": "1 minute",
7
+ "5m": "5 minutes",
8
+ "15m": "15 minutes",
9
+ "1h": "1 hour",
10
+ "6h": "6 hours",
11
+ "1d": "1 day",
12
+ }
13
+
14
+ // The chart bucket width (?step=) and the widths the current window offers,
15
+ // both inertia_share'd by EnvironmentScoped like `window` is. Changing the
16
+ // window drops the step so the new window draws at its own default.
17
+ export function useStep() {
18
+ const { step = "1h", steps = [] } = usePage<SharedProps>().props
19
+
20
+ const set = (value: Step) => {
21
+ const url = new URL(globalThis.location.href)
22
+ url.searchParams.set("step", value)
23
+ router.visit(url.toString(), { preserveScroll: true, preserveState: true })
24
+ }
25
+
26
+ return { step, steps, set, label: STEP_LABELS[step] ?? step }
27
+ }
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ // Re-renders the calling component every `intervalMs`, so relative-time
4
+ // labels ("2m ago") and freshness checks stay current without a reload.
5
+ export function useTicker(intervalMs = 30_000) {
6
+ const [, setTick] = useState(0)
7
+
8
+ useEffect(() => {
9
+ const id = window.setInterval(() => setTick((tick) => tick + 1), intervalMs)
10
+ return () => window.clearInterval(id)
11
+ }, [intervalMs])
12
+ }
@@ -0,0 +1,58 @@
1
+ import { router, usePage } from "@inertiajs/react"
2
+
3
+ import type { SharedProps, Window } from "@/types"
4
+
5
+ export const WINDOWS: { value: Window; label: string }[] = [
6
+ { value: "1h", label: "1 hour" },
7
+ { value: "6h", label: "6 hours" },
8
+ { value: "24h", label: "24 hours" },
9
+ { value: "7d", label: "7 days" },
10
+ { value: "30d", label: "30 days" },
11
+ ]
12
+
13
+ function formatRange(from: string, to: string) {
14
+ const fmt: Intl.DateTimeFormatOptions = {
15
+ month: "short",
16
+ day: "numeric",
17
+ hour: "2-digit",
18
+ minute: "2-digit",
19
+ }
20
+ return `${new Date(from).toLocaleString(undefined, fmt)} – ${new Date(to).toLocaleString(undefined, fmt)}`
21
+ }
22
+
23
+ export function useWindow() {
24
+ const { window: current = "24h", range } = usePage<SharedProps>().props
25
+
26
+ const set = (value: Window) => {
27
+ const url = new URL(globalThis.location.href)
28
+ url.searchParams.set("window", value)
29
+ url.searchParams.delete("from")
30
+ url.searchParams.delete("to")
31
+ url.searchParams.delete("step")
32
+ router.visit(url.toString(), { preserveScroll: true, preserveState: true })
33
+ }
34
+
35
+ // Sets an explicit ?from=&to= range, dropping the fixed `window` param.
36
+ const setRange = (from: string, to: string) => {
37
+ const url = new URL(globalThis.location.href)
38
+ url.searchParams.set("from", from)
39
+ url.searchParams.set("to", to)
40
+ url.searchParams.delete("window")
41
+ url.searchParams.delete("step")
42
+ router.visit(url.toString(), { preserveScroll: true, preserveState: true })
43
+ }
44
+
45
+ const label =
46
+ current === "custom" && range
47
+ ? formatRange(range.from, range.to)
48
+ : (WINDOWS.find((w) => w.value === current)?.label ?? current)
49
+
50
+ return {
51
+ window: current,
52
+ from: range?.from,
53
+ to: range?.to,
54
+ set,
55
+ setRange,
56
+ label,
57
+ }
58
+ }
@@ -0,0 +1,107 @@
1
+ import { router, usePage } from "@inertiajs/react"
2
+ import { type PropsWithChildren, useEffect, useState } from "react"
3
+
4
+ import { AppContent } from "@/components/app-content"
5
+ import { AppShell } from "@/components/app-shell"
6
+ import { AppSidebar } from "@/components/app-sidebar"
7
+ import { AppSidebarHeader } from "@/components/app-sidebar-header"
8
+ import { CommandPalette } from "@/components/railwatch/command-palette"
9
+ import { ShortcutsDialog } from "@/components/railwatch/shortcuts-dialog"
10
+ import { useHotkeys } from "@/hooks/use-hotkeys"
11
+ import {
12
+ applicationEnvironmentJobsPath,
13
+ applicationEnvironmentOverviewPath,
14
+ applicationEnvironmentQueriesPath,
15
+ applicationEnvironmentRequestsPath,
16
+ dashboardPath,
17
+ issuesPath,
18
+ } from "@/routes"
19
+ import type { BreadcrumbItem, SharedProps } from "@/types"
20
+
21
+ export default function AppSidebarLayout({
22
+ children,
23
+ breadcrumbs = [],
24
+ }: PropsWithChildren<{
25
+ breadcrumbs?: BreadcrumbItem[]
26
+ }>) {
27
+ const [paletteOpen, setPaletteOpen] = useState(false)
28
+ const [shortcutsOpen, setShortcutsOpen] = useState(false)
29
+ const { environment, window } = usePage<SharedProps>().props
30
+
31
+ useEffect(() => {
32
+ const onKeyDown = (event: KeyboardEvent) => {
33
+ if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
34
+ event.preventDefault()
35
+ setPaletteOpen((open) => !open)
36
+ }
37
+ }
38
+ document.addEventListener("keydown", onKeyDown)
39
+ return () => document.removeEventListener("keydown", onKeyDown)
40
+ }, [])
41
+
42
+ // "g" then a letter jumps to the current environment's Overview/Requests/
43
+ // Jobs/Queries/Issues when a page has one loaded (env-scoped pages share
44
+ // it via EnvironmentScoped); otherwise "g i" falls back to global issues.
45
+ useHotkeys({
46
+ "?": () => setShortcutsOpen((open) => !open),
47
+ "g d": () => router.visit(dashboardPath()),
48
+ "g i": () =>
49
+ router.visit(
50
+ environment
51
+ ? issuesPath({
52
+ application_id: environment.application_id,
53
+ environment_id: environment.id,
54
+ })
55
+ : issuesPath(),
56
+ ),
57
+ ...(environment && {
58
+ "g o": () =>
59
+ router.visit(
60
+ applicationEnvironmentOverviewPath(
61
+ environment.application_id,
62
+ environment.id,
63
+ { window },
64
+ ),
65
+ ),
66
+ "g r": () =>
67
+ router.visit(
68
+ applicationEnvironmentRequestsPath(
69
+ environment.application_id,
70
+ environment.id,
71
+ { window },
72
+ ),
73
+ ),
74
+ "g j": () =>
75
+ router.visit(
76
+ applicationEnvironmentJobsPath(
77
+ environment.application_id,
78
+ environment.id,
79
+ { window },
80
+ ),
81
+ ),
82
+ "g q": () =>
83
+ router.visit(
84
+ applicationEnvironmentQueriesPath(
85
+ environment.application_id,
86
+ environment.id,
87
+ { window },
88
+ ),
89
+ ),
90
+ }),
91
+ })
92
+
93
+ return (
94
+ <AppShell variant="sidebar">
95
+ <AppSidebar />
96
+ <AppContent variant="sidebar" className="overflow-x-hidden">
97
+ <AppSidebarHeader
98
+ breadcrumbs={breadcrumbs}
99
+ onOpenCommandPalette={() => setPaletteOpen(true)}
100
+ />
101
+ {children}
102
+ </AppContent>
103
+ <CommandPalette open={paletteOpen} onOpenChange={setPaletteOpen} />
104
+ <ShortcutsDialog open={shortcutsOpen} onOpenChange={setShortcutsOpen} />
105
+ </AppShell>
106
+ )
107
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import AppLayoutTemplate from "@/layouts/app/app-sidebar-layout"
4
+ import type { BreadcrumbItem } from "@/types"
5
+
6
+ interface AppLayoutProps {
7
+ children: ReactNode
8
+ breadcrumbs?: BreadcrumbItem[]
9
+ }
10
+
11
+ export default function AppLayout({
12
+ children,
13
+ breadcrumbs,
14
+ ...props
15
+ }: AppLayoutProps) {
16
+ return (
17
+ <AppLayoutTemplate breadcrumbs={breadcrumbs} {...props}>
18
+ {children}
19
+ </AppLayoutTemplate>
20
+ )
21
+ }
@@ -0,0 +1,53 @@
1
+ import { Head, usePage } from "@inertiajs/react"
2
+ import type { ReactNode } from "react"
3
+
4
+ import AppLayout from "@/layouts/app-layout"
5
+ import { applicationEnvironmentOverviewPath, applicationPath } from "@/routes"
6
+ import type { BreadcrumbItem, SharedProps } from "@/types"
7
+
8
+ // Layout for every environment-scoped telemetry page: breadcrumbs from the
9
+ // shared environment prop, page title, and consistent padding.
10
+ export default function EnvLayout({
11
+ title,
12
+ children,
13
+ crumbs = [],
14
+ }: {
15
+ title: string
16
+ children: ReactNode
17
+ crumbs?: BreadcrumbItem[]
18
+ }) {
19
+ const { environment } = usePage<SharedProps>().props
20
+ const base: BreadcrumbItem[] = environment
21
+ ? [
22
+ {
23
+ title: environment.application_name,
24
+ href: applicationPath(environment.application_id),
25
+ },
26
+ {
27
+ title: environment.name,
28
+ href: applicationEnvironmentOverviewPath(
29
+ environment.application_id,
30
+ environment.id,
31
+ ),
32
+ },
33
+ ]
34
+ : []
35
+ const breadcrumbs = [
36
+ ...base,
37
+ ...(crumbs.length ? crumbs : [{ title, href: "#" }]),
38
+ ]
39
+ return (
40
+ <AppLayout breadcrumbs={breadcrumbs}>
41
+ <Head
42
+ title={
43
+ environment
44
+ ? `${title} · ${environment.application_name} ${environment.name}`
45
+ : title
46
+ }
47
+ />
48
+ <div className="flex flex-1 flex-col gap-4 p-3 md:gap-5 md:p-6">
49
+ {children}
50
+ </div>
51
+ </AppLayout>
52
+ )
53
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import { Toaster } from "@/components/ui/sonner"
4
+ import { useFlash } from "@/hooks/use-flash"
5
+
6
+ interface PersistentLayoutProps {
7
+ children: ReactNode
8
+ }
9
+
10
+ export default function PersistentLayout({ children }: PersistentLayoutProps) {
11
+ useFlash()
12
+ return (
13
+ <>
14
+ {children}
15
+ <Toaster position="bottom-right" offset={16} />
16
+ </>
17
+ )
18
+ }
@@ -0,0 +1,10 @@
1
+ import { type Consumer, createConsumer } from "@rails/actioncable"
2
+
3
+ let consumer: Consumer | undefined
4
+
5
+ // One ActionCable consumer (one WebSocket) shared by the whole app, opened
6
+ // lazily on first subscription rather than on every page load.
7
+ export function getConsumer(): Consumer {
8
+ consumer ??= createConsumer()
9
+ return consumer
10
+ }