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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/app/frontend/components/app-content.tsx +26 -0
- data/app/frontend/components/app-logo-icon.tsx +23 -0
- data/app/frontend/components/app-logo.tsx +14 -0
- data/app/frontend/components/app-shell.tsx +38 -0
- data/app/frontend/components/app-sidebar-header.tsx +54 -0
- data/app/frontend/components/app-sidebar.tsx +94 -0
- data/app/frontend/components/app-wordmark.tsx +29 -0
- data/app/frontend/components/breadcrumbs.tsx +46 -0
- data/app/frontend/components/icon.tsx +16 -0
- data/app/frontend/components/input-error.tsx +18 -0
- data/app/frontend/components/nav-footer.tsx +46 -0
- data/app/frontend/components/nav-main.tsx +41 -0
- data/app/frontend/components/nav-user.tsx +50 -0
- data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
- data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
- data/app/frontend/components/railwatch/code.tsx +28 -0
- data/app/frontend/components/railwatch/command-palette.tsx +132 -0
- data/app/frontend/components/railwatch/copy-block.tsx +52 -0
- data/app/frontend/components/railwatch/copy-id.tsx +49 -0
- data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
- data/app/frontend/components/railwatch/data-table.tsx +195 -0
- data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
- data/app/frontend/components/railwatch/empty-state.tsx +159 -0
- data/app/frontend/components/railwatch/env-nav.tsx +130 -0
- data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
- data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
- data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
- data/app/frontend/components/railwatch/frames.tsx +295 -0
- data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
- data/app/frontend/components/railwatch/live-dot.tsx +78 -0
- data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
- data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
- data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
- data/app/frontend/components/railwatch/onboarding.tsx +91 -0
- data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
- data/app/frontend/components/railwatch/page-header.tsx +42 -0
- data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
- data/app/frontend/components/railwatch/relative-time.tsx +18 -0
- data/app/frontend/components/railwatch/saved-views.tsx +232 -0
- data/app/frontend/components/railwatch/segmented.tsx +64 -0
- data/app/frontend/components/railwatch/series-chart.tsx +400 -0
- data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
- data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
- data/app/frontend/components/railwatch/sort-header.tsx +37 -0
- data/app/frontend/components/railwatch/source-link.tsx +57 -0
- data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
- data/app/frontend/components/railwatch/sql-block.tsx +116 -0
- data/app/frontend/components/railwatch/stat.tsx +173 -0
- data/app/frontend/components/railwatch/status-badge.tsx +79 -0
- data/app/frontend/components/railwatch/step-picker.tsx +57 -0
- data/app/frontend/components/railwatch/timeline.tsx +405 -0
- data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
- data/app/frontend/components/railwatch/window-picker.tsx +147 -0
- data/app/frontend/components/ui/avatar.tsx +51 -0
- data/app/frontend/components/ui/badge.tsx +46 -0
- data/app/frontend/components/ui/breadcrumb.tsx +109 -0
- data/app/frontend/components/ui/button.tsx +60 -0
- data/app/frontend/components/ui/card.tsx +92 -0
- data/app/frontend/components/ui/chart.tsx +374 -0
- data/app/frontend/components/ui/checkbox.tsx +32 -0
- data/app/frontend/components/ui/collapsible.tsx +31 -0
- data/app/frontend/components/ui/command.tsx +182 -0
- data/app/frontend/components/ui/dialog.tsx +158 -0
- data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
- data/app/frontend/components/ui/hover-card.tsx +42 -0
- data/app/frontend/components/ui/input.tsx +21 -0
- data/app/frontend/components/ui/label.tsx +24 -0
- data/app/frontend/components/ui/popover.tsx +87 -0
- data/app/frontend/components/ui/select.tsx +185 -0
- data/app/frontend/components/ui/separator.tsx +28 -0
- data/app/frontend/components/ui/sheet.tsx +137 -0
- data/app/frontend/components/ui/sidebar.tsx +726 -0
- data/app/frontend/components/ui/skeleton.tsx +13 -0
- data/app/frontend/components/ui/sonner.tsx +41 -0
- data/app/frontend/components/ui/table.tsx +114 -0
- data/app/frontend/components/ui/tabs.tsx +91 -0
- data/app/frontend/components/ui/textarea.tsx +18 -0
- data/app/frontend/components/ui/tooltip.tsx +61 -0
- data/app/frontend/components/user-info.tsx +32 -0
- data/app/frontend/components/user-menu-content.tsx +94 -0
- data/app/frontend/entrypoints/application.css +244 -0
- data/app/frontend/entrypoints/inertia.tsx +95 -0
- data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
- data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
- data/app/frontend/hooks/use-appearance.tsx +67 -0
- data/app/frontend/hooks/use-clipboard.ts +32 -0
- data/app/frontend/hooks/use-flash.tsx +23 -0
- data/app/frontend/hooks/use-hotkeys.ts +63 -0
- data/app/frontend/hooks/use-initials.tsx +15 -0
- data/app/frontend/hooks/use-live.ts +179 -0
- data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
- data/app/frontend/hooks/use-mobile.ts +21 -0
- data/app/frontend/hooks/use-percentile.ts +24 -0
- data/app/frontend/hooks/use-reduced-motion.ts +25 -0
- data/app/frontend/hooks/use-row-nav.ts +106 -0
- data/app/frontend/hooks/use-step.ts +27 -0
- data/app/frontend/hooks/use-ticker.ts +12 -0
- data/app/frontend/hooks/use-window.ts +58 -0
- data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
- data/app/frontend/layouts/app-layout.tsx +21 -0
- data/app/frontend/layouts/env-layout.tsx +53 -0
- data/app/frontend/layouts/persistent-layout.tsx +18 -0
- data/app/frontend/lib/cable.ts +10 -0
- data/app/frontend/lib/env-pages.ts +236 -0
- data/app/frontend/lib/execution-path.ts +55 -0
- data/app/frontend/lib/explain.ts +50 -0
- data/app/frontend/lib/filter.ts +25 -0
- data/app/frontend/lib/flamegraph.ts +155 -0
- data/app/frontend/lib/format.ts +71 -0
- data/app/frontend/lib/keyboard.ts +18 -0
- data/app/frontend/lib/source-link.ts +217 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/pages/alerts/index.tsx +171 -0
- data/app/frontend/pages/broadcasts/index.tsx +171 -0
- data/app/frontend/pages/cache_events/index.tsx +127 -0
- data/app/frontend/pages/commands/index.tsx +143 -0
- data/app/frontend/pages/deploys/index.tsx +94 -0
- data/app/frontend/pages/deploys/show.tsx +309 -0
- data/app/frontend/pages/deprecations/index.tsx +176 -0
- data/app/frontend/pages/exceptions/index.tsx +271 -0
- data/app/frontend/pages/executions/show.tsx +821 -0
- data/app/frontend/pages/issues/index.tsx +399 -0
- data/app/frontend/pages/issues/show.tsx +1367 -0
- data/app/frontend/pages/jobs/index.tsx +381 -0
- data/app/frontend/pages/jobs/klass.tsx +215 -0
- data/app/frontend/pages/llm_calls/index.tsx +664 -0
- data/app/frontend/pages/logs/index.tsx +303 -0
- data/app/frontend/pages/mails/index.tsx +218 -0
- data/app/frontend/pages/notifications/index.tsx +161 -0
- data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
- data/app/frontend/pages/overview/show.tsx +333 -0
- data/app/frontend/pages/people/index.tsx +102 -0
- data/app/frontend/pages/people/show.tsx +149 -0
- data/app/frontend/pages/processes/index.tsx +335 -0
- data/app/frontend/pages/profiles/index.tsx +110 -0
- data/app/frontend/pages/profiles/show.tsx +105 -0
- data/app/frontend/pages/queries/index.tsx +492 -0
- data/app/frontend/pages/queries/show.tsx +276 -0
- data/app/frontend/pages/releases/index.tsx +190 -0
- data/app/frontend/pages/releases/release-health.tsx +68 -0
- data/app/frontend/pages/releases/release-path.ts +25 -0
- data/app/frontend/pages/releases/show.tsx +262 -0
- data/app/frontend/pages/requests/index.tsx +235 -0
- data/app/frontend/pages/requests/route.tsx +159 -0
- data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
- data/app/frontend/pages/spans/index.tsx +229 -0
- data/app/frontend/pages/spans/show.tsx +220 -0
- data/app/frontend/pages/storage_ops/index.tsx +150 -0
- data/app/frontend/pages/tenants/index.tsx +236 -0
- data/app/frontend/pages/tenants/show.tsx +477 -0
- data/app/frontend/pages/tenants/tenant-path.ts +26 -0
- data/app/frontend/pages/thresholds/index.tsx +414 -0
- data/app/frontend/pages/traces/show.tsx +208 -0
- data/app/frontend/pages/transactions/index.tsx +220 -0
- data/app/frontend/pages/view_renders/index.tsx +241 -0
- data/app/frontend/pages/visits/index.tsx +321 -0
- data/app/frontend/public/icon.png +0 -0
- data/app/frontend/public/icon.svg +5 -0
- data/app/frontend/routes/index.d.ts +2136 -0
- data/app/frontend/routes/index.js +2112 -0
- data/app/frontend/types/globals.d.ts +9 -0
- data/app/frontend/types/index.ts +288 -0
- data/app/frontend/types/vite-env.d.ts +9 -0
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +8 -3
- data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
- data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
- data/public/railwatch/assets/{arrow-up-CLQ-7heQ.js → arrow-up-DG01k9AL.js} +1 -1
- data/public/railwatch/assets/{braces-rSYydpLY.js → braces-C3w7pfxS.js} +1 -1
- data/public/railwatch/assets/{card-DPjFKfen.js → card-Dpwlr0uY.js} +1 -1
- data/public/railwatch/assets/{chart-DWh7l8yM.js → chart-DJHv3R7W.js} +1 -1
- data/public/railwatch/assets/{chart-hover-CfoZUY4J.js → chart-hover-CEH-DtXO.js} +1 -1
- data/public/railwatch/assets/{chart-panel-CC49WTQL.js → chart-panel-CUdIzJvk.js} +1 -1
- data/public/railwatch/assets/code-BRZprwun.js +1 -0
- data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
- data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
- data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
- data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
- data/public/railwatch/assets/{execution-path-FAuIzOXB.js → execution-path-CElXXFO_.js} +1 -1
- data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
- data/public/railwatch/assets/{flamegraph-EaGkP2NT.js → flamegraph-SKD9yxW9.js} +2 -2
- data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
- data/public/railwatch/assets/index-2W-ppwap.js +1 -0
- data/public/railwatch/assets/index-74ocuU5d.js +1 -0
- data/public/railwatch/assets/{index-C8Cggnbw.js → index-B8NA62Lz.js} +2 -2
- data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
- data/public/railwatch/assets/index-BDcglJzX.js +1 -0
- data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
- data/public/railwatch/assets/index-BqK_VKom.js +1 -0
- data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
- data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
- data/public/railwatch/assets/index-C7zREldp.js +1 -0
- data/public/railwatch/assets/index-CEetm4oC.js +1 -0
- data/public/railwatch/assets/index-CIaZkokF.js +1 -0
- data/public/railwatch/assets/index-CNH85TZp.js +1 -0
- data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
- data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
- data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
- data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
- data/public/railwatch/assets/index-CteZ6fop.js +1 -0
- data/public/railwatch/assets/index-CvqfEExO.js +1 -0
- data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
- data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
- data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
- data/public/railwatch/assets/index-DYK_dps4.js +1 -0
- data/public/railwatch/assets/index-DrayARrI.js +1 -0
- data/public/railwatch/assets/index-HRXGir-0.js +1 -0
- data/public/railwatch/assets/index-UJOUALr_.js +1 -0
- data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
- data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
- data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
- data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
- data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
- data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
- data/public/railwatch/assets/{percentile-picker-CeQgllxD.js → percentile-picker-E2aUdLxx.js} +1 -1
- data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
- data/public/railwatch/assets/{release-health-uP-GF3_V.js → release-health-C8WSCDKD.js} +1 -1
- data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
- data/public/railwatch/assets/{segmented-BeIe4uqk.js → segmented-BWee8uc9.js} +1 -1
- data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
- data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
- data/public/railwatch/assets/show-BEWisLQw.js +1 -0
- data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
- data/public/railwatch/assets/{show-YsNpqbcI.js → show-BiT_Aloc.js} +3 -3
- data/public/railwatch/assets/show-BpCO49d3.js +1 -0
- data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
- data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
- data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
- data/public/railwatch/assets/show-DyzHM527.js +1 -0
- data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
- data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
- data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
- data/public/railwatch/assets/{sort-header-mEJUM3Av.js → sort-header-uOq5VlrB.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-Dwg7awwh.js → sparkline-cell-DgGij0cu.js} +1 -1
- data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
- data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
- data/public/railwatch/assets/{tenant-path-CQoP-BeF.js → tenant-path-DU0ezKSU.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-DNefo-ky.js → use-clipboard-DoENmlWY.js} +1 -1
- data/public/railwatch/assets/{use-live-DMTuhKfB.js → use-live-CE_fN5_6.js} +1 -1
- data/public/railwatch/manifest.json +612 -1591
- metadata +242 -117
- data/public/railwatch/assets/app-layout-Zc0v-hYh.js +0 -1
- data/public/railwatch/assets/app-wordmark-BA_60AVb.js +0 -1
- data/public/railwatch/assets/appearance-CcfP9tZ7.js +0 -1
- data/public/railwatch/assets/application-BrN3Sz94.css +0 -1
- data/public/railwatch/assets/auth-layout-C05gEBIQ.js +0 -1
- data/public/railwatch/assets/badge-DRae8XwK.js +0 -1
- data/public/railwatch/assets/checkbox-DPkLUiwM.js +0 -1
- data/public/railwatch/assets/code-CLmYS6FU.js +0 -1
- data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
- data/public/railwatch/assets/copy-block-CGcXxp8J.js +0 -1
- data/public/railwatch/assets/copy-id-vBYHQwxg.js +0 -1
- data/public/railwatch/assets/cursor-load-more-Ddwxe5dZ.js +0 -1
- data/public/railwatch/assets/data-table-CzKTEE-O.js +0 -1
- data/public/railwatch/assets/edit-B1kmWkzd.js +0 -8
- data/public/railwatch/assets/edit-D9cx4pbG.js +0 -1
- data/public/railwatch/assets/edit-yv6j9p-V.js +0 -1
- data/public/railwatch/assets/empty-state-CW4wclK_.js +0 -1
- data/public/railwatch/assets/env-layout-Kz7wks1x.js +0 -1
- data/public/railwatch/assets/filter-bar-CJDjWFib.js +0 -1
- data/public/railwatch/assets/frames-zZuIaNH9.js +0 -1
- data/public/railwatch/assets/google-sign-in-button-2_zgbgVy.js +0 -1
- data/public/railwatch/assets/index-B49SWz7K.js +0 -1
- data/public/railwatch/assets/index-BHlY4wKe.js +0 -1
- data/public/railwatch/assets/index-BZpPtyFY.js +0 -1
- data/public/railwatch/assets/index-BeVK6jCL.js +0 -1
- data/public/railwatch/assets/index-BfbSo01U.js +0 -1
- data/public/railwatch/assets/index-BfgncAv6.js +0 -1
- data/public/railwatch/assets/index-BhNszK1k.js +0 -1
- data/public/railwatch/assets/index-Bu01uWvw.js +0 -1
- data/public/railwatch/assets/index-C1s_hK3p.js +0 -1
- data/public/railwatch/assets/index-CBip6V4z.js +0 -1
- data/public/railwatch/assets/index-CMJGss5R.js +0 -1
- data/public/railwatch/assets/index-CRo3yK20.js +0 -1
- data/public/railwatch/assets/index-CWB_p2J8.js +0 -1
- data/public/railwatch/assets/index-CWHpndGc.js +0 -1
- data/public/railwatch/assets/index-Ca_S4Sc3.js +0 -1
- data/public/railwatch/assets/index-CanPDDOa.js +0 -1
- data/public/railwatch/assets/index-Cie90Yat.js +0 -1
- data/public/railwatch/assets/index-CiepQ_pR.js +0 -1
- data/public/railwatch/assets/index-Cm1uCIGN.js +0 -1
- data/public/railwatch/assets/index-CzitnnSC.js +0 -1
- data/public/railwatch/assets/index-DEUfClv3.js +0 -1
- data/public/railwatch/assets/index-DJKwo-mI.js +0 -1
- data/public/railwatch/assets/index-DK6y0YHp.js +0 -1
- data/public/railwatch/assets/index-DMNPpLH9.js +0 -1
- data/public/railwatch/assets/index-DZO1mSfX.js +0 -1
- data/public/railwatch/assets/index-Db5wj3M5.js +0 -1
- data/public/railwatch/assets/index-Dcy5WktB.js +0 -1
- data/public/railwatch/assets/index-DvG0-7Lx.js +0 -1
- data/public/railwatch/assets/index-Dvj1wuka.js +0 -1
- data/public/railwatch/assets/index-KyZX46qX.js +0 -1
- data/public/railwatch/assets/index-Ze-KP-sl.js +0 -1
- data/public/railwatch/assets/index-mhRbWLBM.js +0 -1
- data/public/railwatch/assets/index-x099JL5f.js +0 -1
- data/public/railwatch/assets/index-x28zb_nf.js +0 -1
- data/public/railwatch/assets/inertia-Cuyz2ZHO.js +0 -97
- data/public/railwatch/assets/input-error-hog6gGxg.js +0 -1
- data/public/railwatch/assets/json-viewer-DH2W9HXf.js +0 -1
- data/public/railwatch/assets/klass-DHbDelLk.js +0 -1
- data/public/railwatch/assets/label-DdCBgiUn.js +0 -1
- data/public/railwatch/assets/layout-Cueyl7c5.js +0 -1
- data/public/railwatch/assets/live-dot-BZgYTYdt.js +0 -41
- data/public/railwatch/assets/nav-BSSGObDZ.js +0 -1
- data/public/railwatch/assets/new-B8FSb8Bl.js +0 -1
- data/public/railwatch/assets/new-C39_v2Ll.js +0 -1
- data/public/railwatch/assets/new-DVOPwaC5.js +0 -1
- data/public/railwatch/assets/new-Dd40nvJR.js +0 -1
- data/public/railwatch/assets/new-SxnYe1SE.js +0 -1
- data/public/railwatch/assets/new-eP5vKD3Y.js +0 -1
- data/public/railwatch/assets/onboarding-CUpZl5KB.js +0 -1
- data/public/railwatch/assets/origin-identity-BYt2uiuo.js +0 -1
- data/public/railwatch/assets/relative-time-D5UbF4oO.js +0 -1
- data/public/railwatch/assets/route-DQAY8JEr.js +0 -1
- data/public/railwatch/assets/select-2R16457Z.js +0 -1
- data/public/railwatch/assets/separator-D5I0UCB5.js +0 -1
- data/public/railwatch/assets/series-chart-gnrzhmM6.js +0 -1
- data/public/railwatch/assets/show-2BkeNRUC.js +0 -2
- data/public/railwatch/assets/show-B0X1hRQH.js +0 -1
- data/public/railwatch/assets/show-BKUV5l5q.js +0 -1
- data/public/railwatch/assets/show-BQJD_CsF.js +0 -6
- data/public/railwatch/assets/show-BhmD6Sxp.js +0 -1
- data/public/railwatch/assets/show-C3KcnVo2.js +0 -1
- data/public/railwatch/assets/show-C95cHd06.js +0 -2
- data/public/railwatch/assets/show-CdB4uVQS.js +0 -1
- data/public/railwatch/assets/show-CoCqcVIp.js +0 -1
- data/public/railwatch/assets/show-D2LqjEsU.js +0 -1
- data/public/railwatch/assets/show-DNpnKyTh.js +0 -1
- data/public/railwatch/assets/show-DOlTxbig.js +0 -1
- data/public/railwatch/assets/show-DQCkttL8.js +0 -1
- data/public/railwatch/assets/show-DcxesipC.js +0 -1
- data/public/railwatch/assets/show-IGJoc_X0.js +0 -1
- data/public/railwatch/assets/show-qNV6H8SH.js +0 -2
- data/public/railwatch/assets/stat-ZtxGU8lE.js +0 -1
- data/public/railwatch/assets/status-badge-CH5P-Xkj.js +0 -1
- data/public/railwatch/assets/text-link-DHJ8BXx5.js +0 -1
- data/public/railwatch/assets/textarea-BeQtQyl5.js +0 -1
- data/public/railwatch/assets/timeline-CaKQVu48.js +0 -1
- 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,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
|
+
}
|