mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-22 20:41:40 +02:00
Merge branch 'master' into 166_add_scopes_analysis
This commit is contained in:
223
web/frontend/package-lock.json
generated
223
web/frontend/package-lock.json
generated
@@ -11,7 +11,9 @@
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@urql/svelte": "^4.0.1",
|
||||
"chart.js": "^4.3.3",
|
||||
"graphql": "^16.6.0",
|
||||
"svelte-chartjs": "^3.1.2",
|
||||
"sveltestrap": "^5.10.0",
|
||||
"uplot": "^1.6.24",
|
||||
"wonka": "^6.3.2"
|
||||
@@ -27,9 +29,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@0no-co/graphql.web": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.1.tgz",
|
||||
"integrity": "sha512-6Yaxyv6rOwRkLIvFaL0NrLDgfNqC/Ng9QOPmTmlqW4mORXMEKmh5NYGkIvvt5Yw8fZesnMAqkj8cIqTj8f40cQ==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.4.tgz",
|
||||
"integrity": "sha512-W3ezhHGfO0MS1PtGloaTpg0PbaT8aZSmmaerL7idtU5F7oCI+uu25k+MsMS31BVFlp4aMkHSrNRxiD72IlK8TA==",
|
||||
"peerDependencies": {
|
||||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
},
|
||||
@@ -40,9 +42,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
@@ -54,9 +56,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -72,9 +74,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
|
||||
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
@@ -82,24 +84,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
|
||||
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
|
||||
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
|
||||
"integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==",
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
@@ -131,9 +138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz",
|
||||
"integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==",
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.0.tgz",
|
||||
"integrity": "sha512-mKur03xNGT8O9ODO6FtT43ITGqHWZbKPdVJHZb+iV9QYcdlhUUB0wgknvA4KCUmC5oHJF6O2W1EgmyOQyVUI4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
@@ -176,14 +183,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-terser": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.1.tgz",
|
||||
"integrity": "sha512-aKS32sw5a7hy+fEXVy+5T95aDIwjpGHCTv833HXVtyKMDoVS7pBr5K3L9hEQoNqbJFjfANPrNpIXlTQ7is00eA==",
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz",
|
||||
"integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"serialize-javascript": "^6.0.0",
|
||||
"smob": "^0.0.6",
|
||||
"terser": "^5.15.1"
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"smob": "^1.0.0",
|
||||
"terser": "^5.17.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -197,19 +204,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
|
||||
"integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.3.tgz",
|
||||
"integrity": "sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g==",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
@@ -239,30 +237,30 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@urql/core": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.0.7.tgz",
|
||||
"integrity": "sha512-UtZ9oSbSFODXzFydgLCXpAQz26KGT1d6uEfcylKphiRWNXSWZi8k7vhJXNceNm/Dn0MiZ+kaaJHKcnGY1jvHRQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.1.1.tgz",
|
||||
"integrity": "sha512-iIoAy6BY+BUZZ7KIpnMT7C9q+ULf5ZCVxGe3/i7WZSJBrQa2h1QkIMhL+8fAKmOn9gt83jSIv5drWWnhZ9izEA==",
|
||||
"dependencies": {
|
||||
"@0no-co/graphql.web": "^1.0.1",
|
||||
"wonka": "^6.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@urql/svelte": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.0.1.tgz",
|
||||
"integrity": "sha512-WbsVjuK7IUNlJlvXAgevjQunoso0T+AngFlb0zafDvay6HN47Zc3CSVbAlP8KjETjERUMJLuiqknmPFFm2GEFQ==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.0.4.tgz",
|
||||
"integrity": "sha512-HYz9dHdqEcs9d82WWczQ3XG+zuup3TS01H+txaij/QfQ+KHjrlrn0EkOHQQd1S+H8+nFjFU2x9+HE3+3fuwL1A==",
|
||||
"dependencies": {
|
||||
"@urql/core": "^4.0.0",
|
||||
"@urql/core": "^4.1.0",
|
||||
"wonka": "^6.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0"
|
||||
"svelte": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -304,6 +302,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.3.tgz",
|
||||
"integrity": "sha512-aTk7pBw+x6sQYhon/NR3ikfUJuym/LdgpTlgZRe2PaEhjUMKBKyNaFCMVRAyTEWYFNO7qRu7iQVqOw/OqzxZxQ==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=7"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -313,13 +322,13 @@
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -376,9 +385,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.6.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
|
||||
"integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==",
|
||||
"version": "16.8.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.0.tgz",
|
||||
"integrity": "sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
@@ -427,9 +436,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
|
||||
"integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
|
||||
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has": "^1.0.3"
|
||||
@@ -441,7 +450,7 @@
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
@@ -512,12 +521,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.2",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
|
||||
"version": "1.22.4",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
|
||||
"integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.11.0",
|
||||
"is-core-module": "^2.13.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
@@ -538,9 +547,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz",
|
||||
"integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==",
|
||||
"version": "3.28.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.0.tgz",
|
||||
"integrity": "sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@@ -569,9 +578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-svelte": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.4.tgz",
|
||||
"integrity": "sha512-Jm0FCydR7k8bBGe7wimXAes8x2zEK10Ew3f3lEZwYor/Zya3X0AZVeSAPRH7yiXB9hWQVzJu597EUeNwGDTdjQ==",
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.6.tgz",
|
||||
"integrity": "sha512-nVFRBpGWI2qUY1OcSiEEA/kjCY2+vAjO9BI8SzA7NRrh2GTunLd6w2EYmnMt/atgdg8GvcNjLsmZmbQs/u4SQA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^4.1.0",
|
||||
@@ -618,10 +627,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smob": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/smob/-/smob-0.0.6.tgz",
|
||||
"integrity": "sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz",
|
||||
"integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
@@ -656,32 +674,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "3.58.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz",
|
||||
"integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==",
|
||||
"version": "3.59.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
|
||||
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-chartjs": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz",
|
||||
"integrity": "sha512-3+6gY2IJ9Ua8R9pk3iS1ypa7Z9OoXCJb9oPwIfTp7caJM+X+RrWnH2CTkGAq7FeSxc2nnmW08tYN88Q8Y+5M+w==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.5.0 || ^4.0.0",
|
||||
"svelte": "^3.45.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sveltestrap": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.10.0.tgz",
|
||||
"integrity": "sha512-k6Ob+6G2AMYvBidXHBKM9W28fJqFHbmosqCe/NC8pv6TV7K+v47Yw+zmnLWkjqCzzmjkSLkL48SrHZrlWc9mYQ==",
|
||||
"version": "5.11.1",
|
||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.1.tgz",
|
||||
"integrity": "sha512-FIvPIEU1VolqMN1wi2XrC8aehWVbIJEST7zPfPbOUUfPimyx9giN4nA3We5wkXrBUaifXA8CSIwuHFvf3CmYQw==",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2"
|
||||
"@popperjs/core": "^2.11.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.29.0"
|
||||
"svelte": "^3.53.1"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz",
|
||||
"integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==",
|
||||
"version": "5.19.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
|
||||
"integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
@@ -693,14 +720,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uplot": {
|
||||
"version": "1.6.24",
|
||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.24.tgz",
|
||||
"integrity": "sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg=="
|
||||
"version": "1.6.25",
|
||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.25.tgz",
|
||||
"integrity": "sha512-eWLAhEaGtIcVBiS67mC2UC0yV+G6eYLS2rU67N4F2JVWjt7uBMg4xKXUYGW0dEz9G+m7fNatjCVXHts4gjyuMQ=="
|
||||
},
|
||||
"node_modules/wonka": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.2.tgz",
|
||||
"integrity": "sha512-2xXbQ1LnwNS7egVm1HPhW2FyKrekolzhpM3mCwXdQr55gO+tAiY76rhb32OL9kKsW8taj++iP7C6hxlVzbnvrw=="
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.4.tgz",
|
||||
"integrity": "sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg=="
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
|
@@ -18,7 +18,9 @@
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@urql/svelte": "^4.0.1",
|
||||
"chart.js": "^4.3.3",
|
||||
"graphql": "^16.6.0",
|
||||
"svelte-chartjs": "^3.1.2",
|
||||
"sveltestrap": "^5.10.0",
|
||||
"uplot": "^1.6.24",
|
||||
"wonka": "^6.3.2"
|
||||
|
@@ -1,11 +1,13 @@
|
||||
<script>
|
||||
import { init } from './utils.js'
|
||||
import { init, convert2uplot } from './utils.js'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import { Row, Col, Spinner, Card, Table } from 'sveltestrap'
|
||||
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
|
||||
import Filters from './filters/Filters.svelte'
|
||||
import PlotSelection from './PlotSelection.svelte'
|
||||
import Histogram, { binsFromFootprint } from './plots/Histogram.svelte'
|
||||
import Histogram from './plots/Histogram.svelte'
|
||||
import Pie, { colors } from './plots/Pie.svelte'
|
||||
import { binsFromFootprint } from './utils.js'
|
||||
import ScatterPlot from './plots/Scatter.svelte'
|
||||
import PlotTable from './PlotTable.svelte'
|
||||
import Roofline from './plots/Roofline.svelte'
|
||||
@@ -29,7 +31,7 @@
|
||||
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 jobFilters = [];
|
||||
let rooflineMaxY;
|
||||
let colWidth;
|
||||
let colWidth1, colWidth2, colWidth3, colWidth4;
|
||||
let numBins = 50;
|
||||
let maxY = -1;
|
||||
const ccconfig = getContext('cc-config')
|
||||
@@ -134,78 +136,104 @@
|
||||
</Col>
|
||||
</Row>
|
||||
{:else if $statsQuery.data}
|
||||
<Row>
|
||||
<div class="col-3" bind:clientWidth={colWidth}>
|
||||
<div style="height: 40%">
|
||||
<Table>
|
||||
<tr>
|
||||
<th scope="col">Total Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].totalJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Short Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].shortJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Walltime</th>
|
||||
<td>{$statsQuery.data.stats[0].totalWalltime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Core Hours</th>
|
||||
<td>{$statsQuery.data.stats[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</div>
|
||||
<div style="height: 60%;">
|
||||
{#key $statsQuery.data.topUsers}
|
||||
<h4>Top Users (by node hours)</h4>
|
||||
<Histogram
|
||||
width={colWidth - 25} height={300 * 0.5} small={true}
|
||||
data={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
|
||||
label={(x) => x < $statsQuery.data.topUsers.length ? $statsQuery.data.topUsers[Math.floor(x)].name : 'No Users'}
|
||||
ylabel="Node Hours [h]"/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
{#key $statsQuery.data.stats[0].histDuration}
|
||||
<h4>Duration Distribution</h4>
|
||||
<Histogram
|
||||
width={colWidth - 25}
|
||||
data={$statsQuery.data.stats[0].histDuration}
|
||||
xlabel="Current Runtimes [h]"
|
||||
ylabel="Number of Jobs"/>
|
||||
<Row cols={3} class="mb-4">
|
||||
<Col>
|
||||
<Table>
|
||||
<tr>
|
||||
<th scope="col">Total Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].totalJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Short Jobs</th>
|
||||
<td>{$statsQuery.data.stats[0].shortJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Walltime</th>
|
||||
<td>{$statsQuery.data.stats[0].totalWalltime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Total Core Hours</th>
|
||||
<td>{$statsQuery.data.stats[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
</Table>
|
||||
</Col>
|
||||
<Col>
|
||||
<div bind:clientWidth={colWidth1}>
|
||||
<h5>Top Users</h5>
|
||||
{#key $statsQuery.data.topUsers}
|
||||
<Pie
|
||||
size={colWidth1}
|
||||
sliceLabel='Hours'
|
||||
quantities={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.count)}
|
||||
entities={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.name)}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
{#key $statsQuery.data.stats[0].histNumNodes}
|
||||
<h4>Number of Nodes Distribution</h4>
|
||||
<Histogram
|
||||
width={colWidth - 25}
|
||||
data={$statsQuery.data.stats[0].histNumNodes}
|
||||
xlabel="Allocated Nodes [#]"
|
||||
ylabel="Number of Jobs" />
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Table>
|
||||
<tr class="mb-2"><th>Legend</th><th>User Name</th><th>Node Hours</th></tr>
|
||||
{#each $statsQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }, i}
|
||||
<tr>
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||
<th scope="col"><a href="/monitoring/user/{name}?cluster={cluster.name}">{name}</a></th>
|
||||
<td>{count}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row cols={3} class="mb-2">
|
||||
<Col>
|
||||
{#if $rooflineQuery.fetching}
|
||||
<Spinner />
|
||||
{:else if $rooflineQuery.error}
|
||||
<Card body color="danger">{$rooflineQuery.error.message}</Card>
|
||||
{:else if $rooflineQuery.data && cluster}
|
||||
<div bind:clientWidth={colWidth2}>
|
||||
{#key $rooflineQuery.data}
|
||||
<Roofline
|
||||
width={colWidth - 25}
|
||||
width={colWidth2} height={300}
|
||||
tiles={$rooflineQuery.data.rooflineHeatmap}
|
||||
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
|
||||
maxY={rooflineMaxY} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div bind:clientWidth={colWidth3}>
|
||||
{#key $statsQuery.data.stats[0].histDuration}
|
||||
<Histogram
|
||||
width={colWidth3} height={300}
|
||||
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div bind:clientWidth={colWidth4}>
|
||||
{#key $statsQuery.data.stats[0].histNumNodes}
|
||||
<Histogram
|
||||
width={colWidth4} height={300}
|
||||
data={convert2uplot($statsQuery.data.stats[0].histNumNodes)}
|
||||
title="Number of Nodes Distribution"
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
|
||||
<br/>
|
||||
<hr class="my-6"/>
|
||||
|
||||
{#if $footprintsQuery.error}
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -233,15 +261,16 @@
|
||||
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
|
||||
|
||||
<h4>Average Distribution of '{item.metric}'</h4>
|
||||
<Histogram
|
||||
data={convert2uplot(item.bins)}
|
||||
width={width} height={250}
|
||||
min={item.min} max={item.max}
|
||||
data={item.bins}
|
||||
label={item.label}
|
||||
xlabel={`${item.metric} Average [${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
|
||||
title="Average Distribution of '{item.metric}'"
|
||||
xlabel={`${item.metric} average [${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
|
||||
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}]`}
|
||||
ylabel="Node Hours [h]" />
|
||||
xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
|
||||
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}`}
|
||||
ylabel="Node Hours"
|
||||
yunit="Hours"/>
|
||||
</PlotTable>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -260,6 +289,7 @@
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="analysis"
|
||||
items={metricsInScatterplots.map(([m1, m2]) => ({
|
||||
m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
|
||||
m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
|
||||
@@ -278,7 +308,7 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
h4 {
|
||||
h5 {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
@@ -93,7 +93,7 @@
|
||||
<InputGroup>
|
||||
<Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/>
|
||||
<Button outline type="submit"><Icon name="search"/></Button>
|
||||
<InputGroupText style="cursor:help;" title={(authlevel >= roles.support) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId"}><Icon name="info-circle"/></InputGroupText>
|
||||
<InputGroupText style="cursor:help;" title={(authlevel >= roles.support) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}><Icon name="info-circle"/></InputGroupText>
|
||||
</InputGroup>
|
||||
</form>
|
||||
{#if username}
|
||||
|
@@ -20,12 +20,11 @@
|
||||
} from "sveltestrap";
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import Metric from "./Metric.svelte";
|
||||
import PolarPlot from "./plots/Polar.svelte";
|
||||
import Polar from "./plots/Polar.svelte";
|
||||
import Roofline from "./plots/Roofline.svelte";
|
||||
import JobInfo from "./joblist/JobInfo.svelte";
|
||||
import TagManagement from "./TagManagement.svelte";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import Zoom from "./Zoom.svelte";
|
||||
import StatsTable from "./StatsTable.svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
@@ -33,6 +32,9 @@
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
|
||||
const accMetrics = ['acc_utilization', 'acc_mem_used', 'acc_power', 'nv_mem_util', 'nv_sm_clock', 'nv_temp'];
|
||||
let accNodeOnly
|
||||
|
||||
const { query: initq } = init(`
|
||||
job(id: "${dbid}") {
|
||||
id, jobId, user, project, cluster, startTime,
|
||||
@@ -48,7 +50,8 @@
|
||||
`);
|
||||
|
||||
const ccconfig = getContext("cc-config"),
|
||||
clusters = getContext("clusters");
|
||||
clusters = getContext("clusters"),
|
||||
metrics = getContext("metrics")
|
||||
|
||||
let isMetricsSelectionOpen = false,
|
||||
selectedMetrics = [],
|
||||
@@ -74,16 +77,25 @@
|
||||
ccconfig[`job_view_nodestats_selectedMetrics`]),
|
||||
]);
|
||||
|
||||
// Select default Scopes to load
|
||||
if (job.numAcc === 0) {
|
||||
// No Accels
|
||||
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
|
||||
accNodeOnly = [...toFetch].some(function(m) {
|
||||
if (accMetrics.includes(m)) {
|
||||
const mc = metrics(job.cluster, m)
|
||||
return mc.scope !== 'accelerator'
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (job.numAcc === 0 || accNodeOnly === true) {
|
||||
// No Accels or Accels on Node Scope
|
||||
startFetching(
|
||||
job,
|
||||
[...toFetch],
|
||||
job.numNodes > 2 ? ["node"] : ["node", "core"]
|
||||
);
|
||||
} else {
|
||||
// Accels
|
||||
// Accels and not on node scope
|
||||
startFetching(
|
||||
job,
|
||||
[...toFetch],
|
||||
@@ -121,7 +133,6 @@
|
||||
jobTags,
|
||||
fullWidth,
|
||||
statsTable;
|
||||
$: polarPlotSize = Math.min(fullWidth / 3 - 10, 300);
|
||||
$: document.title = $initq.fetching
|
||||
? "Loading..."
|
||||
: $initq.error
|
||||
@@ -233,9 +244,8 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<Col>
|
||||
<PolarPlot
|
||||
width={polarPlotSize}
|
||||
height={polarPlotSize}
|
||||
<Polar
|
||||
size={fullWidth / 4.1}
|
||||
metrics={ccconfig[
|
||||
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
||||
] || ccconfig[`job_view_polarPlotMetrics`]}
|
||||
@@ -246,7 +256,7 @@
|
||||
<Col>
|
||||
<Roofline
|
||||
width={fullWidth / 3 - 10}
|
||||
height={polarPlotSize}
|
||||
height={fullWidth / 5}
|
||||
cluster={clusters
|
||||
.find((c) => c.name == $initq.data.job.cluster)
|
||||
.subClusters.find(
|
||||
@@ -279,9 +289,9 @@
|
||||
</Button>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<!-- <Col xs="auto">
|
||||
<Zoom timeseriesPlots={plots} />
|
||||
</Col>
|
||||
</Col> -->
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
@@ -318,6 +328,7 @@
|
||||
scopes={item.data.map((x) => x.scope)}
|
||||
{width}
|
||||
isShared={$initq.data.job.exclusive != 1}
|
||||
resources={$initq.data.job.resources}
|
||||
/>
|
||||
{:else}
|
||||
<Card body color="warning"
|
||||
@@ -385,6 +396,8 @@
|
||||
bind:this={statsTable}
|
||||
job={$initq.data.job}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
accMetrics={accMetrics}
|
||||
accNodeOnly={accNodeOnly}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
@@ -119,10 +119,10 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<!-- {({ -->
|
||||
<!-- USER: "Username", -->
|
||||
<!-- PROJECT: "Project Name", -->
|
||||
<!-- })[type]} -->
|
||||
{({
|
||||
USER: "Username",
|
||||
PROJECT: "Project Name",
|
||||
})[type]}
|
||||
<Button
|
||||
color={sorting.field == "id" ? "primary" : "light"}
|
||||
size="sm"
|
||||
@@ -216,14 +216,14 @@
|
||||
>
|
||||
{:else if type == "PROJECT"}
|
||||
<a href="/monitoring/jobs/?project={row.id}"
|
||||
>{row.id}</a
|
||||
>{scrambleNames ? scramble(row.id) : row.id}</a
|
||||
>
|
||||
{:else}
|
||||
{row.id}
|
||||
{/if}
|
||||
</td>
|
||||
{#if type == "USER"}
|
||||
<td>{row?.name ? row.name : ""}</td>
|
||||
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
|
||||
{/if}
|
||||
<td>{row.totalJobs}</td>
|
||||
<td>{row.totalWalltime}</td>
|
||||
|
@@ -89,6 +89,7 @@
|
||||
timestep={data.timestep}
|
||||
scope={selectedScope} metric={metricName}
|
||||
series={series}
|
||||
isShared={isShared} />
|
||||
isShared={isShared}
|
||||
resources={job.resources} />
|
||||
{/if}
|
||||
{/key}
|
||||
|
@@ -11,6 +11,7 @@
|
||||
} from "sveltestrap";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import TimeSelection from "./filters/TimeSelection.svelte";
|
||||
import Refresher from './joblist/Refresher.svelte';
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import MetricPlot from "./plots/MetricPlot.svelte";
|
||||
import { getContext } from "svelte";
|
||||
@@ -160,6 +161,13 @@
|
||||
No currently running jobs.
|
||||
{/if}
|
||||
</Col>
|
||||
<Col>
|
||||
<Refresher on:reload={() => {
|
||||
const diff = Date.now() - to
|
||||
from = new Date(from.getTime() + diff)
|
||||
to = new Date(to.getTime() + diff)
|
||||
}} />
|
||||
</Col>
|
||||
<Col>
|
||||
<TimeSelection bind:from bind:to />
|
||||
</Col>
|
||||
@@ -203,6 +211,7 @@
|
||||
subCluster={$nodeMetricsData.data.nodeMetrics[0]
|
||||
.subCluster}
|
||||
series={item.metric.series}
|
||||
resources={[{hostname: hostname}]}
|
||||
/>
|
||||
{:else if item.disabled === true && item.metric}
|
||||
<Card
|
||||
|
@@ -7,6 +7,8 @@
|
||||
|
||||
export let job
|
||||
export let jobMetrics
|
||||
export let accMetrics
|
||||
export let accNodeOnly
|
||||
|
||||
const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(),
|
||||
scopesForMetric = (metric) => jobMetrics
|
||||
@@ -19,9 +21,19 @@
|
||||
isMetricSelectionOpen = false,
|
||||
selectedMetrics = getContext('cc-config')[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|
||||
|| getContext('cc-config')['job_view_nodestats_selectedMetrics']
|
||||
|
||||
|
||||
for (let metric of allMetrics) {
|
||||
selectedScopes[metric] = maxScope(scopesForMetric(metric))
|
||||
// Not Exclusive or Single Node: Get maxScope()
|
||||
// No Accelerators in Job and not Acc-Metric: Use 'core'
|
||||
// Accelerator Metric available on accelerator scope: Use 'accelerator'
|
||||
// Accelerator Metric only on node scope: Fallback to 'node'
|
||||
selectedScopes[metric] = (job.exclusive != 1 || job.numNodes == 1) ?
|
||||
(job.numAccs != 0 && accMetrics.includes(metric)) ?
|
||||
accNodeOnly ?
|
||||
'node'
|
||||
: 'accelerator'
|
||||
: 'core'
|
||||
: maxScope(scopesForMetric(metric))
|
||||
sorting[metric] = {
|
||||
min: { dir: 'up', active: false },
|
||||
avg: { dir: 'up', active: false },
|
||||
|
@@ -1,12 +1,49 @@
|
||||
<script>
|
||||
import { Icon } from 'sveltestrap'
|
||||
|
||||
export let host
|
||||
export let metric
|
||||
export let scope
|
||||
export let jobMetrics
|
||||
|
||||
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 = {
|
||||
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)
|
||||
</script>
|
||||
|
||||
{#if series == null || series.length == 0}
|
||||
@@ -24,6 +61,14 @@
|
||||
{:else}
|
||||
<td colspan="4">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
{#each ['id', 'min', 'avg', 'max'] as field}
|
||||
<th on:click={() => sortByField(field)}>
|
||||
Sort
|
||||
<Icon name="caret-{sorting[field].dir}{sorting[field].active ? '-fill' : ''}" />
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{#each series as s, i}
|
||||
<tr>
|
||||
<th>{s.id ?? i}</th>
|
||||
|
@@ -1,9 +1,10 @@
|
||||
<script>
|
||||
import Refresher from './joblist/Refresher.svelte'
|
||||
import Roofline, { transformPerNodeData } from './plots/Roofline.svelte'
|
||||
import Pie, { colors } from './plots/Pie.svelte'
|
||||
import Histogram from './plots/Histogram.svelte'
|
||||
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
|
||||
import { init } from './utils.js'
|
||||
import { init, convert2uplot } from './utils.js'
|
||||
import { scaleNumbers } from './units.js'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
|
||||
@@ -159,21 +160,24 @@
|
||||
<Row cols={4}>
|
||||
<Col class="p-2">
|
||||
<div bind:clientWidth={colWidth1}>
|
||||
<h4 class="mb-3 text-center">Top Users</h4>
|
||||
<h4 class="text-center">Top Users</h4>
|
||||
{#key $mainQuery.data}
|
||||
<Histogram
|
||||
width={colWidth1 - 25}
|
||||
data={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
|
||||
label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'}
|
||||
xlabel="User Name" ylabel="Number of Jobs" />
|
||||
<Pie
|
||||
size={colWidth1}
|
||||
sliceLabel='Jobs'
|
||||
quantities={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.count)}
|
||||
entities={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.name)}
|
||||
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="px-4 py-2">
|
||||
<Table>
|
||||
<tr class="mb-2"><th>User Name</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }}
|
||||
<tr class="mb-2"><th>Legend</th><th>User Name</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }, i}
|
||||
<tr>
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||
<th scope="col"><a href="/monitoring/user/{name}?cluster={cluster}&state=running">{name}</a></th>
|
||||
<td>{count}</td>
|
||||
</tr>
|
||||
@@ -181,20 +185,22 @@
|
||||
</Table>
|
||||
</Col>
|
||||
<Col class="p-2">
|
||||
<h4 class="mb-3 text-center">Top Projects</h4>
|
||||
<h4 class="text-center">Top Projects</h4>
|
||||
{#key $mainQuery.data}
|
||||
<Histogram
|
||||
width={colWidth1 - 25}
|
||||
data={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
|
||||
label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'}
|
||||
xlabel="Project Code" ylabel="Number of Jobs" />
|
||||
<Pie
|
||||
size={colWidth1}
|
||||
sliceLabel='Jobs'
|
||||
quantities={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map((tp) => tp.count)}
|
||||
entities={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map((tp) => tp.name)}
|
||||
/>
|
||||
{/key}
|
||||
</Col>
|
||||
<Col class="px-4 py-2">
|
||||
<Table>
|
||||
<tr class="mb-2"><th>Project Code</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }}
|
||||
<tr class="mb-2"><th>Legend</th><th>Project Code</th><th>Number of Nodes</th></tr>
|
||||
{#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }, i}
|
||||
<tr>
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
||||
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster}&state=running&project={name}&projectMatch=eq">{name}</a></th>
|
||||
<td>{count}</td>
|
||||
</tr>
|
||||
@@ -202,28 +208,33 @@
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row cols={2} class="mt-3">
|
||||
<hr class="my-2"/>
|
||||
<Row cols={2}>
|
||||
<Col class="p-2">
|
||||
<div bind:clientWidth={colWidth2}>
|
||||
<h4 class="mb-3 text-center">Duration Distribution</h4>
|
||||
{#key $mainQuery.data.stats}
|
||||
<Histogram
|
||||
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
|
||||
width={colWidth2 - 25}
|
||||
data={$mainQuery.data.stats[0].histDuration}
|
||||
xlabel="Current Runtimes [h]"
|
||||
ylabel="Number of Jobs" />
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="p-2">
|
||||
<h4 class="mb-3 text-center">Number of Nodes Distribution</h4>
|
||||
{#key $mainQuery.data.stats}
|
||||
<Histogram
|
||||
data={convert2uplot($mainQuery.data.stats[0].histNumNodes)}
|
||||
width={colWidth2 - 25}
|
||||
data={$mainQuery.data.stats[0].histNumNodes}
|
||||
xlabel="Allocated Nodes [#]"
|
||||
ylabel="Number of Jobs" />
|
||||
title="Number of Nodes Distribution"
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
{/if}
|
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { init, checkMetricDisabled } from './utils.js'
|
||||
import Refresher from './joblist/Refresher.svelte'
|
||||
import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import TimeSelection from './filters/TimeSelection.svelte'
|
||||
@@ -78,6 +79,13 @@
|
||||
{:else if $initq.fetching}
|
||||
<Spinner/>
|
||||
{:else}
|
||||
<Col>
|
||||
<Refresher on:reload={() => {
|
||||
const diff = Date.now() - to
|
||||
from = new Date(from.getTime() + diff)
|
||||
to = new Date(to.getTime() + diff)
|
||||
}} />
|
||||
</Col>
|
||||
<Col>
|
||||
<TimeSelection
|
||||
bind:from={from}
|
||||
@@ -136,7 +144,8 @@
|
||||
series={item.data.metric.series}
|
||||
metric={item.data.name}
|
||||
cluster={clusters.find(c => c.name == cluster)}
|
||||
subCluster={item.subCluster} />
|
||||
subCluster={item.subCluster}
|
||||
resources={[{hostname: item.host}]}/>
|
||||
{:else if item.disabled === true && item.data}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
|
||||
{:else}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { onMount, getContext } from 'svelte'
|
||||
import { init } from './utils.js'
|
||||
import { init, convert2uplot } from './utils.js'
|
||||
import { Table, Row, Col, Button, Icon, Card, Spinner, Input } from 'sveltestrap'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import Filters from './filters/Filters.svelte'
|
||||
@@ -25,13 +25,6 @@
|
||||
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
|
||||
let w1, w2, histogramHeight = 250
|
||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
|
||||
let resize = false
|
||||
/* Resize Context
|
||||
* A) Each viewport change triggers histogram rerender due to variable dimensions clearing canvas if not rerendered
|
||||
* B) Opening filters (and some other things) triggers small change in viewport dimensions (Fix here?)
|
||||
* A+B) Histogram rerenders if filters opened, high performance impact if dataload heavy
|
||||
* Solution: Default to fixed histogram dimensions, allow user to enable automatic resizing
|
||||
*/
|
||||
|
||||
const client = getContextClient();
|
||||
$: stats = queryStore({
|
||||
@@ -137,47 +130,31 @@
|
||||
<th scope="row">Total Core Hours</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<th scope="row">Toggle Histogram Resizing</th>
|
||||
<td><Input id="c3" value={resize} type="switch" on:change={() => (resize = !resize)}/></td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
<div class="col-4" style="text-align: center;" bind:clientWidth={w1}>
|
||||
<b>Duration Distribution</b>
|
||||
<div class="col-4 text-center" bind:clientWidth={w1}>
|
||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||
{#if resize == true}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histDuration}
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||
width={w1 - 25} height={histogramHeight}
|
||||
xlabel="Current Runtimes [h]"
|
||||
ylabel="Number of Jobs"/>
|
||||
{:else}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histDuration}
|
||||
width={400} height={250}
|
||||
xlabel="Current Runtimes [h]"
|
||||
ylabel="Number of Jobs"/>
|
||||
{/if}
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-4" style="text-align: center;" bind:clientWidth={w2}>
|
||||
<b>Number of Nodes Distribution</b>
|
||||
<div class="col-4 text-center" bind:clientWidth={w2}>
|
||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||
{#if resize == true}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histNumNodes}
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||
width={w2 - 25} height={histogramHeight}
|
||||
xlabel="Allocated Nodes [#]"
|
||||
ylabel="Number of Jobs" />
|
||||
{:else}
|
||||
<Histogram
|
||||
data={$stats.data.jobsStatistics[0].histNumNodes}
|
||||
width={400} height={250}
|
||||
xlabel="Allocated Nodes [#]"
|
||||
ylabel="Number of Jobs" />
|
||||
{/if}
|
||||
title="Number of Nodes Distribution"
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
|
@@ -7,6 +7,7 @@
|
||||
export let customEnabled = true
|
||||
export let anyEnabled = false
|
||||
export let options = {
|
||||
'Last quarter hour': 15*60,
|
||||
'Last half hour': 30*60,
|
||||
'Last hour': 60*60,
|
||||
'Last 2hrs': 2*60*60,
|
||||
|
@@ -7,7 +7,10 @@
|
||||
-->
|
||||
<script context="module">
|
||||
export const scrambleNames = window.localStorage.getItem("cc-scramble-names")
|
||||
export const scramble = (str) => [...str].reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5).toString(32)
|
||||
export const scramble = function(str) {
|
||||
if (str === '-') return str
|
||||
else return [...str].reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5).toString(32).substr(0, 6)
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
import Tag from '../Tag.svelte';
|
||||
|
@@ -136,6 +136,7 @@
|
||||
{cluster}
|
||||
subCluster={job.subCluster}
|
||||
isShared={(job.exclusive != 1)}
|
||||
resources={job.resources}
|
||||
/>
|
||||
{:else if metric.disabled == true && metric.data}
|
||||
<Card body color="info">Metric disabled for subcluster <code>{metric.data.name}:{job.subCluster}</code></Card>
|
||||
|
@@ -1,229 +1,216 @@
|
||||
<!--
|
||||
@component
|
||||
Properties:
|
||||
- width, height: Number
|
||||
- min, max: Number
|
||||
- label: (x-Value) => String
|
||||
- data: [{ value: Number, count: Number }]
|
||||
- Todo
|
||||
-->
|
||||
|
||||
<div
|
||||
on:mousemove={mousemove}
|
||||
on:mouseleave={() => (infoText = '')}>
|
||||
<span style="left: {paddingLeft + 5}px;">{infoText}</span>
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import uPlot from 'uplot'
|
||||
import { formatNumber } from '../units.js'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Card } from 'sveltestrap'
|
||||
|
||||
export let data
|
||||
export let width = 500
|
||||
export let height = 300
|
||||
export let title = ''
|
||||
export let xlabel = ''
|
||||
export let xunit = 'X'
|
||||
export let ylabel = ''
|
||||
export let min = null
|
||||
export let max = null
|
||||
export let small = false
|
||||
export let label = formatNumber
|
||||
export let yunit = 'Y'
|
||||
|
||||
const fontSize = 12
|
||||
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
|
||||
const paddingLeft = 50, paddingRight = 20, paddingTop = 20, paddingBottom = 20
|
||||
const { bars } = uPlot.paths
|
||||
|
||||
let ctx, canvasElement
|
||||
const drawStyles = {
|
||||
bars: 1,
|
||||
points: 2,
|
||||
};
|
||||
|
||||
const maxCount = data.reduce((max, point) => Math.max(max, point.count), 0),
|
||||
maxValue = data.reduce((max, point) => Math.max(max, point.value), 0.1)
|
||||
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
||||
let s = u.series[seriesIdx];
|
||||
let style = s.drawStyle;
|
||||
|
||||
function getStepSize(valueRange, pixelRange, minSpace) {
|
||||
const proposition = valueRange / (pixelRange / minSpace)
|
||||
const getStepSize = n => Math.pow(10, Math.floor(n / 3)) *
|
||||
(n < 0 ? [1., 5., 2.][-n % 3] : [1., 2., 5.][n % 3])
|
||||
let renderer = ( // If bars to wide, change here
|
||||
style == drawStyles.bars ? (
|
||||
bars({size: [0.75, 100]})
|
||||
) :
|
||||
() => null
|
||||
)
|
||||
|
||||
let n = 0
|
||||
let stepsize = getStepSize(n)
|
||||
while (true) {
|
||||
let bigger = getStepSize(n + 1)
|
||||
if (proposition > bigger) {
|
||||
n += 1
|
||||
stepsize = bigger
|
||||
} else {
|
||||
return stepsize
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
}
|
||||
|
||||
// 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, {
|
||||
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";});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let infoText = ''
|
||||
function mousemove(event) {
|
||||
let rect = event.target.getBoundingClientRect()
|
||||
let x = event.clientX - rect.left
|
||||
if (x < paddingLeft || x > width - paddingRight) {
|
||||
infoText = ''
|
||||
return
|
||||
}
|
||||
|
||||
const w = width - paddingLeft - paddingRight
|
||||
const barWidth = Math.round(w / (maxValue + 1))
|
||||
x = Math.floor((x - paddingLeft) / (w - barWidth) * maxValue)
|
||||
let point = data.find(point => point.value == x)
|
||||
|
||||
if (point)
|
||||
infoText = `count: ${point.count} (value: ${label(x)})`
|
||||
else
|
||||
infoText = ''
|
||||
};
|
||||
}
|
||||
|
||||
let plotWrapper = null
|
||||
let uplot = null
|
||||
let timeoutId = null
|
||||
|
||||
function render() {
|
||||
const labelOffset = Math.floor(height * 0.1)
|
||||
const h = height - paddingTop - paddingBottom - labelOffset
|
||||
const w = width - paddingLeft - paddingRight
|
||||
const barGap = 5
|
||||
const barWidth = Math.ceil(w / (maxValue + 1)) - barGap
|
||||
let opts = {
|
||||
width: width,
|
||||
height: height,
|
||||
title: title,
|
||||
plugins: [
|
||||
legendAsTooltipPlugin()
|
||||
],
|
||||
cursor: {
|
||||
points: {
|
||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
||||
width: (u, seriesIdx, size) => size / 4,
|
||||
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90',
|
||||
fill: (u, seriesIdx) => "#fff",
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
time: false
|
||||
},
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'x',
|
||||
label: xlabel,
|
||||
labelGap: 10,
|
||||
size: 25,
|
||||
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map(v => formatNumber(v)),
|
||||
},
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'y',
|
||||
label: ylabel,
|
||||
labelGap: 10,
|
||||
size: 35,
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map(v => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
label: xunit !== '' ? xunit : null,
|
||||
},
|
||||
Object.assign({
|
||||
label: yunit !== '' ? yunit : null,
|
||||
width: 1 / devicePixelRatio,
|
||||
drawStyle: drawStyles.points,
|
||||
lineInterpolation: null,
|
||||
paths,
|
||||
}, {
|
||||
drawStyle: drawStyles.bars,
|
||||
lineInterpolation: null,
|
||||
stroke: "#85abce",
|
||||
fill: "#85abce", // + "1A", // Transparent Fill
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
||||
if (Number.isNaN(barWidth))
|
||||
return
|
||||
uplot = new uPlot(opts, data, plotWrapper)
|
||||
}
|
||||
|
||||
const getCanvasX = (value) => (value / maxValue) * (w - barWidth) + paddingLeft + (barWidth / 2.)
|
||||
const getCanvasY = (count) => (h - (count / maxCount) * h) + paddingTop
|
||||
|
||||
// X Axis
|
||||
ctx.font = `bold ${fontSize}px ${fontFamily}`
|
||||
ctx.fillStyle = 'black'
|
||||
if (xlabel != '') {
|
||||
let textWidth = ctx.measureText(xlabel).width
|
||||
ctx.fillText(xlabel, Math.floor((width / 2) - (textWidth / 2) + barGap), height - Math.floor(labelOffset / 2))
|
||||
}
|
||||
ctx.textAlign = 'center'
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
if (min != null && max != null) {
|
||||
const stepsizeX = getStepSize(max - min, w, 75)
|
||||
let startX = 0
|
||||
while (startX < min)
|
||||
startX += stepsizeX
|
||||
|
||||
for (let x = startX; x < max; x += stepsizeX) {
|
||||
let px = ((x - min) / (max - min)) * (w - barWidth) + paddingLeft + (barWidth / 2.)
|
||||
ctx.fillText(`${formatNumber(x)}`, px, height - paddingBottom - Math.floor(labelOffset / 2))
|
||||
}
|
||||
} else {
|
||||
const stepsizeX = getStepSize(maxValue, w, 120)
|
||||
for (let x = 0; x <= maxValue; x += stepsizeX) {
|
||||
ctx.fillText(label(x), getCanvasX(x), height - paddingBottom - Math.floor(labelOffset / (small ? 8 : 2)))
|
||||
}
|
||||
}
|
||||
|
||||
// Y Axis
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.strokeStyle = '#bbbbbb'
|
||||
ctx.font = `bold ${fontSize}px ${fontFamily}`
|
||||
if (ylabel != '') {
|
||||
ctx.save()
|
||||
ctx.translate(15, Math.floor(h / 2))
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.fillText(ylabel, 0, 0)
|
||||
ctx.restore()
|
||||
}
|
||||
ctx.textAlign = 'right'
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
ctx.beginPath()
|
||||
const stepsizeY = getStepSize(maxCount, h, 50)
|
||||
for (let y = stepsizeY; y <= maxCount; y += stepsizeY) {
|
||||
const py = Math.floor(getCanvasY(y))
|
||||
ctx.fillText(`${formatNumber(y)}`, paddingLeft - 5, py)
|
||||
ctx.moveTo(paddingLeft, py)
|
||||
ctx.lineTo(width, py)
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
// Draw bars
|
||||
ctx.fillStyle = '#85abce'
|
||||
for (let p of data) {
|
||||
ctx.fillRect(
|
||||
getCanvasX(p.value) - (barWidth / 2.),
|
||||
getCanvasY(p.count),
|
||||
barWidth,
|
||||
(p.count / maxCount) * h)
|
||||
}
|
||||
|
||||
// Fat lines left and below plotting area
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, height - paddingBottom - labelOffset)
|
||||
ctx.lineTo(width, height - paddingBottom - labelOffset)
|
||||
ctx.moveTo(paddingLeft, 0)
|
||||
ctx.lineTo(paddingLeft, height - Math.floor(labelOffset / 2))
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
let mounted = false
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
ctx = canvasElement.getContext('2d')
|
||||
render()
|
||||
})
|
||||
|
||||
let timeoutId = null;
|
||||
onDestroy(() => {
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
if (!canvasElement)
|
||||
return
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
ctx = canvasElement.getContext('2d')
|
||||
render()
|
||||
}, 250)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: relative;
|
||||
}
|
||||
div > span {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
</style>
|
||||
{#if data.length > 0}
|
||||
<div bind:this={plotWrapper}/>
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
|
||||
{/if}
|
||||
|
||||
<script context="module">
|
||||
import { formatNumber } from '../units.js'
|
||||
|
||||
export function binsFromFootprint(weights, values, numBins) {
|
||||
let min = 0, max = 0
|
||||
if (values.length != 0) {
|
||||
for (let x of values) {
|
||||
min = Math.min(min, x)
|
||||
max = Math.max(max, x)
|
||||
}
|
||||
max += 1 // So that we have an exclusive range.
|
||||
}
|
||||
|
||||
if (numBins == null || numBins < 3)
|
||||
numBins = 3
|
||||
|
||||
const bins = new Array(numBins).fill(0)
|
||||
for (let i = 0; i < values.length; i++)
|
||||
bins[Math.floor(((values[i] - min) / (max - min)) * numBins)] += weights ? weights[i] : 1
|
||||
|
||||
return {
|
||||
label: idx => {
|
||||
let start = min + (idx / numBins) * (max - min)
|
||||
let stop = min + ((idx + 1) / numBins) * (max - min)
|
||||
return `${formatNumber(start)} - ${formatNumber(stop)}`
|
||||
},
|
||||
bins: bins.map((count, idx) => ({ value: idx, count: count })),
|
||||
min: min,
|
||||
max: max
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -26,17 +26,19 @@
|
||||
import { getContext, onMount, onDestroy } from 'svelte'
|
||||
import { Card } from 'sveltestrap'
|
||||
|
||||
export let metric
|
||||
export let scope = 'node'
|
||||
export let resources = []
|
||||
export let width
|
||||
export let height
|
||||
export let timestep
|
||||
export let series
|
||||
export let useStatsSeries = null
|
||||
export let statisticsSeries = null
|
||||
export let cluster
|
||||
export let subCluster
|
||||
export let metric
|
||||
export let useStatsSeries = null
|
||||
export let scope = 'node'
|
||||
export let isShared = false
|
||||
export let forNode = false
|
||||
|
||||
if (useStatsSeries == null)
|
||||
useStatsSeries = statisticsSeries != null
|
||||
@@ -53,6 +55,70 @@
|
||||
const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' }
|
||||
const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster)
|
||||
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
||||
let legendEl;
|
||||
const dataSize = series.length
|
||||
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
|
||||
uPlot.assign(legendEl.style, {
|
||||
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
|
||||
});
|
||||
|
||||
// conditional hide series color markers:
|
||||
if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory
|
||||
dataSize === 1 || // Only one Y-Dataseries
|
||||
dataSize > 6 ){ // More than 6 Y-Dataseries
|
||||
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";});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
const width = u.over.querySelector(".u-legend").offsetWidth;
|
||||
legendEl.style.transform = "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
if (dataSize <= 12 || useStatsSeries === true) {
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
}
|
||||
}
|
||||
} else { // Setting legend-opts show/live as object with false here will not work ...
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function backgroundColor() {
|
||||
if (clusterCockpitConfig.plot_general_colorBackground == false
|
||||
|| !thresholds
|
||||
@@ -88,24 +154,48 @@
|
||||
? statisticsSeries.mean.length
|
||||
: series.reduce((n, series) => Math.max(n, series.data.length), 0)
|
||||
const maxX = longestSeries * timestep
|
||||
const maxY = thresholds != null
|
||||
? useStatsSeries
|
||||
let maxY = null
|
||||
|
||||
if (thresholds !== null) {
|
||||
maxY = useStatsSeries
|
||||
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal)
|
||||
: (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal)
|
||||
: null
|
||||
const plotSeries = [{}]
|
||||
|
||||
if (maxY >= (10 * thresholds.normal)) { // Hard y-range render limit if outliers in series data
|
||||
maxY = (10 * thresholds.normal)
|
||||
}
|
||||
}
|
||||
|
||||
const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts)}]
|
||||
const plotData = [new Array(longestSeries)]
|
||||
for (let i = 0; i < longestSeries; i++) // TODO: Cache/Reuse this array?
|
||||
plotData[0][i] = i * timestep
|
||||
|
||||
if (forNode === true) {
|
||||
// Negative Timestamp Buildup
|
||||
for (let i = 0; i <= longestSeries; i++) {
|
||||
plotData[0][i] = (longestSeries - i) * timestep * -1
|
||||
}
|
||||
} else {
|
||||
// Positive Timestamp Buildup
|
||||
for (let j = 0; j < longestSeries; j++) // TODO: Cache/Reuse this array?
|
||||
plotData[0][j] = j * timestep
|
||||
}
|
||||
|
||||
let plotBands = undefined
|
||||
if (useStatsSeries) {
|
||||
plotData.push(statisticsSeries.min)
|
||||
plotData.push(statisticsSeries.max)
|
||||
plotData.push(statisticsSeries.mean)
|
||||
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'red' })
|
||||
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'green' })
|
||||
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'black' })
|
||||
|
||||
if (forNode === true) { // timestamp 0 with null value for reversed time axis
|
||||
if (plotData[1].length != 0) plotData[1].push(null)
|
||||
if (plotData[2].length != 0) plotData[2].push(null)
|
||||
if (plotData[3].length != 0) plotData[3].push(null)
|
||||
}
|
||||
|
||||
plotSeries.push({ label: 'min', scale: 'y', width: lineWidth, stroke: 'red' })
|
||||
plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' })
|
||||
plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' })
|
||||
|
||||
plotBands = [
|
||||
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' },
|
||||
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' }
|
||||
@@ -113,7 +203,11 @@
|
||||
} else {
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
plotData.push(series[i].data)
|
||||
if (forNode === true && plotData[1].length != 0) plotData[1].push(null) // timestamp 0 with null value for reversed time axis
|
||||
plotSeries.push({
|
||||
label: scope === 'node' ? resources[i].hostname :
|
||||
// scope === 'accelerator' ? resources[0].accelerators[i] :
|
||||
scope + ' #' + (i+1),
|
||||
scale: 'y',
|
||||
width: lineWidth,
|
||||
stroke: lineColor(i, series.length)
|
||||
@@ -124,12 +218,15 @@
|
||||
const opts = {
|
||||
width,
|
||||
height,
|
||||
plugins: [
|
||||
legendAsTooltipPlugin()
|
||||
],
|
||||
series: plotSeries,
|
||||
axes: [
|
||||
{
|
||||
scale: 'x',
|
||||
space: 35,
|
||||
incrs: timeIncrs(timestep, maxX),
|
||||
incrs: timeIncrs(timestep, maxX, forNode),
|
||||
values: (_, vals) => vals.map(v => formatTime(v))
|
||||
},
|
||||
{
|
||||
@@ -177,8 +274,11 @@
|
||||
x: { time: false },
|
||||
y: maxY ? { range: [0., maxY * 1.1] } : {}
|
||||
},
|
||||
cursor: { show: false },
|
||||
legend: { show: false, live: false }
|
||||
legend : { // Display legend until max 12 Y-dataseries
|
||||
show: (series.length <= 12 || useStatsSeries === true) ? true : false,
|
||||
live: (series.length <= 12 || useStatsSeries === true) ? true : false
|
||||
},
|
||||
cursor: { drag: { x: true, y: true } }
|
||||
}
|
||||
|
||||
// console.log(opts)
|
||||
@@ -249,24 +349,33 @@
|
||||
}
|
||||
</script>
|
||||
<script context="module">
|
||||
|
||||
export function formatTime(t) {
|
||||
let h = Math.floor(t / 3600)
|
||||
let m = Math.floor((t % 3600) / 60)
|
||||
if (h == 0)
|
||||
return `${m}m`
|
||||
else if (m == 0)
|
||||
return `${h}h`
|
||||
else
|
||||
return `${h}:${m}h`
|
||||
if (t !== null) {
|
||||
if (isNaN(t)) {
|
||||
return t
|
||||
} else {
|
||||
let h = Math.floor(t / 3600)
|
||||
let m = Math.floor((t % 3600) / 60)
|
||||
if (h == 0)
|
||||
return `${m}m`
|
||||
else if (m == 0)
|
||||
return `${h}h`
|
||||
else
|
||||
return `${h}:${m}h`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function timeIncrs(timestep, maxX) {
|
||||
let incrs = []
|
||||
for (let t = timestep; t < maxX; t *= 10)
|
||||
incrs.push(t, t * 2, t * 3, t * 5)
|
||||
export function timeIncrs(timestep, maxX, forNode) {
|
||||
if (forNode === true) {
|
||||
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600] // forNode fixed increments
|
||||
} else {
|
||||
let incrs = []
|
||||
for (let t = timestep; t < maxX; t *= 10)
|
||||
incrs.push(t, t * 2, t * 3, t * 5)
|
||||
|
||||
return incrs
|
||||
return incrs
|
||||
}
|
||||
}
|
||||
|
||||
export function findThresholds(metricConfig, scope, subCluster) {
|
||||
@@ -323,8 +432,9 @@
|
||||
{#if series[0].data.length > 0}
|
||||
<div bind:this={plotWrapper} class="cc-plot"></div>
|
||||
{:else}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
|
||||
<Card class="mx-4" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.cc-plot {
|
||||
border-radius: 5px;
|
||||
|
81
web/frontend/src/plots/Pie.svelte
Normal file
81
web/frontend/src/plots/Pie.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script context="module">
|
||||
// http://tsitsul.in/blog/coloropt/ : 12 colors normal
|
||||
export const colors = [
|
||||
'rgb(235,172,35)',
|
||||
'rgb(184,0,88)',
|
||||
'rgb(0,140,249)',
|
||||
'rgb(0,110,0)',
|
||||
'rgb(0,187,173)',
|
||||
'rgb(209,99,230)',
|
||||
'rgb(178,69,2)',
|
||||
'rgb(255,146,135)',
|
||||
'rgb(89,84,214)',
|
||||
'rgb(0,198,248)',
|
||||
'rgb(135,133,0)',
|
||||
'rgb(0,167,108)',
|
||||
'rgb(189,189,189)'
|
||||
]
|
||||
</script>
|
||||
<script>
|
||||
import { Pie } from 'svelte-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ArcElement,
|
||||
CategoryScale
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ArcElement,
|
||||
CategoryScale
|
||||
);
|
||||
|
||||
export let size
|
||||
export let sliceLabel
|
||||
export let quantities
|
||||
export let entities
|
||||
export let displayLegend = false
|
||||
|
||||
const data = {
|
||||
labels: entities,
|
||||
datasets: [
|
||||
{
|
||||
label: sliceLabel,
|
||||
data: quantities,
|
||||
fill: 1,
|
||||
backgroundColor: colors.slice(0, quantities.length),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: displayLegend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="chart-container" style="--container-width: {size}; --container-height: {size}">
|
||||
<Pie {data} {options}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
height: var(--container-height);
|
||||
width: var(--container-width);
|
||||
}
|
||||
</style>
|
@@ -1,22 +1,34 @@
|
||||
<div>
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { onMount, getContext } from 'svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { Radar } from 'svelte-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
PointElement,
|
||||
RadialLinearScale,
|
||||
LineElement
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
PointElement,
|
||||
RadialLinearScale,
|
||||
LineElement
|
||||
);
|
||||
|
||||
export let size
|
||||
export let metrics
|
||||
export let width
|
||||
export let height
|
||||
export let cluster
|
||||
export let jobMetrics
|
||||
|
||||
const fontSize = 12
|
||||
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
|
||||
const metricConfig = getContext('metrics')
|
||||
|
||||
let ctx, canvasElement
|
||||
|
||||
const labels = metrics.filter(name => {
|
||||
if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
|
||||
console.warn(`PolarPlot: No metric data for '${name}'`)
|
||||
@@ -46,145 +58,49 @@
|
||||
return avg / metric.series.length
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'Max',
|
||||
values: getValuesForStat(getMax),
|
||||
color: 'rgb(0, 102, 255)',
|
||||
areaColor: 'rgba(0, 102, 255, 0.25)'
|
||||
},
|
||||
{
|
||||
name: 'Avg',
|
||||
values: getValuesForStat(getAvg),
|
||||
color: 'rgb(255, 153, 0)',
|
||||
areaColor: 'rgba(255, 153, 0, 0.25)'
|
||||
}
|
||||
]
|
||||
|
||||
function render() {
|
||||
if (!width || Number.isNaN(width))
|
||||
return
|
||||
|
||||
const centerX = width / 2
|
||||
const centerY = height / 2 - 15
|
||||
const radius = (Math.min(width, height) / 2) - 50
|
||||
|
||||
// Draw circles
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = '#999999'
|
||||
ctx.beginPath()
|
||||
ctx.arc(centerX, centerY, radius * 1.0, 0, Math.PI * 2, false)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(centerX, centerY, radius * 0.666, 0, Math.PI * 2, false)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(centerX, centerY, radius * 0.333, 0, Math.PI * 2, false)
|
||||
ctx.stroke()
|
||||
|
||||
// Axis
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('1/3',
|
||||
Math.floor(centerX + radius * 0.333),
|
||||
Math.floor(centerY + 15))
|
||||
ctx.fillText('2/3',
|
||||
Math.floor(centerX + radius * 0.666),
|
||||
Math.floor(centerY + 15))
|
||||
ctx.fillText('1.0',
|
||||
Math.floor(centerX + radius * 1.0),
|
||||
Math.floor(centerY + 15))
|
||||
|
||||
// Label text and straight lines from center
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
const angle = 2 * Math.PI * ((i + 1) / labels.length)
|
||||
const dx = Math.cos(angle) * radius
|
||||
const dy = Math.sin(angle) * radius
|
||||
ctx.fillText(labels[i],
|
||||
Math.floor(centerX + dx * 1.1),
|
||||
Math.floor(centerY + dy * 1.1))
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(centerX, centerY)
|
||||
ctx.lineTo(centerX + dx, centerY + dy)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
for (let dataset of data) {
|
||||
console.assert(dataset.values.length === labels.length, 'this will look confusing')
|
||||
ctx.fillStyle = dataset.color
|
||||
ctx.strokeStyle = dataset.color
|
||||
const points = []
|
||||
for (let i = 0; i < dataset.values.length; i++) {
|
||||
const value = dataset.values[i]
|
||||
const angle = 2 * Math.PI * ((i + 1) / labels.length)
|
||||
const x = centerX + Math.cos(angle) * radius * value
|
||||
const y = centerY + Math.sin(angle) * radius * value
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, 3, 0, Math.PI * 2, false)
|
||||
ctx.fill()
|
||||
|
||||
points.push({ x, y })
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Max',
|
||||
data: getValuesForStat(getMax),
|
||||
fill: 1,
|
||||
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
||||
borderColor: 'rgb(0, 102, 255)',
|
||||
pointBackgroundColor: 'rgb(0, 102, 255)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(0, 102, 255)'
|
||||
},
|
||||
{
|
||||
label: 'Avg',
|
||||
data: getValuesForStat(getAvg),
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
||||
borderColor: 'rgb(255, 153, 0)',
|
||||
pointBackgroundColor: 'rgb(255, 153, 0)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(255, 153, 0)'
|
||||
}
|
||||
|
||||
// "Fill" the shape this dataset has
|
||||
ctx.fillStyle = dataset.areaColor
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(points[0].x, points[0].y)
|
||||
for (let p of points)
|
||||
ctx.lineTo(p.x, p.y)
|
||||
ctx.lineTo(points[0].x, points[0].y)
|
||||
ctx.stroke()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// Legend at the bottom left corner
|
||||
ctx.textAlign = 'left'
|
||||
let paddingLeft = 0
|
||||
for (let dataset of data) {
|
||||
const text = `${dataset.name}: `
|
||||
const textWidth = ctx.measureText(text).width
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.fillText(text, paddingLeft, height - 20)
|
||||
|
||||
ctx.fillStyle = dataset.color
|
||||
ctx.beginPath()
|
||||
ctx.arc(paddingLeft + textWidth + 5, height - 25, 5, 0, Math.PI * 2, false)
|
||||
ctx.fill()
|
||||
|
||||
paddingLeft += textWidth + 15
|
||||
}
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.fillText(`Values relative to respective peak.`, 0, height - 7)
|
||||
]
|
||||
}
|
||||
|
||||
let mounted = false
|
||||
onMount(() => {
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
ctx = canvasElement.getContext('2d')
|
||||
render(ctx, data, width, height)
|
||||
mounted = true
|
||||
})
|
||||
|
||||
let timeoutId = null
|
||||
function sizeChanged() {
|
||||
if (!mounted)
|
||||
return;
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
ctx = canvasElement.getContext('2d')
|
||||
render(ctx, data, width, height)
|
||||
}, 250)
|
||||
// No custom defined options but keep for clarity
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
animation: false
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
</script>
|
||||
|
||||
<div class="chart-container">
|
||||
<Radar {data} {options} width={size} height={size}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-container {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
@@ -6,11 +6,15 @@ const power = [1, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21]
|
||||
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
|
||||
|
||||
export function formatNumber(x) {
|
||||
for (let i = 0; i < prefix.length; i++)
|
||||
if (power[i] <= x && x < power[i+1])
|
||||
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
|
||||
if ( isNaN(x) ) {
|
||||
return x // Return if String , used in Histograms
|
||||
} else {
|
||||
for (let i = 0; i < prefix.length; i++)
|
||||
if (power[i] <= x && x < power[i+1])
|
||||
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
|
||||
|
||||
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
|
||||
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export function scaleNumbers(x, y , p = '') {
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
} from "@urql/svelte";
|
||||
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
|
||||
import { readable } from "svelte/store";
|
||||
import { formatNumber } from './units.js'
|
||||
|
||||
/*
|
||||
* Call this function only at component initialization time!
|
||||
@@ -313,3 +314,55 @@ export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubclust
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function convert2uplot(canvasData) {
|
||||
// initial use: Canvas Histogram Data to Uplot
|
||||
let uplotData = [[],[]] // [X, Y1, Y2, ...]
|
||||
canvasData.forEach( pair => {
|
||||
uplotData[0].push(pair.value)
|
||||
uplotData[1].push(pair.count)
|
||||
})
|
||||
return uplotData
|
||||
}
|
||||
|
||||
export function binsFromFootprint(weights, values, numBins) {
|
||||
let min = 0, max = 0
|
||||
if (values.length != 0) {
|
||||
for (let x of values) {
|
||||
min = Math.min(min, x)
|
||||
max = Math.max(max, x)
|
||||
}
|
||||
max += 1 // So that we have an exclusive range.
|
||||
}
|
||||
|
||||
if (numBins == null || numBins < 3)
|
||||
numBins = 3
|
||||
|
||||
const bins = new Array(numBins).fill(0)
|
||||
for (let i = 0; i < values.length; i++)
|
||||
bins[Math.floor(((values[i] - min) / (max - min)) * numBins)] += weights ? weights[i] : 1
|
||||
|
||||
// return {
|
||||
// label: idx => {
|
||||
// let start = min + (idx / numBins) * (max - min)
|
||||
// let stop = min + ((idx + 1) / numBins) * (max - min)
|
||||
// return `${formatNumber(start)} - ${formatNumber(stop)}`
|
||||
// },
|
||||
// bins: bins.map((count, idx) => ({ value: idx, count: count })),
|
||||
// min: min,
|
||||
// max: max
|
||||
// }
|
||||
|
||||
return {
|
||||
bins: bins.map((count, idx) => ({
|
||||
value: idx => { // Get rounded down next integer to bins' Start-Stop Mean Value
|
||||
let start = min + (idx / numBins) * (max - min)
|
||||
let stop = min + ((idx + 1) / numBins) * (max - min)
|
||||
return `${formatNumber(Math.floor((start+stop)/2))}`
|
||||
},
|
||||
count: count
|
||||
})),
|
||||
min: min,
|
||||
max: max
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,20 @@
|
||||
{{define "content"}}
|
||||
{{if .Infos.message }}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-6">
|
||||
<div class="alert alert-info p-3" role="alert">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-2">
|
||||
<h2><i class="bi-info-circle-fill m-3"></i></h2>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
{{.Infos.message}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>Clusters</h2>
|
||||
|
15
web/web.go
15
web/web.go
@@ -9,11 +9,10 @@ import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/config"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/util"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/schema"
|
||||
)
|
||||
@@ -48,7 +47,7 @@ func init() {
|
||||
}
|
||||
|
||||
if path == "templates/login.tmpl" {
|
||||
if _, err := os.Stat("./var/login.tmpl"); err == nil {
|
||||
if util.CheckFileExists("./var/login.tmpl") {
|
||||
log.Info("overwrite login.tmpl with local file")
|
||||
templates[strings.TrimPrefix(path, "templates/")] =
|
||||
template.Must(template.Must(base.Clone()).ParseFiles("./var/login.tmpl"))
|
||||
@@ -56,7 +55,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
if path == "templates/imprint.tmpl" {
|
||||
if _, err := os.Stat("./var/imprint.tmpl"); err == nil {
|
||||
if util.CheckFileExists("./var/imprint.tmpl") {
|
||||
log.Info("overwrite imprint.tmpl with local file")
|
||||
templates[strings.TrimPrefix(path, "templates/")] =
|
||||
template.Must(template.Must(base.Clone()).ParseFiles("./var/imprint.tmpl"))
|
||||
@@ -64,7 +63,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
if path == "templates/privacy.tmpl" {
|
||||
if _, err := os.Stat("./var/privacy.tmpl"); err == nil {
|
||||
if util.CheckFileExists("./var/privacy.tmpl") {
|
||||
log.Info("overwrite privacy.tmpl with local file")
|
||||
templates[strings.TrimPrefix(path, "templates/")] =
|
||||
template.Must(template.Must(base.Clone()).ParseFiles("./var/privacy.tmpl"))
|
||||
@@ -92,8 +91,8 @@ type Page struct {
|
||||
Title string // Page title
|
||||
MsgType string // For generic use in message boxes
|
||||
Message string // For generic use in message boxes
|
||||
User auth.User // Information about the currently logged in user (Full User Info)
|
||||
Roles map[string]auth.Role // Available roles for frontend render checks
|
||||
User schema.User // Information about the currently logged in user (Full User Info)
|
||||
Roles map[string]schema.Role // Available roles for frontend render checks
|
||||
Build Build // Latest information about the application
|
||||
Clusters []schema.ClusterConfig // List of all clusters for use in the Header
|
||||
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
|
||||
@@ -101,7 +100,7 @@ type Page struct {
|
||||
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
|
||||
}
|
||||
|
||||
func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) {
|
||||
func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
|
||||
t, ok := templates[file]
|
||||
if !ok {
|
||||
log.Errorf("WEB/WEB > template '%s' not found", file)
|
||||
|
Reference in New Issue
Block a user