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,57 @@
|
|
|
1
|
+
import { usePage } from "@inertiajs/react"
|
|
2
|
+
import { ExternalLink } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { sourceUrl } from "@/lib/source-link"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import type { SharedProps } from "@/types"
|
|
7
|
+
|
|
8
|
+
// A "file:line" location, linked to the line on the code host (pinned to the
|
|
9
|
+
// deploy that produced it) or opened in the viewer's editor, depending on
|
|
10
|
+
// their profile. Renders plain text when we can't build a URL.
|
|
11
|
+
export function SourceLink({
|
|
12
|
+
location,
|
|
13
|
+
deploy,
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
location: string
|
|
17
|
+
// The deploy the location came from, used as the git ref so the link points
|
|
18
|
+
// at the code that was actually running. (Named `deploy`, not `ref`: React
|
|
19
|
+
// treats a `ref` prop as an element ref.)
|
|
20
|
+
deploy?: string | null
|
|
21
|
+
className?: string
|
|
22
|
+
}) {
|
|
23
|
+
const { auth, environment } = usePage<SharedProps>().props
|
|
24
|
+
const match = /^(.*):(\d+)$/.exec(location)
|
|
25
|
+
const url = match
|
|
26
|
+
? sourceUrl({
|
|
27
|
+
file: match[1],
|
|
28
|
+
line: Number(match[2]),
|
|
29
|
+
repositoryUrl: environment?.repository_url,
|
|
30
|
+
ref: deploy,
|
|
31
|
+
defaultBranch: environment?.default_branch,
|
|
32
|
+
editor: auth.user.editor,
|
|
33
|
+
editorRoot: auth.user.editor_root,
|
|
34
|
+
})
|
|
35
|
+
: null
|
|
36
|
+
|
|
37
|
+
if (!url)
|
|
38
|
+
return (
|
|
39
|
+
<span className={cn("truncate font-mono text-xs", className)}>
|
|
40
|
+
{location}
|
|
41
|
+
</span>
|
|
42
|
+
)
|
|
43
|
+
const external = url.startsWith("http")
|
|
44
|
+
return (
|
|
45
|
+
<a
|
|
46
|
+
href={url}
|
|
47
|
+
{...(external ? { target: "_blank", rel: "noreferrer" } : {})}
|
|
48
|
+
className={cn(
|
|
49
|
+
"inline-flex min-w-0 items-center gap-1 font-mono text-xs hover:underline",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<span className="truncate">{location}</span>
|
|
54
|
+
<ExternalLink className="size-3 shrink-0 opacity-60" />
|
|
55
|
+
</a>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Sparkline } from "@/components/railwatch/series-chart"
|
|
2
|
+
|
|
3
|
+
// Compact trend cell for a DataTable row: a 32x10 area chart, no axes.
|
|
4
|
+
// `hoverKey` (typically the row's group_hash) wires it into
|
|
5
|
+
// ChartHoverProvider so hovering the row emphasizes its own sparkline.
|
|
6
|
+
export function SparklineCell({
|
|
7
|
+
data,
|
|
8
|
+
hoverKey,
|
|
9
|
+
}: {
|
|
10
|
+
data: number[]
|
|
11
|
+
hoverKey?: string
|
|
12
|
+
}) {
|
|
13
|
+
return <Sparkline data={data} className="h-[10px] w-8" hoverKey={hoverKey} />
|
|
14
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
const KEYWORDS = new Set([
|
|
4
|
+
"SELECT",
|
|
5
|
+
"FROM",
|
|
6
|
+
"WHERE",
|
|
7
|
+
"JOIN",
|
|
8
|
+
"LEFT",
|
|
9
|
+
"INNER",
|
|
10
|
+
"ON",
|
|
11
|
+
"AND",
|
|
12
|
+
"OR",
|
|
13
|
+
"ORDER",
|
|
14
|
+
"BY",
|
|
15
|
+
"GROUP",
|
|
16
|
+
"LIMIT",
|
|
17
|
+
"OFFSET",
|
|
18
|
+
"INSERT",
|
|
19
|
+
"INTO",
|
|
20
|
+
"VALUES",
|
|
21
|
+
"UPDATE",
|
|
22
|
+
"SET",
|
|
23
|
+
"DELETE",
|
|
24
|
+
"RETURNING",
|
|
25
|
+
"AS",
|
|
26
|
+
"IN",
|
|
27
|
+
"IS",
|
|
28
|
+
"NULL",
|
|
29
|
+
"NOT",
|
|
30
|
+
"DISTINCT",
|
|
31
|
+
"COUNT",
|
|
32
|
+
"SUM",
|
|
33
|
+
"AVG",
|
|
34
|
+
"MAX",
|
|
35
|
+
"MIN",
|
|
36
|
+
"CASE",
|
|
37
|
+
"WHEN",
|
|
38
|
+
"THEN",
|
|
39
|
+
"ELSE",
|
|
40
|
+
"END",
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
type TokenKind =
|
|
44
|
+
"identifier" | "string" | "placeholder" | "number" | "keyword" | "text"
|
|
45
|
+
|
|
46
|
+
interface Token {
|
|
47
|
+
text: string
|
|
48
|
+
kind: TokenKind
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Quoted identifier | string literal | `?` bind placeholder | number | bare word.
|
|
52
|
+
// Anything not matched (whitespace, punctuation) falls through as plain text.
|
|
53
|
+
const TOKEN_RE =
|
|
54
|
+
/("(?:[^"]|"")*"|`(?:[^`]|``)*`)|('(?:[^']|'')*')|(\?)|(\b\d+(?:\.\d+)?\b)|(\b[A-Za-z_][A-Za-z_0-9]*\b)/g
|
|
55
|
+
|
|
56
|
+
function tokenize(sql: string): Token[] {
|
|
57
|
+
const tokens: Token[] = []
|
|
58
|
+
let last = 0
|
|
59
|
+
TOKEN_RE.lastIndex = 0
|
|
60
|
+
let m: RegExpExecArray | null
|
|
61
|
+
while ((m = TOKEN_RE.exec(sql))) {
|
|
62
|
+
if (m.index > last)
|
|
63
|
+
tokens.push({ text: sql.slice(last, m.index), kind: "text" })
|
|
64
|
+
const [full, ident, str, placeholder, number] = m
|
|
65
|
+
if (ident) tokens.push({ text: full, kind: "identifier" })
|
|
66
|
+
else if (str) tokens.push({ text: full, kind: "string" })
|
|
67
|
+
else if (placeholder) tokens.push({ text: full, kind: "placeholder" })
|
|
68
|
+
else if (number) tokens.push({ text: full, kind: "number" })
|
|
69
|
+
else if (KEYWORDS.has(full.toUpperCase()))
|
|
70
|
+
tokens.push({ text: full, kind: "keyword" })
|
|
71
|
+
else tokens.push({ text: full, kind: "text" })
|
|
72
|
+
last = TOKEN_RE.lastIndex
|
|
73
|
+
}
|
|
74
|
+
if (last < sql.length) tokens.push({ text: sql.slice(last), kind: "text" })
|
|
75
|
+
return tokens
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const toneClass: Record<TokenKind, string> = {
|
|
79
|
+
keyword: "text-primary font-semibold",
|
|
80
|
+
string: "text-emerald-600 dark:text-emerald-400",
|
|
81
|
+
number: "text-amber-600 dark:text-amber-400",
|
|
82
|
+
placeholder: "text-primary font-semibold",
|
|
83
|
+
identifier: "text-muted-foreground",
|
|
84
|
+
text: "",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function SqlBlock({
|
|
88
|
+
sql,
|
|
89
|
+
wrap = false,
|
|
90
|
+
inline = false,
|
|
91
|
+
className,
|
|
92
|
+
}: {
|
|
93
|
+
sql: string
|
|
94
|
+
wrap?: boolean
|
|
95
|
+
inline?: boolean
|
|
96
|
+
className?: string
|
|
97
|
+
}) {
|
|
98
|
+
const tokens = tokenize(sql)
|
|
99
|
+
const Tag = inline ? "span" : "code"
|
|
100
|
+
return (
|
|
101
|
+
<Tag
|
|
102
|
+
className={cn(
|
|
103
|
+
"font-mono text-xs",
|
|
104
|
+
wrap ? "break-words whitespace-pre-wrap" : "block max-w-full truncate",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
title={wrap ? undefined : sql}
|
|
108
|
+
>
|
|
109
|
+
{tokens.map((t, i) => (
|
|
110
|
+
<span key={i} className={toneClass[t.kind] || undefined}>
|
|
111
|
+
{t.text}
|
|
112
|
+
</span>
|
|
113
|
+
))}
|
|
114
|
+
</Tag>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp } from "lucide-react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DIGIT_ROLL_ENABLED,
|
|
6
|
+
DigitRoll,
|
|
7
|
+
} from "@/components/railwatch/digit-roll"
|
|
8
|
+
import { useIsLive } from "@/hooks/use-live"
|
|
9
|
+
import { useReducedMotion } from "@/hooks/use-reduced-motion"
|
|
10
|
+
import { cn } from "@/lib/utils"
|
|
11
|
+
|
|
12
|
+
function Delta({
|
|
13
|
+
current,
|
|
14
|
+
previous,
|
|
15
|
+
goodDirection,
|
|
16
|
+
caption,
|
|
17
|
+
}: {
|
|
18
|
+
current: number
|
|
19
|
+
previous: number
|
|
20
|
+
goodDirection: "up" | "down"
|
|
21
|
+
caption?: ReactNode
|
|
22
|
+
}) {
|
|
23
|
+
if (!previous) return null
|
|
24
|
+
const change = ((current - previous) / previous) * 100
|
|
25
|
+
if (change === 0) return null
|
|
26
|
+
const direction = change > 0 ? "up" : "down"
|
|
27
|
+
const good = direction === goodDirection
|
|
28
|
+
const Icon = direction === "up" ? ArrowUp : ArrowDown
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<span
|
|
32
|
+
className={cn(
|
|
33
|
+
"inline-flex items-center gap-0.5 font-mono text-xs tabular-nums",
|
|
34
|
+
good ? "text-live" : "text-destructive",
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<Icon className="size-3" />
|
|
38
|
+
{Math.abs(change).toFixed(1)}%
|
|
39
|
+
</span>
|
|
40
|
+
{caption && (
|
|
41
|
+
<span className="text-muted-foreground text-[11px]">{caption}</span>
|
|
42
|
+
)}
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A metric a page is happy to see move: the raw number, and the formatter
|
|
48
|
+
// that turns it into what the cell reads. Passing this instead of `value` is
|
|
49
|
+
// how a page opts into the digit roll, which is deliberately the wrong way
|
|
50
|
+
// round from a prop that opts out -- a page that says nothing gets no
|
|
51
|
+
// motion, so a new incident page (issues, executions, traces) cannot inherit
|
|
52
|
+
// it by accident.
|
|
53
|
+
export interface RollingValue {
|
|
54
|
+
value: number
|
|
55
|
+
format: (value: number) => string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The rolling digits are decoration, so they are hidden from assistive tech
|
|
59
|
+
// and the number itself sits beside them in one plain text node. Not an
|
|
60
|
+
// aria-live region: a value that changes every few seconds read aloud all
|
|
61
|
+
// day is not an improvement, and fast live regions misbehave across screen
|
|
62
|
+
// readers anyway. A metric that genuinely needs announcing wants its own
|
|
63
|
+
// debounced role="status" saying what happened, not the number.
|
|
64
|
+
//
|
|
65
|
+
// Four gates stand between a rolling number and motion on the screen, and
|
|
66
|
+
// all of them have to be open: the global switch, the page having opted in
|
|
67
|
+
// at all (this component only renders for `roll`), live updates actually
|
|
68
|
+
// flowing, and the user not having asked for less motion. Live off, paused,
|
|
69
|
+
// disconnected, or reduced motion each mean an instant swap -- which is
|
|
70
|
+
// also WCAG 2.2.2's pause/stop mechanism: the live toggle in the page
|
|
71
|
+
// header stops the motion, not just the data.
|
|
72
|
+
function RollingStatValue({ value, format }: RollingValue) {
|
|
73
|
+
const live = useIsLive()
|
|
74
|
+
const reducedMotion = useReducedMotion()
|
|
75
|
+
const formatted = format(value)
|
|
76
|
+
const animate = DIGIT_ROLL_ENABLED && live && !reducedMotion
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<span aria-hidden="true">
|
|
80
|
+
{animate ? <DigitRoll value={value} format={format} /> : formatted}
|
|
81
|
+
</span>
|
|
82
|
+
<span className="sr-only">{formatted}</span>
|
|
83
|
+
</>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// One metric. Nightwatch-style: a small uppercase mono label, a large
|
|
88
|
+
// tabular number, and an optional sub-line. Renders as a cell, not a card,
|
|
89
|
+
// so a row of them reads as one strip (see StatStrip); pass `card` to get
|
|
90
|
+
// the standalone bordered version.
|
|
91
|
+
export function Stat({
|
|
92
|
+
label,
|
|
93
|
+
value,
|
|
94
|
+
roll,
|
|
95
|
+
hint,
|
|
96
|
+
tone,
|
|
97
|
+
delta,
|
|
98
|
+
deltaCaption,
|
|
99
|
+
card = false,
|
|
100
|
+
className,
|
|
101
|
+
}: {
|
|
102
|
+
label: string
|
|
103
|
+
hint?: ReactNode
|
|
104
|
+
tone?: "default" | "destructive" | "warning" | "success"
|
|
105
|
+
delta?: {
|
|
106
|
+
current: number
|
|
107
|
+
previous: number
|
|
108
|
+
goodDirection: "up" | "down"
|
|
109
|
+
}
|
|
110
|
+
// e.g. "vs previous 24 hours" — shown next to the delta so it's clear what
|
|
111
|
+
// period the percentage compares against.
|
|
112
|
+
deltaCaption?: ReactNode
|
|
113
|
+
card?: boolean
|
|
114
|
+
className?: string
|
|
115
|
+
// A pre-formatted value, or a `roll` the digits of which may animate --
|
|
116
|
+
// one or the other, never both.
|
|
117
|
+
} & (
|
|
118
|
+
{ value: ReactNode; roll?: never } | { value?: never; roll: RollingValue }
|
|
119
|
+
)) {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex min-w-0 flex-col gap-1 px-4 py-3",
|
|
124
|
+
card && "bg-card rounded-lg border",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<div className="label-caps truncate">{label}</div>
|
|
129
|
+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
130
|
+
<div
|
|
131
|
+
className={cn(
|
|
132
|
+
"text-xl font-semibold tracking-tight tabular-nums sm:text-2xl",
|
|
133
|
+
tone === "destructive" && "text-destructive",
|
|
134
|
+
tone === "warning" && "text-warning",
|
|
135
|
+
tone === "success" && "text-live",
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{roll ? <RollingStatValue {...roll} /> : value}
|
|
139
|
+
</div>
|
|
140
|
+
{delta && <Delta {...delta} caption={deltaCaption} />}
|
|
141
|
+
</div>
|
|
142
|
+
{hint && (
|
|
143
|
+
<div className="text-muted-foreground truncate font-mono text-[11px]">
|
|
144
|
+
{hint}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A row of Stats sharing one border, divided by hairlines, that wraps to
|
|
152
|
+
// two columns on phones. The header strip on every Nightwatch page.
|
|
153
|
+
export function StatStrip({
|
|
154
|
+
children,
|
|
155
|
+
className,
|
|
156
|
+
}: {
|
|
157
|
+
children: ReactNode
|
|
158
|
+
className?: string
|
|
159
|
+
}) {
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
className={cn(
|
|
163
|
+
"bg-card divide-border grid grid-cols-2 divide-y overflow-hidden rounded-lg border sm:divide-x sm:divide-y-0",
|
|
164
|
+
"[&>*:nth-child(odd)]:border-r sm:[&>*:nth-child(odd)]:border-r-0",
|
|
165
|
+
"[&>*:nth-child(odd):last-child]:col-span-2 [&>*:nth-child(odd):last-child]:border-r-0 sm:[&>*:nth-child(odd):last-child]:col-span-1",
|
|
166
|
+
"sm:auto-cols-fr sm:grid-flow-col",
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
{children}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Badge } from "@/components/ui/badge"
|
|
2
|
+
import { statusTone } from "@/lib/format"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
// The one status vocabulary: grey is fine, amber is a warning (4xx, elevated),
|
|
6
|
+
// red is broken (5xx, failed, unhandled, open). Small mono caps like
|
|
7
|
+
// Nightwatch's [ALERT] / HANDLED / UNHANDLED chips.
|
|
8
|
+
const tones: Record<string, string> = {
|
|
9
|
+
success: "border-live/30 bg-live/10 text-live",
|
|
10
|
+
warning: "border-warning/40 bg-warning/10 text-warning",
|
|
11
|
+
destructive: "border-danger/40 bg-danger/10 text-danger",
|
|
12
|
+
muted: "border-border bg-muted text-muted-foreground",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const base = "rounded-sm px-1.5 font-mono text-[10px] tracking-wider uppercase"
|
|
16
|
+
|
|
17
|
+
export function StatusBadge({
|
|
18
|
+
status,
|
|
19
|
+
outcome,
|
|
20
|
+
label,
|
|
21
|
+
}: {
|
|
22
|
+
status?: number | null
|
|
23
|
+
outcome?: string | null
|
|
24
|
+
label?: string
|
|
25
|
+
}) {
|
|
26
|
+
const tone = statusTone(status, outcome)
|
|
27
|
+
return (
|
|
28
|
+
<Badge variant="outline" className={cn(base, "tabular-nums", tones[tone])}>
|
|
29
|
+
{label ?? outcome ?? status ?? "–"}
|
|
30
|
+
</Badge>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function LevelBadge({ level }: { level: string }) {
|
|
35
|
+
const tone =
|
|
36
|
+
level === "error" || level === "fatal"
|
|
37
|
+
? "destructive"
|
|
38
|
+
: level === "warn"
|
|
39
|
+
? "warning"
|
|
40
|
+
: level === "event"
|
|
41
|
+
? "success"
|
|
42
|
+
: "muted"
|
|
43
|
+
return (
|
|
44
|
+
<Badge variant="outline" className={cn(base, tones[tone])}>
|
|
45
|
+
{level}
|
|
46
|
+
</Badge>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function IssueStatusBadge({ status }: { status: string }) {
|
|
51
|
+
const tone =
|
|
52
|
+
status === "open"
|
|
53
|
+
? "destructive"
|
|
54
|
+
: status === "resolved"
|
|
55
|
+
? "success"
|
|
56
|
+
: "muted"
|
|
57
|
+
return (
|
|
58
|
+
<Badge variant="outline" className={cn(base, tones[tone])}>
|
|
59
|
+
{status}
|
|
60
|
+
</Badge>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Generic tinted chip for kinds and categories (EXCEPTION, REQUEST, JOB…).
|
|
65
|
+
export function KindBadge({
|
|
66
|
+
children,
|
|
67
|
+
tone = "muted",
|
|
68
|
+
className,
|
|
69
|
+
}: {
|
|
70
|
+
children: React.ReactNode
|
|
71
|
+
tone?: keyof typeof tones
|
|
72
|
+
className?: string
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<Badge variant="outline" className={cn(base, tones[tone], className)}>
|
|
76
|
+
{children}
|
|
77
|
+
</Badge>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ChevronDown } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuLabel,
|
|
8
|
+
DropdownMenuRadioGroup,
|
|
9
|
+
DropdownMenuRadioItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "@/components/ui/dropdown-menu"
|
|
13
|
+
import { STEP_LABELS, useStep } from "@/hooks/use-step"
|
|
14
|
+
import type { Step } from "@/types"
|
|
15
|
+
|
|
16
|
+
// Bucket width for every chart on the page, next to the window picker:
|
|
17
|
+
// "1m", "5m", ... as a mono chip opening a menu of the widths the window
|
|
18
|
+
// offers. Hidden when the window offers only one.
|
|
19
|
+
export function StepPicker() {
|
|
20
|
+
const { step, steps, set } = useStep()
|
|
21
|
+
if (steps.length < 2) return null
|
|
22
|
+
return (
|
|
23
|
+
<DropdownMenu>
|
|
24
|
+
<DropdownMenuTrigger asChild>
|
|
25
|
+
<Button
|
|
26
|
+
variant="outline"
|
|
27
|
+
size="sm"
|
|
28
|
+
className="h-8 gap-1 px-2 font-mono text-xs uppercase"
|
|
29
|
+
aria-label={`Chart buckets: ${STEP_LABELS[step]}`}
|
|
30
|
+
>
|
|
31
|
+
<span className="text-muted-foreground normal-case">per</span>
|
|
32
|
+
{step}
|
|
33
|
+
<ChevronDown className="text-muted-foreground size-3" />
|
|
34
|
+
</Button>
|
|
35
|
+
</DropdownMenuTrigger>
|
|
36
|
+
<DropdownMenuContent align="end" className="w-44">
|
|
37
|
+
<DropdownMenuLabel className="label-caps">
|
|
38
|
+
Chart buckets
|
|
39
|
+
</DropdownMenuLabel>
|
|
40
|
+
<DropdownMenuSeparator />
|
|
41
|
+
<DropdownMenuRadioGroup
|
|
42
|
+
value={step}
|
|
43
|
+
onValueChange={(value) => set(value as Step)}
|
|
44
|
+
>
|
|
45
|
+
{steps.map((s) => (
|
|
46
|
+
<DropdownMenuRadioItem key={s} value={s} className="text-xs">
|
|
47
|
+
<span className="font-mono uppercase">{s}</span>
|
|
48
|
+
<span className="text-muted-foreground ml-auto">
|
|
49
|
+
{STEP_LABELS[s]}
|
|
50
|
+
</span>
|
|
51
|
+
</DropdownMenuRadioItem>
|
|
52
|
+
))}
|
|
53
|
+
</DropdownMenuRadioGroup>
|
|
54
|
+
</DropdownMenuContent>
|
|
55
|
+
</DropdownMenu>
|
|
56
|
+
)
|
|
57
|
+
}
|