mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-03-15 04:17:30 +01:00
Merge latest state branch 'dev' into migrate_svelte5
This commit is contained in:
524
web/frontend/package-lock.json
generated
524
web/frontend/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
110
web/frontend/src/Tags.root.svelte
Normal file
110
web/frontend/src/Tags.root.svelte
Normal 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>
|
||||
@@ -352,7 +352,7 @@
|
||||
bind:metrics
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint
|
||||
footprintSelect={true}
|
||||
footprintSelect
|
||||
/>
|
||||
|
||||
<HistogramSelection
|
||||
|
||||
@@ -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
|
||||
|
||||
394
web/frontend/src/generic/JobCompare.svelte
Normal file
394
web/frontend/src/generic/JobCompare.svelte
Normal 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}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
314
web/frontend/src/generic/plots/Comparogram.svelte
Normal file
314
web/frontend/src/generic/plots/Comparogram.svelte
Normal 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}
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
145
web/frontend/src/job/StatsTab.svelte
Normal file
145
web/frontend/src/job/StatsTab.svelte
Normal 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}
|
||||
/>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
139
web/frontend/src/job/statstab/StatsTable.svelte
Normal file
139
web/frontend/src/job/statstab/StatsTable.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
15
web/frontend/src/tags.entrypoint.js
Normal file
15
web/frontend/src/tags.entrypoint.js
Normal 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]
|
||||
])
|
||||
})
|
||||
Reference in New Issue
Block a user