From 5e2644846c35aa9d6d4c644cb18dff5d71e3c359 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Thu, 25 Jun 2026 00:05:56 +0000 Subject: [PATCH] feat: add router metrics tab and viewer to DeploymentPage --- api/routes.ts | 37 +++++ deno.json | 16 +- deno.lock | 248 +++++++++++++++-------------- web/pages/DeploymentPage.tsx | 298 ++++++++++++++++++++++++++++------- 4 files changed, 409 insertions(+), 190 deletions(-) diff --git a/api/routes.ts b/api/routes.ts index 74c874a..9b12fcd 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -788,6 +788,43 @@ const defs = { output: ARR(MetricSchema, 'Collected query metrics'), description: 'Get SQL metrics from the deployment', }), + 'GET/api/deployment/metrics-router': route({ + authorize: withUserSession, + fn: async (ctx, { deployment }) => { + const dep = await withDeploymentTableAccess(ctx, deployment) + try { + const urlStr = dep.url.startsWith('http') + ? dep.url + : `${isLocal ? 'http' : 'https'}://${dep.url}` + return await fetchJson(`${urlStr}/api/router/metrics`, { + method: 'GET', + }) + } catch (err) { + log.error('fetch-router-metrics-error', { error: err }) + throw new respond.InternalServerErrorError({ + message: err instanceof Error + ? err.message + : 'Failed to fetch router metrics', + }) + } + }, + input: OBJ({ deployment: STR("The deployment's URL") }), + output: ARR( + OBJ({ + key: STR('Route key (method:path) allow to identify which route'), + duration: NUM( + 'Total time the route handler take to respond, in milliseconds', + ), + count: NUM('How many times the route was called'), + error: NUM('Number of time it responded with a status 400 or above'), + success: NUM( + 'Number of time it responded with a status under 399 (Success, Redirect and Info)', + ), + }, 'Route metrics'), + 'Collected route metrics', + ), + description: 'Get router metrics from the deployment', + }), 'GET/api/deployment/doc': route({ authorize: withUserSession, fn: async (_ctx, { deployment }) => { diff --git a/deno.json b/deno.json index ffc89a6..63e30fd 100644 --- a/deno.json +++ b/deno.json @@ -31,7 +31,7 @@ "imports": { "./": "./", "/": "./", - "@01edu/api": "jsr:@01edu/api@^0.2.7", + "@01edu/api": "jsr:@01edu/api@^0.2.11", "@01edu/api-client": "jsr:@01edu/api-client@^0.2.6", "@01edu/api-proxy": "jsr:@01edu/api-proxy@^0.2.1", "@01edu/signal-router": "npm:@01edu/signal-router@^0.2.3", @@ -47,15 +47,15 @@ "@std/streams": "jsr:@std/streams@^1.1.1", "@std/testing": "jsr:@std/testing@^1.0.19", "chdb": "npm:chdb@^2.0.1", - "vite": "npm:vite@^8.0.16", + "vite": "npm:vite@^8.1.0", "preact": "npm:preact@^10.29.2", "@preact/preset-vite": "npm:@preact/preset-vite@^2.10.5", - "@preact/signals": "npm:@preact/signals@^2.9.1", - "@clickhouse/client": "npm:@clickhouse/client@^1.19.0", - "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.3.0", - "tailwindcss": "npm:tailwindcss@^4.3.0", - "daisyui": "npm:daisyui@^5.5.20", - "lucide-preact": "npm:lucide-preact@^1.17.0", + "@preact/signals": "npm:@preact/signals@^2.9.2", + "@clickhouse/client": "npm:@clickhouse/client@^1.22.0", + "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.3.1", + "tailwindcss": "npm:tailwindcss@^4.3.1", + "daisyui": "npm:daisyui@^5.5.23", + "lucide-preact": "npm:lucide-preact@^1.21.0", "@deno/gfm": "jsr:@deno/gfm@0.12.0" }, "fmt": { diff --git a/deno.lock b/deno.lock index 79b4376..df9918b 100644 --- a/deno.lock +++ b/deno.lock @@ -3,7 +3,7 @@ "specifiers": { "jsr:@01edu/api-client@~0.2.6": "0.2.6", "jsr:@01edu/api-proxy@~0.2.1": "0.2.1", - "jsr:@01edu/api@~0.2.7": "0.2.8", + "jsr:@01edu/api@~0.2.11": "0.2.11", "jsr:@01edu/time@0.1": "0.1.0", "jsr:@01edu/types@~0.2.6": "0.2.6", "jsr:@cd/sqlite@~0.13.1": "0.13.1", @@ -11,7 +11,6 @@ "jsr:@denosaurs/emoji@~0.3.1": "0.3.1", "jsr:@denosaurs/plug@1": "1.1.0", "jsr:@std/assert@^1.0.19": "1.0.19", - "jsr:@std/async@^1.4.0": "1.4.0", "jsr:@std/bytes@^1.0.6": "1.0.6", "jsr:@std/cli@^1.0.30": "1.0.30", "jsr:@std/crypto@^1.1.0": "1.1.0", @@ -35,19 +34,19 @@ "jsr:@std/path@^1.1.5": "1.1.5", "jsr:@std/streams@^1.1.1": "1.1.1", "jsr:@std/testing@^1.0.19": "1.0.19", - "npm:@01edu/signal-router@~0.2.3": "0.2.3_@preact+signals@2.9.1__preact@10.29.2_preact@10.29.2", - "npm:@clickhouse/client@^1.19.0": "1.19.0", - "npm:@deno/vite-plugin@^2.0.2": "2.0.2_vite@8.0.16", - "npm:@preact/preset-vite@^2.10.5": "2.10.5_@babel+core@7.29.7_vite@8.0.16_preact@10.29.2", - "npm:@preact/signals@^2.9.0": "2.9.1_preact@10.29.2", - "npm:@preact/signals@^2.9.1": "2.9.1_preact@10.29.2", - "npm:@tailwindcss/vite@^4.3.0": "4.3.0_vite@8.0.16", + "npm:@01edu/signal-router@~0.2.3": "0.2.3_@preact+signals@2.9.2__preact@10.29.2_preact@10.29.2", + "npm:@clickhouse/client@^1.22.0": "1.22.0", + "npm:@deno/vite-plugin@^2.0.2": "2.0.2_vite@8.1.0", + "npm:@preact/preset-vite@^2.10.5": "2.10.5_vite@8.1.0", + "npm:@preact/signals@^2.9.0": "2.9.2_preact@10.29.2", + "npm:@preact/signals@^2.9.2": "2.9.2_preact@10.29.2", + "npm:@tailwindcss/vite@^4.3.1": "4.3.1_vite@8.1.0", "npm:chdb@^2.0.1": "2.0.1", - "npm:daisyui@^5.5.20": "5.5.20", + "npm:daisyui@^5.5.23": "5.5.23", "npm:github-slugger@2": "2.0.0", "npm:he@^1.2.0": "1.2.0", "npm:katex@0.16": "0.16.47", - "npm:lucide-preact@^1.17.0": "1.17.0_preact@10.29.2", + "npm:lucide-preact@^1.21.0": "1.21.0_preact@10.29.2", "npm:marked-alert@^2.1.2": "2.1.2_marked@17.0.6", "npm:marked-footnote@^1.4.0": "1.4.0_marked@17.0.6", "npm:marked-gfm-heading-id@^4.1.3": "4.1.4_marked@17.0.6", @@ -55,13 +54,13 @@ "npm:preact@^10.29.2": "10.29.2", "npm:prismjs@^1.30.0": "1.30.0", "npm:sanitize-html@^2.17.0": "2.17.4", - "npm:tailwindcss@^4.3.0": "4.3.0", - "npm:vite@^8.0.16": "8.0.16", - "npm:vite@^8.0.3": "8.0.16" + "npm:tailwindcss@^4.3.1": "4.3.1", + "npm:vite@^8.0.3": "8.1.0", + "npm:vite@^8.1.0": "8.1.0" }, "jsr": { - "@01edu/api@0.2.8": { - "integrity": "b49fa2db91cef165d0bf819efdc72c690332ec4bd73403ce6db96c8a016ab7d2", + "@01edu/api@0.2.11": { + "integrity": "a0483f14681ab5c67bb66c5b95ba7520a80ccb734c94b79cb8b9908526fa13e8", "dependencies": [ "jsr:@01edu/time", "jsr:@01edu/types", @@ -132,9 +131,6 @@ "jsr:@std/internal@^1.0.12" ] }, - "@std/async@1.4.0": { - "integrity": "4d70b008634f571cff9b554090d628c76141c32613aae0ff283fd5fa23d0c379" - }, "@std/bytes@1.0.6": { "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" }, @@ -205,7 +201,6 @@ "integrity": "f4236172365b216728dc3cc8b5e80a9f4c33083d1e4ede7613d5b25b4014898e", "dependencies": [ "jsr:@std/assert", - "jsr:@std/async", "jsr:@std/data-structures", "jsr:@std/fs@^1.0.24", "jsr:@std/internal@^1.0.14", @@ -214,7 +209,7 @@ } }, "npm": { - "@01edu/signal-router@0.2.3_@preact+signals@2.9.1__preact@10.29.2_preact@10.29.2": { + "@01edu/signal-router@0.2.3_@preact+signals@2.9.2__preact@10.29.2_preact@10.29.2": { "integrity": "sha512-Eg2ORuigaA8i3vM+Lr4k0P7+A53vZ3mKb2wsVnbBslkItaebEFwPUDxXiz2zDCJYHd06wLdvd64r/VRQQw6XFw==", "dependencies": [ "@preact/signals", @@ -375,16 +370,16 @@ "@babel/helper-validator-identifier" ] }, - "@clickhouse/client-common@1.19.0": { - "integrity": "sha512-HHQw7MUt1aFquSlnoQhlRO8e7zhRw7rJZFNSYiHP2wxzEVeoI0cX8kKoTp+xlVySvFi8s0mAC3bi8D031xmIQg==" + "@clickhouse/client-common@1.22.0": { + "integrity": "sha512-MQgXRhoYXut6GhRrTJlub42bnPX7+5Vm+5gHNR0zZXU5+EwZKsBgMXiWXPOerAmQd3weGKm8hzoeZJCfU3Cw2w==" }, - "@clickhouse/client@1.19.0": { - "integrity": "sha512-R/35tIFZjwRyqtTN0cnlvd45zU+YREuQ/cnfi6c+KGGkVSCF+1cl8mZ9kkxshqHx2U4YjcmPVElJaRn2bEqJ4g==", + "@clickhouse/client@1.22.0": { + "integrity": "sha512-iQAAM4VT9fO7mYVOkGB/Ul9Xxuf0atKn+GFceZqfE8xFakV8KOAQxR3tfNrXFMlJ8T+Q3gbrpfLFyj7/TbOwyA==", "dependencies": [ "@clickhouse/client-common" ] }, - "@deno/vite-plugin@2.0.2_vite@8.0.16": { + "@deno/vite-plugin@2.0.2_vite@8.1.0": { "integrity": "sha512-bzuKApn9Jr2x1jSrbuJEJzy++8LUwjFVOAopAbepcE3RgYzdcPEWd36PSp7P5dNMQlNnQlgtm3MeNbcKZ/Eh/Q==", "dependencies": [ "@deno/loader@npm:@jsr/deno__loader@0.5.0", @@ -392,21 +387,21 @@ "vite" ] }, - "@emnapi/core@1.10.0": { - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "@emnapi/core@1.11.1": { + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dependencies": [ "@emnapi/wasi-threads", "tslib" ] }, - "@emnapi/runtime@1.10.0": { - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "@emnapi/runtime@1.11.1": { + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dependencies": [ "tslib" ] }, - "@emnapi/wasi-threads@1.2.1": { - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "@emnapi/wasi-threads@1.2.2": { + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dependencies": [ "tslib" ] @@ -467,18 +462,18 @@ ], "tarball": "https://npm.jsr.io/~/11/@jsr/std__streams/1.1.1.tgz" }, - "@napi-rs/wasm-runtime@1.1.4_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0": { - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "@napi-rs/wasm-runtime@1.1.6_@emnapi+core@1.11.1_@emnapi+runtime@1.11.1": { + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", "@tybys/wasm-util" ] }, - "@oxc-project/types@0.133.0": { - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==" + "@oxc-project/types@0.137.0": { + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==" }, - "@preact/preset-vite@2.10.5_@babel+core@7.29.7_vite@8.0.16_preact@10.29.2": { + "@preact/preset-vite@2.10.5_vite@8.1.0": { "integrity": "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==", "dependencies": [ "@babel/core", @@ -495,11 +490,11 @@ "zimmerframe" ] }, - "@preact/signals-core@1.14.2": { - "integrity": "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==" + "@preact/signals-core@1.14.3": { + "integrity": "sha512-m0K3vnbSLC5rHs2ZVfeAMvBtT1zIyq4mxx5OlNncSgMj5Iz6W5Rn3kPrDxAC+iIKmiVe0lSl6U37t5ZkEWoVAw==" }, - "@preact/signals@2.9.1_preact@10.29.2": { - "integrity": "sha512-xVqN8mJjbSN5IB/8Ubmd9NN+Ew6zJswoRxrjZbH3YsgkMshFeO6d8zxEFpHRTq9GJZx7cnPs2CnCpFqtGXGNsw==", + "@preact/signals@2.9.2_preact@10.29.2": { + "integrity": "sha512-DvFPISNMSh3vPqRwPa1tAVAHl85aDq4pTyNu1bTGfrKr64F3EOCHjdUl9aUdohKBf1v9PRGLYuGFcJpfztkdoQ==", "dependencies": [ "@preact/signals-core", "preact" @@ -517,7 +512,7 @@ "@prefresh/utils@1.2.1": { "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==" }, - "@prefresh/vite@2.4.12_preact@10.29.2_vite@8.0.16": { + "@prefresh/vite@2.4.12_preact@10.29.2_vite@8.1.0": { "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", "dependencies": [ "@babel/core", @@ -529,68 +524,68 @@ "vite" ] }, - "@rolldown/binding-android-arm64@1.0.3": { - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "@rolldown/binding-android-arm64@1.1.3": { + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.3": { - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "@rolldown/binding-darwin-arm64@1.1.3": { + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.3": { - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "@rolldown/binding-darwin-x64@1.1.3": { + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.3": { - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "@rolldown/binding-freebsd-x64@1.1.3": { + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.3": { - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "@rolldown/binding-linux-arm-gnueabihf@1.1.3": { + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.3": { - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "@rolldown/binding-linux-arm64-gnu@1.1.3": { + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.3": { - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "@rolldown/binding-linux-arm64-musl@1.1.3": { + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-ppc64-gnu@1.0.3": { - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "@rolldown/binding-linux-ppc64-gnu@1.1.3": { + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rolldown/binding-linux-s390x-gnu@1.0.3": { - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "@rolldown/binding-linux-s390x-gnu@1.1.3": { + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", "os": ["linux"], "cpu": ["s390x"] }, - "@rolldown/binding-linux-x64-gnu@1.0.3": { - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "@rolldown/binding-linux-x64-gnu@1.1.3": { + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.3": { - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "@rolldown/binding-linux-x64-musl@1.1.3": { + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.3": { - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "@rolldown/binding-openharmony-arm64@1.1.3": { + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.3": { - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "@rolldown/binding-wasm32-wasi@1.1.3": { + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", @@ -598,13 +593,13 @@ ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.3": { - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "@rolldown/binding-win32-arm64-msvc@1.1.3": { + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.3": { - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "@rolldown/binding-win32-x64-msvc@1.1.3": { + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", "os": ["win32"], "cpu": ["x64"] }, @@ -626,8 +621,8 @@ "picomatch@4.0.4" ] }, - "@tailwindcss/node@4.3.0": { - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "@tailwindcss/node@4.3.1": { + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", "dependencies": [ "@jridgewell/remapping", "enhanced-resolve", @@ -638,67 +633,67 @@ "tailwindcss" ] }, - "@tailwindcss/oxide-android-arm64@4.3.0": { - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "@tailwindcss/oxide-android-arm64@4.3.1": { + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", "os": ["android"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-darwin-arm64@4.3.0": { - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "@tailwindcss/oxide-darwin-arm64@4.3.1": { + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", "os": ["darwin"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-darwin-x64@4.3.0": { - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "@tailwindcss/oxide-darwin-x64@4.3.1": { + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", "os": ["darwin"], "cpu": ["x64"] }, - "@tailwindcss/oxide-freebsd-x64@4.3.0": { - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "@tailwindcss/oxide-freebsd-x64@4.3.1": { + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", "os": ["freebsd"], "cpu": ["x64"] }, - "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0": { - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1": { + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", "os": ["linux"], "cpu": ["arm"] }, - "@tailwindcss/oxide-linux-arm64-gnu@4.3.0": { - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "@tailwindcss/oxide-linux-arm64-gnu@4.3.1": { + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-linux-arm64-musl@4.3.0": { - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "@tailwindcss/oxide-linux-arm64-musl@4.3.1": { + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", "os": ["linux"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-linux-x64-gnu@4.3.0": { - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "@tailwindcss/oxide-linux-x64-gnu@4.3.1": { + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", "os": ["linux"], "cpu": ["x64"] }, - "@tailwindcss/oxide-linux-x64-musl@4.3.0": { - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "@tailwindcss/oxide-linux-x64-musl@4.3.1": { + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", "os": ["linux"], "cpu": ["x64"] }, - "@tailwindcss/oxide-wasm32-wasi@4.3.0": { - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "@tailwindcss/oxide-wasm32-wasi@4.3.1": { + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", "cpu": ["wasm32"] }, - "@tailwindcss/oxide-win32-arm64-msvc@4.3.0": { - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "@tailwindcss/oxide-win32-arm64-msvc@4.3.1": { + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", "os": ["win32"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-win32-x64-msvc@4.3.0": { - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "@tailwindcss/oxide-win32-x64-msvc@4.3.1": { + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", "os": ["win32"], "cpu": ["x64"] }, - "@tailwindcss/oxide@4.3.0": { - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "@tailwindcss/oxide@4.3.1": { + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", "optionalDependencies": [ "@tailwindcss/oxide-android-arm64", "@tailwindcss/oxide-darwin-arm64", @@ -714,8 +709,8 @@ "@tailwindcss/oxide-win32-x64-msvc" ] }, - "@tailwindcss/vite@4.3.0_vite@8.0.16": { - "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "@tailwindcss/vite@4.3.1_vite@8.1.0": { + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", "dependencies": [ "@tailwindcss/node", "@tailwindcss/oxide", @@ -723,8 +718,8 @@ "vite" ] }, - "@tybys/wasm-util@0.10.2": { - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "@tybys/wasm-util@0.10.3": { + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dependencies": [ "tslib" ] @@ -786,8 +781,8 @@ "css-what@6.2.2": { "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==" }, - "daisyui@5.5.20": { - "integrity": "sha512-HemJcjl0Gk9rQ8BcgofN6p+EURrqftQG9wK1Hkxs98i49xe68+QxpNvry+PyxwkIUgrbMpNmZ5ZWjmtffAjfhQ==" + "daisyui@5.5.23": { + "integrity": "sha512-xuheNUSL4T6ZVtWXoioqcNkjoyGX85QTDz4HTw2aBPfqk4fuMjax5HDo8qCmpV6M1YN8bGvfx5BpYCoDeRlt+A==" }, "dayjs@1.11.21": { "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==" @@ -832,8 +827,8 @@ "electron-to-chromium@1.5.366": { "integrity": "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==" }, - "enhanced-resolve@5.22.1": { - "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "enhanced-resolve@5.21.6": { + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "dependencies": [ "graceful-fs", "tapable" @@ -1004,8 +999,8 @@ "yallist" ] }, - "lucide-preact@1.17.0_preact@10.29.2": { - "integrity": "sha512-QoHqgMY9WJanT+zd8x5Hq9quOqY/MHt7oTpuPAZzCkfUhfxsTJ+L0CFnUaBBh+H54OySAElm2+UQ1JwQ2CDPmg==", + "lucide-preact@1.21.0_preact@10.29.2": { + "integrity": "sha512-GUJs/vYSHNM5HU0UwNADfKvDbNDQ7i6vciniCvtpRVMsXy+FOcaRHgRhqVcY/CiORqA6oMC8xK5JJ7Wvb1r1Sw==", "dependencies": [ "preact" ] @@ -1095,8 +1090,8 @@ "prismjs@1.30.0": { "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==" }, - "rolldown@1.0.3": { - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "rolldown@1.1.3": { + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -1151,8 +1146,8 @@ "stack-trace@1.0.0": { "integrity": "sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==" }, - "tailwindcss@4.3.0": { - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==" + "tailwindcss@4.3.1": { + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==" }, "tapable@2.3.3": { "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==" @@ -1176,7 +1171,7 @@ ], "bin": true }, - "vite-prerender-plugin@0.5.13_vite@8.0.16": { + "vite-prerender-plugin@0.5.13_vite@8.1.0": { "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==", "dependencies": [ "kolorist", @@ -1188,8 +1183,8 @@ "vite" ] }, - "vite@8.0.16": { - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "vite@8.1.0": { + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", "dependencies": [ "lightningcss", "picomatch@4.0.4", @@ -1209,11 +1204,14 @@ "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==" } }, + "remote": { + "https://gistcdn.githack.com/kigiri/21df06d173fcdced5281b86ba6ac1382/raw/crypto.js": "e85976e655898538dbade9d87b05ca0a6bb167b3128cd4098622000a582f5f6d" + }, "workspace": { "dependencies": [ "jsr:@01edu/api-client@~0.2.6", "jsr:@01edu/api-proxy@~0.2.1", - "jsr:@01edu/api@~0.2.7", + "jsr:@01edu/api@~0.2.11", "jsr:@01edu/time@0.1", "jsr:@deno/gfm@0.12.0", "jsr:@std/assert@^1.0.19", @@ -1226,17 +1224,17 @@ "jsr:@std/streams@^1.1.1", "jsr:@std/testing@^1.0.19", "npm:@01edu/signal-router@~0.2.3", - "npm:@clickhouse/client@^1.19.0", + "npm:@clickhouse/client@^1.22.0", "npm:@deno/vite-plugin@^2.0.2", "npm:@preact/preset-vite@^2.10.5", - "npm:@preact/signals@^2.9.1", - "npm:@tailwindcss/vite@^4.3.0", + "npm:@preact/signals@^2.9.2", + "npm:@tailwindcss/vite@^4.3.1", "npm:chdb@^2.0.1", - "npm:daisyui@^5.5.20", - "npm:lucide-preact@^1.17.0", + "npm:daisyui@^5.5.23", + "npm:lucide-preact@^1.21.0", "npm:preact@^10.29.2", - "npm:tailwindcss@^4.3.0", - "npm:vite@^8.0.16" + "npm:tailwindcss@^4.3.1", + "npm:vite@^8.1.0" ] } } diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index a6c9dc9..dccc66e 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -4,6 +4,7 @@ import { AlertCircle, AlertTriangle, ArrowDown, + ArrowLeftRight, ArrowUp, BarChart, BarChart2, @@ -16,9 +17,11 @@ import { Database, Download, FileText, + Globe, Hash, Info, Link2, + LucideIcon, Play, Plus, RefreshCw, @@ -52,6 +55,7 @@ export const tableData = api['POST/api/deployment/table/data'].signal() export const rowDetailsData = api['POST/api/deployment/table/data'].signal() export const logDetailsData = api['POST/api/deployment/logs'].signal() export const metricsData = api['GET/api/deployment/metrics-sql'].signal() +const routerMetricsData = api['GET/api/deployment/metrics-router'].signal() const toastSignal = new Signal< { message: string; type: 'info' | 'error' } | null @@ -195,7 +199,7 @@ effect(() => { dep && schema.fetch({ url: dep }) }) -const tabNames = ['tables', 'queries', 'logs', 'metrics'] as const +const tabNames = ['tables', 'queries', 'logs', 'metrics', 'routes'] as const type TabName = (typeof tabNames)[number] const activeTab = computed(() => { @@ -874,7 +878,7 @@ function SchemaPanel() { } const TabButton = ( - { tabName }: { tabName: 'tables' | 'queries' | 'logs' | 'metrics' }, + { tabName }: { tabName: TabName }, ) => ( +
@@ -939,7 +944,7 @@ function TabNavigation() { /> )} - {tab !== 'logs' && ( + {tab !== 'logs' && tab !== 'routes' && tab !== 'metrics' && ( )} - {tab !== 'queries' && tab !== 'metrics' && ( + {tab !== 'queries' && tab !== 'metrics' && tab !== 'routes' && ( <> @@ -1479,7 +1484,7 @@ const TabViews = { ), logs: , metrics: , - // Add other tab views here as needed + routes: , } satisfies Record effect(() => { @@ -1489,9 +1494,17 @@ effect(() => { } }) +effect(() => { + const { dep, tab } = url.params + if (dep && tab === 'routes') { + routerMetricsData.fetch({ deployment: dep }) + } +}) + // ─── Metrics types ─────────────────────────────────────────────────────────── type Metric = ApiOutput['GET/api/deployment/metrics-sql'][number] +type RouterMetric = ApiOutput['GET/api/deployment/metrics-router'][number] type MetricStatus = NonNullable type MetricExplain = NonNullable @@ -1518,7 +1531,10 @@ type StatCellProps = { width?: string } -type MetricRowProps = { metric: Metric & { id: string } } +type MetricRowProps = { + type: 'sql' | 'router' + metric: (Metric & { id: string }) | RouterMetric +} type StatusCountersProps = { status: MetricStatus } type QueryPlanProps = { explain: MetricExplain } @@ -1836,12 +1852,76 @@ function MetricDetail() { ) } -function MetricRow({ metric }: MetricRowProps) { - const isExpanded = url.params.expanded === metric.id - const avg = formatDuration(metric.count && (metric.duration / metric.count)) +const methodColors: Record = { + GET: 'badge-info', + POST: 'badge-success', + PUT: 'badge-warning', + PATCH: 'badge-warning', + DELETE: 'badge-error', +} + +const RouteLabel = ({ metric }: { metric: RouterMetric }) => { + const colonIdx = metric.key.indexOf(':') + const method = colonIdx > -1 ? metric.key.slice(0, colonIdx) : 'GET' + const path = colonIdx > -1 ? metric.key.slice(colonIdx + 1) : metric.key + return ( +
+ + {method} + + + {path} + +
+ ) +} + +const SqlMaxDurationCell = ({ metric }: { metric: Metric }) => { const maxFmt = metric.max != null ? formatDuration(metric.max) : null + return ( + + ) +} + +const RouterErrorsCell = ({ metric }: { metric: RouterMetric }) => { + const errorRate = metric.count + ? ((metric.error / metric.count) * 100).toFixed(1) + : '0.0' + return ( + 5 ? 'text-error' : 'text-success'} + unitClass={Number(errorRate) > 5 ? 'text-error/40' : 'text-success/40'} + /> + ) +} + +const MetricRow = ({ type, metric }: MetricRowProps) => { + const isSql = type === 'sql' + const id = isSql + ? (metric as Metric & { id: string }).id + : (metric as RouterMetric).key + const isExpanded = url.params.expanded === id + + const avg = formatDuration(metric.count ? metric.duration / metric.count : 0) const totalFmt = formatDuration(metric.duration) - const pct = (metric.duration / stats.value.totalDuration) * 100 + + const totalDuration = isSql + ? stats.value.totalDuration + : routerStats.value.totalDuration + const pct = totalDuration ? (metric.duration / totalDuration) * 100 : 0 return (
@@ -1849,15 +1929,19 @@ function MetricRow({ metric }: MetricRowProps) { class={`px-5 py-3 flex items-center gap-4 cursor-pointer hover:bg-base-200/40 transition-colors ${ isExpanded ? 'bg-base-200/30' : '' }`} - params={{ expanded: isExpanded ? null : metric.id }} + params={{ expanded: isExpanded ? null : id }} >
-
highlightSQL(e)} - class='font-mono text-[13px] text-base-content/85 truncate' - > - {metric.query} -
+ {isSql + ? ( +
highlightSQL(e)} + class='font-mono text-[13px] text-base-content/85 truncate' + > + {(metric as Metric).query} +
+ ) + : }
- + {isSql + ? + : }
- {isExpanded && } + {isExpanded && isSql && }
) } -function MetricsSummaryBar() { - const totalDuration = formatDuration(stats.value.totalDuration) +type SummaryItem = { + icon: LucideIcon + value: string | number + label: string +} + +function MetricsSummaryBar({ items }: { items: SummaryItem[] }) { return (
-
- - - {stats.value.totalCalls.toLocaleString()} - - total calls -
-
-
- - - {totalDuration.value} {totalDuration.unit} - - total time -
-
-
- - {stats.value.count} - unique queries -
+ {items.map((item, index) => ( + <> + {index > 0 &&
} +
+ + + {item.value} + + {item.label} +
+ + ))}
) } -function MetricsEmpty() { +type MetricsEmptyProps = { + icon: LucideIcon + title: string + description: string +} + +const MetricsEmpty = ( + { icon: Icon, title, description }: MetricsEmptyProps, +) => { return (
- +
-

- No metrics recorded -

+

{title}

- Execute database queries to see performance data here. + {description}

@@ -1957,6 +2039,100 @@ function MetricsEmpty() { function MetricsViewer() { const isPending = metricsData.pending const sorted = sortedMetrics.value + const totalDuration = formatDuration(stats.value.totalDuration) + + const items = [ + { + icon: Activity, + value: stats.value.totalCalls.toLocaleString(), + label: 'total calls', + }, + { + icon: Timer, + value: `${totalDuration.value} ${totalDuration.unit}`, + label: 'total time', + }, + { + icon: BarChart2, + value: stats.value.count, + label: 'unique queries', + }, + ] + + return ( +
+ {!!isPending && ( +
+
+
+ )} + +
+ {sorted.map((metric) => ( + + ))} + {sorted.length === 0 && !isPending && ( + + )} +
+
+ ) +} + +// ─── RouterMetricsViewer ──────────────────────────────────────────────────── + +const sortedRouterMetrics = computed(() => { + const data = routerMetricsData.data || [] + return [...data].sort((a, b) => b.duration - a.duration) +}) + +const routerStats = computed(() => { + const metrics = sortedRouterMetrics.value + return { + count: metrics.length, + totalCalls: metrics.reduce((acc, m) => acc + (m.count || 0), 0), + totalDuration: metrics.reduce((acc, m) => acc + (m.duration || 0), 0), + totalErrors: metrics.reduce((acc, m) => acc + (m.error || 0), 0), + totalSuccess: metrics.reduce((acc, m) => acc + (m.success || 0), 0), + } +}) + +function RouterMetricsViewer() { + const isPending = routerMetricsData.pending + const sorted = sortedRouterMetrics.value + const totalDuration = formatDuration(routerStats.value.totalDuration) + const errorRate = routerStats.value.totalCalls + ? ((routerStats.value.totalErrors / routerStats.value.totalCalls) * 100) + .toFixed(1) + : '0.0' + + const items = [ + { + icon: Activity, + value: routerStats.value.totalCalls.toLocaleString(), + label: 'total requests', + }, + { + icon: Timer, + value: `${totalDuration.value} ${totalDuration.unit}`, + label: 'total time', + }, + { + icon: Globe, + value: routerStats.value.count, + label: 'routes', + }, + { + icon: XCircle, + value: `${errorRate}%`, + label: 'error rate', + }, + ] + return (
{!!isPending && ( @@ -1964,10 +2140,18 @@ function MetricsViewer() {
)} - +
- {sorted.map((metric) => )} - {sorted.length === 0 && !isPending && } + {sorted.map((metric) => ( + + ))} + {sorted.length === 0 && !isPending && ( + + )}
)