railwatch 0.6.0 → 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/AGENTS.md +42 -26
- data/CHANGELOG.md +41 -0
- data/README.md +40 -29
- 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/app/jobs/railwatch/rollup_job.rb +3 -1
- data/app/models/railwatch/ingest/rollup_absorber.rb +1 -1
- data/docs/ai-and-mcp.md +9 -3
- data/docs/configuration.md +68 -27
- data/docs/embedded.md +76 -43
- data/docs/faq.md +22 -10
- data/docs/getting-started.md +116 -41
- data/docs/records.md +13 -10
- data/docs/replacing-nightwatch.md +16 -14
- data/docs/replacing-sentry.md +19 -10
- data/docs/security.md +24 -3
- data/docs/self-hosting.md +9 -1
- data/docs/testing.md +14 -4
- data/docs/troubleshooting.md +62 -23
- data/lib/generators/railwatch/install/install_generator.rb +5 -4
- data/lib/generators/railwatch/install/templates/initializer.rb.tt +2 -2
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +8 -3
- data/llms.txt +21 -18
- 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-EmePsTKP.js → arrow-up-DG01k9AL.js} +1 -1
- data/public/railwatch/assets/{braces-iZ7MEfYP.js → braces-C3w7pfxS.js} +1 -1
- data/public/railwatch/assets/{card-DqdEEKcJ.js → card-Dpwlr0uY.js} +1 -1
- data/public/railwatch/assets/{chart-C4k7Ub9W.js → chart-DJHv3R7W.js} +1 -1
- data/public/railwatch/assets/{chart-hover-2yZG3w2u.js → chart-hover-CEH-DtXO.js} +1 -1
- data/public/railwatch/assets/{chart-panel-CONkZcn9.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-DWovhgLi.js → execution-path-CElXXFO_.js} +1 -1
- data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
- data/public/railwatch/assets/{flamegraph-BNbNIuKT.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-Zkt4W_CM.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-BxIVblBw.js → percentile-picker-E2aUdLxx.js} +1 -1
- data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
- data/public/railwatch/assets/{release-health-Cb1tMOOO.js → release-health-C8WSCDKD.js} +1 -1
- data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
- data/public/railwatch/assets/{segmented-C4oWE73o.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-gtwKsTsl.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-CPM6fhXv.js → sort-header-uOq5VlrB.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-CXCxXPWt.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-C4aM_cO0.js → tenant-path-DU0ezKSU.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-C1ApSwzA.js → use-clipboard-DoENmlWY.js} +1 -1
- data/public/railwatch/assets/{use-live-DWwLg1Nj.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-2xAKndVD.js +0 -1
- data/public/railwatch/assets/app-wordmark-Bt6XqURo.js +0 -1
- data/public/railwatch/assets/appearance-DyCfeh9o.js +0 -1
- data/public/railwatch/assets/application-B7h1MIhi.css +0 -1
- data/public/railwatch/assets/auth-layout-BuwogWMZ.js +0 -1
- data/public/railwatch/assets/badge-Dv7I-ebP.js +0 -1
- data/public/railwatch/assets/checkbox-DKbThbxr.js +0 -1
- data/public/railwatch/assets/code-CYahYQHZ.js +0 -1
- data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
- data/public/railwatch/assets/copy-block-CbJum9s8.js +0 -1
- data/public/railwatch/assets/copy-id-JmvLfkmx.js +0 -1
- data/public/railwatch/assets/cursor-load-more-C1ylbqqL.js +0 -1
- data/public/railwatch/assets/data-table-CVKR2wkq.js +0 -1
- data/public/railwatch/assets/edit-BXF5oNYA.js +0 -1
- data/public/railwatch/assets/edit-BwSEVrTG.js +0 -8
- data/public/railwatch/assets/edit-D5BRDwLT.js +0 -1
- data/public/railwatch/assets/empty-state-Bn48Gplm.js +0 -1
- data/public/railwatch/assets/env-layout-CKLL8Ds7.js +0 -1
- data/public/railwatch/assets/filter-bar-xcbJC93z.js +0 -1
- data/public/railwatch/assets/frames-Bk39xfJG.js +0 -1
- data/public/railwatch/assets/google-sign-in-button-B5as2ps1.js +0 -1
- data/public/railwatch/assets/index-Atp1FTgX.js +0 -1
- data/public/railwatch/assets/index-B-FXAo1w.js +0 -1
- data/public/railwatch/assets/index-B0xgzHuN.js +0 -1
- data/public/railwatch/assets/index-BDVShWMs.js +0 -1
- data/public/railwatch/assets/index-BRvRkfob.js +0 -1
- data/public/railwatch/assets/index-BVWapDeB.js +0 -1
- data/public/railwatch/assets/index-Bd4YpOIO.js +0 -1
- data/public/railwatch/assets/index-BeQw69GM.js +0 -1
- data/public/railwatch/assets/index-BfitxVBv.js +0 -1
- data/public/railwatch/assets/index-C-EwH4lO.js +0 -1
- data/public/railwatch/assets/index-C8JIdb3_.js +0 -1
- data/public/railwatch/assets/index-CBFCtJAR.js +0 -1
- data/public/railwatch/assets/index-CDXlITyG.js +0 -1
- data/public/railwatch/assets/index-COd9lT84.js +0 -1
- data/public/railwatch/assets/index-C_hQmVTP.js +0 -1
- data/public/railwatch/assets/index-CcennT28.js +0 -1
- data/public/railwatch/assets/index-CiaeS-n7.js +0 -1
- data/public/railwatch/assets/index-CrA7jBTK.js +0 -1
- data/public/railwatch/assets/index-D0mKj3Hg.js +0 -1
- data/public/railwatch/assets/index-DV5MdmpY.js +0 -1
- data/public/railwatch/assets/index-DaE-1xhx.js +0 -1
- data/public/railwatch/assets/index-DdVg9LKX.js +0 -1
- data/public/railwatch/assets/index-DeLtMWou.js +0 -1
- data/public/railwatch/assets/index-DiQ4mqT_.js +0 -1
- data/public/railwatch/assets/index-LKgpCzoR.js +0 -1
- data/public/railwatch/assets/index-S0NVaLj9.js +0 -1
- data/public/railwatch/assets/index-Vj93BTR5.js +0 -1
- data/public/railwatch/assets/index-XjXcV-EO.js +0 -1
- data/public/railwatch/assets/index-aPCZX-pW.js +0 -1
- data/public/railwatch/assets/index-cTzBycCg.js +0 -1
- data/public/railwatch/assets/index-dpG-19ZN.js +0 -1
- data/public/railwatch/assets/index-gCvuyG1l.js +0 -1
- data/public/railwatch/assets/index-o1xqGvLo.js +0 -1
- data/public/railwatch/assets/index-w_skgMmK.js +0 -1
- data/public/railwatch/assets/inertia-CqnzqPVD.js +0 -97
- data/public/railwatch/assets/input-error-ByT28UdP.js +0 -1
- data/public/railwatch/assets/json-viewer-Bnu9RsJL.js +0 -1
- data/public/railwatch/assets/klass-nLiBV-Az.js +0 -1
- data/public/railwatch/assets/label-1nSzpSB6.js +0 -1
- data/public/railwatch/assets/layout-D7lE1Doo.js +0 -1
- data/public/railwatch/assets/live-dot-UdzOfzJb.js +0 -41
- data/public/railwatch/assets/nav-BRCgp2w3.js +0 -1
- data/public/railwatch/assets/new-C60G72DR.js +0 -1
- data/public/railwatch/assets/new-COyDKOOb.js +0 -1
- data/public/railwatch/assets/new-Cd9zPNJ8.js +0 -1
- data/public/railwatch/assets/new-DMJGKjmH.js +0 -1
- data/public/railwatch/assets/new-Omvf-c9s.js +0 -1
- data/public/railwatch/assets/new-vjRzERL3.js +0 -1
- data/public/railwatch/assets/onboarding-gUjJQc-6.js +0 -1
- data/public/railwatch/assets/origin-identity-M1YUPYS0.js +0 -1
- data/public/railwatch/assets/relative-time-DQ4kjowZ.js +0 -1
- data/public/railwatch/assets/route-BJkoEm7r.js +0 -1
- data/public/railwatch/assets/select-ocdQJdQo.js +0 -1
- data/public/railwatch/assets/separator-Ct2UcZ2x.js +0 -1
- data/public/railwatch/assets/series-chart-D6upsrSn.js +0 -1
- data/public/railwatch/assets/show-B4bwI9x2.js +0 -2
- data/public/railwatch/assets/show-BAjfddEn.js +0 -6
- data/public/railwatch/assets/show-BLJOyLb_.js +0 -1
- data/public/railwatch/assets/show-BoVyMQS6.js +0 -1
- data/public/railwatch/assets/show-Bsu1rvnK.js +0 -1
- data/public/railwatch/assets/show-C6OiJ6Hr.js +0 -1
- data/public/railwatch/assets/show-C8TkpEgm.js +0 -1
- data/public/railwatch/assets/show-CBFBiBY2.js +0 -1
- data/public/railwatch/assets/show-CEhPVMMT.js +0 -1
- data/public/railwatch/assets/show-CV-lG2b6.js +0 -2
- data/public/railwatch/assets/show-CX_pab9S.js +0 -2
- data/public/railwatch/assets/show-D6lgzL6n.js +0 -1
- data/public/railwatch/assets/show-DDVsv-uS.js +0 -1
- data/public/railwatch/assets/show-Dwiq-8hA.js +0 -1
- data/public/railwatch/assets/show-eKazflRI.js +0 -1
- data/public/railwatch/assets/show-yEOrk6Pt.js +0 -1
- data/public/railwatch/assets/stat-BNEfE8X1.js +0 -1
- data/public/railwatch/assets/status-badge-D0aylrM5.js +0 -1
- data/public/railwatch/assets/text-link-CP4lqUi6.js +0 -1
- data/public/railwatch/assets/textarea-DzJo2ds4.js +0 -1
- data/public/railwatch/assets/timeline-BNcJ9PMc.js +0 -1
- data/public/railwatch/assets/transition-BX2M1iaq.js +0 -5
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Activity,
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
ArrowUpRight,
|
|
5
|
+
Bell,
|
|
6
|
+
BellRing,
|
|
7
|
+
Bug,
|
|
8
|
+
Building2,
|
|
9
|
+
Cable,
|
|
10
|
+
CalendarClock,
|
|
11
|
+
Database,
|
|
12
|
+
FileText,
|
|
13
|
+
Flame,
|
|
14
|
+
Gauge,
|
|
15
|
+
GitCommit,
|
|
16
|
+
HardDrive,
|
|
17
|
+
Layers,
|
|
18
|
+
LayoutTemplate,
|
|
19
|
+
Mail,
|
|
20
|
+
MonitorSmartphone,
|
|
21
|
+
Package,
|
|
22
|
+
PieChart,
|
|
23
|
+
Rocket,
|
|
24
|
+
Server,
|
|
25
|
+
Siren,
|
|
26
|
+
Sparkles,
|
|
27
|
+
Terminal,
|
|
28
|
+
Timer,
|
|
29
|
+
Users,
|
|
30
|
+
Workflow,
|
|
31
|
+
} from "lucide-react"
|
|
32
|
+
|
|
33
|
+
import * as R from "@/routes"
|
|
34
|
+
import type { NavItem } from "@/types"
|
|
35
|
+
|
|
36
|
+
export type EnvPageGroup = "activity" | "monitoring" | "settings"
|
|
37
|
+
|
|
38
|
+
export interface EnvPage extends NavItem {
|
|
39
|
+
group: EnvPageGroup
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shared list of environment sub-pages, used by the sidebar EnvNav, the
|
|
43
|
+
// command palette, and the mobile page switcher so they never drift apart.
|
|
44
|
+
// Grouped the way Nightwatch groups its sidebar: Activity (what the app
|
|
45
|
+
// did), Monitoring (people, deploys, health), Settings (per-environment).
|
|
46
|
+
export function envPages(
|
|
47
|
+
applicationId: number,
|
|
48
|
+
environmentId: number,
|
|
49
|
+
window: string,
|
|
50
|
+
): EnvPage[] {
|
|
51
|
+
const a = applicationId
|
|
52
|
+
const e = environmentId
|
|
53
|
+
const w = { window }
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
title: "Overview",
|
|
57
|
+
href: R.applicationEnvironmentOverviewPath(a, e, w),
|
|
58
|
+
icon: Gauge,
|
|
59
|
+
group: "activity",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Requests",
|
|
63
|
+
href: R.applicationEnvironmentRequestsPath(a, e, w),
|
|
64
|
+
icon: Activity,
|
|
65
|
+
group: "activity",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: "Jobs",
|
|
69
|
+
href: R.applicationEnvironmentJobsPath(a, e, w),
|
|
70
|
+
icon: Workflow,
|
|
71
|
+
group: "activity",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: "Scheduled tasks",
|
|
75
|
+
href: R.applicationEnvironmentScheduledTasksPath(a, e, w),
|
|
76
|
+
icon: CalendarClock,
|
|
77
|
+
group: "activity",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
title: "Commands",
|
|
81
|
+
href: R.applicationEnvironmentCommandsPath(a, e, w),
|
|
82
|
+
icon: Terminal,
|
|
83
|
+
group: "activity",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "Exceptions",
|
|
87
|
+
href: R.applicationEnvironmentExceptionsPath(a, e, w),
|
|
88
|
+
icon: Bug,
|
|
89
|
+
group: "activity",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: "Queries",
|
|
93
|
+
href: R.applicationEnvironmentQueriesPath(a, e, w),
|
|
94
|
+
icon: Database,
|
|
95
|
+
group: "activity",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: "Spans",
|
|
99
|
+
href: R.applicationEnvironmentSpansPath(a, e, w),
|
|
100
|
+
icon: Timer,
|
|
101
|
+
group: "activity",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: "Profiles",
|
|
105
|
+
href: R.applicationEnvironmentProfilesPath(a, e, w),
|
|
106
|
+
icon: Flame,
|
|
107
|
+
group: "activity",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: "Transactions",
|
|
111
|
+
href: R.applicationEnvironmentTransactionsPath(a, e, w),
|
|
112
|
+
icon: GitCommit,
|
|
113
|
+
group: "activity",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: "View renders",
|
|
117
|
+
href: R.applicationEnvironmentViewRendersPath(a, e, w),
|
|
118
|
+
icon: LayoutTemplate,
|
|
119
|
+
group: "activity",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
title: "Cache",
|
|
123
|
+
href: R.applicationEnvironmentCacheEventsPath(a, e, w),
|
|
124
|
+
icon: Layers,
|
|
125
|
+
group: "activity",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
title: "Mail",
|
|
129
|
+
href: R.applicationEnvironmentMailsPath(a, e, w),
|
|
130
|
+
icon: Mail,
|
|
131
|
+
group: "activity",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
title: "Notifications",
|
|
135
|
+
href: R.applicationEnvironmentNotificationsPath(a, e, w),
|
|
136
|
+
icon: BellRing,
|
|
137
|
+
group: "activity",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: "Broadcasts",
|
|
141
|
+
href: R.applicationEnvironmentBroadcastsPath(a, e, w),
|
|
142
|
+
icon: Cable,
|
|
143
|
+
group: "activity",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
title: "Outgoing requests",
|
|
147
|
+
href: R.applicationEnvironmentOutgoingRequestsPath(a, e, w),
|
|
148
|
+
icon: ArrowUpRight,
|
|
149
|
+
group: "activity",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: "LLM",
|
|
153
|
+
href: R.applicationEnvironmentLlmCallsPath(a, e, w),
|
|
154
|
+
icon: Sparkles,
|
|
155
|
+
group: "activity",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
title: "Storage",
|
|
159
|
+
href: R.applicationEnvironmentStorageOpsPath(a, e, w),
|
|
160
|
+
icon: HardDrive,
|
|
161
|
+
group: "activity",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
title: "Logs",
|
|
165
|
+
href: R.applicationEnvironmentLogsPath(a, e, w),
|
|
166
|
+
icon: FileText,
|
|
167
|
+
group: "activity",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
title: "Deprecations",
|
|
171
|
+
href: R.applicationEnvironmentDeprecationsPath(a, e, w),
|
|
172
|
+
icon: AlertTriangle,
|
|
173
|
+
group: "activity",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
title: "Visits",
|
|
177
|
+
href: R.applicationEnvironmentVisitsPath(a, e, w),
|
|
178
|
+
icon: MonitorSmartphone,
|
|
179
|
+
group: "monitoring",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
title: "Users",
|
|
183
|
+
href: R.applicationEnvironmentPeoplePath(a, e, w),
|
|
184
|
+
icon: Users,
|
|
185
|
+
group: "monitoring",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
title: "Tenants",
|
|
189
|
+
href: R.applicationEnvironmentTenantsPath(a, e, w),
|
|
190
|
+
icon: Building2,
|
|
191
|
+
group: "monitoring",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
title: "Deploys",
|
|
195
|
+
href: R.applicationEnvironmentDeploysPath(a, e),
|
|
196
|
+
icon: Rocket,
|
|
197
|
+
group: "monitoring",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
title: "Releases",
|
|
201
|
+
href: R.applicationEnvironmentReleasesPath(a, e, w),
|
|
202
|
+
icon: Package,
|
|
203
|
+
group: "monitoring",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
title: "Processes",
|
|
207
|
+
href: R.applicationEnvironmentProcessesPath(a, e),
|
|
208
|
+
icon: Server,
|
|
209
|
+
group: "monitoring",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
title: "Alerts",
|
|
213
|
+
href: R.applicationEnvironmentAlertsPath(a, e),
|
|
214
|
+
icon: Siren,
|
|
215
|
+
group: "monitoring",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
title: "Thresholds",
|
|
219
|
+
href: R.applicationEnvironmentThresholdsPath(a, e),
|
|
220
|
+
icon: Bell,
|
|
221
|
+
group: "settings",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
title: "Usage",
|
|
225
|
+
href: R.applicationEnvironmentUsagePath(a, e),
|
|
226
|
+
icon: PieChart,
|
|
227
|
+
group: "settings",
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const ENV_PAGE_GROUPS: { key: EnvPageGroup; title: string }[] = [
|
|
233
|
+
{ key: "activity", title: "Activity" },
|
|
234
|
+
{ key: "monitoring", title: "Monitoring" },
|
|
235
|
+
{ key: "settings", title: "Settings" },
|
|
236
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as R from "@/routes"
|
|
2
|
+
|
|
3
|
+
interface ExecutionPathOptions {
|
|
4
|
+
applicationId: number
|
|
5
|
+
environmentId: number
|
|
6
|
+
source: string | null | undefined
|
|
7
|
+
executionId: string | null | undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Child telemetry uses `job` while the parent record itself is a
|
|
11
|
+
// `job_attempt`, so accept both wire names. Unknown server-side parent kinds
|
|
12
|
+
// use the generic execution surface; browser events intentionally have no
|
|
13
|
+
// server execution to open.
|
|
14
|
+
export function executionPath({
|
|
15
|
+
applicationId,
|
|
16
|
+
environmentId,
|
|
17
|
+
source,
|
|
18
|
+
executionId,
|
|
19
|
+
}: ExecutionPathOptions): string | null {
|
|
20
|
+
if (!executionId || source === "browser") return null
|
|
21
|
+
|
|
22
|
+
switch (source) {
|
|
23
|
+
case "request":
|
|
24
|
+
return R.applicationEnvironmentRequestPath(
|
|
25
|
+
applicationId,
|
|
26
|
+
environmentId,
|
|
27
|
+
executionId,
|
|
28
|
+
)
|
|
29
|
+
case "job":
|
|
30
|
+
case "job_attempt":
|
|
31
|
+
return R.applicationEnvironmentJobPath(
|
|
32
|
+
applicationId,
|
|
33
|
+
environmentId,
|
|
34
|
+
executionId,
|
|
35
|
+
)
|
|
36
|
+
case "scheduled_task":
|
|
37
|
+
return R.applicationEnvironmentScheduledTaskPath(
|
|
38
|
+
applicationId,
|
|
39
|
+
environmentId,
|
|
40
|
+
executionId,
|
|
41
|
+
)
|
|
42
|
+
case "command":
|
|
43
|
+
return R.applicationEnvironmentCommandPath(
|
|
44
|
+
applicationId,
|
|
45
|
+
environmentId,
|
|
46
|
+
executionId,
|
|
47
|
+
)
|
|
48
|
+
default:
|
|
49
|
+
return R.applicationEnvironmentExecutionPath(
|
|
50
|
+
applicationId,
|
|
51
|
+
environmentId,
|
|
52
|
+
executionId,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface ExplainWarning {
|
|
2
|
+
// 1-based line number within the plan text.
|
|
3
|
+
line: number
|
|
4
|
+
message: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// The parts of a captured query plan that mean the database is doing more
|
|
8
|
+
// work than an index would let it. Each rule carries the plain-English
|
|
9
|
+
// reason, so the panel can say what's wrong rather than just colouring a
|
|
10
|
+
// line. Covers SQLite (EXPLAIN QUERY PLAN), Postgres and MySQL.
|
|
11
|
+
const RULES: { test: RegExp; skip?: RegExp; message: string }[] = [
|
|
12
|
+
{
|
|
13
|
+
test: /\bSCAN\b/i,
|
|
14
|
+
skip: /\bUSING\s+(?:COVERING\s+)?INDEX\b/i,
|
|
15
|
+
message: "Reads every row of the table — no index covers this filter.",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
test: /\bSeq Scan\b/i,
|
|
19
|
+
message:
|
|
20
|
+
"Sequential scan — Postgres reads the whole table; an index on the filtered column avoids it.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
test: /\bUSING TEMP B-TREE\b/i,
|
|
24
|
+
message:
|
|
25
|
+
"Builds a temporary B-tree to sort — an index matching the ORDER BY removes it.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
test: /filesort/i,
|
|
29
|
+
message:
|
|
30
|
+
"Filesort — MySQL sorts the rows itself; an index matching the ORDER BY removes it.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
test: /\btype:\s*ALL\b/i,
|
|
34
|
+
message:
|
|
35
|
+
"Join type ALL — MySQL has no usable index and scans the whole table.",
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// Risky lines of `plan`, in order. One line can raise more than one warning
|
|
40
|
+
// (a full scan that also sorts), so callers should collect by line number
|
|
41
|
+
// rather than assume a single hit.
|
|
42
|
+
export function explainWarnings(plan: string): ExplainWarning[] {
|
|
43
|
+
return plan
|
|
44
|
+
.split("\n")
|
|
45
|
+
.flatMap((text, index) =>
|
|
46
|
+
RULES.filter((r) => r.test.test(text) && !r.skip?.test(text)).map(
|
|
47
|
+
(r) => ({ line: index + 1, message: r.message }),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Mirrors app/models/filters.rb: "key:value key2:value2 free text" tokens.
|
|
2
|
+
const TOKEN = /^([a-z_]+):(\S+)$/
|
|
3
|
+
|
|
4
|
+
export interface ParsedFilter {
|
|
5
|
+
text: string
|
|
6
|
+
fields: Record<string, string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseFilter(query: string): ParsedFilter {
|
|
10
|
+
const text: string[] = []
|
|
11
|
+
const fields: Record<string, string> = {}
|
|
12
|
+
for (const word of query.trim().split(/\s+/)) {
|
|
13
|
+
if (!word) continue
|
|
14
|
+
const m = TOKEN.exec(word)
|
|
15
|
+
if (m) fields[m[1]] = m[2]
|
|
16
|
+
else text.push(word)
|
|
17
|
+
}
|
|
18
|
+
return { text: text.join(" "), fields }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function serializeFilter(parsed: ParsedFilter): string {
|
|
22
|
+
const tokens = Object.entries(parsed.fields).map(([k, v]) => `${k}:${v}`)
|
|
23
|
+
if (parsed.text) tokens.push(parsed.text)
|
|
24
|
+
return tokens.join(" ")
|
|
25
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Collapsed stacks ("outer;inner;leaf count\n", the format stackprof and
|
|
2
|
+
// vernier both fold to) turned into the tree and rectangles the flamegraph
|
|
3
|
+
// component draws. Pure data — no React, no DOM — so the layout maths is
|
|
4
|
+
// unit-testable on its own.
|
|
5
|
+
|
|
6
|
+
export interface FlameNode {
|
|
7
|
+
id: number
|
|
8
|
+
name: string
|
|
9
|
+
// Samples in this frame and everything it called.
|
|
10
|
+
value: number
|
|
11
|
+
// Samples where this frame was on top of the stack.
|
|
12
|
+
self: number
|
|
13
|
+
children: FlameNode[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FlameRect {
|
|
17
|
+
id: number
|
|
18
|
+
node: FlameNode
|
|
19
|
+
x: number
|
|
20
|
+
width: number
|
|
21
|
+
depth: number
|
|
22
|
+
// The value of the enclosing frame, for the tooltip's "% of parent".
|
|
23
|
+
parentValue: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FrameStat {
|
|
27
|
+
name: string
|
|
28
|
+
self: number
|
|
29
|
+
total: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parses collapsed stacks into a tree rooted at a synthetic "all" frame,
|
|
33
|
+
// merging every stack that shares a prefix. Lines without a trailing sample
|
|
34
|
+
// count, with a non-numeric or non-positive count, or with no frames at all
|
|
35
|
+
// are skipped rather than throwing: a profile is a best-effort artefact and
|
|
36
|
+
// one bad line should not blank the page.
|
|
37
|
+
export function parseCollapsed(text: string): FlameNode {
|
|
38
|
+
let nextId = 0
|
|
39
|
+
const make = (name: string): FlameNode => ({
|
|
40
|
+
id: nextId++,
|
|
41
|
+
name,
|
|
42
|
+
value: 0,
|
|
43
|
+
self: 0,
|
|
44
|
+
children: [],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const root = make("all")
|
|
48
|
+
const index = new Map<string, FlameNode>()
|
|
49
|
+
|
|
50
|
+
for (const raw of text.split("\n")) {
|
|
51
|
+
const line = raw.trim()
|
|
52
|
+
const sep = line.lastIndexOf(" ")
|
|
53
|
+
if (sep <= 0) continue
|
|
54
|
+
const count = Number(line.slice(sep + 1))
|
|
55
|
+
if (!Number.isFinite(count) || count <= 0) continue
|
|
56
|
+
const frames = line
|
|
57
|
+
.slice(0, sep)
|
|
58
|
+
.split(";")
|
|
59
|
+
.filter((f) => f.length > 0)
|
|
60
|
+
if (frames.length === 0) continue
|
|
61
|
+
|
|
62
|
+
root.value += count
|
|
63
|
+
let current = root
|
|
64
|
+
let path = ""
|
|
65
|
+
for (const name of frames) {
|
|
66
|
+
path += ";" + name
|
|
67
|
+
let child = index.get(path)
|
|
68
|
+
if (!child) {
|
|
69
|
+
child = make(name)
|
|
70
|
+
current.children.push(child)
|
|
71
|
+
index.set(path, child)
|
|
72
|
+
}
|
|
73
|
+
child.value += count
|
|
74
|
+
current = child
|
|
75
|
+
}
|
|
76
|
+
current.self += count
|
|
77
|
+
}
|
|
78
|
+
return root
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// x/width for every node, proportional to its share of `root`, with depth
|
|
82
|
+
// counted from the given root — so zooming is just calling this again with
|
|
83
|
+
// the clicked node.
|
|
84
|
+
export function layout(root: FlameNode, width: number): FlameRect[] {
|
|
85
|
+
const rects: FlameRect[] = []
|
|
86
|
+
const scale = root.value > 0 ? width / root.value : 0
|
|
87
|
+
|
|
88
|
+
const walk = (node: FlameNode, x: number, depth: number, parent: number) => {
|
|
89
|
+
rects.push({
|
|
90
|
+
id: node.id,
|
|
91
|
+
node,
|
|
92
|
+
x,
|
|
93
|
+
width: node.value * scale,
|
|
94
|
+
depth,
|
|
95
|
+
parentValue: parent,
|
|
96
|
+
})
|
|
97
|
+
let childX = x
|
|
98
|
+
for (const child of node.children) {
|
|
99
|
+
walk(child, childX, depth + 1, node.value)
|
|
100
|
+
childX += child.value * scale
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
walk(root, 0, 0, root.value)
|
|
105
|
+
return rects
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Same frame can appear under many callers; the hottest-frames table folds
|
|
109
|
+
// them by name the way stackprof's own report does. The synthetic root is
|
|
110
|
+
// left out — "all" is never an interesting frame.
|
|
111
|
+
function byName(root: FlameNode): Map<string, FrameStat> {
|
|
112
|
+
const stats = new Map<string, FrameStat>()
|
|
113
|
+
const walk = (node: FlameNode, isRoot: boolean) => {
|
|
114
|
+
if (!isRoot) {
|
|
115
|
+
const stat = stats.get(node.name) ?? {
|
|
116
|
+
name: node.name,
|
|
117
|
+
self: 0,
|
|
118
|
+
total: 0,
|
|
119
|
+
}
|
|
120
|
+
stat.self += node.self
|
|
121
|
+
stat.total += node.value
|
|
122
|
+
stats.set(node.name, stat)
|
|
123
|
+
}
|
|
124
|
+
for (const child of node.children) walk(child, false)
|
|
125
|
+
}
|
|
126
|
+
walk(root, true)
|
|
127
|
+
return stats
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function hottestSelf(root: FlameNode, n: number): FrameStat[] {
|
|
131
|
+
return [...byName(root).values()]
|
|
132
|
+
.sort((a, b) => b.self - a.self || b.total - a.total)
|
|
133
|
+
.slice(0, n)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hottestTotal(root: FlameNode, n: number): FrameStat[] {
|
|
137
|
+
return [...byName(root).values()]
|
|
138
|
+
.sort((a, b) => b.total - a.total || b.self - a.self)
|
|
139
|
+
.slice(0, n)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Ids of every node whose frame name contains `query`, case-insensitively.
|
|
143
|
+
// A blank query matches nothing rather than everything, so clearing the box
|
|
144
|
+
// clears the highlight.
|
|
145
|
+
export function search(root: FlameNode, query: string): Set<number> {
|
|
146
|
+
const matches = new Set<number>()
|
|
147
|
+
const needle = query.trim().toLowerCase()
|
|
148
|
+
if (needle === "") return matches
|
|
149
|
+
const walk = (node: FlameNode) => {
|
|
150
|
+
if (node.name.toLowerCase().includes(needle)) matches.add(node.id)
|
|
151
|
+
for (const child of node.children) walk(child)
|
|
152
|
+
}
|
|
153
|
+
walk(root)
|
|
154
|
+
return matches
|
|
155
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function ms(value: number | null | undefined, digits = 1): string {
|
|
2
|
+
if (value === null || value === undefined) return "–"
|
|
3
|
+
if (value >= 1000) return `${(value / 1000).toFixed(2)}s`
|
|
4
|
+
if (value < 1 && value > 0) return `${(value * 1000).toFixed(0)}µs`
|
|
5
|
+
return `${value.toFixed(digits)}ms`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function count(value: number | null | undefined): string {
|
|
9
|
+
if (value === null || value === undefined) return "–"
|
|
10
|
+
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
|
|
11
|
+
if (value >= 10_000) return `${(value / 1000).toFixed(1)}k`
|
|
12
|
+
return value.toLocaleString()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function bytes(value: number | null | undefined): string {
|
|
16
|
+
if (!value) return "–"
|
|
17
|
+
if (value >= 1 << 30) return `${(value / (1 << 30)).toFixed(1)} GB`
|
|
18
|
+
if (value >= 1 << 20) return `${(value / (1 << 20)).toFixed(1)} MB`
|
|
19
|
+
if (value >= 1 << 10) return `${(value / (1 << 10)).toFixed(1)} KB`
|
|
20
|
+
return `${value} B`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A single cheap model call costs a fraction of a cent, so the precision
|
|
24
|
+
// has to follow the magnitude: $0.0043 is the useful reading, $0.00 is not.
|
|
25
|
+
// null means unpriced, which is not the same as free -- callers that can
|
|
26
|
+
// say so in words should, rather than relying on the dash.
|
|
27
|
+
export function usd(value: number | null | undefined): string {
|
|
28
|
+
if (value === null || value === undefined) return "–"
|
|
29
|
+
if (value === 0) return "$0"
|
|
30
|
+
if (value < 0.01) return `$${value.toFixed(4)}`
|
|
31
|
+
if (value < 1000) return `$${value.toFixed(2)}`
|
|
32
|
+
return `$${Math.round(value).toLocaleString()}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pct(numerator: number, denominator: number): string {
|
|
36
|
+
if (!denominator) return "0%"
|
|
37
|
+
return `${((numerator / denominator) * 100).toFixed(1)}%`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ago(iso: string | null | undefined): string {
|
|
41
|
+
if (!iso) return "never"
|
|
42
|
+
const diff = (Date.now() - new Date(iso).getTime()) / 1000
|
|
43
|
+
if (diff < 60) return `${Math.max(0, Math.floor(diff))}s ago`
|
|
44
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
|
45
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
|
46
|
+
return `${Math.floor(diff / 86400)}d ago`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function when(iso: string | null | undefined): string {
|
|
50
|
+
if (!iso) return "–"
|
|
51
|
+
return new Date(iso).toLocaleString(undefined, {
|
|
52
|
+
month: "short",
|
|
53
|
+
day: "numeric",
|
|
54
|
+
hour: "2-digit",
|
|
55
|
+
minute: "2-digit",
|
|
56
|
+
second: "2-digit",
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function statusTone(
|
|
61
|
+
status: number | null | undefined,
|
|
62
|
+
outcome?: string | null,
|
|
63
|
+
) {
|
|
64
|
+
if (outcome === "failed") return "destructive"
|
|
65
|
+
if (outcome === "processed") return "success"
|
|
66
|
+
if (!status) return "muted"
|
|
67
|
+
if (status >= 500) return "destructive"
|
|
68
|
+
if (status >= 400) return "warning"
|
|
69
|
+
if (status >= 300) return "muted"
|
|
70
|
+
return "success"
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Page shortcuts must yield to browser shortcuts, IME input, and controls.
|
|
2
|
+
export function ignorePageShortcut(event: KeyboardEvent) {
|
|
3
|
+
return (
|
|
4
|
+
event.defaultPrevented ||
|
|
5
|
+
event.isComposing ||
|
|
6
|
+
event.repeat ||
|
|
7
|
+
event.metaKey ||
|
|
8
|
+
event.ctrlKey ||
|
|
9
|
+
event.altKey ||
|
|
10
|
+
event.shiftKey ||
|
|
11
|
+
(event.target instanceof Element &&
|
|
12
|
+
Boolean(
|
|
13
|
+
event.target.closest(
|
|
14
|
+
'input, textarea, select, button, a, [contenteditable]:not([contenteditable="false"]), [role="button"], [role="combobox"], [role="textbox"], [role="menu"], [role="listbox"], [role="dialog"]',
|
|
15
|
+
),
|
|
16
|
+
))
|
|
17
|
+
)
|
|
18
|
+
}
|