Merge latest state branch 'dev' into migrate_svelte5

This commit is contained in:
Christoph Kluge
2025-05-13 18:25:54 +02:00
92 changed files with 6082 additions and 3223 deletions

View File

@@ -31,9 +31,9 @@
}
},
"node_modules/@0no-co/graphql.web": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.13.tgz",
"integrity": "sha512-jqYxOevheVTU1S36ZdzAkJIdvRp2m3OYIG5SEoKDw5NI8eVwkoI0D/Q3DYNGmXCxkA6CQuoa7zvMiDPTLqUNuw==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.1.2.tgz",
"integrity": "sha512-N2NGsU5FLBhT8NZ+3l2YrzZSHITjNXNuDhC4iDiikv0IujaJ0Xc6xIxQZ/Ek3Cb+rgPjnLHYyJm11tInuJn+cw==",
"license": "MIT",
"peerDependencies": {
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
@@ -58,13 +58,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
@@ -134,19 +131,6 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@lambdatest/node-tunnel": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@lambdatest/node-tunnel/-/node-tunnel-4.0.8.tgz",
"integrity": "sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==",
"license": "ISC",
"dependencies": {
"adm-zip": "^0.5.10",
"axios": "^1.6.2",
"get-port": "^1.0.0",
"https-proxy-agent": "^5.0.0",
"split": "^1.0.1"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -158,9 +142,9 @@
}
},
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz",
"integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==",
"version": "28.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
"integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -185,9 +169,9 @@
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz",
"integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==",
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -276,9 +260,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.1.tgz",
"integrity": "sha512-kwctwVlswSEsr4ljpmxKrRKp1eG1v2NAhlzFzDf1x1OdYaMjBYjDCbHkzWm57ZXzTwqn8stMXgROrnMw8dJK3w==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
"integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
"cpu": [
"arm"
],
@@ -290,9 +274,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.1.tgz",
"integrity": "sha512-4H5ZtZitBPlbPsTv6HBB8zh1g5d0T8TzCmpndQdqq20Ugle/nroOyDMf9p7f88Gsu8vBLU78/cuh8FYHZqdXxw==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
"integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
"cpu": [
"arm64"
],
@@ -304,9 +288,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.1.tgz",
"integrity": "sha512-f2AJ7Qwx9z25hikXvg+asco8Sfuc5NCLg8rmqQBIOUoWys5sb/ZX9RkMZDPdnnDevXAMJA5AWLnRBmgdXGEUiA==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
"integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
"cpu": [
"arm64"
],
@@ -318,9 +302,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.1.tgz",
"integrity": "sha512-+/2JBrRfISCsWE4aEFXxd+7k9nWGXA8+wh7ZUHn/u8UDXOU9LN+QYKKhd57sIn6WRcorOnlqPMYFIwie/OHXWw==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
"integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
"cpu": [
"x64"
],
@@ -332,9 +316,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.1.tgz",
"integrity": "sha512-SUeB0pYjIXwT2vfAMQ7E4ERPq9VGRrPR7Z+S4AMssah5EHIilYqjWQoTn5dkDtuIJUSTs8H+C9dwoEcg3b0sCA==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
"integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
"cpu": [
"arm64"
],
@@ -346,9 +330,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.1.tgz",
"integrity": "sha512-L3T66wAZiB/ooiPbxz0s6JEX6Sr2+HfgPSK+LMuZkaGZFAFCQAHiP3dbyqovYdNaiUXcl9TlgnIbcsIicAnOZg==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
"integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
"cpu": [
"x64"
],
@@ -360,9 +344,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.1.tgz",
"integrity": "sha512-UBXdQ4+ATARuFgsFrQ+tAsKvBi/Hly99aSVdeCUiHV9dRTTpMU7OrM3WXGys1l40wKVNiOl0QYY6cZQJ2xhKlQ==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
"integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
"cpu": [
"arm"
],
@@ -374,9 +358,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.1.tgz",
"integrity": "sha512-m/yfZ25HGdcCSwmopEJm00GP7xAUyVcBPjttGLRAqZ60X/bB4Qn6gP7XTwCIU6bITeKmIhhwZ4AMh2XLro+4+w==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
"integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
"cpu": [
"arm"
],
@@ -388,9 +372,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.1.tgz",
"integrity": "sha512-Wy+cUmFuvziNL9qWRRzboNprqSQ/n38orbjRvd6byYWridp5TJ3CD+0+HUsbcWVSNz9bxkDUkyASGP0zS7GAvg==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
"cpu": [
"arm64"
],
@@ -402,9 +386,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.1.tgz",
"integrity": "sha512-CQ3MAGgiFmQW5XJX5W3wnxOBxKwFlUAgSXFA2SwgVRjrIiVt5LHfcQLeNSHKq5OEZwv+VCBwlD1+YKCjDG8cpg==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
"integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
"cpu": [
"arm64"
],
@@ -416,9 +400,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.1.tgz",
"integrity": "sha512-rSzb1TsY4lSwH811cYC3OC2O2mzNMhM13vcnA7/0T6Mtreqr3/qs6WMDriMRs8yvHDI54qxHgOk8EV5YRAHFbw==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
"integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
"cpu": [
"loong64"
],
@@ -430,9 +414,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.1.tgz",
"integrity": "sha512-fwr0n6NS0pG3QxxlqVYpfiY64Fd1Dqd8Cecje4ILAV01ROMp4aEdCj5ssHjRY3UwU7RJmeWd5fi89DBqMaTawg==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
"integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
"cpu": [
"ppc64"
],
@@ -444,9 +428,23 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.1.tgz",
"integrity": "sha512-4uJb9qz7+Z/yUp5RPxDGGGUcoh0PnKF33QyWgEZ3X/GocpWb6Mb+skDh59FEt5d8+Skxqs9mng6Swa6B2AmQZg==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
"integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
"integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
"cpu": [
"riscv64"
],
@@ -458,9 +456,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.1.tgz",
"integrity": "sha512-QlIo8ndocWBEnfmkYqj8vVtIUpIqJjfqKggjy7IdUncnt8BGixte1wDON7NJEvLg3Kzvqxtbo8tk+U1acYEBlw==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
"integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
"cpu": [
"s390x"
],
@@ -472,9 +470,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.1.tgz",
"integrity": "sha512-hzpleiKtq14GWjz3ahWvJXgU1DQC9DteiwcsY4HgqUJUGxZThlL66MotdUEK9zEo0PK/2ADeZGM9LIondE302A==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
"integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
"cpu": [
"x64"
],
@@ -486,9 +484,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.1.tgz",
"integrity": "sha512-jqtKrO715hDlvUcEsPn55tZt2TEiBvBtCMkUuU0R6fO/WPT7lO9AONjPbd8II7/asSiNVQHCMn4OLGigSuxVQA==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
"integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
"cpu": [
"x64"
],
@@ -500,9 +498,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.1.tgz",
"integrity": "sha512-RnHy7yFf2Wz8Jj1+h8klB93N0NHNHXFhNwAmiy9zJdpY7DE01VbEVtPdrK1kkILeIbHGRJjvfBDBhnxBr8kD4g==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
"integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
"cpu": [
"arm64"
],
@@ -514,9 +512,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.1.tgz",
"integrity": "sha512-i7aT5HdiZIcd7quhzvwQ2oAuX7zPYrYfkrd1QFfs28Po/i0q6kas/oRrzGlDhAEyug+1UfUtkWdmoVlLJj5x9Q==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
"integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
"cpu": [
"ia32"
],
@@ -528,9 +526,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.1.tgz",
"integrity": "sha512-k3MVFD9Oq+laHkw2N2v7ILgoa9017ZMF/inTtHzyTVZjYs9cSH18sdyAf6spBAJIGwJ5UaC7et2ZH1WCdlhkMw==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
"integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
"cpu": [
"x64"
],
@@ -541,10 +539,19 @@
"win32"
]
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
}
},
"node_modules/@sveltestrap/sveltestrap": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-7.0.3.tgz",
"integrity": "sha512-lvZpVlq7pHVxJbjq2d6JAAr/Z1mkSaPOw3pwpZiuQ9FK97/Pr66m5Bf9qZIc1FUkLnbNiDtRAbhVyR8LVdr3FQ==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-7.1.0.tgz",
"integrity": "sha512-TpIx25kqLV+z+VD3yfqYayOI1IaCeWFbT0uqM6NfA4vQgDs9PjFwmjkU4YEAlV/ngs9e7xPmaRWE7lkrg4Miow==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8"
@@ -561,9 +568,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT"
},
"node_modules/@types/resolve": {
@@ -574,9 +581,9 @@
"license": "MIT"
},
"node_modules/@urql/core": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-5.1.0.tgz",
"integrity": "sha512-yC3sw8yqjbX45GbXxfiBY8GLYCiyW/hLBbQF9l3TJrv4ro00Y0ChkKaD9I2KntRxAVm9IYBqh0awX8fwWAe/Yw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-5.1.1.tgz",
"integrity": "sha512-aGh024z5v2oINGD/In6rAtVKTm4VmQ2TxKQBAtk2ZSME5dunZFcjltw4p5ENQg+5CBhZ3FHMzl0Oa+rwqiWqlg==",
"license": "MIT",
"dependencies": {
"@0no-co/graphql.web": "^1.0.5",
@@ -584,12 +591,12 @@
}
},
"node_modules/@urql/svelte": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.2.2.tgz",
"integrity": "sha512-6ntLGsWcnNtaMZVmFpePfFTSpYxYpznCAqnuvLDjt7Oa7YqHcFiyPnz7IIsiPD9VE6hZSi0+RwmRk5BMba/teQ==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.2.3.tgz",
"integrity": "sha512-v3eArfymhdjaM5VQFp3QZxq9veYPadmDfX7ueid/kD4DlRplIycPakJ2FrKigh46SXa5mWqJ3QWuWyRKVu61sw==",
"license": "MIT",
"dependencies": {
"@urql/core": "^5.0.0",
"@urql/core": "^5.1.1",
"wonka": "^6.3.2"
},
"peerDependencies": {
@@ -598,9 +605,9 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -609,36 +616,6 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-typescript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
"license": "MIT",
"peerDependencies": {
"acorn": ">=8.9.0"
}
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -648,23 +625,6 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -682,9 +642,9 @@
"license": "MIT"
},
"node_modules/chart.js": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
@@ -702,18 +662,6 @@
"node": ">=6"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -751,23 +699,6 @@
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decimal.js": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
@@ -784,15 +715,6 @@
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
@@ -806,9 +728,9 @@
"license": "MIT"
},
"node_modules/esrap": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.3.tgz",
"integrity": "sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==",
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz",
"integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -821,9 +743,9 @@
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -835,44 +757,10 @@
}
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz",
"integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.2.tgz",
"integrity": "sha512-uXBDv5knpYmv/2gLzWQ5mBHGBRk9wcKTeWu6GLTUEQfjCxO09uM/mHDrojlL+Q1mVGIIFo149Gba7od1XPgSzQ==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -907,22 +795,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz",
"integrity": "sha512-vg59F3kcXBOtcIijwtdAyCxFocyv/fVkGQvw1kVGrxFO1U4SSGkGjrbASg5DN3TVekVle/jltwOjYRnZWc1YdA==",
"license": "MIT",
"bin": {
"get-port": "cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/graphql": {
"version": "16.10.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz",
"integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==",
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
@@ -941,19 +817,6 @@
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -1009,13 +872,12 @@
}
},
"node_modules/mathjs": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.2.0.tgz",
"integrity": "sha512-CcJV1cQwRSrQIAAX3sWejFPUvUsQnTZYisEEuoMBw3gMDJDQzvKQlrul/vjKAbdtW7zaDzPCl04h1sf0wh41TA==",
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
"integrity": "sha512-CpoYDhNENefjIG9wU9epr+0pBHzlaySfpWcblZdAf5qXik/j/U8eSmx/oNbmXO0F5PyfwPGVD/wK4VWsTho1SA==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@lambdatest/node-tunnel": "^4.0.8",
"@babel/runtime": "^7.26.10",
"complex.js": "^2.2.5",
"decimal.js": "^10.4.3",
"escape-latex": "^1.2.0",
@@ -1032,33 +894,6 @@
"node": ">= 18"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -1078,12 +913,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -1094,12 +923,6 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -1132,13 +955,13 @@
}
},
"node_modules/rollup": {
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.1.tgz",
"integrity": "sha512-iYZ/+PcdLYSGfH3S+dGahlW/RWmsqDhLgj1BT9DH/xXJ0ggZN7xkdP9wipPNjjNLczI+fmMLmTB9pye+d2r4GQ==",
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
"@types/estree": "1.0.7"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -1148,25 +971,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.1",
"@rollup/rollup-android-arm64": "4.34.1",
"@rollup/rollup-darwin-arm64": "4.34.1",
"@rollup/rollup-darwin-x64": "4.34.1",
"@rollup/rollup-freebsd-arm64": "4.34.1",
"@rollup/rollup-freebsd-x64": "4.34.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.1",
"@rollup/rollup-linux-arm-musleabihf": "4.34.1",
"@rollup/rollup-linux-arm64-gnu": "4.34.1",
"@rollup/rollup-linux-arm64-musl": "4.34.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.1",
"@rollup/rollup-linux-riscv64-gnu": "4.34.1",
"@rollup/rollup-linux-s390x-gnu": "4.34.1",
"@rollup/rollup-linux-x64-gnu": "4.34.1",
"@rollup/rollup-linux-x64-musl": "4.34.1",
"@rollup/rollup-win32-arm64-msvc": "4.34.1",
"@rollup/rollup-win32-ia32-msvc": "4.34.1",
"@rollup/rollup-win32-x64-msvc": "4.34.1",
"@rollup/rollup-android-arm-eabi": "4.40.2",
"@rollup/rollup-android-arm64": "4.40.2",
"@rollup/rollup-darwin-arm64": "4.40.2",
"@rollup/rollup-darwin-x64": "4.40.2",
"@rollup/rollup-freebsd-arm64": "4.40.2",
"@rollup/rollup-freebsd-x64": "4.40.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
"@rollup/rollup-linux-arm-musleabihf": "4.40.2",
"@rollup/rollup-linux-arm64-gnu": "4.40.2",
"@rollup/rollup-linux-arm64-musl": "4.40.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
"@rollup/rollup-linux-riscv64-gnu": "4.40.2",
"@rollup/rollup-linux-riscv64-musl": "4.40.2",
"@rollup/rollup-linux-s390x-gnu": "4.40.2",
"@rollup/rollup-linux-x64-gnu": "4.40.2",
"@rollup/rollup-linux-x64-musl": "4.40.2",
"@rollup/rollup-win32-arm64-msvc": "4.40.2",
"@rollup/rollup-win32-ia32-msvc": "4.40.2",
"@rollup/rollup-win32-x64-msvc": "4.40.2",
"fsevents": "~2.3.2"
}
},
@@ -1296,18 +1120,6 @@
"source-map": "^0.6.0"
}
},
"node_modules/split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
"license": "MIT",
"dependencies": {
"through": "2"
},
"engines": {
"node": "*"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -1322,21 +1134,21 @@
}
},
"node_modules/svelte": {
"version": "5.19.6",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.6.tgz",
"integrity": "sha512-6ydekB3qyqUal+UhfMjmVOjRGtxysR8vuiMhi2nwuBtPJWnctVlsGspjVFB05qmR+TXI1emuqtZt81c0XiFleA==",
"version": "5.28.6",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.6.tgz",
"integrity": "sha512-9qqr7mw8YR9PAnxGFfzCK6PUlNGtns7wVavrhnxyf3fpB1mP/Ol55Z2UnIapsSzNNl3k9qw7cZ22PdE8+xT/jQ==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.3",
"esrap": "^1.4.6",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -1356,9 +1168,9 @@
}
},
"node_modules/terser": {
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"version": "5.39.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz",
"integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -1374,12 +1186,6 @@
"node": ">=10"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
@@ -1396,15 +1202,15 @@
}
},
"node_modules/uplot": {
"version": "1.6.31",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.31.tgz",
"integrity": "sha512-sQZqSwVCbJGnFB4IQjQYopzj5CoTZJ4Br1fG/xdONimqgHmsacvCjNesdGDypNKFbrhLGIeshYhy89FxPF+H+w==",
"version": "1.6.32",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz",
"integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==",
"license": "MIT"
},
"node_modules/wonka": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.4.tgz",
"integrity": "sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==",
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
"integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==",
"license": "MIT"
},
"node_modules/zimmerframe": {

View File

@@ -62,6 +62,7 @@ export default [
entrypoint('jobs', 'src/jobs.entrypoint.js'),
entrypoint('user', 'src/user.entrypoint.js'),
entrypoint('list', 'src/list.entrypoint.js'),
entrypoint('taglist', 'src/tags.entrypoint.js'),
entrypoint('job', 'src/job.entrypoint.js'),
entrypoint('systems', 'src/systems.entrypoint.js'),
entrypoint('node', 'src/node.entrypoint.js'),

View File

@@ -26,6 +26,8 @@
init,
convert2uplot,
binsFromFootprint,
scramble,
scrambleNames,
} from "./generic/utils.js";
import PlotSelection from "./analysis/PlotSelection.svelte";
import Filters from "./generic/Filters.svelte";
@@ -396,7 +398,7 @@
quantities={$topQuery.data.topList.map(
(t) => t[sortSelection.key],
)}
entities={$topQuery.data.topList.map((t) => t.id)}
entities={$topQuery.data.topList.map((t) => scrambleNames ? scramble(t.id) : t.id)}
/>
{/if}
{/key}
@@ -429,21 +431,21 @@
{#if groupSelection.key == "user"}
<th scope="col" id="topName-{te.id}"
><a href="/monitoring/user/{te.id}?cluster={clusterName}"
>{te.id}</a
>{scrambleNames ? scramble(te.id) : te.id}</a
></th
>
{#if te?.name}
<Tooltip
target={`topName-${te.id}`}
placement="left"
>{te.name}</Tooltip
>{scrambleNames ? scramble(te.name) : te.name}</Tooltip
>
{/if}
{:else}
<th scope="col"
><a
href="/monitoring/jobs/?cluster={clusterName}&project={te.id}&projectMatch=eq"
>{te.id}</a
>{scrambleNames ? scramble(te.id) : te.id}</a
></th
>
{/if}

View File

@@ -40,7 +40,7 @@
import JobRoofline from "./job/JobRoofline.svelte";
import EnergySummary from "./job/EnergySummary.svelte";
import PlotGrid from "./generic/PlotGrid.svelte";
import StatsTable from "./job/StatsTable.svelte";
import StatsTab from "./job/StatsTab.svelte";
export let dbid;
export let username;
@@ -53,10 +53,8 @@
let isMetricsSelectionOpen = false,
selectedMetrics = [],
selectedScopes = [];
let plots = {},
statsTable
selectedScopes = [],
plots = {};
let availableMetrics = new Set(),
missingMetrics = [],
@@ -127,19 +125,16 @@
let job = $initq.data.job;
if (!job) return;
const pendingMetrics = [
...(ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
$initq.data.globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster)) {
names.push(gm.name);
}
return names;
}, [])
),
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
ccconfig[`job_view_nodestats_selectedMetrics`]
),
];
const pendingMetrics = (
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
) ||
$initq.data.globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
names.push(gm.name);
}
return names;
}, [])
// Select default Scopes to load: Check before if any metric has accelerator scope by default
const accScopeDefault = [...pendingMetrics].some(function (m) {
@@ -222,7 +217,7 @@
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.data}
{:else if $initq?.data}
<Card class="overflow-auto" style="height: 400px;">
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
{#if $initq.data?.job?.metaData?.message}
@@ -296,7 +291,7 @@
<Card class="mb-3">
<CardBody>
<Row class="mb-2">
{#if $initq.data}
{#if $initq?.data}
<Col xs="auto">
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
@@ -309,7 +304,7 @@
{#if $jobMetrics.error}
<Row class="mt-2">
<Col>
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
{#if $initq?.data && ($initq.data.job?.monitoringStatus == 0 || $initq.data.job?.monitoringStatus == 2)}
<Card body color="warning">Not monitored or archiving failed</Card>
<br />
{/if}
@@ -334,7 +329,6 @@
{#if item.data}
<Metric
bind:this={plots[item.metric]}
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
job={$initq.data.job}
metricName={item.metric}
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
@@ -343,10 +337,25 @@
scopes={item.data.map((x) => x.scope)}
isShared={$initq.data.job.exclusive != 1}
/>
{:else if item.disabled == true}
<Card color="info">
<CardHeader class="mb-0">
<b>Disabled Metric</b>
</CardHeader>
<CardBody>
<p>Metric <b>{item.metric}</b> is disabled for subcluster <b>{$initq.data.job.subCluster}</b>.</p>
<p class="mb-1">To remove this card, open metric selection and press "Close and Apply".</p>
</CardBody>
</Card>
{:else}
<Card body color="warning" class="mt-2"
>No dataset returned for <code>{item.metric}</code></Card
>
<Card color="warning" class="mt-2">
<CardHeader class="mb-0">
<b>Missing Metric</b>
</CardHeader>
<CardBody>
<p class="mb-1">No dataset returned for <b>{item.metric}</b>.</p>
</CardBody>
</Card>
{/if}
</PlotGrid>
{/if}
@@ -356,7 +365,7 @@
<!-- Statistcics Table -->
<Row class="mb-3">
<Col>
{#if $initq.data}
{#if $initq?.data}
<Card>
<TabContent>
{#if somethingMissing}
@@ -389,22 +398,8 @@
</div>
</TabPane>
{/if}
<TabPane
tabId="stats"
tab="Statistics Table"
class="overflow-x-auto"
active={!somethingMissing}
>
{#if $jobMetrics?.data?.jobMetrics}
{#key $jobMetrics.data.jobMetrics}
<StatsTable
bind:this={statsTable}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
{/key}
{/if}
</TabPane>
<!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
<StatsTab job={$initq.data.job} clusters={$initq.data.clusters} tabActive={!somethingMissing}/>
<TabPane tabId="job-script" tab="Job Script">
<div class="pre-wrapper">
{#if $initq.data.job.metaData?.jobScript}
@@ -431,9 +426,10 @@
</Col>
</Row>
{#if $initq.data}
{#if $initq?.data}
<MetricSelection
cluster={$initq.data.job.cluster}
subCluster={$initq.data.job.subCluster}
configName="job_view_selectedMetrics"
bind:metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}

View File

@@ -21,6 +21,7 @@
import { init } from "./generic/utils.js";
import Filters from "./generic/Filters.svelte";
import JobList from "./generic/JobList.svelte";
import JobCompare from "./generic/JobCompare.svelte";
import TextFilter from "./generic/helper/TextFilter.svelte";
import Refresher from "./generic/helper/Refresher.svelte";
import Sorting from "./generic/select/SortSelection.svelte";
@@ -35,8 +36,12 @@
export let roles;
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let filterBuffer = [];
let selectedJobs = [];
let jobList,
matchedJobs = null;
jobCompare,
matchedListJobs,
matchedCompareJobs = null;
let sorting = { field: "startTime", type: "col", order: "DESC" },
isSortingOpen = false,
isMetricsSelectionOpen = false;
@@ -49,11 +54,16 @@
: !!ccconfig.plot_list_showFootprint;
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
let presetProject = filterPresets?.project ? filterPresets.project : ""
let showCompare = false;
// The filterPresets are handled by the Filters component,
// so we need to wait for it to be ready before we can start a query.
// This is also why JobList component starts out with a paused query.
onMount(() => filterComponent.updateFilters());
$: if (filterComponent && selectedJobs.length == 0) {
filterComponent.updateFilters({dbId: []})
}
</script>
<!-- ROW1: Status-->
@@ -72,10 +82,10 @@
{/if}
<!-- ROW2: Tools-->
<Row cols={{ xs: 1, md: 2, lg: 4}} class="mb-3">
<Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3">
<Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Button outline color="primary" on:click={() => (isSortingOpen = true)} disabled={showCompare}>
<Icon name="sort-up" /> Sorting
</Button>
<Button
@@ -87,49 +97,88 @@
</Button>
</ButtonGroup>
</Col>
<Col lg="4" xl="{(presetProject !== '') ? 5 : 6}" class="mb-1 mb-lg-0">
<Col lg="4" class="mb-1 mb-lg-0">
<Filters
showFilter={!showCompare}
{filterPresets}
{matchedJobs}
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
bind:this={filterComponent}
on:update-filters={({ detail }) => {
selectedCluster = detail.filters[0]?.cluster
? detail.filters[0].cluster.eq
: null;
jobList.queryJobs(detail.filters);
filterBuffer = [...detail.filters]
if (showCompare) {
jobCompare.queryJobs(detail.filters);
} else {
jobList.queryJobs(detail.filters);
}
}}
/>
</Col>
<Col lg="3" xl="{(presetProject !== '') ? 3 : 2}" class="mb-2 mb-lg-0">
<TextFilter
{presetProject}
bind:authlevel
bind:roles
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/>
<Col lg="2" class="mb-2 mb-lg-0">
{#if !showCompare}
<TextFilter
{presetProject}
bind:authlevel
bind:roles
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/>
{/if}
</Col>
<Col lg="3" xl="2" class="mb-1 mb-lg-0">
<Refresher on:refresh={() => {
jobList.refreshJobs()
jobList.refreshAllMetrics()
}} />
<Col lg="2" class="mb-1 mb-lg-0">
{#if !showCompare}
<Refresher on:refresh={() => {
jobList.refreshJobs()
jobList.refreshAllMetrics()
}} />
{/if}
</Col>
<Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100">
<Button color="primary" disabled={matchedListJobs >= 500 && !(selectedJobs.length != 0)} on:click={() => {
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true)
showCompare = !showCompare
}} >
{showCompare ? 'Return to List' :
'Compare Jobs' + (selectedJobs.length != 0 ? ` (${selectedJobs.length} selected)` : matchedListJobs >= 500 ? ` (Too Many)` : ``)}
</Button>
{#if !showCompare && selectedJobs.length != 0}
<Button color="warning" on:click={() => {
selectedJobs = [] // Only empty array, filters handled by reactive reset
}}>
Clear
</Button>
{/if}
</ButtonGroup>
</Col>
</Row>
<!-- ROW3: Job List-->
<!-- ROW3: Job List / Job Compare-->
<Row>
<Col>
<JobList
bind:this={jobList}
bind:metrics
bind:sorting
bind:matchedJobs
bind:showFootprint
/>
{#if !showCompare}
<JobList
bind:this={jobList}
bind:metrics
bind:sorting
bind:matchedListJobs
bind:showFootprint
bind:selectedJobs
{filterBuffer}
/>
{:else}
<JobCompare
bind:this={jobCompare}
bind:metrics
bind:matchedCompareJobs
{filterBuffer}
/>
{/if}
</Col>
</Row>
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
<Sorting bind:sorting bind:isOpen={isSortingOpen}/>
<MetricSelection
bind:cluster={selectedCluster}
@@ -137,5 +186,5 @@
bind:metrics
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint
footprintSelect={true}
footprintSelect
/>

View File

@@ -31,6 +31,8 @@
init,
convert2uplot,
transformPerNodeDataForRoofline,
scramble,
scrambleNames,
} from "./generic/utils.js";
import { scaleNumbers } from "./generic/units.js";
import PlotGrid from "./generic/PlotGrid.svelte";
@@ -487,7 +489,7 @@
quantities={$topUserQuery.data.topUser.map(
(tu) => tu[topUserSelection.key],
)}
entities={$topUserQuery.data.topUser.map((tu) => tu.id)}
entities={$topUserQuery.data.topUser.map((tu) => scrambleNames ? scramble(tu.id) : tu.id)}
/>
{/if}
{/key}
@@ -521,14 +523,14 @@
<th scope="col" id="topName-{tu.id}"
><a
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
>{tu.id}</a
>{scrambleNames ? scramble(tu.id) : tu.id}</a
></th
>
{#if tu?.name}
<Tooltip
target={`topName-${tu.id}`}
placement="left"
>{tu.name}</Tooltip
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu[topUserSelection.key]}</td>
@@ -555,7 +557,7 @@
quantities={$topProjectQuery.data.topProjects.map(
(tp) => tp[topProjectSelection.key],
)}
entities={$topProjectQuery.data.topProjects.map((tp) => tp.id)}
entities={$topProjectQuery.data.topProjects.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)}
/>
{/if}
{/key}
@@ -588,7 +590,7 @@
<th scope="col"
><a
href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
>{tp.id}</a
>{scrambleNames ? scramble(tp.id) : tp.id}</a
></th
>
<td>{tp[topProjectSelection.key]}</td>

View File

@@ -29,8 +29,8 @@
import Refresher from "./generic/helper/Refresher.svelte";
export let displayType;
export let cluster;
export let subCluster = "";
export let cluster = null;
export let subCluster = null;
export let from = null;
export let to = null;
@@ -60,7 +60,10 @@
let hostnameFilter = "";
let pendingHostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric || "";
let selectedMetrics = ccconfig[`node_list_selectedMetrics:${cluster}`] || [ccconfig.system_view_selectedMetric];
let selectedMetrics = (
ccconfig[`node_list_selectedMetrics:${cluster}:${subCluster}`] ||
ccconfig[`node_list_selectedMetrics:${cluster}`]
) || [ccconfig.system_view_selectedMetric];
let isMetricsSelectionOpen = false;
/*
@@ -191,6 +194,7 @@
<MetricSelection
{cluster}
{subCluster}
configName="node_list_selectedMetrics"
metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}

View File

@@ -0,0 +1,110 @@
<!--
@component Tag List Svelte Component. Displays All Tags, Allows deletion.
Properties:
- `username String!`: Users username.
- `isAdmin Bool!`: User has Admin Auth.
- `tagmap Object!`: Map of accessible, appwide tags. Prefiltered in backend.
-->
<script>
import {
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import {
Badge,
InputGroup,
Icon,
Button,
Spinner,
} from "@sveltestrap/sveltestrap";
import {
init,
} from "./generic/utils.js";
export let username;
export let isAdmin;
export let tagmap;
const {} = init();
const client = getContextClient();
let pendingChange = "none";
const removeTagMutation = ({ tagIds }) => {
return mutationStore({
client: client,
query: gql`
mutation ($tagIds: [ID!]!) {
removeTagFromList(tagIds: $tagIds)
}
`,
variables: { tagIds },
});
};
function removeTag(tag, tagType) {
if (confirm("Are you sure you want to completely remove this tag?\n\n" + tagType + ':' + tag.name)) {
pendingChange = tagType;
removeTagMutation({tagIds: [tag.id] }).subscribe(
(res) => {
if (res.fetching === false && !res.error) {
tagmap[tagType] = tagmap[tagType].filter((t) => !res.data.removeTagFromList.includes(t.id));
if (tagmap[tagType].length === 0) {
delete tagmap[tagType]
}
pendingChange = "none";
} else if (res.fetching === false && res.error) {
throw res.error;
}
},
);
}
}
</script>
<div class="container">
<div class="row justify-content-center">
<div class="col-10">
{#each Object.entries(tagmap) as [tagType, tagList]}
<div class="my-3 p-2 bg-secondary rounded text-white"> <!-- text-capitalize -->
Tag Type: <b>{tagType}</b>
{#if pendingChange === tagType}
<Spinner size="sm" secondary />
{/if}
<span style="float: right; padding-bottom: 0.4rem; padding-top: 0.4rem;" class="badge bg-light text-secondary">
{tagList.length} Tag{(tagList.length != 1)?'s':''}
</span>
</div>
<div class="d-inline-flex flex-wrap">
{#each tagList as tag (tag.id)}
<InputGroup class="w-auto flex-nowrap" style="margin-right: 0.5rem; margin-bottom: 0.5rem;">
<Button outline color="secondary" href="/monitoring/jobs/?tag={tag.id}" target="_blank">
<Badge color="light" style="font-size:medium;" border>{tag.name}</Badge> :
<Badge color="primary" pill>{tag.count} Job{(tag.count != 1)?'s':''}</Badge>
{#if tag.scope == "global"}
<Badge style="background-color:#c85fc8 !important;" pill>Global</Badge>
{:else if tag.scope == "admin"}
<Badge style="background-color:#19e5e6 !important;" pill>Admin</Badge>
{:else}
<Badge color="warning" pill>Private</Badge>
{/if}
</Button>
{#if (isAdmin && (tag.scope == "admin" || tag.scope == "global")) || tag.scope == username }
<Button
size="sm"
color="danger"
on:click={() => removeTag(tag, tagType)}
>
<Icon name="x" />
</Button>
{/if}
</InputGroup>
{/each}
</div>
{/each}
</div>
</div>
</div>

View File

@@ -352,7 +352,7 @@
bind:metrics
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint
footprintSelect={true}
footprintSelect
/>
<HistogramSelection

View File

@@ -44,11 +44,59 @@
export let disableClusterSelection = false;
export let startTimeQuickSelect = false;
export let matchedJobs = -2;
export let showFilter = true;
const startTimeSelectOptions = [
{ range: "", rangeLabel: "No Selection"},
{ range: "last6h", rangeLabel: "Last 6hrs"},
{ range: "last24h", rangeLabel: "Last 24hrs"},
{ range: "last7d", rangeLabel: "Last 7 days"},
{ range: "last30d", rangeLabel: "Last 30 days"}
];
const nodeMatchLabels = {
eq: "",
contains: " Contains",
}
const filterReset = {
projectMatch: "contains",
userMatch: "contains",
jobIdMatch: "eq",
nodeMatch: "eq",
cluster: null,
partition: null,
states: allJobStates,
startTime: { from: null, to: null, range: ""},
tags: [],
duration: {
lessThan: null,
moreThan: null,
from: null,
to: null,
},
dbId: [],
jobId: "",
arrayJobId: null,
user: "",
project: "",
jobName: "",
node: null,
energy: { from: null, to: null },
numNodes: { from: null, to: null },
numHWThreads: { from: null, to: null },
numAccelerators: { from: null, to: null },
stats: [],
};
let filters = {
projectMatch: filterPresets.projectMatch || "contains",
userMatch: filterPresets.userMatch || "contains",
jobIdMatch: filterPresets.jobIdMatch || "eq",
nodeMatch: filterPresets.nodeMatch || "eq",
cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null,
@@ -56,7 +104,7 @@
filterPresets.states || filterPresets.state
? [filterPresets.state].flat()
: allJobStates,
startTime: filterPresets.startTime || { from: null, to: null },
startTime: filterPresets.startTime || { from: null, to: null, range: ""},
tags: filterPresets.tags || [],
duration: filterPresets.duration || {
lessThan: null,
@@ -64,6 +112,7 @@
from: null,
to: null,
},
dbId: filterPresets.dbId || [],
jobId: filterPresets.jobId || "",
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || "",
@@ -92,13 +141,20 @@
isAccsModified = false;
// Can be called from the outside to trigger a 'update' event from this component.
export function updateFilters(additionalFilters = null) {
if (additionalFilters != null)
// 'force' option empties existing filters and then applies only 'additionalFilters'
export function updateFilters(additionalFilters = null, force = false) {
// Empty Current Filter For Force
if (additionalFilters != null && force) {
filters = {...filterReset}
}
// Add Additional Filters
if (additionalFilters != null) {
for (let key in additionalFilters) filters[key] = additionalFilters[key];
}
// Construct New Filter
let items = [];
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
if (filters.node) items.push({ node: { contains: filters.node } });
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
if (filters.partition) items.push({ partition: { eq: filters.partition } });
if (filters.states.length != allJobStates.length)
items.push({ state: filters.states });
@@ -123,6 +179,8 @@
items.push({
energy: { from: filters.energy.from, to: filters.energy.to },
});
if (filters.dbId.length != 0)
items.push({ dbId: filters.dbId });
if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
if (filters.arrayJobId != null)
@@ -166,10 +224,12 @@
function changeURL() {
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
let opts = [];
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
if (filters.node) opts.push(`node=${filters.node}`);
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
opts.push(`nodeMatch=${filters.nodeMatch}`);
if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.states.length != allJobStates.length)
for (let state of filters.states) opts.push(`state=${state}`);
@@ -180,6 +240,11 @@
if (filters.startTime.range) {
opts.push(`startTime=${filters.startTime.range}`)
}
if (filters.dbId.length != 0) {
for (let dbi of filters.dbId) {
opts.push(`dbId=${dbi}`);
}
}
if (filters.jobId.length != 0)
if (filters.jobIdMatch != "in") {
opts.push(`jobId=${filters.jobId}`);
@@ -188,7 +253,7 @@
opts.push(`jobId=${singleJobId}`);
}
if (filters.jobIdMatch != "eq")
opts.push(`jobIdMatch=${filters.jobIdMatch}`);
opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case
for (let tag of filters.tags) opts.push(`tag=${tag}`);
if (filters.duration.from && filters.duration.to)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
@@ -210,19 +275,19 @@
} else {
for (let singleUser of filters.user) opts.push(`user=${singleUser}`);
}
if (filters.userMatch != "contains")
if (filters.userMatch != "contains") // "contains" is default-case
opts.push(`userMatch=${filters.userMatch}`);
if (filters.project) opts.push(`project=${filters.project}`);
if (filters.project && filters.projectMatch != "contains") // "contains" is default-case
opts.push(`projectMatch=${filters.projectMatch}`);
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
if (filters.project && filters.projectMatch != "contains")
opts.push(`projectMatch=${filters.projectMatch}`);
if (filters.stats.length != 0)
for (let stat of filters.stats) {
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
}
if (opts.length == 0 && window.location.search.length <= 1) return;
let newurl = `${window.location.pathname}?${opts.join("&")}`;
window.history.replaceState(null, "", newurl);
}
@@ -230,59 +295,63 @@
<!-- Dropdown-Button -->
<ButtonGroup>
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}">
<DropdownToggle outline caret color="success">
<Icon name="sliders" />
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>Manage Filters</DropdownItem>
{#if menuText}
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range" /> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch" /> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags" /> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack" /> Resources
</DropdownItem>
<DropdownItem on:click={() => (isEnergyOpen = true)}>
<Icon name="lightning-charge-fill" /> Energy
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider />
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
{#each [{ text: "Last 6hrs", range: "last6h" }, { text: "Last 24hrs", range: "last24h" }, { text: "Last 7 days", range: "last7d" }, { text: "Last 30 days", range: "last30d" }] as { text, range }}
<DropdownItem
on:click={() => {
filters.startTime.range = range;
filters.startTime.text = text;
updateFilters();
}}
>
<Icon name="calendar-range" />
{text}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</ButtonDropdown>
{#if showFilter}
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}">
<DropdownToggle outline caret color="success">
<Icon name="sliders" />
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>Manage Filters</DropdownItem>
{#if menuText}
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range" /> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch" /> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags" /> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack" /> Resources
</DropdownItem>
<DropdownItem on:click={() => (isEnergyOpen = true)}>
<Icon name="lightning-charge-fill" /> Energy
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider />
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
<DropdownItem
on:click={() => {
filters.startTime.from = null
filters.startTime.to = null
filters.startTime.range = range;
updateFilters();
}}
>
<Icon name="calendar-range" />
{rangeLabel}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</ButtonDropdown>
{/if}
{#if matchedJobs >= -1}
<Button class="mb-1" style="margin-right: 0.5rem;" disabled outline>
{matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
@@ -290,109 +359,111 @@
{/if}
</ButtonGroup>
<!-- SELECTED FILTER PILLS -->
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
{/if}
{#if showFilter}
<!-- SELECTED FILTER PILLS -->
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
{/if}
{#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(", ")}
</Info>
{/if}
{#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(", ")}
</Info>
{/if}
{#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
filters.startTime.to,
).toLocaleString()}
</Info>
{/if}
{#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
filters.startTime.to,
).toLocaleString()}
</Info>
{/if}
{#if filters.startTime.range}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{filters?.startTime?.text ? filters.startTime.text : filters.startTime.range }
</Info>
{/if}
{#if filters.startTime.range}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
</Info>
{/if}
{#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
(filters.duration.from % 3600) / 60,
)}m -
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
(filters.duration.to % 3600) / 60,
)}m
</Info>
{/if}
{#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
(filters.duration.from % 3600) / 60,
)}m -
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
(filters.duration.to % 3600) / 60,
)}m
</Info>
{/if}
{#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration less than {Math.floor(
filters.duration.lessThan / 3600,
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration less than {Math.floor(
filters.duration.lessThan / 3600,
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration more than {Math.floor(
filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration more than {Math.floor(
filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId}
{#key tagId}
<Tag id={tagId} clickable={false} />
{/key}
{/each}
</Info>
{/if}
{#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId}
{#key tagId}
<Tag id={tagId} clickable={false} />
{/key}
{/each}
</Info>
{/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
{#if isNodesModified}
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{/if}
{#if isNodesModified && isHwthreadsModified},
{/if}
{#if isHwthreadsModified}
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
{/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
{/if}
{#if isAccsModified}
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
{/if}
</Info>
{/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
{#if isNodesModified}
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{/if}
{#if isNodesModified && isHwthreadsModified},
{/if}
{#if isHwthreadsModified}
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
{/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
{/if}
{#if isAccsModified}
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
{/if}
</Info>
{/if}
{#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node}
</Info>
{/if}
{#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
</Info>
{/if}
{#if filters.energy.from || filters.energy.to}
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}>
Total Energy: {filters.energy.from} - {filters.energy.to}
</Info>
{/if}
{#if filters.energy.from || filters.energy.to}
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}>
Total Energy: {filters.energy.from} - {filters.energy.to}
</Info>
{/if}
{#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
.join(", ")}
</Info>
{#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
.join(", ")}
</Info>
{/if}
{/if}
<Cluster
@@ -414,11 +485,8 @@
bind:from={filters.startTime.from}
bind:to={filters.startTime.to}
bind:range={filters.startTime.range}
on:set-filter={() => {
delete filters.startTime["text"];
delete filters.startTime["range"];
updateFilters();
}}
{startTimeSelectOptions}
on:set-filter={() => updateFilters()}
/>
<Duration
@@ -443,6 +511,7 @@
bind:numHWThreads={filters.numHWThreads}
bind:numAccelerators={filters.numAccelerators}
bind:namedNode={filters.node}
bind:nodeMatch={filters.nodeMatch}
bind:isNodesModified
bind:isHwthreadsModified
bind:isAccsModified

View File

@@ -0,0 +1,394 @@
<!--
@component jobCompare component; compares jobs according to set filters or job selection
Properties:
- `matchedJobs Number?`: Number of matched jobs for selected filters [Default: 0]
- `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
- `showFootprint Bool`: If to display the jobFootprint component
Functions:
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
-->
<script>
import { getContext } from "svelte";
import uPlot from "uplot";
import {
queryStore,
gql,
getContextClient,
// mutationStore,
} from "@urql/svelte";
import { Row, Col, Card, Spinner, Table, Input, InputGroup, InputGroupText, Icon } from "@sveltestrap/sveltestrap";
import { formatTime, roundTwoDigits } from "./units.js";
import Comparogram from "./plots/Comparogram.svelte";
const ccconfig = getContext("cc-config"),
// initialized = getContext("initialized"),
globalMetrics = getContext("globalMetrics");
export let matchedCompareJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
export let filterBuffer = [];
let filter = [...filterBuffer] || [];
let comparePlotData = {};
let compareTableData = [];
let compareTableSorting = {};
let jobIds = [];
let jobClusters = [];
let tableJobIDFilter = "";
/*uPlot*/
let plotSync = uPlot.sync("compareJobsView");
/* GQL */
const client = getContextClient();
// Pull All Series For Metrics Statistics Only On Node Scope
const compareQuery = gql`
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
jobsMetricStats(filter: $filter, metrics: $metrics) {
id
jobId
startTime
duration
cluster
subCluster
numNodes
numHWThreads
numAccelerators
stats {
name
data {
min
avg
max
}
}
}
}
`;
/* REACTIVES */
$: compareData = queryStore({
client: client,
query: compareQuery,
variables:{ filter, metrics },
});
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1;
$: if ($compareData.data != null) {
jobIds = [];
jobClusters = [];
comparePlotData = {};
compareTableData = [...$compareData.data.jobsMetricStats];
jobs2uplot($compareData.data.jobsMetricStats, metrics);
}
$: if ((!$compareData.fetching && !$compareData.error) && metrics) {
// Meta
compareTableSorting['meta'] = {
startTime: { dir: "down", active: true },
duration: { dir: "up", active: false },
cluster: { dir: "up", active: false },
};
// Resources
compareTableSorting['resources'] = {
Nodes: { dir: "up", active: false },
Threads: { dir: "up", active: false },
Accs: { dir: "up", active: false },
};
// Metrics
for (let metric of metrics) {
compareTableSorting[metric] = {
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
}
}
/* FUNCTIONS */
// (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor });
}
filter = filters;
}
}
function sortBy(key, field) {
let s = compareTableSorting[key][field];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let key in compareTableSorting)
for (let field in compareTableSorting[key]) compareTableSorting[key][field].active = false;
s.active = true;
}
compareTableSorting = { ...compareTableSorting };
if (key == 'resources') {
let longField = "";
switch (field) {
case "Nodes":
longField = "numNodes"
break
case "Threads":
longField = "numHWThreads"
break
case "Accs":
longField = "numAccelerators"
break
default:
console.log("Unknown Res Field", field)
}
compareTableData = compareTableData.sort((j1, j2) => {
if (j1[longField] == null || j2[longField] == null) return -1;
return s.dir != "up" ? j1[longField] - j2[longField] : j2[longField] - j1[longField];
});
} else if (key == 'meta') {
compareTableData = compareTableData.sort((j1, j2) => {
if (j1[field] == null || j2[field] == null) return -1;
if (field == 'cluster') {
let c1 = `${j1.cluster} (${j1.subCluster})`
let c2 = `${j2.cluster} (${j2.subCluster})`
return s.dir != "up" ? c1.localeCompare(c2) : c2.localeCompare(c1)
} else {
return s.dir != "up" ? j1[field] - j2[field] : j2[field] - j1[field];
}
});
} else {
compareTableData = compareTableData.sort((j1, j2) => {
let s1 = j1.stats.find((m) => m.name == key)?.data;
let s2 = j2.stats.find((m) => m.name == key)?.data;
if (s1 == null || s2 == null) return -1;
return s.dir != "up" ? s1[field] - s2[field] : s2[field] - s1[field];
});
}
}
function jobs2uplot(jobs, metrics) {
// Resources Init
comparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
// Metric Init
for (let m of metrics) {
// Get Unit
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
}
// Iterate jobs if exists
if (jobs) {
let plotIndex = 0
jobs.forEach((j) => {
// Collect JobIDs & Clusters for X-Ticks and Legend
jobIds.push(j.jobId)
jobClusters.push(`${j.cluster} ${j.subCluster}`)
// Resources
comparePlotData['resources'].data[0].push(plotIndex)
comparePlotData['resources'].data[1].push(j.startTime)
comparePlotData['resources'].data[2].push(j.duration)
comparePlotData['resources'].data[3].push(j.numNodes)
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
// Metrics
for (let s of j.stats) {
comparePlotData[s.name].data[0].push(plotIndex)
comparePlotData[s.name].data[1].push(j.startTime)
comparePlotData[s.name].data[2].push(j.duration)
comparePlotData[s.name].data[3].push(s.data.min)
comparePlotData[s.name].data[4].push(s.data.avg)
comparePlotData[s.name].data[5].push(s.data.max)
}
plotIndex++
})
}
}
// Adapt for Persisting Job Selections in DB later down the line
// const updateConfigurationMutation = ({ name, value }) => {
// return mutationStore({
// client: client,
// query: gql`
// mutation ($name: String!, $value: String!) {
// updateConfiguration(name: $name, value: $value)
// }
// `,
// variables: { name, value },
// });
// };
// function updateConfiguration(value, page) {
// updateConfigurationMutation({
// name: "plot_list_jobsPerPage",
// value: value,
// }).subscribe((res) => {
// if (res.fetching === false && !res.error) {
// jobs = [] // Empty List
// paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
// } else if (res.fetching === false && res.error) {
// throw res.error;
// }
// });
// }
</script>
{#if $compareData.fetching}
<Row>
<Col>
<Spinner secondary />
</Col>
</Row>
{:else if $compareData.error}
<Row>
<Col>
<Card body color="danger" class="mb-3"
><h2>{$compareData.error.message}</h2></Card
>
</Col>
</Row>
{:else}
{#key comparePlotData}
<Row>
<Col>
<Comparogram
title={'Compare Resources'}
xlabel="JobIDs"
xticks={jobIds}
xinfo={jobClusters}
ylabel={'Resource Counts'}
data={comparePlotData['resources'].data}
{plotSync}
forResources
/>
</Col>
</Row>
{#each metrics as m}
<Row>
<Col>
<Comparogram
title={`Compare Metric '${m}'`}
xlabel="JobIDs"
xticks={jobIds}
xinfo={jobClusters}
ylabel={m}
metric={m}
yunit={comparePlotData[m].unit}
data={comparePlotData[m].data}
{plotSync}
/>
</Col>
</Row>
{/each}
{/key}
<hr/>
<Card>
<Table hover>
<thead>
<!-- Header Row 1 -->
<tr>
<th style="width:8%; max-width:10%;">JobID</th>
<th>StartTime</th>
<th>Duration</th>
<th>Cluster</th>
<th colspan="3">Resources</th>
{#each metrics as metric}
<th colspan="3">{metric} {comparePlotData[metric]?.unit? `(${comparePlotData[metric]?.unit})` : ''}</th>
{/each}
</tr>
<!-- Header Row 2: Fields -->
<tr>
<th>
<InputGroup size="sm">
<Input type="text" bind:value={tableJobIDFilter}/>
<InputGroupText>
<Icon name="search"></Icon>
</InputGroupText>
</InputGroup>
</th>
<th on:click={() => sortBy('meta', 'startTime')}>
Sort
<Icon
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
.active
? '-fill'
: ''}"
/>
</th>
<th on:click={() => sortBy('meta', 'duration')}>
Sort
<Icon
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
.active
? '-fill'
: ''}"
/>
</th>
<th on:click={() => sortBy('meta', 'cluster')}>
Sort
<Icon
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
.active
? '-fill'
: ''}"
/>
</th>
{#each ["Nodes", "Threads", "Accs"] as res}
<th on:click={() => sortBy('resources', res)}>
{res}
<Icon
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
.active
? '-fill'
: ''}"
/>
</th>
{/each}
{#each metrics as metric}
{#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}>
{stat.charAt(0).toUpperCase() + stat.slice(1)}
<Icon
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
.active
? '-fill'
: ''}"
/>
</th>
{/each}
{/each}
</tr>
</thead>
<tbody>
{#each compareTableData.filter((j) => j.jobId.includes(tableJobIDFilter)) as job (job.id)}
<tr>
<td><b><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a></b></td>
<td>{new Date(job.startTime * 1000).toLocaleString()}</td>
<td>{formatTime(job.duration)}</td>
<td>{job.cluster} ({job.subCluster})</td>
<td>{job.numNodes}</td>
<td>{job.numHWThreads}</td>
<td>{job.numAccelerators}</td>
{#each metrics as metric}
<td>{roundTwoDigits(job.stats.find((s) => s.name == metric).data.min)}</td>
<td>{roundTwoDigits(job.stats.find((s) => s.name == metric).data.avg)}</td>
<td>{roundTwoDigits(job.stats.find((s) => s.name == metric).data.max)}</td>
{/each}
</tr>
{:else}
<tr>
<td colspan={7 + (metrics.length * 3)}><b>No jobs found.</b></td>
</tr>
{/each}
</tbody>
</Table>
</Card>
{/if}

View File

@@ -35,15 +35,17 @@
}
export let sorting = { field: "startTime", type: "col", order: "DESC" };
export let matchedJobs = 0;
export let matchedListJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
export let showFootprint;
export let filterBuffer = [];
export let selectedJobs = [];
let usePaging = ccconfig.job_list_usePaging
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
let page = 1;
let paging = { itemsPerPage, page };
let filter = [];
let filter = [...filterBuffer];
let lastFilter = [];
let lastSorting = null;
let triggerMetricRefresh = false;
@@ -141,7 +143,7 @@
}
}
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refreshJobs() {
@@ -284,7 +286,10 @@
</tr>
{:else}
{#each jobs as job (job)}
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} />
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]}
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)}
/>
{:else}
<tr>
<td colspan={metrics.length + 1}> No jobs found </td>
@@ -310,7 +315,7 @@
bind:page
{itemsPerPage}
itemText="Jobs"
totalItems={matchedJobs}
totalItems={matchedListJobs}
on:update-paging={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page);

View File

@@ -24,6 +24,7 @@
ModalBody,
ModalHeader,
ModalFooter,
Input
} from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
@@ -40,11 +41,18 @@
export let isHwthreadsModified = false;
export let isAccsModified = false;
export let namedNode = null;
export let nodeMatch = "eq"
let pendingNumNodes = numNodes,
pendingNumHWThreads = numHWThreads,
pendingNumAccelerators = numAccelerators,
pendingNamedNode = namedNode;
pendingNamedNode = namedNode,
pendingNodeMatch = nodeMatch;
const nodeMatchLabels = {
eq: "Equal To",
contains: "Contains",
}
const findMaxNumAccels = (clusters) =>
clusters.reduce(
@@ -145,7 +153,17 @@
<ModalHeader>Select number of utilized Resources</ModalHeader>
<ModalBody>
<h6>Named Node</h6>
<input type="text" class="form-control" bind:value={pendingNamedNode} />
<div class="d-flex">
<Input type="text" class="w-75" bind:value={pendingNamedNode} />
<div class="mx-1"></div>
<Input type="select" class="w-25" bind:value={pendingNodeMatch}>
{#each Object.entries(nodeMatchLabels) as [nodeMatchKey, nodeMatchLabel]}
<option value={nodeMatchKey}>
{nodeMatchLabel}
</option>
{/each}
</Input>
</div>
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
@@ -215,11 +233,13 @@
to: pendingNumAccelerators.to,
};
namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
nodeMatch
});
}}
>
@@ -233,6 +253,7 @@
pendingNumHWThreads = { from: null, to: null };
pendingNumAccelerators = { from: null, to: null };
pendingNamedNode = null;
pendingNodeMatch = null;
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = {
from: pendingNumHWThreads.from,
@@ -246,11 +267,13 @@
isHwthreadsModified = false;
isAccsModified = false;
namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
nodeMatch
});
}}>Reset</Button
>

View File

@@ -17,7 +17,6 @@
import { parse, format, sub } from "date-fns";
import {
Row,
Col,
Button,
Input,
Modal,
@@ -34,8 +33,7 @@
export let from = null;
export let to = null;
export let range = "";
let pendingFrom, pendingTo;
export let startTimeSelectOptions;
const now = new Date(Date.now());
const ago = sub(now, { months: 1 });
@@ -48,12 +46,24 @@
time: format(now, "HH:mm"),
};
function reset() {
pendingFrom = from == null ? defaultFrom : fromRFC3339(from);
pendingTo = to == null ? defaultTo : fromRFC3339(to);
}
$: pendingFrom = (from == null) ? defaultFrom : fromRFC3339(from)
$: pendingTo = (to == null) ? defaultTo : fromRFC3339(to)
$: pendingRange = range
reset();
$: isModified =
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
(range != pendingRange) &&
!(
from == null &&
pendingFrom.date == "0000-00-00" &&
pendingFrom.time == "00:00"
) &&
!(
to == null &&
pendingTo.date == "0000-00-00" &&
pendingTo.time == "00:00"
) &&
!( range == "" && pendingRange == "");
function toRFC3339({ date, time }, secs = "00") {
const parsedDate = parse(
@@ -71,19 +81,6 @@
time: format(parsedDate, "HH:mm"),
};
}
$: isModified =
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
!(
from == null &&
pendingFrom.date == "0000-00-00" &&
pendingFrom.time == "00:00"
) &&
!(
to == null &&
pendingTo.date == "0000-00-00" &&
pendingTo.time == "00:00"
);
</script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -92,52 +89,82 @@
{#if range !== ""}
<h4>Current Range</h4>
<Row>
<Col>
<Input type="text" value={range} disabled/>
</Col>
<FormGroup class="col">
<Input type ="select" bind:value={pendingRange} >
{#each startTimeSelectOptions as { rangeLabel, range }}
<option label={rangeLabel} value={range}/>
{/each}
</Input>
</FormGroup>
</Row>
{/if}
<h4>From</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date} />
<Input type="date" bind:value={pendingFrom.date} disabled={pendingRange !== ""}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time} />
<Input type="time" bind:value={pendingFrom.time} disabled={pendingRange !== ""}/>
</FormGroup>
</Row>
<h4>To</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingTo.date} />
<Input type="date" bind:value={pendingTo.date} disabled={pendingRange !== ""}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingTo.time} />
<Input type="time" bind:value={pendingTo.time} disabled={pendingRange !== ""}/>
</FormGroup>
</Row>
</ModalBody>
<ModalFooter>
<Button
color="primary"
disabled={pendingFrom.date == "0000-00-00" ||
pendingTo.date == "0000-00-00"}
on:click={() => {
isOpen = false;
from = toRFC3339(pendingFrom);
to = toRFC3339(pendingTo, "59");
dispatch("set-filter", { from, to });
}}
>
Close & Apply
</Button>
{#if pendingRange !== ""}
<Button
color="warning"
disabled={pendingRange === ""}
on:click={() => {
pendingRange = ""
}}
>
Reset Range
</Button>
<Button
color="primary"
disabled={pendingRange === ""}
on:click={() => {
isOpen = false;
from = null;
to = null;
range = pendingRange;
dispatch("set-filter", { from, to, range });
}}
>
Close & Apply Range
</Button>
{:else}
<Button
color="primary"
disabled={pendingFrom.date == "0000-00-00" ||
pendingTo.date == "0000-00-00"}
on:click={() => {
isOpen = false;
from = toRFC3339(pendingFrom);
to = toRFC3339(pendingTo, "59");
range = "";
dispatch("set-filter", { from, to, range });
}}
>
Close & Apply Dates
</Button>
{/if}
<Button
color="danger"
on:click={() => {
isOpen = false;
from = null;
to = null;
reset();
dispatch("set-filter", { from, to });
range = "";
dispatch("set-filter", { from, to, range });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>

View File

@@ -18,6 +18,8 @@
export let username = null;
export let authlevel= null;
export let roles = null;
export let isSelected = null;
export let showSelect = false;
function formatDuration(duration) {
const hours = Math.floor(duration / 3600);
@@ -76,18 +78,39 @@
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
({job.cluster})
</span>
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={() => clipJobId(job.jobId)} >
{#if displayCheck}
<Icon name="clipboard2-check-fill"/>
{:else}
<Icon name="clipboard2"/>
<span>
{#if showSelect}
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
on:click={() => {
isSelected = !isSelected
}}>
{#if isSelected}
<Icon name="check-square"/>
{:else if isSelected == false}
<Icon name="square"/>
{:else}
<Icon name="plus-square-dotted" />
{/if}
</Button>
<Tooltip
target={`${job.cluster}-${job.jobId}-select`}
placement="left">
{ 'Add or Remove Job to/from Comparison Selection' }
</Tooltip>
{/if}
</Button>
<Tooltip
target={`${job.cluster}-${job.jobId}-clipboard`}
placement="right">
{ displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' }
</Tooltip>
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} >
{#if displayCheck}
<Icon name="clipboard2-check-fill"/>
{:else}
<Icon name="clipboard2"/>
{/if}
</Button>
<Tooltip
target={`${job.cluster}-${job.jobId}-clipboard`}
placement="right">
{ displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' }
</Tooltip>
</span>
</span>
{#if job.metaData?.jobName}
{#if job.metaData?.jobName.length <= 25}

View File

@@ -12,7 +12,7 @@
<script>
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte";
import { getContext, createEventDispatcher } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../utils.js";
import JobInfo from "./JobInfo.svelte";
@@ -25,7 +25,9 @@
export let plotHeight = 275;
export let showFootprint;
export let triggerMetricRefresh = false;
export let previousSelect = false;
const dispatch = createEventDispatcher();
const resampleConfig = getContext("resampling") || null;
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
@@ -38,6 +40,8 @@
let selectedResolution = resampleDefault;
let zoomStates = {};
let thresholdStates = {};
$: isSelected = previousSelect || null;
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const client = getContextClient();
@@ -112,6 +116,12 @@
refreshMetrics();
}
$: if (isSelected == true && previousSelect == false) {
dispatch("select-job", job.id)
} else if (isSelected == false && previousSelect == true) {
dispatch("unselect-job", job.id)
}
// Helper
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
@@ -152,7 +162,7 @@
<tr>
<td>
<JobInfo {job} />
<JobInfo {job} bind:isSelected showSelect/>
</td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan={metrics.length}>

View File

@@ -0,0 +1,314 @@
<!--
@component Main plot component, based on uPlot; metricdata values by time
Only width/height should change reactively.
Properties:
- `metric String`: The metric name
- `width Number?`: The plot width [Default: 0]
- `height Number?`: The plot height [Default: 300]
- `data [Array]`: The metric data object
- `cluster String`: Cluster name of the parent job / data
- `subCluster String`: Name of the subCluster of the parent job
-->
<script>
import uPlot from "uplot";
import { roundTwoDigits, formatTime, formatNumber } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap";
export let metric = "";
export let width = 0;
export let height = 300;
export let data = null;
export let xlabel = "";
export let xticks = [];
export let xinfo = [];
export let ylabel = "";
export let yunit = "";
export let title = "";
export let forResources = false;
export let plotSync;
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
const clusterCockpitConfig = getContext("cc-config");
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
// UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
minWidth: "100px",
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// hide series color markers:
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
}
function update(u) {
const { left, top } = u.cursor;
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
},
};
}
const plotSeries = [
{
label: "JobID",
scale: "x",
value: (u, ts, sidx, didx) => {
return `${xticks[didx]} | ${xinfo[didx]}`;
},
},
{
label: "Starttime",
scale: "xst",
value: (u, ts, sidx, didx) => {
return new Date(ts * 1000).toLocaleString();
},
},
{
label: "Duration",
scale: "xrt",
value: (u, ts, sidx, didx) => {
return formatTime(ts);
},
},
]
if (forResources) {
const resSeries = [
{
label: "Nodes",
scale: "y",
width: lineWidth,
stroke: "black",
},
{
label: "Threads",
scale: "y",
width: lineWidth,
stroke: "rgb(0,0,255)",
},
{
label: "Accelerators",
scale: "y",
width: lineWidth,
stroke: cbmode ? "rgb(0,255,0)" : "red",
}
];
plotSeries.push(...resSeries)
} else {
const statsSeries = [
{
label: "Min",
scale: "y",
width: lineWidth,
stroke: cbmode ? "rgb(0,255,0)" : "red",
value: (u, ts, sidx, didx) => {
return `${roundTwoDigits(ts)} ${yunit}`;
},
},
{
label: "Avg",
scale: "y",
width: lineWidth,
stroke: "black",
value: (u, ts, sidx, didx) => {
return `${roundTwoDigits(ts)} ${yunit}`;
},
},
{
label: "Max",
scale: "y",
width: lineWidth,
stroke: cbmode ? "rgb(0,0,255)" : "green",
value: (u, ts, sidx, didx) => {
return `${roundTwoDigits(ts)} ${yunit}`;
},
}
];
plotSeries.push(...statsSeries)
};
const plotBands = [
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
];
const opts = {
width,
height,
title,
plugins: [legendAsTooltipPlugin()],
series: plotSeries,
axes: [
{
scale: "x",
space: 25, // Tick Spacing
rotate: 30,
show: true,
label: xlabel,
values(self, splits) {
return splits.map(s => xticks[s]);
}
},
{
scale: "xst",
show: false,
},
{
scale: "xrt",
show: false,
},
{
scale: "y",
grid: { show: true },
labelFont: "sans-serif",
label: ylabel + (yunit ? ` (${yunit})` : ''),
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
bands: forResources ? [] : plotBands,
padding: [5, 10, 0, 0],
hooks: {
draw: [
(u) => {
// Draw plot type label:
let textl = forResources ? "Job Resources by Type" : "Metric Min/Avg/Max for Job Duration";
let textr = "Earlier <- StartTime -> Later";
u.ctx.save();
u.ctx.textAlign = "start";
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10);
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + 10,
);
u.ctx.restore();
return;
},
]
},
scales: {
x: { time: false },
xst: { time: false },
xrt: { time: false },
y: {auto: true, distr: forResources ? 3 : 1},
},
legend: {
// Display legend
show: true,
live: true,
},
cursor: {
drag: { x: true, y: true },
sync: {
key: plotSync.key,
scales: ["x", null],
}
}
};
// RENDER HANDLING
let plotWrapper = null;
let uplot = null;
let timeoutId = null;
function render(ren_width, ren_height) {
if (!uplot) {
opts.width = ren_width;
opts.height = ren_height;
uplot = new uPlot(opts, data, plotWrapper); // Data is uplot formatted [[X][Ymin][Yavg][Ymax]]
plotSync.sub(uplot)
} else {
uplot.setSize({ width: ren_width, height: ren_height });
}
}
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
onMount(() => {
if (plotWrapper) {
render(width, height);
}
});
onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId);
if (uplot) uplot.destroy();
});
// This updates plot on all size changes if wrapper (== data) exists
$: if (plotWrapper) {
onSizeChange(width, height);
}
</script>
<!-- Define $width Wrapper and NoData Card -->
{#if data && data[0].length > 0}
<div bind:this={plotWrapper} bind:clientWidth={width}
style="background-color: rgba(255, 255, 255, 1.0);" class="rounded"
/>
{:else}
<Card body color="warning" class="mx-4 my-2"
>Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card
>
{/if}

View File

@@ -16,7 +16,7 @@
<script>
import uPlot from "uplot";
import { onMount, onDestroy } from "svelte";
import { formatNumber } from "../units.js";
import { formatNumber, formatTime } from "../units.js";
import { Card } from "@sveltestrap/sveltestrap";
export let data;
@@ -36,21 +36,6 @@
points: 2,
};
function formatTime(t) {
if (t !== null) {
if (isNaN(t)) {
return t;
} else {
const tAbs = Math.abs(t);
const h = Math.floor(tAbs / 3600);
const m = Math.floor((tAbs % 3600) / 60);
if (h == 0) return `${m}m`;
else if (m == 0) return `${h}h`;
else return `${h}:${m}h`;
}
}
}
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
let s = u.series[seriesIdx];
let style = s.drawStyle;

View File

@@ -21,22 +21,6 @@
-->
<script context="module">
function formatTime(t, forNode = false) {
if (t !== null) {
if (isNaN(t)) {
return t;
} else {
const tAbs = Math.abs(t);
const h = Math.floor(tAbs / 3600);
const m = Math.floor((tAbs % 3600) / 60);
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
else return `${forNode ? "-" : ""}${h}:${m}h`;
}
}
}
function timeIncrs(timestep, maxX, forNode) {
if (forNode === true) {
return [60, 120, 240, 300, 360, 480, 600, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
@@ -118,7 +102,7 @@
<script>
import uPlot from "uplot";
import { formatNumber } from "../units.js";
import { formatNumber, formatTime } from "../units.js";
import { getContext, onMount, onDestroy, createEventDispatcher } from "svelte";
import { Card } from "@sveltestrap/sveltestrap";

View File

@@ -58,7 +58,7 @@
const getValues = (type) => labels.map(name => {
// Peak is adapted and scaled for job shared state
const peak = polarMetrics.find(m => m?.name == name)?.peak
const metric = polarData.find(m => m?.name == name)?.stats
const metric = polarData.find(m => m?.name == name)?.data
const value = (peak && metric) ? (metric[type] / peak) : 0
return value <= 1. ? value : 1.
})

View File

@@ -28,6 +28,7 @@
export let configName;
export let allMetrics = null;
export let cluster = null;
export let subCluster = null;
export let showFootprint = false;
export let footprintSelect = false;
@@ -44,25 +45,29 @@
for (let metric of globalMetrics) allMetrics.add(metric.name);
});
$: if (newMetricsOrder.length === 0) {
if (allMetrics != null) {
if (cluster == null) {
for (let metric of globalMetrics) allMetrics.add(metric.name);
} else {
allMetrics.clear();
for (let gm of globalMetrics) {
$: {
if (allMetrics != null) {
if (!cluster) {
for (let metric of globalMetrics) allMetrics.add(metric.name);
} else {
allMetrics.clear();
for (let gm of globalMetrics) {
if (!subCluster) {
if (gm.availability.find((av) => av.cluster === cluster)) allMetrics.add(gm.name);
} else {
if (gm.availability.find((av) => av.cluster === cluster && av.subClusters.includes(subCluster))) allMetrics.add(gm.name);
}
}
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
}
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
}
}
function printAvailability(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (cluster == null) {
if (!cluster) {
return avail.map((av) => av.cluster).join(',')
} else {
return avail.find((av) => av.cluster === cluster).subClusters.join(',')
@@ -110,10 +115,17 @@
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
isOpen = false;
showFootprint = !!pendingShowFootprint;
let configKey;
if (cluster && subCluster) {
configKey = `${configName}:${cluster}:${subCluster}`;
} else if (cluster && !subCluster) {
configKey = `${configName}:${cluster}`;
} else {
configKey = `${configName}`;
}
updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`,
name: configKey,
value: JSON.stringify(metrics),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
@@ -121,17 +133,20 @@
}
});
updateConfigurationMutation({
name:
cluster == null
? "plot_list_showFootprint"
: `plot_list_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
}
});
if (footprintSelect) {
showFootprint = !!pendingShowFootprint;
updateConfigurationMutation({
name:
!cluster
? "plot_list_showFootprint"
: `plot_list_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
}
});
};
dispatch('update-metrics', metrics);
}

View File

@@ -17,6 +17,10 @@ export function formatNumber(x) {
}
}
export function roundTwoDigits(x) {
return Math.round(x * 100) / 100
}
export function scaleNumbers(x, y , p = '') {
const oldPower = power[prefix.indexOf(p)]
const rawXValue = x * oldPower
@@ -31,4 +35,20 @@ export function scaleNumbers(x, y , p = '') {
return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}`
}
export function formatTime(t, forNode = false) {
if (t !== null) {
if (isNaN(t)) {
return t;
} else {
const tAbs = Math.abs(t);
const h = Math.floor(tAbs / 3600);
const m = Math.floor((tAbs % 3600) / 60);
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
else return `${forNode ? "-" : ""}${h}:${m}h`;
}
}
}
// export const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);

View File

@@ -461,11 +461,11 @@ export function convert2uplot(canvasData, secondsToMinutes = false, secondsToHou
} else { // Default -> Fill Histodata with zero values on unused value placing -> maybe allows zoom trigger as known
if (secondsToHours) {
let hours = cd.value / 3600
console.log("x seconds to y hours", cd.value, hours)
// console.log("x seconds to y hours", cd.value, hours)
uplotData[0].push(hours)
} else if (secondsToMinutes) {
let minutes = cd.value / 60
console.log("x seconds to y minutes", cd.value, minutes)
// console.log("x seconds to y minutes", cd.value, minutes)
uplotData[0].push(minutes)
} else {
uplotData[0].push(cd.value)

View File

@@ -14,7 +14,6 @@
<script>
import {
getContext,
createEventDispatcher
} from "svelte";
import {
queryStore,
@@ -56,7 +55,6 @@
let pendingZoomState = null;
let thresholdState = null;
const dispatch = createEventDispatcher();
const statsPattern = /(.*)-stat$/;
const unit = (metricUnit?.prefix ? metricUnit.prefix : "") + (metricUnit?.base ? metricUnit.base : "");
const client = getContextClient();
@@ -150,11 +148,6 @@
// On additional scope request
if (selectedScope == "load-all") {
// Push scope to statsTable (Needs to be in this case, else newly selected 'Metric.svelte' renders cause statsTable race condition)
const statsTableData = $metricData.data.singleUpdate.filter((x) => x.scope !== "node")
if (statsTableData.length > 0) {
dispatch("more-loaded", statsTableData);
}
// Set selected scope to min of returned scopes
selectedScope = minScope(scopes)
nodeOnly = (selectedScope == "node") // "node" still only scope after load-all

View File

@@ -0,0 +1,145 @@
<!--
@component Job-View subcomponent; Wraps the statsTable in a TabPane and contains GQL query for scoped statsData
Properties:
- `job Object`: The job object
- `clusters Object`: The clusters object
- `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation
-->
<script>
import {
queryStore,
gql,
getContextClient
} from "@urql/svelte";
import { getContext } from "svelte";
import {
Card,
Button,
Row,
Col,
TabPane,
Spinner,
Icon
} from "@sveltestrap/sveltestrap";
import MetricSelection from "../generic/select/MetricSelection.svelte";
import StatsTable from "./statstab/StatsTable.svelte";
export let job;
export let clusters;
export let tabActive;
let loadScopes = false;
let selectedScopes = [];
let selectedMetrics = [];
let availableMetrics = new Set(); // For Info Only, filled by MetricSelection Component
let isMetricSelectionOpen = false;
const client = getContextClient();
const query = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
name
scope
stats {
hostname
id
data {
min
avg
max
}
}
}
}
`;
$: scopedStats = queryStore({
client: client,
query: query,
variables: { dbid: job.id, selectedMetrics, selectedScopes },
});
$: if (loadScopes) {
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"];
}
// Handle Job Query on Init -> is not executed anymore
getContext("on-init")(() => {
if (!job) return;
const pendingMetrics = (
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`]
) || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
// Select default Scopes to load: Check before if any metric has accelerator scope by default
const accScopeDefault = [...pendingMetrics].some(function (m) {
const cluster = clusters.find((c) => c.name == job.cluster);
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
});
const pendingScopes = ["node"]
if (job.numNodes === 1) {
pendingScopes.push("socket")
pendingScopes.push("core")
pendingScopes.push("hwthread")
if (accScopeDefault) { pendingScopes.push("accelerator") }
}
selectedMetrics = [...pendingMetrics];
selectedScopes = [...pendingScopes];
});
</script>
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
<Row>
<Col class="m-2">
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
</Button>
{#if job.numNodes > 1}
<Button class="px-2 ml-auto" color="success" outline on:click={() => (loadScopes = !loadScopes)} disabled={loadScopes}>
{#if !loadScopes}
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
{:else}
<Icon name="check-square-fill" style="margin-right:0.25rem"/> OK: Scopes Added
{/if}
</Button>
{/if}
</Col>
</Row>
<hr class="mb-1 mt-1"/>
<!-- ROW1: Status-->
{#if $scopedStats.fetching}
<Row>
<Col class="m-3" style="text-align: center;">
<Spinner secondary/>
</Col>
</Row>
{:else if $scopedStats.error}
<Row>
<Col class="m-2">
<Card body color="danger">{$scopedStats.error.message}</Card>
</Col>
</Row>
{:else}
<StatsTable
hosts={job.resources.map((r) => r.hostname).sort()}
data={$scopedStats?.data?.scopedJobStats}
{selectedMetrics}
/>
{/if}
</TabPane>
<MetricSelection
cluster={job.cluster}
subCluster={job.subCluster}
configName="job_view_nodestats_selectedMetrics"
bind:allMetrics={availableMetrics}
bind:metrics={selectedMetrics}
bind:isOpen={isMetricSelectionOpen}
/>

View File

@@ -1,176 +0,0 @@
<!--
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
Properties:
- `job Object`: The job object
- `jobMetrics [Object]`: The jobs metricdata
Exported:
- `moreLoaded`: Adds additional scopes requested from Metric.svelte in Job-View
-->
<script>
import { getContext } from "svelte";
import {
Button,
Table,
Input,
InputGroup,
InputGroupText,
Icon,
Row,
Col
} from "@sveltestrap/sveltestrap";
import { maxScope } from "../generic/utils.js";
import StatsTableEntry from "./StatsTableEntry.svelte";
import MetricSelection from "../generic/select/MetricSelection.svelte";
export let job;
export let jobMetrics;
const sortedJobMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort()
const scopesForMetric = (metric) =>
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
let hosts = job.resources.map((r) => r.hostname).sort(),
selectedScopes = {},
sorting = {},
isMetricSelectionOpen = false,
availableMetrics = new Set(),
selectedMetrics =
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
getContext("cc-config")["job_view_nodestats_selectedMetrics"];
for (let metric of sortedJobMetrics) {
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
// -> Else: Load smallest available granularity as default as per availability
const availableScopes = scopesForMetric(metric);
if (job.exclusive != 1 || job.numNodes == 1) {
if (availableScopes.includes("accelerator")) {
selectedScopes[metric] = "accelerator";
} else if (availableScopes.includes("core")) {
selectedScopes[metric] = "core";
} else if (availableScopes.includes("socket")) {
selectedScopes[metric] = "socket";
} else {
selectedScopes[metric] = "node";
}
} else {
selectedScopes[metric] = maxScope(availableScopes);
}
sorting[metric] = {
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
}
function sortBy(metric, stat) {
let s = sorting[metric][stat];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let metric in sorting)
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
s.active = true;
}
let series = jobMetrics.find(
(jm) => jm.name == metric && jm.scope == "node",
)?.metric.series;
sorting = { ...sorting };
hosts = hosts.sort((h1, h2) => {
let s1 = series.find((s) => s.hostname == h1)?.statistics;
let s2 = series.find((s) => s.hostname == h2)?.statistics;
if (s1 == null || s2 == null) return -1;
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
});
}
export function moreLoaded(moreJobMetrics) {
moreJobMetrics.forEach(function (newMetric) {
if (!jobMetrics.some((m) => m.scope == newMetric.scope)) {
jobMetrics = [...jobMetrics, newMetric]
}
});
};
</script>
<Row>
<Col class="m-2">
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="w-auto px-2" color="primary">
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
</Button>
</Col>
</Row>
<hr class="mb-1 mt-1"/>
<Table class="mb-0">
<thead>
<!-- Header Row 1: Selectors -->
<tr>
<th/>
{#each selectedMetrics as metric}
<!-- To Match Row-2 Header Field Count-->
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
<InputGroup>
<InputGroupText>
{metric}
</InputGroupText>
<Input type="select" bind:value={selectedScopes[metric]}>
{#each scopesForMetric(metric, jobMetrics) as scope}
<option value={scope}>{scope}</option>
{/each}
</Input>
</InputGroup>
</th>
{/each}
</tr>
<!-- Header Row 2: Fields -->
<tr>
<th>Node</th>
{#each selectedMetrics as metric}
{#if selectedScopes[metric] != "node"}
<th>Id</th>
{/if}
{#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}>
{stat}
{#if selectedScopes[metric] == "node"}
<Icon
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
.active
? '-fill'
: ''}"
/>
{/if}
</th>
{/each}
{/each}
</tr>
</thead>
<tbody>
{#each hosts as host (host)}
<tr>
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
{host}
{metric}
scope={selectedScopes[metric]}
{jobMetrics}
/>
{/each}
</tr>
{/each}
</tbody>
</Table>
<MetricSelection
cluster={job.cluster}
configName="job_view_nodestats_selectedMetrics"
bind:allMetrics={availableMetrics}
bind:metrics={selectedMetrics}
bind:isOpen={isMetricSelectionOpen}
/>

View File

@@ -40,14 +40,14 @@
const client = getContextClient();
const polarQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!) {
jobMetricStats(id: $dbid, metrics: $selectedMetrics) {
jobStats(id: $dbid, metrics: $selectedMetrics) {
name
stats {
min
avg
max
}
data {
min
avg
max
}
}
}
`;
@@ -66,7 +66,7 @@
{:else}
<Polar
{polarMetrics}
polarData={$polarData.data.jobMetricStats}
polarData={$polarData.data.jobStats}
/>
{/if}
</CardBody>

View File

@@ -0,0 +1,139 @@
<!--:
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
Properties:
- `data Object`: The data object
- `selectedMetrics [String]`: The selected metrics
- `hosts [String]`: The list of hostnames of this job
-->
<script>
import {
Table,
Input,
InputGroup,
InputGroupText,
Icon,
} from "@sveltestrap/sveltestrap";
import StatsTableEntry from "./StatsTableEntry.svelte";
export let data = [];
export let selectedMetrics = [];
export let hosts = [];
let sorting = {};
let availableScopes = {};
let selectedScopes = {};
const scopesForMetric = (metric) =>
data?.filter((jm) => jm.name == metric)?.map((jm) => jm.scope) || [];
const setScopeForMetric = (metric, scope) =>
selectedScopes[metric] = scope
$: if (data && selectedMetrics) {
for (let metric of selectedMetrics) {
availableScopes[metric] = scopesForMetric(metric);
// Set Initial Selection, but do not use selectedScopes: Skips reactivity
if (availableScopes[metric].includes("accelerator")) {
setScopeForMetric(metric, "accelerator");
} else if (availableScopes[metric].includes("core")) {
setScopeForMetric(metric, "core");
} else if (availableScopes[metric].includes("socket")) {
setScopeForMetric(metric, "socket");
} else {
setScopeForMetric(metric, "node");
}
sorting[metric] = {
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
}
}
function sortBy(metric, stat) {
let s = sorting[metric][stat];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let metric in sorting)
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
s.active = true;
}
let stats = data.find(
(d) => d.name == metric && d.scope == "node",
)?.stats || [];
sorting = { ...sorting };
hosts = hosts.sort((h1, h2) => {
let s1 = stats.find((s) => s.hostname == h1)?.data;
let s2 = stats.find((s) => s.hostname == h2)?.data;
if (s1 == null || s2 == null) return -1;
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
});
}
</script>
<Table class="mb-0">
<thead>
<!-- Header Row 1: Selectors -->
<tr>
<th/>
{#each selectedMetrics as metric}
<!-- To Match Row-2 Header Field Count-->
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
<InputGroup>
<InputGroupText>
{metric}
</InputGroupText>
<Input type="select" bind:value={selectedScopes[metric]} disabled={availableScopes[metric].length === 1}>
{#each (availableScopes[metric] || []) as scope}
<option value={scope}>{scope}</option>
{/each}
</Input>
</InputGroup>
</th>
{/each}
</tr>
<!-- Header Row 2: Fields -->
<tr>
<th>Node</th>
{#each selectedMetrics as metric}
{#if selectedScopes[metric] != "node"}
<th>Id</th>
{/if}
{#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}>
{stat}
{#if selectedScopes[metric] == "node"}
<Icon
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
.active
? '-fill'
: ''}"
/>
{/if}
</th>
{/each}
{/each}
</tr>
</thead>
<tbody>
{#each hosts as host (host)}
<tr>
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
{data}
{host}
{metric}
scope={selectedScopes[metric]}
/>
{/each}
</tr>
{/each}
</tbody>
</Table>

View File

@@ -1,11 +1,11 @@
<!--
@component Job-View subcomponent; Single Statistics entry component fpr statstable
@component Job-View subcomponent; Single Statistics entry component for statstable
Properties:
- `host String`: The hostname (== node)
- `metric String`: The metric name
- `scope String`: The selected scope
- `jobMetrics [Object]`: The jobs metricdata
- `data [Object]`: The jobs statsdata
-->
<script>
@@ -14,59 +14,61 @@
export let host;
export let metric;
export let scope;
export let jobMetrics;
export let data;
function compareNumbers(a, b) {
return a.id - b.id;
}
function sortByField(field) {
let s = sorting[field];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let field in sorting) sorting[field].active = false;
s.active = true;
}
sorting = { ...sorting };
series = series.sort((a, b) => {
if (a == null || b == null) return -1;
if (field === "id") {
return s.dir != "up" ? a[field] - b[field] : b[field] - a[field];
} else {
return s.dir != "up"
? a.statistics[field] - b.statistics[field]
: b.statistics[field] - a.statistics[field];
}
});
}
let sorting = {
let entrySorting = {
id: { dir: "down", active: true },
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
$: series = jobMetrics
.find((jm) => jm.name == metric && jm.scope == scope)
?.metric.series.filter((s) => s.hostname == host && s.statistics != null)
?.sort(compareNumbers);
function compareNumbers(a, b) {
return a.id - b.id;
}
function sortByField(field) {
let s = entrySorting[field];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let field in entrySorting) entrySorting[field].active = false;
s.active = true;
}
entrySorting = { ...entrySorting };
stats = stats.sort((a, b) => {
if (a == null || b == null) return -1;
if (field === "id") {
return s.dir != "up" ?
a[field].localeCompare(b[field], undefined, {numeric: true, sensitivity: 'base'}) :
b[field].localeCompare(a[field], undefined, {numeric: true, sensitivity: 'base'})
} else {
return s.dir != "up"
? a.data[field] - b.data[field]
: b.data[field] - a.data[field];
}
});
}
$: stats = data
?.find((d) => d.name == metric && d.scope == scope)
?.stats.filter((s) => s.hostname == host && s.data != null)
?.sort(compareNumbers) || [];
</script>
{#if series == null || series.length == 0}
{#if stats == null || stats.length == 0}
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
{:else if series.length == 1 && scope == "node"}
{:else if stats.length == 1 && scope == "node"}
<td>
{series[0].statistics.min}
{stats[0].data.min}
</td>
<td>
{series[0].statistics.avg}
{stats[0].data.avg}
</td>
<td>
{series[0].statistics.max}
{stats[0].data.max}
</td>
{:else}
<td colspan="4">
@@ -77,19 +79,19 @@
<th on:click={() => sortByField(field)}>
Sort
<Icon
name="caret-{sorting[field].dir}{sorting[field].active
name="caret-{entrySorting[field].dir}{entrySorting[field].active
? '-fill'
: ''}"
/>
</th>
{/each}
</tr>
{#each series as s, i}
{#each stats as s, i}
<tr>
<th>{s.id ?? i}</th>
<td>{s.statistics.min}</td>
<td>{s.statistics.avg}</td>
<td>{s.statistics.max}</td>
<td>{s.data.min}</td>
<td>{s.data.avg}</td>
<td>{s.data.max}</td>
</tr>
{/each}
</tbody>

View File

@@ -205,7 +205,7 @@
</Col>
</Row>
{:else}
{#each nodes as nodeData}
{#each nodes as nodeData (nodeData.host)}
<NodeListRow {nodeData} {cluster} {selectedMetrics}/>
{:else}
<tr>
@@ -221,7 +221,7 @@
<p><b>
Loading nodes {nodes.length + 1} to
{ matchedNodes
? `${((nodes.length + paging.itemsPerPage) > matchedNodes) ? matchedNodes : (nodes.length + paging.itemsPerPage)} of ${matchedNodes} total`
? `${(nodes.length + paging.itemsPerPage) > matchedNodes ? matchedNodes : (nodes.length + paging.itemsPerPage)} of ${matchedNodes} total`
: (nodes.length + paging.itemsPerPage)
}
</b></p>

View File

@@ -17,6 +17,9 @@
Input,
InputGroup,
InputGroupText, } from "@sveltestrap/sveltestrap";
import {
scramble,
scrambleNames, } from "../../generic/utils.js";
export let cluster;
export let subCluster
@@ -32,8 +35,8 @@
let userList;
let projectList;
$: if (nodeJobsData) {
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.user) : j.user))).sort((a, b) => a.localeCompare(b));
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.project) : j.project))).sort((a, b) => a.localeCompare(b));
}
</script>
@@ -102,6 +105,19 @@
Shared
</Button>
</InputGroup>
<!-- Fallback -->
{:else if nodeJobsData.jobs.count >= 1}
<InputGroup>
<InputGroupText>
<Icon name="circle-fill"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="success" disabled>
Allocated Jobs
</Button>
</InputGroup>
{:else}
<InputGroup>
<InputGroupText>

View File

@@ -14,7 +14,7 @@
getContextClient,
} from "@urql/svelte";
import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
import { maxScope, checkMetricDisabled, scramble, scrambleNames } from "../../generic/utils.js";
import MetricPlot from "../../generic/plots/MetricPlot.svelte";
import NodeInfo from "./NodeInfo.svelte";
@@ -98,21 +98,24 @@
let extendedLegendData = null;
$: if ($nodeJobsData?.data) {
// Get Shared State of Node: Only Build extended Legend For Shared Nodes
if ($nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive) {
// Build Extended for allocated nodes [Commented: Only Build extended Legend For Shared Nodes]
if ($nodeJobsData.data.jobs.count >= 1) { // "&& !$nodeJobsData.data.jobs.items[0].exclusive)"
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
.map((i) => i.resources
.filter((r) => r.hostname === nodeData.host)
.map((r) => r.accelerators)
.filter((r) => (r.hostname === nodeData.host) && r?.accelerators)
.map((r) => r?.accelerators)
)
)).flat(2)
extendedLegendData = {}
for (const accId of accSet) {
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
const matchUser = matchJob?.user ? matchJob.user : null
extendedLegendData[accId] = {
user: matchJob?.user ? matchJob?.user : '-',
job: matchJob?.jobId ? matchJob?.jobId : '-',
user: (scrambleNames && matchUser)
? scramble(matchUser)
: (matchUser ? matchUser : '-'),
job: matchJob?.jobId ? matchJob.jobId : '-',
}
}
// Theoretically extendable for hwthreadIDs

View File

@@ -0,0 +1,15 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js'
import Tags from './Tags.root.svelte'
mount(Tags, {
target: document.getElementById('svelte-app'),
props: {
username: username,
isAdmin: isAdmin,
tagmap: tagmap,
},
context: new Map([
['cc-config', clusterCockpitConfig]
])
})