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,295 @@
|
|
|
1
|
+
import { Fragment, useState } from "react"
|
|
2
|
+
|
|
3
|
+
import { SourceLink } from "@/components/railwatch/source-link"
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import type { ExceptionDetail, Frame, TimelineEntry } from "@/types"
|
|
7
|
+
|
|
8
|
+
// A hashed build asset -- "assets/index-Bq1x9K2v.js", what a browser frame
|
|
9
|
+
// points at in production. It is the app's own code, but not at any path
|
|
10
|
+
// that exists in the repository, so there is nothing to link it to until
|
|
11
|
+
// Railwatch can resolve it through a source map (docs/roadmap.md); until then
|
|
12
|
+
// it is shown exactly as the browser named it.
|
|
13
|
+
const HASHED_ASSET = /-[A-Za-z0-9_-]{8,}\.(?:js|mjs|cjs|css)$/
|
|
14
|
+
|
|
15
|
+
function FrameRow({
|
|
16
|
+
frame,
|
|
17
|
+
deploy,
|
|
18
|
+
open,
|
|
19
|
+
onToggle,
|
|
20
|
+
}: {
|
|
21
|
+
frame: Frame
|
|
22
|
+
deploy?: string | null
|
|
23
|
+
open: boolean
|
|
24
|
+
onToggle: () => void
|
|
25
|
+
}) {
|
|
26
|
+
const lines = frame.code ? Object.entries(frame.code) : []
|
|
27
|
+
const linkable = frame.in_app && !HASHED_ASSET.test(frame.file)
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
"border-b last:border-b-0",
|
|
32
|
+
frame.in_app ? "bg-background" : "bg-muted/40",
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<div className="hover:bg-muted/60 flex w-full items-center gap-2 px-3 py-1.5 font-mono text-xs">
|
|
36
|
+
{linkable && (
|
|
37
|
+
<SourceLink
|
|
38
|
+
location={`${frame.file}:${frame.line}`}
|
|
39
|
+
deploy={deploy}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onToggle}
|
|
45
|
+
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
|
46
|
+
>
|
|
47
|
+
{!linkable && (
|
|
48
|
+
<span className="text-muted-foreground truncate">
|
|
49
|
+
{frame.file}:{frame.line}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
<span className="text-muted-foreground ml-auto truncate">
|
|
53
|
+
in {frame.function}
|
|
54
|
+
</span>
|
|
55
|
+
{frame.in_app && (
|
|
56
|
+
<Badge variant="outline" className="shrink-0 text-[10px]">
|
|
57
|
+
app
|
|
58
|
+
</Badge>
|
|
59
|
+
)}
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
{open && lines.length > 0 && (
|
|
63
|
+
<pre className="bg-muted/60 overflow-x-auto px-3 py-2 font-mono text-xs leading-5">
|
|
64
|
+
{lines.map(([n, src]) => (
|
|
65
|
+
<div
|
|
66
|
+
key={n}
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex gap-3 border-l-2 border-transparent pl-2",
|
|
69
|
+
Number(n) === frame.line &&
|
|
70
|
+
"border-destructive bg-destructive/10 font-semibold",
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<span className="text-muted-foreground w-10 shrink-0 text-right select-none">
|
|
74
|
+
{n}
|
|
75
|
+
</span>
|
|
76
|
+
<span className="whitespace-pre">{src}</span>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</pre>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function Frames({
|
|
86
|
+
frames,
|
|
87
|
+
deploy,
|
|
88
|
+
}: {
|
|
89
|
+
frames: Frame[]
|
|
90
|
+
deploy?: string | null
|
|
91
|
+
}) {
|
|
92
|
+
const appIndexes = frames.flatMap((f, i) => (f.in_app ? [i] : []))
|
|
93
|
+
const [open, setOpen] = useState<Set<number>>(
|
|
94
|
+
new Set(appIndexes.length > 0 ? appIndexes : [0]),
|
|
95
|
+
)
|
|
96
|
+
const [showAll, setShowAll] = useState(false)
|
|
97
|
+
const visible = showAll ? frames : frames.filter((f, i) => f.in_app || i < 3)
|
|
98
|
+
|
|
99
|
+
function toggle(idx: number) {
|
|
100
|
+
setOpen((prev) => {
|
|
101
|
+
const next = new Set(prev)
|
|
102
|
+
if (next.has(idx)) next.delete(idx)
|
|
103
|
+
else next.add(idx)
|
|
104
|
+
return next
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="overflow-hidden rounded-lg border">
|
|
110
|
+
{visible.map((f) => {
|
|
111
|
+
const idx = frames.indexOf(f)
|
|
112
|
+
return (
|
|
113
|
+
<FrameRow
|
|
114
|
+
key={idx}
|
|
115
|
+
frame={f}
|
|
116
|
+
deploy={deploy}
|
|
117
|
+
open={open.has(idx)}
|
|
118
|
+
onToggle={() => toggle(idx)}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
{visible.length < frames.length && (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
className="text-muted-foreground w-full px-3 py-1.5 text-left text-xs hover:underline"
|
|
126
|
+
onClick={() => setShowAll(true)}
|
|
127
|
+
>
|
|
128
|
+
Show {frames.length - visible.length} framework frames
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ExceptionCard({
|
|
136
|
+
exception,
|
|
137
|
+
deploy,
|
|
138
|
+
}: {
|
|
139
|
+
exception: ExceptionDetail
|
|
140
|
+
deploy?: string | null
|
|
141
|
+
}) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="space-y-3">
|
|
144
|
+
<div>
|
|
145
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
146
|
+
<span className="font-mono text-sm font-semibold">
|
|
147
|
+
{exception.class_name}
|
|
148
|
+
</span>
|
|
149
|
+
<Badge variant={exception.handled ? "secondary" : "destructive"}>
|
|
150
|
+
{exception.handled ? "handled" : "unhandled"}
|
|
151
|
+
</Badge>
|
|
152
|
+
{exception.severity && (
|
|
153
|
+
<Badge variant="outline">{exception.severity}</Badge>
|
|
154
|
+
)}
|
|
155
|
+
{exception.source && (
|
|
156
|
+
<span className="text-muted-foreground font-mono text-xs">
|
|
157
|
+
{exception.source}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
<p className="mt-1 font-mono text-sm break-words">
|
|
162
|
+
{exception.message}
|
|
163
|
+
</p>
|
|
164
|
+
{exception.cause && (
|
|
165
|
+
<p className="text-muted-foreground mt-1 font-mono text-xs">
|
|
166
|
+
caused by {exception.cause.class}: {exception.cause.message}
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<Frames frames={exception.frames} deploy={deploy} />
|
|
171
|
+
{exception.locals && Object.keys(exception.locals).length > 0 && (
|
|
172
|
+
<div className="overflow-hidden rounded-lg border">
|
|
173
|
+
<div className="label-caps border-b px-3 py-1.5">
|
|
174
|
+
Local variables at the raise site
|
|
175
|
+
</div>
|
|
176
|
+
<dl className="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 px-3 py-2 font-mono text-xs">
|
|
177
|
+
{Object.entries(exception.locals).map(([name, value]) => (
|
|
178
|
+
<Fragment key={name}>
|
|
179
|
+
<dt className="text-muted-foreground">{name}</dt>
|
|
180
|
+
<dd className="break-all">{value}</dd>
|
|
181
|
+
</Fragment>
|
|
182
|
+
))}
|
|
183
|
+
</dl>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
{exception.context && exception.context !== "{}" && (
|
|
187
|
+
<pre className="bg-muted/60 overflow-x-auto rounded-lg p-3 font-mono text-xs">
|
|
188
|
+
{exception.context}
|
|
189
|
+
</pre>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const crumbColors: Record<string, string> = {
|
|
196
|
+
query: "bg-sky-500",
|
|
197
|
+
transaction: "bg-sky-300",
|
|
198
|
+
cache_event: "bg-violet-500",
|
|
199
|
+
outgoing_request: "bg-orange-500",
|
|
200
|
+
mail: "bg-pink-500",
|
|
201
|
+
broadcast: "bg-teal-500",
|
|
202
|
+
enqueued_job: "bg-amber-500",
|
|
203
|
+
view_render: "bg-emerald-500",
|
|
204
|
+
storage_op: "bg-indigo-500",
|
|
205
|
+
exception: "bg-red-500",
|
|
206
|
+
log: "bg-neutral-400",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Sentry-style breadcrumbs: what the execution did in the run-up to the
|
|
210
|
+
// exception, oldest first, each with its offset from the start.
|
|
211
|
+
export function Breadcrumbs({ entries }: { entries: TimelineEntry[] }) {
|
|
212
|
+
if (entries.length === 0) return null
|
|
213
|
+
return (
|
|
214
|
+
<div className="overflow-hidden rounded-lg border">
|
|
215
|
+
<div className="label-caps border-b px-3 py-1.5">
|
|
216
|
+
Breadcrumbs · {entries.length} events before the exception
|
|
217
|
+
</div>
|
|
218
|
+
<ol className="divide-y">
|
|
219
|
+
{entries.map((e) => (
|
|
220
|
+
<li
|
|
221
|
+
key={`${e.type}-${e.id}`}
|
|
222
|
+
className="grid grid-cols-[3.5rem_5rem_1fr_4rem] items-center gap-2 px-3 py-1 font-mono text-xs"
|
|
223
|
+
>
|
|
224
|
+
<span className="text-muted-foreground tabular-nums">
|
|
225
|
+
+{e.offset.toFixed(1)}ms
|
|
226
|
+
</span>
|
|
227
|
+
<span className="text-muted-foreground flex items-center gap-1.5 truncate">
|
|
228
|
+
<span
|
|
229
|
+
className={cn(
|
|
230
|
+
"inline-block size-1.5 shrink-0 rounded-full",
|
|
231
|
+
crumbColors[e.type] ?? "bg-neutral-500",
|
|
232
|
+
)}
|
|
233
|
+
/>
|
|
234
|
+
{e.type.replace("_", " ")}
|
|
235
|
+
</span>
|
|
236
|
+
<span className="truncate">{e.label}</span>
|
|
237
|
+
<span className="text-muted-foreground text-right tabular-nums">
|
|
238
|
+
{e.duration != null ? `${e.duration.toFixed(1)}ms` : ""}
|
|
239
|
+
</span>
|
|
240
|
+
</li>
|
|
241
|
+
))}
|
|
242
|
+
</ol>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface BrowserCrumb {
|
|
248
|
+
at: number
|
|
249
|
+
kind: string
|
|
250
|
+
text: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const browserCrumbColors: Record<string, string> = {
|
|
254
|
+
console: "bg-red-500",
|
|
255
|
+
click: "bg-violet-500",
|
|
256
|
+
navigate: "bg-sky-500",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The browser's half of the same idea: what the user did in the run-up to a
|
|
260
|
+
// JavaScript error -- console errors, clicks, Inertia navigations -- offset
|
|
261
|
+
// from the first of them, since a browser error has no execution whose
|
|
262
|
+
// timeline would mean anything (its execution is the beacon that carried it).
|
|
263
|
+
export function BrowserBreadcrumbs({ crumbs }: { crumbs: BrowserCrumb[] }) {
|
|
264
|
+
if (crumbs.length === 0) return null
|
|
265
|
+
const start = crumbs[0].at
|
|
266
|
+
return (
|
|
267
|
+
<div className="overflow-hidden rounded-lg border">
|
|
268
|
+
<div className="label-caps border-b px-3 py-1.5">
|
|
269
|
+
Breadcrumbs · {crumbs.length} events before the error
|
|
270
|
+
</div>
|
|
271
|
+
<ol className="divide-y">
|
|
272
|
+
{crumbs.map((c, i) => (
|
|
273
|
+
<li
|
|
274
|
+
key={`${c.at}-${c.kind}-${i}`}
|
|
275
|
+
className="grid grid-cols-[3.5rem_5rem_1fr] items-center gap-2 px-3 py-1 font-mono text-xs"
|
|
276
|
+
>
|
|
277
|
+
<span className="text-muted-foreground tabular-nums">
|
|
278
|
+
+{((c.at - start) / 1000).toFixed(1)}s
|
|
279
|
+
</span>
|
|
280
|
+
<span className="text-muted-foreground flex items-center gap-1.5 truncate">
|
|
281
|
+
<span
|
|
282
|
+
className={cn(
|
|
283
|
+
"inline-block size-1.5 shrink-0 rounded-full",
|
|
284
|
+
browserCrumbColors[c.kind] ?? "bg-neutral-500",
|
|
285
|
+
)}
|
|
286
|
+
/>
|
|
287
|
+
{c.kind}
|
|
288
|
+
</span>
|
|
289
|
+
<span className="truncate">{c.text}</span>
|
|
290
|
+
</li>
|
|
291
|
+
))}
|
|
292
|
+
</ol>
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Check, ChevronDown, ChevronRight, Copy } from "lucide-react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { useClipboard } from "@/hooks/use-clipboard"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
type Json = null | boolean | number | string | Json[] | { [key: string]: Json }
|
|
9
|
+
|
|
10
|
+
function isContainer(v: unknown): v is Json[] | Record<string, Json> {
|
|
11
|
+
return v !== null && typeof v === "object"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function primitiveClass(v: Json) {
|
|
15
|
+
if (v === null) return "text-muted-foreground"
|
|
16
|
+
if (typeof v === "string") return "text-emerald-600 dark:text-emerald-400"
|
|
17
|
+
if (typeof v === "number") return "text-amber-600 dark:text-amber-400"
|
|
18
|
+
if (typeof v === "boolean") return "text-primary"
|
|
19
|
+
return ""
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Primitive({ value }: { value: string | number | boolean | null }) {
|
|
23
|
+
const text =
|
|
24
|
+
value === null
|
|
25
|
+
? "null"
|
|
26
|
+
: typeof value === "string"
|
|
27
|
+
? `"${value}"`
|
|
28
|
+
: String(value)
|
|
29
|
+
return <span className={primitiveClass(value)}>{text}</span>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Node({
|
|
33
|
+
label,
|
|
34
|
+
value,
|
|
35
|
+
depth,
|
|
36
|
+
collapsed,
|
|
37
|
+
}: {
|
|
38
|
+
label?: string
|
|
39
|
+
value: Json
|
|
40
|
+
depth: number
|
|
41
|
+
collapsed: boolean
|
|
42
|
+
}) {
|
|
43
|
+
const [open, setOpen] = useState(!collapsed)
|
|
44
|
+
const indent = { paddingLeft: `${depth * 12}px` }
|
|
45
|
+
|
|
46
|
+
if (!isContainer(value)) {
|
|
47
|
+
return (
|
|
48
|
+
<div style={indent}>
|
|
49
|
+
{label !== undefined && (
|
|
50
|
+
<span className="text-muted-foreground">{label}: </span>
|
|
51
|
+
)}
|
|
52
|
+
<Primitive value={value} />
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entries = Array.isArray(value)
|
|
58
|
+
? value.map((v, i) => [String(i), v] as const)
|
|
59
|
+
: Object.entries(value)
|
|
60
|
+
const [open_, close_] = Array.isArray(value) ? ["[", "]"] : ["{", "}"]
|
|
61
|
+
|
|
62
|
+
if (entries.length === 0) {
|
|
63
|
+
return (
|
|
64
|
+
<div style={indent}>
|
|
65
|
+
{label !== undefined && (
|
|
66
|
+
<span className="text-muted-foreground">{label}: </span>
|
|
67
|
+
)}
|
|
68
|
+
<span className="text-muted-foreground">
|
|
69
|
+
{open_}
|
|
70
|
+
{close_}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => setOpen((o) => !o)}
|
|
81
|
+
className="hover:bg-muted/60 inline-flex items-center gap-0.5 rounded"
|
|
82
|
+
style={indent}
|
|
83
|
+
>
|
|
84
|
+
{open ? (
|
|
85
|
+
<ChevronDown className="text-muted-foreground size-3" />
|
|
86
|
+
) : (
|
|
87
|
+
<ChevronRight className="text-muted-foreground size-3" />
|
|
88
|
+
)}
|
|
89
|
+
{label !== undefined && (
|
|
90
|
+
<span className="text-muted-foreground">{label}:</span>
|
|
91
|
+
)}
|
|
92
|
+
<span className="text-muted-foreground">
|
|
93
|
+
{open_}
|
|
94
|
+
{!open && `…${entries.length}…${close_}`}
|
|
95
|
+
</span>
|
|
96
|
+
</button>
|
|
97
|
+
{open && (
|
|
98
|
+
<div>
|
|
99
|
+
{entries.map(([k, v]) => (
|
|
100
|
+
<Node
|
|
101
|
+
key={k}
|
|
102
|
+
label={Array.isArray(value) ? undefined : k}
|
|
103
|
+
value={v}
|
|
104
|
+
depth={depth + 1}
|
|
105
|
+
collapsed={collapsed}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
<div className="text-muted-foreground" style={indent}>
|
|
109
|
+
{close_}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function JsonViewer({
|
|
118
|
+
data,
|
|
119
|
+
collapsed = false,
|
|
120
|
+
className,
|
|
121
|
+
}: {
|
|
122
|
+
data: unknown
|
|
123
|
+
collapsed?: boolean
|
|
124
|
+
className?: string
|
|
125
|
+
}) {
|
|
126
|
+
const [, copy] = useClipboard()
|
|
127
|
+
const [copied, setCopied] = useState(false)
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
"bg-muted/60 relative rounded p-2 pr-8 font-mono text-xs",
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="icon-sm"
|
|
139
|
+
className="absolute top-1 right-1"
|
|
140
|
+
onClick={() => {
|
|
141
|
+
void (async () => {
|
|
142
|
+
const ok = await copy(JSON.stringify(data, null, 2))
|
|
143
|
+
if (ok) {
|
|
144
|
+
setCopied(true)
|
|
145
|
+
setTimeout(() => setCopied(false), 1200)
|
|
146
|
+
}
|
|
147
|
+
})()
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{copied ? (
|
|
151
|
+
<Check className="size-3.5" />
|
|
152
|
+
) : (
|
|
153
|
+
<Copy className="size-3.5" />
|
|
154
|
+
)}
|
|
155
|
+
</Button>
|
|
156
|
+
<Node value={data as Json} depth={0} collapsed={collapsed} />
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useTicker } from "@/hooks/use-ticker"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
function isFresh(lastSeenAt: string | null, thresholdMs: number): boolean {
|
|
5
|
+
if (lastSeenAt === null) return false
|
|
6
|
+
return Date.now() - new Date(lastSeenAt).getTime() < thresholdMs
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const UNLIT = "bg-neutral-400/15"
|
|
10
|
+
|
|
11
|
+
// A three-aspect signal head standing in for the usual status dot, lamps
|
|
12
|
+
// in their real order: stop on top, caution in the middle, clear at the
|
|
13
|
+
// bottom. Fresh telemetry lights the bottom lamp green and pings it; stale
|
|
14
|
+
// lights the middle one amber; nothing yet leaves all three unlit. `erroring`
|
|
15
|
+
// lights the top lamp red instead, for an environment that is reporting
|
|
16
|
+
// but reporting failures. Same colour vocabulary as the empty-state signal
|
|
17
|
+
// and the status badges.
|
|
18
|
+
export function LiveDot({
|
|
19
|
+
lastSeenAt,
|
|
20
|
+
thresholdMs = 60_000,
|
|
21
|
+
erroring = false,
|
|
22
|
+
className,
|
|
23
|
+
}: {
|
|
24
|
+
lastSeenAt: string | null
|
|
25
|
+
thresholdMs?: number
|
|
26
|
+
erroring?: boolean
|
|
27
|
+
className?: string
|
|
28
|
+
}) {
|
|
29
|
+
useTicker()
|
|
30
|
+
const fresh = isFresh(lastSeenAt, thresholdMs)
|
|
31
|
+
const state =
|
|
32
|
+
lastSeenAt === null
|
|
33
|
+
? "none"
|
|
34
|
+
: erroring
|
|
35
|
+
? "stop"
|
|
36
|
+
: fresh
|
|
37
|
+
? "clear"
|
|
38
|
+
: "caution"
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
className={cn(
|
|
43
|
+
"border-foreground/30 bg-background/60 inline-flex w-2.5 shrink-0 flex-col items-center justify-between rounded-full border px-px py-[3px]",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
style={{ height: 24 }}
|
|
47
|
+
>
|
|
48
|
+
<span className="relative inline-flex size-1.5">
|
|
49
|
+
{state === "stop" && (
|
|
50
|
+
<span className="bg-danger absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
|
|
51
|
+
)}
|
|
52
|
+
<span
|
|
53
|
+
className={cn(
|
|
54
|
+
"relative inline-flex size-1.5 rounded-full transition-colors duration-300",
|
|
55
|
+
state === "stop" ? "bg-danger" : UNLIT,
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
</span>
|
|
59
|
+
<span
|
|
60
|
+
className={cn(
|
|
61
|
+
"inline-flex size-1.5 rounded-full transition-colors duration-300",
|
|
62
|
+
state === "caution" ? "bg-warning" : UNLIT,
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
<span className="relative inline-flex size-1.5">
|
|
66
|
+
{state === "clear" && (
|
|
67
|
+
<span className="bg-live absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
|
|
68
|
+
)}
|
|
69
|
+
<span
|
|
70
|
+
className={cn(
|
|
71
|
+
"relative inline-flex size-1.5 rounded-full transition-colors duration-300",
|
|
72
|
+
state === "clear" ? "bg-live" : UNLIT,
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
</span>
|
|
76
|
+
</span>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { usePage } from "@inertiajs/react"
|
|
2
|
+
|
|
3
|
+
import { badgeVariants } from "@/components/ui/badge"
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from "@/components/ui/tooltip"
|
|
9
|
+
import { useLive } from "@/hooks/use-live"
|
|
10
|
+
import { useTicker } from "@/hooks/use-ticker"
|
|
11
|
+
import { ago } from "@/lib/format"
|
|
12
|
+
import { cn } from "@/lib/utils"
|
|
13
|
+
import type { SharedProps } from "@/types"
|
|
14
|
+
|
|
15
|
+
import { LiveDot } from "./live-dot"
|
|
16
|
+
|
|
17
|
+
// Small pill embedded in every environment page's PageHeader: pulses while
|
|
18
|
+
// live ingest events are flowing, click to pause/resume the automatic
|
|
19
|
+
// background reloads that keep the page's data fresh.
|
|
20
|
+
export function LiveToggle({ environmentId }: { environmentId: number }) {
|
|
21
|
+
useTicker()
|
|
22
|
+
const { environment, telemetry_freshness: freshness } =
|
|
23
|
+
usePage<SharedProps>().props
|
|
24
|
+
const { connected, lastEventAt, lastRefreshedAt, paused, setPaused } =
|
|
25
|
+
useLive(environmentId)
|
|
26
|
+
const lastEventIso =
|
|
27
|
+
lastEventAt != null
|
|
28
|
+
? new Date(lastEventAt).toISOString()
|
|
29
|
+
: (freshness?.received_at ?? environment?.last_seen_at ?? null)
|
|
30
|
+
const refreshedIso =
|
|
31
|
+
lastRefreshedAt != null ? new Date(lastRefreshedAt).toISOString() : null
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Tooltip>
|
|
35
|
+
<TooltipTrigger asChild>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-pressed={paused}
|
|
39
|
+
aria-label={paused ? "Resume live updates" : "Pause live updates"}
|
|
40
|
+
onClick={() => setPaused(!paused)}
|
|
41
|
+
className={cn(
|
|
42
|
+
badgeVariants({ variant: "outline" }),
|
|
43
|
+
"hover:bg-accent h-7 cursor-pointer gap-1.5 rounded-md font-mono text-[11px] tracking-wide uppercase",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<LiveDot
|
|
47
|
+
lastSeenAt={connected ? lastEventIso : null}
|
|
48
|
+
thresholdMs={10_000}
|
|
49
|
+
/>
|
|
50
|
+
{paused ? "Paused" : connected ? "Live" : "Disconnected"}
|
|
51
|
+
</button>
|
|
52
|
+
</TooltipTrigger>
|
|
53
|
+
<TooltipContent>
|
|
54
|
+
<p>
|
|
55
|
+
{lastEventIso
|
|
56
|
+
? `Telemetry received ${ago(lastEventIso)}`
|
|
57
|
+
: "No telemetry received yet"}
|
|
58
|
+
</p>
|
|
59
|
+
<p>
|
|
60
|
+
{refreshedIso
|
|
61
|
+
? `Page refreshed ${ago(refreshedIso)}`
|
|
62
|
+
: "No live refresh yet"}
|
|
63
|
+
</p>
|
|
64
|
+
{freshness?.processing_lag_seconds != null && (
|
|
65
|
+
<p>
|
|
66
|
+
Ingest processing pending:{" "}
|
|
67
|
+
{Math.round(freshness.processing_lag_seconds)}s
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
{freshness?.aggregated_at && (
|
|
71
|
+
<p>Charts updated {ago(freshness.aggregated_at)}</p>
|
|
72
|
+
)}
|
|
73
|
+
</TooltipContent>
|
|
74
|
+
</Tooltip>
|
|
75
|
+
)
|
|
76
|
+
}
|