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,64 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
// Nightwatch's period bar: a bordered pill holding mono buttons, the selected
|
|
6
|
+
// one filled blue. Used for time windows, percentiles, and status tabs so
|
|
7
|
+
// every segmented choice in the app looks the same.
|
|
8
|
+
export function Segmented({
|
|
9
|
+
children,
|
|
10
|
+
className,
|
|
11
|
+
}: {
|
|
12
|
+
children: ReactNode
|
|
13
|
+
className?: string
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn(
|
|
18
|
+
"bg-card flex h-8 shrink-0 items-center gap-px rounded-lg border p-0.5",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SegmentedItem({
|
|
28
|
+
active,
|
|
29
|
+
onClick,
|
|
30
|
+
children,
|
|
31
|
+
className,
|
|
32
|
+
mono = true,
|
|
33
|
+
disabled,
|
|
34
|
+
"aria-label": ariaLabel,
|
|
35
|
+
}: {
|
|
36
|
+
active: boolean
|
|
37
|
+
onClick: () => void
|
|
38
|
+
children: ReactNode
|
|
39
|
+
className?: string
|
|
40
|
+
mono?: boolean
|
|
41
|
+
disabled?: boolean
|
|
42
|
+
"aria-label"?: string
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={onClick}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
aria-pressed={active}
|
|
50
|
+
aria-label={ariaLabel}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex h-full min-w-8 items-center justify-center gap-1.5 rounded-md border px-2.5 text-xs whitespace-nowrap transition-colors",
|
|
53
|
+
mono && "font-mono uppercase",
|
|
54
|
+
active
|
|
55
|
+
? "bg-primary border-primary text-primary-foreground"
|
|
56
|
+
: "text-muted-foreground hover:bg-accent hover:text-foreground border-transparent",
|
|
57
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</button>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Area,
|
|
3
|
+
AreaChart,
|
|
4
|
+
Bar,
|
|
5
|
+
BarChart,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Line,
|
|
8
|
+
LineChart,
|
|
9
|
+
ReferenceLine,
|
|
10
|
+
XAxis,
|
|
11
|
+
YAxis,
|
|
12
|
+
} from "recharts"
|
|
13
|
+
|
|
14
|
+
import { useChartHover } from "@/components/railwatch/chart-hover"
|
|
15
|
+
import {
|
|
16
|
+
type ChartConfig,
|
|
17
|
+
ChartContainer,
|
|
18
|
+
ChartTooltip,
|
|
19
|
+
ChartTooltipContent,
|
|
20
|
+
} from "@/components/ui/chart"
|
|
21
|
+
import { ms } from "@/lib/format"
|
|
22
|
+
import type {
|
|
23
|
+
DeployMarker,
|
|
24
|
+
Percentile,
|
|
25
|
+
SeriesPoint,
|
|
26
|
+
SessionStatusPoint,
|
|
27
|
+
} from "@/types"
|
|
28
|
+
|
|
29
|
+
// Status vocabulary, same as the badges: ok is quiet grey so the amber and
|
|
30
|
+
// red slices are what the eye lands on; latency lines use the accent for
|
|
31
|
+
// the chosen percentile and a muted amber for the average.
|
|
32
|
+
const config = {
|
|
33
|
+
count: { label: "OK", color: "var(--ok)" },
|
|
34
|
+
errors: { label: "5xx", color: "var(--danger)" },
|
|
35
|
+
client_errors: { label: "4xx", color: "var(--warning)" },
|
|
36
|
+
p50: { label: "p50", color: "var(--primary)" },
|
|
37
|
+
p95: { label: "p95", color: "var(--primary)" },
|
|
38
|
+
p99: { label: "p99", color: "var(--primary)" },
|
|
39
|
+
avg: { label: "avg", color: "var(--warning)" },
|
|
40
|
+
} satisfies ChartConfig
|
|
41
|
+
|
|
42
|
+
// Bucket width in ms, read off the data so the axis needs no page context:
|
|
43
|
+
// a minute chart labels ticks by time, a day chart by date.
|
|
44
|
+
function bucketWidth(data: { t: string }[]) {
|
|
45
|
+
if (data.length < 2) return 0
|
|
46
|
+
return new Date(data[1].t).getTime() - new Date(data[0].t).getTime()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DAY = 24 * 60 * 60 * 1000
|
|
50
|
+
|
|
51
|
+
function tickFormatter(data: { t: string }[]) {
|
|
52
|
+
const width = bucketWidth(data)
|
|
53
|
+
const span =
|
|
54
|
+
data.length > 1
|
|
55
|
+
? new Date(data[data.length - 1].t).getTime() -
|
|
56
|
+
new Date(data[0].t).getTime()
|
|
57
|
+
: 0
|
|
58
|
+
if (width >= DAY || span > 2 * DAY)
|
|
59
|
+
return (t: string) =>
|
|
60
|
+
new Date(t).toLocaleDateString(undefined, {
|
|
61
|
+
month: "short",
|
|
62
|
+
day: "numeric",
|
|
63
|
+
})
|
|
64
|
+
return (t: string) =>
|
|
65
|
+
new Date(t).toLocaleTimeString(undefined, {
|
|
66
|
+
hour: "2-digit",
|
|
67
|
+
minute: "2-digit",
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stamp(t: string) {
|
|
72
|
+
const d = new Date(t)
|
|
73
|
+
return d.toLocaleString(undefined, {
|
|
74
|
+
day: "2-digit",
|
|
75
|
+
month: "short",
|
|
76
|
+
hour: "2-digit",
|
|
77
|
+
minute: "2-digit",
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const axisTick = { fontSize: 10, fontFamily: "var(--font-mono)" }
|
|
82
|
+
|
|
83
|
+
// A line only joins neighbouring points, so a bucket with data between two
|
|
84
|
+
// quiet ones (common at a minute's width) would draw nothing. Mark those
|
|
85
|
+
// lone points with a dot; everything else stays a plain line. Recharts can
|
|
86
|
+
// call this with an index past the array it was built over (a live reload
|
|
87
|
+
// swapping in a shorter series mid-render did, LC-43), so neighbours are
|
|
88
|
+
// looked up defensively rather than assumed.
|
|
89
|
+
export function loneDot(
|
|
90
|
+
data: Record<string, unknown>[],
|
|
91
|
+
key: string,
|
|
92
|
+
color: string,
|
|
93
|
+
) {
|
|
94
|
+
const LoneDot = (props: { cx?: number; cy?: number; index?: number }) => {
|
|
95
|
+
const { cx, cy, index } = props
|
|
96
|
+
if (cx == null || cy == null || index == null) return <g key={index} />
|
|
97
|
+
const before = data[index - 1]?.[key] ?? null
|
|
98
|
+
const after = data[index + 1]?.[key] ?? null
|
|
99
|
+
if (before != null || after != null) return <g key={index} />
|
|
100
|
+
return <circle key={index} cx={cx} cy={cy} r={2} fill={color} />
|
|
101
|
+
}
|
|
102
|
+
return LoneDot
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function markers(deploys: DeployMarker[] = []) {
|
|
106
|
+
return deploys.map((d) => (
|
|
107
|
+
<ReferenceLine
|
|
108
|
+
key={d.deploy}
|
|
109
|
+
x={new Date(d.at).toISOString()}
|
|
110
|
+
stroke="var(--primary)"
|
|
111
|
+
strokeDasharray="3 3"
|
|
112
|
+
label={{
|
|
113
|
+
value: d.ref,
|
|
114
|
+
position: "insideTopRight",
|
|
115
|
+
fontSize: 9,
|
|
116
|
+
fill: "var(--primary)",
|
|
117
|
+
fontFamily: "var(--font-mono)",
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Recharts calls onMouseMove with the hovered point's x value as
|
|
124
|
+
// `activeLabel` (our bucket ISO timestamp) — feed that into ChartHoverProvider
|
|
125
|
+
// so a table elsewhere on the page can highlight the matching row.
|
|
126
|
+
function useBucketHover() {
|
|
127
|
+
const { setHoveredKey } = useChartHover()
|
|
128
|
+
return {
|
|
129
|
+
onMouseMove: (state: { activeLabel?: string | number }) => {
|
|
130
|
+
if (typeof state?.activeLabel === "string")
|
|
131
|
+
setHoveredKey(state.activeLabel)
|
|
132
|
+
},
|
|
133
|
+
onMouseLeave: () => setHoveredKey(null),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Bucket-range caption under a chart, like Nightwatch's "02 Nov 18:00 UTC".
|
|
138
|
+
function Range({ data }: { data: { t: string }[] }) {
|
|
139
|
+
if (data.length === 0) return null
|
|
140
|
+
return (
|
|
141
|
+
<div className="text-muted-foreground mt-1 flex justify-between font-mono text-[10px]">
|
|
142
|
+
<span>{stamp(data[0].t)}</span>
|
|
143
|
+
<span>{stamp(data[data.length - 1].t)}</span>
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function ThroughputChart({
|
|
149
|
+
data,
|
|
150
|
+
deploys,
|
|
151
|
+
label = "Requests",
|
|
152
|
+
className = "h-40",
|
|
153
|
+
}: {
|
|
154
|
+
data: SeriesPoint[]
|
|
155
|
+
deploys?: DeployMarker[]
|
|
156
|
+
label?: string
|
|
157
|
+
className?: string
|
|
158
|
+
}) {
|
|
159
|
+
const cfg = { ...config, count: { ...config.count, label } }
|
|
160
|
+
const hover = useBucketHover()
|
|
161
|
+
const tick = tickFormatter(data)
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<ChartContainer config={cfg} className={`${className} w-full`}>
|
|
165
|
+
<BarChart
|
|
166
|
+
data={data.map((d) => ({
|
|
167
|
+
...d,
|
|
168
|
+
t: new Date(d.t).toISOString(),
|
|
169
|
+
ok: Math.max(0, d.count - d.errors - d.client_errors),
|
|
170
|
+
}))}
|
|
171
|
+
barCategoryGap={2}
|
|
172
|
+
onMouseMove={hover.onMouseMove}
|
|
173
|
+
onMouseLeave={hover.onMouseLeave}
|
|
174
|
+
>
|
|
175
|
+
<CartesianGrid vertical={false} strokeDasharray="2 4" />
|
|
176
|
+
<XAxis
|
|
177
|
+
dataKey="t"
|
|
178
|
+
tickFormatter={tick}
|
|
179
|
+
tickLine={false}
|
|
180
|
+
axisLine={false}
|
|
181
|
+
minTickGap={40}
|
|
182
|
+
tick={axisTick}
|
|
183
|
+
/>
|
|
184
|
+
<YAxis
|
|
185
|
+
width={36}
|
|
186
|
+
tickLine={false}
|
|
187
|
+
axisLine={false}
|
|
188
|
+
tick={axisTick}
|
|
189
|
+
tickFormatter={(v) => compact(Number(v))}
|
|
190
|
+
/>
|
|
191
|
+
<ChartTooltip
|
|
192
|
+
content={
|
|
193
|
+
<ChartTooltipContent labelFormatter={(v) => stamp(String(v))} />
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
<Bar
|
|
197
|
+
dataKey="ok"
|
|
198
|
+
name="count"
|
|
199
|
+
fill="var(--color-count)"
|
|
200
|
+
stackId="a"
|
|
201
|
+
/>
|
|
202
|
+
<Bar
|
|
203
|
+
dataKey="client_errors"
|
|
204
|
+
fill="var(--color-client_errors)"
|
|
205
|
+
stackId="a"
|
|
206
|
+
/>
|
|
207
|
+
<Bar
|
|
208
|
+
dataKey="errors"
|
|
209
|
+
fill="var(--color-errors)"
|
|
210
|
+
stackId="a"
|
|
211
|
+
radius={[2, 2, 0, 0]}
|
|
212
|
+
/>
|
|
213
|
+
{markers(deploys)}
|
|
214
|
+
</BarChart>
|
|
215
|
+
</ChartContainer>
|
|
216
|
+
<Range data={data} />
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Release health's own vocabulary: sessions stacked by the worst status
|
|
222
|
+
// they reached, so a regression reads as amber and red eating the band.
|
|
223
|
+
const sessionConfig = {
|
|
224
|
+
ok: { label: "ok", color: "var(--ok)" },
|
|
225
|
+
errored: { label: "errored", color: "var(--warning)" },
|
|
226
|
+
crashed: { label: "crashed", color: "var(--danger)" },
|
|
227
|
+
} satisfies ChartConfig
|
|
228
|
+
|
|
229
|
+
export function SessionStatusChart({
|
|
230
|
+
data,
|
|
231
|
+
deploys,
|
|
232
|
+
className = "h-40",
|
|
233
|
+
}: {
|
|
234
|
+
data: SessionStatusPoint[]
|
|
235
|
+
deploys?: DeployMarker[]
|
|
236
|
+
className?: string
|
|
237
|
+
}) {
|
|
238
|
+
const hover = useBucketHover()
|
|
239
|
+
const tick = tickFormatter(data)
|
|
240
|
+
return (
|
|
241
|
+
<div>
|
|
242
|
+
<ChartContainer config={sessionConfig} className={`${className} w-full`}>
|
|
243
|
+
<AreaChart
|
|
244
|
+
data={data.map((d) => ({ ...d, t: new Date(d.t).toISOString() }))}
|
|
245
|
+
onMouseMove={hover.onMouseMove}
|
|
246
|
+
onMouseLeave={hover.onMouseLeave}
|
|
247
|
+
>
|
|
248
|
+
<CartesianGrid vertical={false} strokeDasharray="2 4" />
|
|
249
|
+
<XAxis
|
|
250
|
+
dataKey="t"
|
|
251
|
+
tickFormatter={tick}
|
|
252
|
+
tickLine={false}
|
|
253
|
+
axisLine={false}
|
|
254
|
+
minTickGap={40}
|
|
255
|
+
tick={axisTick}
|
|
256
|
+
/>
|
|
257
|
+
<YAxis
|
|
258
|
+
width={36}
|
|
259
|
+
tickLine={false}
|
|
260
|
+
axisLine={false}
|
|
261
|
+
tick={axisTick}
|
|
262
|
+
tickFormatter={(v) => compact(Number(v))}
|
|
263
|
+
/>
|
|
264
|
+
<ChartTooltip
|
|
265
|
+
content={
|
|
266
|
+
<ChartTooltipContent labelFormatter={(v) => stamp(String(v))} />
|
|
267
|
+
}
|
|
268
|
+
/>
|
|
269
|
+
{(["ok", "errored", "crashed"] as const).map((key) => (
|
|
270
|
+
<Area
|
|
271
|
+
key={key}
|
|
272
|
+
dataKey={key}
|
|
273
|
+
stackId="sessions"
|
|
274
|
+
stroke={`var(--color-${key})`}
|
|
275
|
+
fill={`var(--color-${key})`}
|
|
276
|
+
fillOpacity={0.3}
|
|
277
|
+
strokeWidth={1.25}
|
|
278
|
+
isAnimationActive={false}
|
|
279
|
+
/>
|
|
280
|
+
))}
|
|
281
|
+
{markers(deploys)}
|
|
282
|
+
</AreaChart>
|
|
283
|
+
</ChartContainer>
|
|
284
|
+
<Range data={data} />
|
|
285
|
+
</div>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function LatencyChart({
|
|
290
|
+
data,
|
|
291
|
+
deploys,
|
|
292
|
+
percentile = "p95",
|
|
293
|
+
className = "h-40",
|
|
294
|
+
}: {
|
|
295
|
+
data: SeriesPoint[]
|
|
296
|
+
deploys?: DeployMarker[]
|
|
297
|
+
percentile?: Percentile
|
|
298
|
+
className?: string
|
|
299
|
+
}) {
|
|
300
|
+
const hover = useBucketHover()
|
|
301
|
+
const tick = tickFormatter(data)
|
|
302
|
+
const points = data.map((d) => ({ ...d, t: new Date(d.t).toISOString() }))
|
|
303
|
+
return (
|
|
304
|
+
<div>
|
|
305
|
+
<ChartContainer config={config} className={`${className} w-full`}>
|
|
306
|
+
<LineChart
|
|
307
|
+
data={points}
|
|
308
|
+
onMouseMove={hover.onMouseMove}
|
|
309
|
+
onMouseLeave={hover.onMouseLeave}
|
|
310
|
+
>
|
|
311
|
+
<CartesianGrid vertical={false} strokeDasharray="2 4" />
|
|
312
|
+
<XAxis
|
|
313
|
+
dataKey="t"
|
|
314
|
+
tickFormatter={tick}
|
|
315
|
+
tickLine={false}
|
|
316
|
+
axisLine={false}
|
|
317
|
+
minTickGap={40}
|
|
318
|
+
tick={axisTick}
|
|
319
|
+
/>
|
|
320
|
+
<YAxis
|
|
321
|
+
width={44}
|
|
322
|
+
tickLine={false}
|
|
323
|
+
axisLine={false}
|
|
324
|
+
tick={axisTick}
|
|
325
|
+
tickFormatter={(v) => ms(Number(v), 0)}
|
|
326
|
+
/>
|
|
327
|
+
<ChartTooltip
|
|
328
|
+
content={
|
|
329
|
+
<ChartTooltipContent
|
|
330
|
+
labelFormatter={(v) => stamp(String(v))}
|
|
331
|
+
formatter={(value, name) => [
|
|
332
|
+
value == null ? "—" : ms(Number(value)),
|
|
333
|
+
String(name),
|
|
334
|
+
]}
|
|
335
|
+
/>
|
|
336
|
+
}
|
|
337
|
+
/>
|
|
338
|
+
<Line
|
|
339
|
+
dataKey={percentile}
|
|
340
|
+
stroke={`var(--color-${percentile})`}
|
|
341
|
+
dot={loneDot(points, percentile, `var(--color-${percentile})`)}
|
|
342
|
+
strokeWidth={1.75}
|
|
343
|
+
isAnimationActive={false}
|
|
344
|
+
/>
|
|
345
|
+
<Line
|
|
346
|
+
dataKey="avg"
|
|
347
|
+
stroke="var(--color-avg)"
|
|
348
|
+
dot={loneDot(points, "avg", "var(--color-avg)")}
|
|
349
|
+
strokeWidth={1.25}
|
|
350
|
+
strokeOpacity={0.8}
|
|
351
|
+
isAnimationActive={false}
|
|
352
|
+
/>
|
|
353
|
+
{markers(deploys)}
|
|
354
|
+
</LineChart>
|
|
355
|
+
</ChartContainer>
|
|
356
|
+
<Range data={data} />
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function Sparkline({
|
|
362
|
+
data,
|
|
363
|
+
className = "h-10 w-32",
|
|
364
|
+
hoverKey,
|
|
365
|
+
}: {
|
|
366
|
+
data: number[]
|
|
367
|
+
className?: string
|
|
368
|
+
hoverKey?: string
|
|
369
|
+
}) {
|
|
370
|
+
const { hoveredKey, setHoveredKey } = useChartHover()
|
|
371
|
+
const active = hoverKey != null && hoveredKey === hoverKey
|
|
372
|
+
return (
|
|
373
|
+
<ChartContainer config={config} className={className}>
|
|
374
|
+
{/* recharts pads every side by 5px by default, which leaves a table
|
|
375
|
+
cell's 10px-tall sparkline no plot area at all; the stroke's
|
|
376
|
+
half-width is the only margin a chart this small needs. */}
|
|
377
|
+
<AreaChart
|
|
378
|
+
data={data.map((v) => ({ v }))}
|
|
379
|
+
margin={{ top: 1, right: 1, bottom: 1, left: 1 }}
|
|
380
|
+
onMouseEnter={hoverKey ? () => setHoveredKey(hoverKey) : undefined}
|
|
381
|
+
onMouseLeave={hoverKey ? () => setHoveredKey(null) : undefined}
|
|
382
|
+
>
|
|
383
|
+
<Area
|
|
384
|
+
dataKey="v"
|
|
385
|
+
stroke={active ? "var(--primary)" : "var(--muted-foreground)"}
|
|
386
|
+
fill={active ? "var(--primary)" : "var(--muted-foreground)"}
|
|
387
|
+
fillOpacity={active ? 0.35 : 0.15}
|
|
388
|
+
strokeWidth={active ? 2 : 1.25}
|
|
389
|
+
isAnimationActive={false}
|
|
390
|
+
/>
|
|
391
|
+
</AreaChart>
|
|
392
|
+
</ChartContainer>
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function compact(n: number) {
|
|
397
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
398
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(n >= 10_000 ? 0 : 1)}k`
|
|
399
|
+
return String(n)
|
|
400
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogContent,
|
|
4
|
+
DialogDescription,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
} from "@/components/ui/dialog"
|
|
8
|
+
|
|
9
|
+
interface Shortcut {
|
|
10
|
+
keys: string[]
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const GROUPS: { title: string; shortcuts: Shortcut[] }[] = [
|
|
15
|
+
{
|
|
16
|
+
title: "Navigation",
|
|
17
|
+
shortcuts: [
|
|
18
|
+
{ keys: ["⌘", "K"], description: "Open command palette" },
|
|
19
|
+
{ keys: ["g", "d"], description: "Go to dashboard" },
|
|
20
|
+
{ keys: ["g", "o"], description: "Go to Overview" },
|
|
21
|
+
{ keys: ["g", "r"], description: "Go to Requests" },
|
|
22
|
+
{ keys: ["g", "j"], description: "Go to Jobs" },
|
|
23
|
+
{ keys: ["g", "q"], description: "Go to Queries" },
|
|
24
|
+
{ keys: ["g", "i"], description: "Go to Issues" },
|
|
25
|
+
{ keys: ["?"], description: "Show this help" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: "Lists (requests, jobs, queries, issues, logs, …)",
|
|
30
|
+
shortcuts: [
|
|
31
|
+
{ keys: ["j"], description: "Move highlight down" },
|
|
32
|
+
{ keys: ["k"], description: "Move highlight up" },
|
|
33
|
+
{ keys: ["↵"], description: "Open highlighted row" },
|
|
34
|
+
{ keys: ["o"], description: "Open highlighted row in a new tab" },
|
|
35
|
+
{ keys: ["Esc"], description: "Clear highlight" },
|
|
36
|
+
{ keys: ["/"], description: "Focus the filter bar" },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
export function ShortcutsDialog({
|
|
42
|
+
open,
|
|
43
|
+
onOpenChange,
|
|
44
|
+
}: {
|
|
45
|
+
open: boolean
|
|
46
|
+
onOpenChange: (open: boolean) => void
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
50
|
+
<DialogContent className="sm:max-w-sm">
|
|
51
|
+
<DialogHeader>
|
|
52
|
+
<DialogTitle>Keyboard shortcuts</DialogTitle>
|
|
53
|
+
<DialogDescription>Available anywhere in the app.</DialogDescription>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
<div className="flex flex-col gap-4">
|
|
56
|
+
{GROUPS.map((group) => (
|
|
57
|
+
<div key={group.title} className="flex flex-col gap-2">
|
|
58
|
+
<div className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
59
|
+
{group.title}
|
|
60
|
+
</div>
|
|
61
|
+
<ul className="flex flex-col gap-2">
|
|
62
|
+
{group.shortcuts.map((shortcut) => (
|
|
63
|
+
<li
|
|
64
|
+
key={shortcut.description}
|
|
65
|
+
className="flex items-center justify-between text-sm"
|
|
66
|
+
>
|
|
67
|
+
<span className="text-muted-foreground">
|
|
68
|
+
{shortcut.description}
|
|
69
|
+
</span>
|
|
70
|
+
<span className="flex items-center gap-1">
|
|
71
|
+
{shortcut.keys.map((key, i) => (
|
|
72
|
+
<kbd
|
|
73
|
+
key={i}
|
|
74
|
+
className="bg-muted rounded border px-1.5 py-0.5 font-mono text-xs"
|
|
75
|
+
>
|
|
76
|
+
{key}
|
|
77
|
+
</kbd>
|
|
78
|
+
))}
|
|
79
|
+
</span>
|
|
80
|
+
</li>
|
|
81
|
+
))}
|
|
82
|
+
</ul>
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
</DialogContent>
|
|
87
|
+
</Dialog>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { X } from "lucide-react"
|
|
2
|
+
import { toast } from "sonner"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
// Notices spoken in the signal vocabulary the rest of the UI uses. The
|
|
7
|
+
// aspect carries the meaning, so there is no icon to interpret: green is
|
|
8
|
+
// clear (done, saved, sent), amber is caution (something to look at, still
|
|
9
|
+
// safe), red is stop (this failed). The head sits at the left of the card
|
|
10
|
+
// like a lineside signal; the lit lamp is the only colour on the card.
|
|
11
|
+
export type ToastAspect = "clear" | "caution" | "stop"
|
|
12
|
+
|
|
13
|
+
const LAMP_CLASS: Record<ToastAspect, string> = {
|
|
14
|
+
stop: "bg-danger",
|
|
15
|
+
caution: "bg-warning",
|
|
16
|
+
clear: "bg-live",
|
|
17
|
+
}
|
|
18
|
+
const LAMP_ORDER: ToastAspect[] = ["stop", "caution", "clear"]
|
|
19
|
+
|
|
20
|
+
// Clear notices leave on their own; the line is clear and there is nothing
|
|
21
|
+
// to act on. Caution and stop wait to be read.
|
|
22
|
+
const DURATION: Record<ToastAspect, number> = {
|
|
23
|
+
clear: 4000,
|
|
24
|
+
caution: 8000,
|
|
25
|
+
stop: Infinity,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Head({ aspect }: { aspect: ToastAspect }) {
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
aria-hidden
|
|
32
|
+
className="border-foreground/30 bg-background/60 inline-flex w-3 shrink-0 flex-col items-center justify-between rounded-full border px-px py-1"
|
|
33
|
+
style={{ height: 30 }}
|
|
34
|
+
>
|
|
35
|
+
{LAMP_ORDER.map((lamp) => (
|
|
36
|
+
<span key={lamp} className="relative inline-flex size-2">
|
|
37
|
+
{lamp === aspect && (
|
|
38
|
+
<span
|
|
39
|
+
className={cn(
|
|
40
|
+
"absolute inline-flex h-full w-full rounded-full opacity-40 blur-[2px]",
|
|
41
|
+
LAMP_CLASS[lamp],
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
<span
|
|
46
|
+
className={cn(
|
|
47
|
+
"relative inline-flex size-2 rounded-full",
|
|
48
|
+
lamp === aspect ? LAMP_CLASS[lamp] : "bg-neutral-400/15",
|
|
49
|
+
)}
|
|
50
|
+
/>
|
|
51
|
+
</span>
|
|
52
|
+
))}
|
|
53
|
+
</span>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The card itself. `onDismiss` is what the X does; the marketing hero
|
|
58
|
+
// renders one with no dismiss as a static example.
|
|
59
|
+
export function SignalCard({
|
|
60
|
+
aspect,
|
|
61
|
+
title,
|
|
62
|
+
description,
|
|
63
|
+
onDismiss,
|
|
64
|
+
className,
|
|
65
|
+
}: {
|
|
66
|
+
aspect: ToastAspect
|
|
67
|
+
title: string
|
|
68
|
+
description?: string
|
|
69
|
+
onDismiss?: () => void
|
|
70
|
+
className?: string
|
|
71
|
+
}) {
|
|
72
|
+
const role = aspect === "stop" ? "alert" : "status"
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
role={onDismiss ? role : undefined}
|
|
76
|
+
data-aspect={aspect}
|
|
77
|
+
className={cn(
|
|
78
|
+
"bg-popover text-popover-foreground flex w-full items-start gap-3 rounded-lg border px-3.5 py-3 shadow-lg md:w-[360px]",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<Head aspect={aspect} />
|
|
83
|
+
<div className="min-w-0 flex-1 pt-0.5">
|
|
84
|
+
<p className="text-sm leading-snug font-medium">{title}</p>
|
|
85
|
+
{description && (
|
|
86
|
+
<p className="text-muted-foreground mt-0.5 text-xs leading-relaxed">
|
|
87
|
+
{description}
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
{onDismiss && (
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={onDismiss}
|
|
95
|
+
aria-label="Dismiss"
|
|
96
|
+
className="text-muted-foreground hover:text-foreground -mt-0.5 -mr-1 rounded p-1"
|
|
97
|
+
>
|
|
98
|
+
<X className="size-3.5" />
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function signalToast(
|
|
106
|
+
aspect: ToastAspect,
|
|
107
|
+
title: string,
|
|
108
|
+
description?: string,
|
|
109
|
+
) {
|
|
110
|
+
return toast.custom(
|
|
111
|
+
(id) => (
|
|
112
|
+
<SignalCard
|
|
113
|
+
aspect={aspect}
|
|
114
|
+
title={title}
|
|
115
|
+
description={description}
|
|
116
|
+
onDismiss={() => toast.dismiss(id)}
|
|
117
|
+
/>
|
|
118
|
+
),
|
|
119
|
+
{ duration: DURATION[aspect], unstyled: true },
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
// Clickable DataTable column header for a sortable numeric column. Dumb by
|
|
6
|
+
// design: the page builds the sort/dir URL with its own route helper and
|
|
7
|
+
// passes the click handler in.
|
|
8
|
+
export function SortHeader({
|
|
9
|
+
label,
|
|
10
|
+
active,
|
|
11
|
+
dir,
|
|
12
|
+
onClick,
|
|
13
|
+
}: {
|
|
14
|
+
label: string
|
|
15
|
+
active: boolean
|
|
16
|
+
dir: "asc" | "desc"
|
|
17
|
+
onClick: () => void
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
onClick={onClick}
|
|
23
|
+
className={cn(
|
|
24
|
+
"hover:text-foreground inline-flex items-center gap-1",
|
|
25
|
+
active && "text-foreground font-semibold",
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
{label}
|
|
29
|
+
{active &&
|
|
30
|
+
(dir === "asc" ? (
|
|
31
|
+
<ArrowUp className="size-3" />
|
|
32
|
+
) : (
|
|
33
|
+
<ArrowDown className="size-3" />
|
|
34
|
+
))}
|
|
35
|
+
</button>
|
|
36
|
+
)
|
|
37
|
+
}
|