This commit is contained in:
Christoph Kluge 2024-03-14 11:08:37 +01:00
commit 7940317857
51 changed files with 8733 additions and 6982 deletions

View File

@ -38,7 +38,6 @@ var metricDataRepos map[string]MetricDataRepository = map[string]MetricDataRepos
var useArchive bool
func Init(disableArchive bool) error {
useArchive = !disableArchive
for _, cluster := range config.Keys.Clusters {
if cluster.MetricDataRepository != nil {
@ -80,7 +79,8 @@ var cache *lrucache.Cache = lrucache.New(128 * 1024 * 1024)
func LoadData(job *schema.Job,
metrics []string,
scopes []schema.MetricScope,
ctx context.Context) (schema.JobData, error) {
ctx context.Context,
) (schema.JobData, error) {
data := cache.Get(cacheKey(job, metrics, scopes), func() (_ interface{}, ttl time.Duration, size int) {
var jd schema.JobData
var err error
@ -109,7 +109,8 @@ func LoadData(job *schema.Job,
jd, err = repo.LoadData(job, metrics, scopes, ctx)
if err != nil {
if len(jd) != 0 {
log.Warnf("partial error: %s", err.Error())
log.Errorf("partial error: %s", err.Error())
return err, 0, 0
} else {
log.Error("Error while loading job data from metric repository")
return err, 0, 0
@ -179,8 +180,8 @@ func LoadAverages(
job *schema.Job,
metrics []string,
data [][]schema.Float,
ctx context.Context) error {
ctx context.Context,
) error {
if job.State != schema.JobStateRunning && useArchive {
return archive.LoadAveragesFromArchive(job, metrics, data) // #166 change also here?
}
@ -219,8 +220,8 @@ func LoadNodeData(
metrics, nodes []string,
scopes []schema.MetricScope,
from, to time.Time,
ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) {
ctx context.Context,
) (map[string]map[string][]*schema.JobMetric, error) {
repo, ok := metricDataRepos[cluster]
if !ok {
return nil, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", cluster)
@ -252,8 +253,8 @@ func LoadNodeData(
func cacheKey(
job *schema.Job,
metrics []string,
scopes []schema.MetricScope) string {
scopes []schema.MetricScope,
) string {
// Duration and StartTime do not need to be in the cache key as StartTime is less unique than
// job.ID and the TTL of the cache entry makes sure it does not stay there forever.
return fmt.Sprintf("%d(%s):[%v],[%v]",
@ -267,8 +268,8 @@ func cacheKey(
func prepareJobData(
job *schema.Job,
jobData schema.JobData,
scopes []schema.MetricScope) {
scopes []schema.MetricScope,
) {
const maxSeriesSize int = 15
for _, scopes := range jobData {
for _, jm := range scopes {
@ -295,7 +296,6 @@ func prepareJobData(
// Writes a running job to the job-archive
func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.JobMeta, error) {
allMetrics := make([]string, 0)
metricConfigs := archive.GetCluster(job.Cluster).MetricConfig
for _, mc := range metricConfigs {

View File

@ -9,27 +9,26 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@rollup/plugin-replace": "^5.0.2",
"@urql/svelte": "^4.0.1",
"chart.js": "^4.3.3",
"@rollup/plugin-replace": "^5.0.5",
"@sveltestrap/sveltestrap": "^6.2.6",
"@urql/svelte": "^4.1.0",
"chart.js": "^4.4.2",
"date-fns": "^2.30.0",
"date-fns": "^2.30.0",
"graphql": "^16.6.0",
"mathjs": "^12.0.0",
"svelte-chartjs": "^3.1.2",
"sveltestrap": "^5.11.1",
"uplot": "^1.6.24",
"wonka": "^6.3.2"
"graphql": "^16.8.1",
"mathjs": "^12.4.0",
"svelte-chartjs": "^3.1.5",
"uplot": "^1.6.30",
"wonka": "^6.3.4"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.1",
"@timohausmann/quadtree-js": "^1.2.5",
"rollup": "^3.21.0",
"rollup-plugin-css-only": "^4.3.0",
"rollup-plugin-svelte": "^7.1.4",
"svelte": "^3.58.0"
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@timohausmann/quadtree-js": "^1.2.6",
"rollup": "^4.12.1",
"rollup-plugin-css-only": "^4.5.2",
"rollup-plugin-svelte": "^7.1.6",
"svelte": "^4.2.12"
}
},
"node_modules/@0no-co/graphql.web": {
@ -45,10 +44,22 @@
}
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
"integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
"integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -57,33 +68,30 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
"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,
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"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,
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": {
"node": ">=6.0.0"
}
@ -104,13 +112,9 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dev": true,
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -131,9 +135,9 @@
}
},
"node_modules/@rollup/plugin-commonjs": {
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz",
"integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==",
"version": "25.0.7",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
"integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
@ -141,13 +145,13 @@
"estree-walker": "^2.0.2",
"glob": "^8.0.3",
"is-reference": "1.2.1",
"magic-string": "^0.27.0"
"magic-string": "^0.30.3"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0"
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
@ -156,9 +160,6 @@
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
@ -176,7 +177,6 @@
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
@ -185,23 +185,18 @@
}
},
"node_modules/@rollup/plugin-replace": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"magic-string": "^0.30.3"
"magic-string": "^0.30.3"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
@ -209,32 +204,7 @@
}
}
},
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/plugin-terser": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
@ -249,7 +219,6 @@
},
"peerDependencies": {
"rollup": "^2.0.0||^3.0.0||^4.0.0"
"rollup": "^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
@ -271,7 +240,6 @@
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
@ -279,6 +247,186 @@
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz",
"integrity": "sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.1.tgz",
"integrity": "sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.1.tgz",
"integrity": "sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.1.tgz",
"integrity": "sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.1.tgz",
"integrity": "sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.1.tgz",
"integrity": "sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.1.tgz",
"integrity": "sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.1.tgz",
"integrity": "sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.1.tgz",
"integrity": "sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.1.tgz",
"integrity": "sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.1.tgz",
"integrity": "sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.1.tgz",
"integrity": "sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.1.tgz",
"integrity": "sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@sveltestrap/sveltestrap": {
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-6.2.6.tgz",
"integrity": "sha512-iB50tbVzsFXp0M10pe3XywRkNxjKPIHXJzV44mb1FhajWNWwxme8MkBis9m2QNivM2hyw5zDHjgGuzwTOB76JQ==",
"dependencies": {
"@popperjs/core": "^2.11.8"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0 || ^5.0.0-next.0"
}
},
"node_modules/@timohausmann/quadtree-js": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz",
@ -289,9 +437,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/resolve": {
"version": "1.20.2",
@ -300,20 +445,20 @@
"dev": true
},
"node_modules/@urql/core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.2.0.tgz",
"integrity": "sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.3.0.tgz",
"integrity": "sha512-wT+FeL8DG4x5o6RfHEnONNFVDM3616ouzATMYUClB6CB+iIu2mwfBKd7xSUxYOZmwtxna5/hDRQdMl3nbQZlnw==",
"dependencies": {
"@0no-co/graphql.web": "^1.0.1",
"wonka": "^6.3.2"
}
},
"node_modules/@urql/svelte": {
"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==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.1.0.tgz",
"integrity": "sha512-Ov3EclCjaXPPTjKNTcIDlAG3qY/jhLjl/J9yyz9FeLUQ9S2jEgsvlzNXibrY27f4ihD4gH36CNGuj1XOi5hEEQ==",
"dependencies": {
"@urql/core": "^4.1.0",
"@urql/core": "^4.3.0",
"wonka": "^6.3.2"
},
"peerDependencies": {
@ -321,13 +466,9 @@
}
},
"node_modules/acorn": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"dev": true,
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": {
"acorn": "bin/acorn"
},
@ -335,6 +476,22 @@
"node": ">=0.4.0"
}
},
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/axobject-query": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -369,14 +526,34 @@
}
},
"node_modules/chart.js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
"integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
"integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=7"
"pnpm": ">=8"
}
},
"node_modules/code-red": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"@types/estree": "^1.0.1",
"acorn": "^8.10.0",
"estree-walker": "^3.0.3",
"periscopic": "^3.1.0"
}
},
"node_modules/code-red/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/commander": {
@ -403,6 +580,18 @@
"url": "https://www.patreon.com/infusion"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -432,6 +621,14 @@
"node": ">=0.10.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
@ -482,13 +679,6 @@
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob": {
"version": "8.1.0",
@ -510,9 +700,6 @@
}
},
"node_modules/graphql": {
"version": "16.8.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
"version": "16.8.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
@ -521,21 +708,15 @@
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
"node": ">= 0.4"
}
},
"node_modules/inflight": {
@ -570,16 +751,12 @@
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true,
"dependencies": {
"hasown": "^2.0.0"
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -605,25 +782,28 @@
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
},
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
},
"node_modules/magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"dev": true,
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/mathjs": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.0.0.tgz",
"integrity": "sha512-Oz3swPplNPe7taoP6WrkKhQzhDE2SwvOgLzu8H3EN+hEadw2GjEJUm6Xl+hrioHoB8g2BYb3gfw1glSzhdBKYw==",
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.0.tgz",
"integrity": "sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==",
"dependencies": {
"@babel/runtime": "^7.23.2",
"@babel/runtime": "^7.23.9",
"complex.js": "^2.1.1",
"decimal.js": "^10.4.3",
"escape-latex": "^1.2.0",
@ -640,6 +820,11 @@
"node": ">= 18"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
},
"node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@ -667,6 +852,32 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/periscopic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^3.0.0",
"is-reference": "^3.0.0"
}
},
"node_modules/periscopic/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/periscopic/node_modules/is-reference": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -688,19 +899,11 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
@ -727,28 +930,38 @@
}
},
"node_modules/rollup": {
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz",
"integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==",
"devOptional": true,
"dependencies": {
"@types/estree": "1.0.5"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=14.18.0",
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.12.1",
"@rollup/rollup-android-arm64": "4.12.1",
"@rollup/rollup-darwin-arm64": "4.12.1",
"@rollup/rollup-darwin-x64": "4.12.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.12.1",
"@rollup/rollup-linux-arm64-gnu": "4.12.1",
"@rollup/rollup-linux-arm64-musl": "4.12.1",
"@rollup/rollup-linux-riscv64-gnu": "4.12.1",
"@rollup/rollup-linux-x64-gnu": "4.12.1",
"@rollup/rollup-linux-x64-musl": "4.12.1",
"@rollup/rollup-win32-arm64-msvc": "4.12.1",
"@rollup/rollup-win32-ia32-msvc": "4.12.1",
"@rollup/rollup-win32-x64-msvc": "4.12.1",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-css-only": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
@ -761,7 +974,6 @@
},
"peerDependencies": {
"rollup": "<5"
"rollup": "<5"
}
},
"node_modules/rollup-plugin-svelte": {
@ -820,18 +1032,15 @@
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"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==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
}
},
"node_modules/smob": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
@ -846,6 +1055,14 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -869,40 +1086,58 @@
}
},
"node_modules/svelte": {
"version": "3.59.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@jridgewell/trace-mapping": "^0.3.18",
"@types/estree": "^1.0.1",
"acorn": "^8.9.0",
"aria-query": "^5.3.0",
"axobject-query": "^4.0.0",
"code-red": "^1.0.3",
"css-tree": "^2.3.1",
"estree-walker": "^3.0.3",
"is-reference": "^3.0.1",
"locate-character": "^3.0.0",
"magic-string": "^0.30.4",
"periscopic": "^3.1.0"
},
"engines": {
"node": ">= 8"
"node": ">=16"
}
},
"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==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz",
"integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==",
"peerDependencies": {
"chart.js": "^3.5.0 || ^4.0.0",
"svelte": "^3.45.0"
"svelte": "^4.0.0"
}
},
"node_modules/sveltestrap": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
"node_modules/svelte/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dependencies": {
"@popperjs/core": "^2.11.8"
"@types/estree": "^1.0.0"
}
},
"peerDependencies": {
"svelte": "^3.53.1"
"node_modules/svelte/node_modules/is-reference": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/terser": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz",
"integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==",
"version": "5.29.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
"integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -931,12 +1166,9 @@
}
},
"node_modules/uplot": {
"version": "1.6.27",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
"version": "1.6.27",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
"version": "1.6.30",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.30.tgz",
"integrity": "sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q=="
},
"node_modules/wonka": {
"version": "6.3.4",

View File

@ -7,25 +7,25 @@
"dev": "rollup -c -w"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-terser": "^0.4.1",
"@timohausmann/quadtree-js": "^1.2.5",
"rollup": "^3.21.0",
"rollup-plugin-css-only": "^4.3.0",
"rollup-plugin-svelte": "^7.1.4",
"svelte": "^3.58.0"
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@timohausmann/quadtree-js": "^1.2.6",
"rollup": "^4.12.1",
"rollup-plugin-css-only": "^4.5.2",
"rollup-plugin-svelte": "^7.1.6",
"svelte": "^4.2.12"
},
"dependencies": {
"@rollup/plugin-replace": "^5.0.2",
"@urql/svelte": "^4.0.1",
"chart.js": "^4.3.3",
"@rollup/plugin-replace": "^5.0.5",
"@sveltestrap/sveltestrap": "^6.2.6",
"@urql/svelte": "^4.1.0",
"chart.js": "^4.4.2",
"date-fns": "^2.30.0",
"graphql": "^16.6.0",
"mathjs": "^12.0.0",
"svelte-chartjs": "^3.1.2",
"sveltestrap": "^5.11.1",
"uplot": "^1.6.24",
"wonka": "^6.3.2"
"graphql": "^16.8.1",
"mathjs": "^12.4.0",
"svelte-chartjs": "^3.1.5",
"uplot": "^1.6.30",
"wonka": "^6.3.4"
}
}

View File

@ -1,30 +1,44 @@
<script>
import { init, convert2uplot } from './utils.js'
import { getContext, onMount } from 'svelte'
import { queryStore, gql, getContextClient, mutationStore } from '@urql/svelte'
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
import Filters from './filters/Filters.svelte'
import PlotSelection from './PlotSelection.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 RooflineHeatmap from './plots/RooflineHeatmap.svelte'
import { init, convert2uplot } from "./utils.js";
import { getContext, onMount } from "svelte";
import {
queryStore,
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import {
Row,
Col,
Spinner,
Card,
Table,
Icon,
} from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte";
import PlotSelection from "./PlotSelection.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 RooflineHeatmap from "./plots/RooflineHeatmap.svelte";
const { query: initq } = init()
const { query: initq } = init();
export let filterPresets
export let filterPresets;
// By default, look at the jobs of the last 6 hours:
if (filterPresets?.startTime == null) {
if (filterPresets == null)
filterPresets = {}
if (filterPresets == null) filterPresets = {};
let now = new Date(Date.now())
let hourAgo = new Date(now)
hourAgo.setHours(hourAgo.getHours() - 6)
filterPresets.startTime = { from: hourAgo.toISOString(), to: now.toISOString() }
let now = new Date(Date.now());
let hourAgo = new Date(now);
hourAgo.setHours(hourAgo.getHours() - 6);
filterPresets.startTime = {
from: hourAgo.toISOString(),
to: now.toISOString(),
};
}
let cluster;
@ -34,44 +48,68 @@
let colWidth1, colWidth2, colWidth3, colWidth4;
let numBins = 50;
let maxY = -1;
const ccconfig = getContext('cc-config')
const metricConfig = getContext('metrics')
const ccconfig = getContext("cc-config");
const metricConfig = getContext("metrics");
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics;
$: metrics = [...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
$: metrics = [
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
];
const sortOptions = [
{key: 'totalWalltime', label: 'Walltime'},
{key: 'totalNodeHours', label: 'Node Hours'},
{key: 'totalCoreHours', label: 'Core Hours'},
{key: 'totalAccHours', label: 'Accelerator Hours'}
]
{ key: "totalWalltime", label: "Walltime" },
{ key: "totalNodeHours", label: "Node Hours" },
{ key: "totalCoreHours", label: "Core Hours" },
{ key: "totalAccHours", label: "Accelerator Hours" },
];
const groupOptions = [
{key: 'user', label: 'User Name'},
{key: 'project', label: 'Project ID'}
]
{ key: "user", label: "User Name" },
{ key: "project", label: "Project ID" },
];
let sortSelection = sortOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`]) || sortOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopCategory)
let groupSelection = groupOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`]) || groupOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopEntity)
let sortSelection =
sortOptions.find(
(option) =>
option.key ==
ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`],
) ||
sortOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopCategory,
);
let groupSelection =
groupOptions.find(
(option) =>
option.key ==
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`],
) ||
groupOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopEntity,
);
getContext('on-init')(({ data }) => {
getContext("on-init")(({ data }) => {
if (data != null) {
cluster = data.clusters.find(c => c.name == filterPresets.cluster)
console.assert(cluster != null, `This cluster could not be found: ${filterPresets.cluster}`)
cluster = data.clusters.find((c) => c.name == filterPresets.cluster);
console.assert(
cluster != null,
`This cluster could not be found: ${filterPresets.cluster}`,
);
rooflineMaxY = cluster.subClusters.reduce((max, part) => Math.max(max, part.flopRateSimd.value), 0)
maxY = rooflineMaxY
rooflineMaxY = cluster.subClusters.reduce(
(max, part) => Math.max(max, part.flopRateSimd.value),
0,
);
maxY = rooflineMaxY;
}
})
});
const client = getContextClient();
$: statsQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
query ($jobFilters: [JobFilter!]!) {
stats: jobsStatistics(filter: $jobFilters) {
totalJobs
shortJobs
@ -79,19 +117,35 @@
totalNodeHours
totalCoreHours
totalAccHours
histDuration { count, value }
histNumCores { count, value }
histDuration {
count
value
}
histNumCores {
count
value
}
}
}
`,
variables: { jobFilters }
})
variables: { jobFilters },
});
$: topQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $paging: PageRequest!, $sortBy: SortByAggregate!, $groupBy: Aggregate!) {
topList: jobsStatistics(filter: $jobFilters, page: $paging, sortBy: $sortBy, groupBy: $groupBy) {
query (
$jobFilters: [JobFilter!]!
$paging: PageRequest!
$sortBy: SortByAggregate!
$groupBy: Aggregate!
) {
topList: jobsStatistics(
filter: $jobFilters
page: $paging
sortBy: $sortBy
groupBy: $groupBy
) {
id
totalWalltime
totalNodeHours
@ -100,32 +154,67 @@
}
}
`,
variables: { jobFilters, paging: { itemsPerPage: 10, page: 1 }, sortBy: sortSelection.key.toUpperCase(), groupBy: groupSelection.key.toUpperCase() }
})
variables: {
jobFilters,
paging: { itemsPerPage: 10, page: 1 },
sortBy: sortSelection.key.toUpperCase(),
groupBy: groupSelection.key.toUpperCase(),
},
});
$: footprintsQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
timeWeights { nodeHours, accHours, coreHours },
metrics { metric, data }
timeWeights {
nodeHours
accHours
coreHours
}
}`,
variables: { jobFilters, metrics }
})
metrics {
metric
data
}
}
}
`,
variables: { jobFilters, metrics },
});
$: rooflineQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $rows: Int!, $cols: Int!,
$minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) {
rooflineHeatmap(filter: $jobFilters, rows: $rows, cols: $cols,
minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY)
query (
$jobFilters: [JobFilter!]!
$rows: Int!
$cols: Int!
$minX: Float!
$minY: Float!
$maxX: Float!
$maxY: Float!
) {
rooflineHeatmap(
filter: $jobFilters
rows: $rows
cols: $cols
minX: $minX
minY: $minY
maxX: $maxX
maxY: $maxY
)
}
`,
variables: { jobFilters, rows: 50, cols: 50, minX: 0.01, minY: 1., maxX: 1000., maxY }
})
variables: {
jobFilters,
rows: 50,
cols: 50,
minX: 0.01,
minY: 1,
maxX: 1000,
maxY,
},
});
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
@ -135,46 +224,54 @@
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
variables: { name, value },
});
}
};
function updateEntityConfiguration(select) {
if (ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] != select) {
updateConfigurationMutation({ name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`, value: JSON.stringify(select) })
.subscribe(res => {
if (
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] !=
select
) {
updateConfigurationMutation({
name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`,
value: JSON.stringify(select),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
// console.log(`analysis_view_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error
throw res.error;
}
})
});
} else {
// console.log('No Mutation Required: Entity')
}
};
}
function updateCategoryConfiguration(select) {
if (ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] != select) {
updateConfigurationMutation({ name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`, value: JSON.stringify(select) })
.subscribe(res => {
if (
ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] !=
select
) {
updateConfigurationMutation({
name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`,
value: JSON.stringify(select),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
// console.log(`analysis_view_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error
throw res.error;
}
})
});
} else {
// console.log('No Mutation Required: Category')
}
}
};
$: updateEntityConfiguration(groupSelection.key);
$: updateCategoryConfiguration(sortSelection.key);
$: updateEntityConfiguration(groupSelection.key)
$: updateCategoryConfiguration(sortSelection.key)
onMount(() => filterComponent.update())
onMount(() => filterComponent.update());
</script>
<Row>
@ -188,24 +285,26 @@
<Card body color="danger">{$initq.error.message}</Card>
{:else if cluster}
<PlotSelection
availableMetrics={cluster.metricConfig.map(mc => mc.name)}
bind:metricsInHistograms={metricsInHistograms}
bind:metricsInScatterplots={metricsInScatterplots} />
availableMetrics={cluster.metricConfig.map((mc) => mc.name)}
bind:metricsInHistograms
bind:metricsInScatterplots
/>
{/if}
</Col>
<Col xs="auto">
<Filters
bind:this={filterComponent}
filterPresets={filterPresets}
{filterPresets}
disableClusterSelection={true}
startTimeQuickSelect={true}
on:update={({ detail }) => {
jobFilters = detail.filters;
}} />
}}
/>
</Col>
</Row>
<br/>
<br />
{#if $statsQuery.error}
<Row>
<Col>
@ -244,7 +343,8 @@
</Col>
<Col>
<div bind:clientWidth={colWidth1}>
<h5>Top
<h5>
Top
<select class="p-0" bind:value={groupSelection}>
{#each groupOptions as option}
<option value={option}>
@ -255,14 +355,16 @@
</h5>
{#key $topQuery.data}
{#if $topQuery.fetching}
<Spinner/>
<Spinner />
{:else if $topQuery.error}
<Card body color="danger">{$topQuery.error.message}</Card>
{:else}
<Pie
size={colWidth1}
sliceLabel={sortSelection.label}
quantities={$topQuery.data.topList.map((t) => t[sortSelection.key])}
quantities={$topQuery.data.topList.map(
(t) => t[sortSelection.key],
)}
entities={$topQuery.data.topList.map((t) => t.id)}
/>
{/if}
@ -272,7 +374,7 @@
<Col>
{#key $topQuery.data}
{#if $topQuery.fetching}
<Spinner/>
<Spinner />
{:else if $topQuery.error}
<Card body color="danger">{$topQuery.error.message}</Card>
{:else}
@ -292,11 +394,20 @@
</tr>
{#each $topQuery.data.topList as te, i}
<tr>
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
{#if groupSelection.key == 'user'}
<th scope="col"><a href="/monitoring/user/{te.id}?cluster={cluster.name}">{te.id}</a></th>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
{#if groupSelection.key == "user"}
<th scope="col"
><a href="/monitoring/user/{te.id}?cluster={cluster.name}"
>{te.id}</a
></th
>
{:else}
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq">{te.id}</a></th>
<th scope="col"
><a
href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq"
>{te.id}</a
></th
>
{/if}
<td>{te[sortSelection.key]}</td>
</tr>
@ -316,10 +427,14 @@
<div bind:clientWidth={colWidth2}>
{#key $rooflineQuery.data}
<RooflineHeatmap
width={colWidth2} height={300}
width={colWidth2}
height={300}
tiles={$rooflineQuery.data.rooflineHeatmap}
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
maxY={rooflineMaxY} />
cluster={cluster.subClusters.length == 1
? cluster.subClusters[0]
: null}
maxY={rooflineMaxY}
/>
{/key}
</div>
{/if}
@ -328,13 +443,15 @@
<div bind:clientWidth={colWidth3}>
{#key $statsQuery.data.stats[0].histDuration}
<Histogram
width={colWidth3} height={300}
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"/>
yunit="Jobs"
/>
{/key}
</div>
</Col>
@ -342,20 +459,22 @@
<div bind:clientWidth={colWidth4}>
{#key $statsQuery.data.stats[0].histNumCores}
<Histogram
width={colWidth4} height={300}
width={colWidth4}
height={300}
data={convert2uplot($statsQuery.data.stats[0].histNumCores)}
title="Number of Cores Distribution"
xlabel="Allocated Cores"
xunit="Cores"
ylabel="Number of Jobs"
yunit="Jobs"/>
yunit="Jobs"
/>
{/key}
</div>
</Col>
</Row>
{/if}
<hr class="my-6"/>
<hr class="my-6" />
{#if $footprintsQuery.error}
<Row>
@ -367,11 +486,14 @@
<Row>
<Col>
<Card body>
These histograms show the distribution of the averages of all jobs matching the filters. Each job/average is weighted by its node hours by default
(Accelerator hours for native accelerator scope metrics, coreHours for native core scope metrics).
Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
These histograms show the distribution of the averages of all jobs
matching the filters. Each job/average is weighted by its node hours by
default (Accelerator hours for native accelerator scope metrics,
coreHours for native core scope metrics). Note that some metrics could
be disabled for specific subclusters as per metricConfig and thus could
affect shown average values.
</Card>
<br/>
<br />
</Col>
</Row>
<Row>
@ -380,34 +502,58 @@
let:item
let:width
renderFor="analysis"
items={metricsInHistograms.map(metric => ({ metric, ...binsFromFootprint(
items={metricsInHistograms.map((metric) => ({
metric,
...binsFromFootprint(
$footprintsQuery.data.footprints.timeWeights,
metricConfig(cluster.name, metric)?.scope,
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
$footprintsQuery.data.footprints.metrics.find(
(f) => f.metric == metric,
).data,
numBins,
),
}))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
<Histogram
data={convert2uplot(item.bins)}
width={width} height={250}
{width}
height={250}
usesBins={true}
title="Average Distribution of '{item.metric}'"
xlabel={`${item.metric} bin maximum ${(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 + ']' : '')}`}
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 : '')}`}
xlabel={`${item.metric} bin maximum ${
(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 + "]"
: "")
}`}
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="Normalized Hours"
yunit="Hours"/>
yunit="Hours"
/>
</PlotTable>
</Col>
</Row>
<br/>
<br />
<Row>
<Col>
<Card body>
Each circle represents one job. The size of a circle is proportional to its node hours. Darker circles mean multiple jobs have the same averages for the respective metrics.
Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
Each circle represents one job. The size of a circle is proportional to
its node hours. Darker circles mean multiple jobs have the same averages
for the respective metrics. Note that some metrics could be disabled for
specific subclusters as per metricConfig and thus could affect shown
average values.
</Card>
<br/>
<br />
</Col>
</Row>
<Row>
@ -417,17 +563,41 @@
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 }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
m1,
f1: $footprintsQuery.data.footprints.metrics.find(
(f) => f.metric == m1,
).data,
m2,
f2: $footprintsQuery.data.footprints.metrics.find(
(f) => f.metric == m2,
).data,
}))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
<ScatterPlot
width={width} height={250} color={"rgba(0, 102, 204, 0.33)"}
xLabel={`${item.m1} [${(metricConfig(cluster.name, item.m1)?.unit?.prefix ? metricConfig(cluster.name, item.m1)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.m1)?.unit?.base ? metricConfig(cluster.name, item.m1)?.unit?.base : '')}]`}
yLabel={`${item.m2} [${(metricConfig(cluster.name, item.m2)?.unit?.prefix ? metricConfig(cluster.name, item.m2)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.m2)?.unit?.base ? metricConfig(cluster.name, item.m2)?.unit?.base : '')}]`}
X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.timeWeights.nodeHours} />
{width}
height={250}
color={"rgba(0, 102, 204, 0.33)"}
xLabel={`${item.m1} [${
(metricConfig(cluster.name, item.m1)?.unit?.prefix
? metricConfig(cluster.name, item.m1)?.unit?.prefix
: "") +
(metricConfig(cluster.name, item.m1)?.unit?.base
? metricConfig(cluster.name, item.m1)?.unit?.base
: "")
}]`}
yLabel={`${item.m2} [${
(metricConfig(cluster.name, item.m2)?.unit?.prefix
? metricConfig(cluster.name, item.m2)?.unit?.prefix
: "") +
(metricConfig(cluster.name, item.m2)?.unit?.base
? metricConfig(cluster.name, item.m2)?.unit?.base
: "")
}]`}
X={item.f1}
Y={item.f2}
S={$footprintsQuery.data.footprints.timeWeights.nodeHours}
/>
</PlotTable>
</Col>
</Row>

View File

@ -1,31 +1,30 @@
<script>
import { getContext } from 'svelte'
import { init } from './utils.js'
import { Card, CardHeader, CardTitle } from 'sveltestrap'
import { getContext } from "svelte";
import { init } from "./utils.js";
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
import PlotSettings from './config/PlotSettings.svelte'
import AdminSettings from './config/AdminSettings.svelte'
import PlotSettings from "./config/PlotSettings.svelte";
import AdminSettings from "./config/AdminSettings.svelte";
const { query: initq } = init()
const { query: initq } = init();
const ccconfig = getContext('cc-config')
export let isAdmin
const ccconfig = getContext("cc-config");
export let isAdmin;
</script>
{#if isAdmin == true}
<Card style="margin-bottom: 1.5em;">
<Card style="margin-bottom: 1.5em;">
<CardHeader>
<CardTitle class="mb-1">Admin Options</CardTitle>
</CardHeader>
<AdminSettings/>
</Card>
<AdminSettings />
</Card>
{/if}
<Card>
<CardHeader>
<CardTitle class="mb-1">Plotting Options</CardTitle>
</CardHeader>
<PlotSettings config={ccconfig}/>
<PlotSettings config={ccconfig} />
</Card>

View File

@ -9,7 +9,7 @@
Dropdown,
DropdownToggle,
DropdownMenu,
} from "sveltestrap";
} from "@sveltestrap/sveltestrap";
import NavbarLinks from "./NavbarLinks.svelte";
import NavbarTools from "./NavbarTools.svelte";
@ -116,17 +116,13 @@
{#if screenSize > 1500 || screenSize < 768}
<NavbarLinks
{clusters}
links={views.filter(
(item) => item.requiredRole <= authlevel
)}
links={views.filter((item) => item.requiredRole <= authlevel)}
/>
{:else if screenSize > 1300}
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu != "Stats"
(item) => item.requiredRole <= authlevel && item.menu != "Stats",
)}
/>
<Dropdown nav>
@ -139,8 +135,7 @@
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu == "Stats"
item.requiredRole <= authlevel && item.menu == "Stats",
)}
/>
</DropdownMenu>
@ -149,9 +144,7 @@
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu == "none"
(item) => item.requiredRole <= authlevel && item.menu == "none",
)}
/>
{#each Array("Groups", "Stats") as menu}
@ -163,9 +156,7 @@
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu == menu
(item) => item.requiredRole <= authlevel && item.menu == menu,
)}
/>
</DropdownMenu>

View File

@ -1,65 +1,73 @@
<script>
import { Modal, ModalBody, ModalHeader, ModalFooter,
Button, ListGroup, ListGroupItem } from 'sveltestrap'
import { gql, getContextClient , mutationStore } from '@urql/svelte'
import {
Modal,
ModalBody,
ModalHeader,
ModalFooter,
Button,
ListGroup,
ListGroupItem,
} from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let cluster
export let metricsInHistograms
export let isOpen
export let cluster;
export let metricsInHistograms;
export let isOpen;
let availableMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // 'net_bw', 'file_bw'
let pendingMetrics = [...metricsInHistograms] // Copy
const client = getContextClient()
let availableMetrics = ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // 'net_bw', 'file_bw'
let pendingMetrics = [...metricsInHistograms]; // Copy
const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`mutation($name: String!, $value: String!) {
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}`,
variables: { name, value }
})
}
`,
variables: { name, value },
});
};
function updateConfiguration(data) {
updateConfigurationMutation({
name: data.name,
value: JSON.stringify(data.value)
}).subscribe(res => {
value: JSON.stringify(data.value),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
});
}
function closeAndApply() {
metricsInHistograms = [...pendingMetrics] // Set for parent
isOpen = !isOpen
metricsInHistograms = [...pendingMetrics]; // Set for parent
isOpen = !isOpen;
updateConfiguration({
name: cluster ? `user_view_histogramMetrics:${cluster}` : 'user_view_histogramMetrics',
value: metricsInHistograms
})
name: cluster
? `user_view_histogramMetrics:${cluster}`
: "user_view_histogramMetrics",
value: metricsInHistograms,
});
}
</script>
<Modal {isOpen}
toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select metrics presented in histograms
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select metrics presented in histograms</ModalHeader>
<ModalBody>
<ListGroup>
{#each availableMetrics as metric (metric)}
<ListGroupItem>
<input type="checkbox" bind:group={pendingMetrics} value={metric}>
<input type="checkbox" bind:group={pendingMetrics} value={metric} />
{metric}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={closeAndApply}> Close & Apply </Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}> Close </Button>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -4,7 +4,7 @@
groupByScope,
fetchMetricsStore,
checkMetricDisabled,
transformDataForRoofline
transformDataForRoofline,
} from "./utils.js";
import {
Row,
@ -18,7 +18,7 @@
CardTitle,
Button,
Icon,
} from "sveltestrap";
} from "@sveltestrap/sveltestrap";
import PlotTable from "./PlotTable.svelte";
import Metric from "./Metric.svelte";
import Polar from "./plots/Polar.svelte";
@ -34,8 +34,15 @@
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 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}") {
@ -54,7 +61,7 @@
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
metrics = getContext("metrics")
metrics = getContext("metrics");
let isMetricsSelectionOpen = false,
selectedMetrics = [],
@ -81,23 +88,21 @@
]);
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
accNodeOnly = [...toFetch].some(function(m) {
accNodeOnly = [...toFetch].some(function (m) {
if (accMetrics.includes(m)) {
const mc = metrics(job.cluster, m)
return mc.scope !== 'accelerator'
const mc = metrics(job.cluster, m);
return mc.scope !== "accelerator";
} else {
return false
return false;
}
})
});
if (job.numAcc === 0 || accNodeOnly === true) {
// No Accels or Accels on Node Scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node"]
: ["node", "socket", "core"]
job.numNodes > 2 ? ["node"] : ["node", "socket", "core"],
);
} else {
// Accels and not on node scope
@ -106,7 +111,7 @@
[...toFetch],
job.numNodes > 2
? ["node", "accelerator"]
: ["node", "accelerator", "socket", "core"]
: ["node", "accelerator", "socket", "core"],
);
}
@ -126,7 +131,7 @@
startFetching(
$initq.data.job,
[...notYetFetched],
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"]
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"],
);
};
@ -163,8 +168,8 @@
!checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster
)
$initq.data.job.subCluster,
),
);
missingHosts = job.resources
.map(({ hostname }) => ({
@ -174,10 +179,8 @@
!metrics.some(
(jm) =>
jm.scope == "node" &&
jm.metric.series.some(
(series) => series.hostname == hostname
)
)
jm.metric.series.some((series) => series.hostname == hostname),
),
),
}))
.filter(({ metrics }) => metrics.length > 0);
@ -191,7 +194,7 @@
disabled: checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster
$initq.data.job.subCluster,
),
}));
</script>
@ -231,16 +234,15 @@
<ul>
<li>
<a
href="/monitoring/jobs/?{$initq.data.job
.concurrentJobs.listQuery}"
href="/monitoring/jobs/?{$initq.data.job.concurrentJobs
.listQuery}"
target="_blank">See All</a
>
</li>
{#each $initq.data.job.concurrentJobs.items as pjob, index}
<li>
<a
href="/monitoring/job/{pjob.id}"
target="_blank">{pjob.jobId}</a
<a href="/monitoring/job/{pjob.id}" target="_blank"
>{pjob.jobId}</a
>
</li>
{/each}
@ -249,12 +251,10 @@
{:else}
<Col>
<h5>
{$initq.data.job.concurrentJobs.items.length} Concurrent
Jobs
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
</h5>
<p>
Number of shared jobs on the same node with overlapping
runtimes.
Number of shared jobs on the same node with overlapping runtimes.
</p>
</Col>
{/if}
@ -273,15 +273,15 @@
renderTime={true}
cluster={clusters
.find((c) => c.name == $initq.data.job.cluster)
.subClusters.find(
(sc) => sc.name == $initq.data.job.subCluster
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
data={transformDataForRoofline(
$jobMetrics.data.jobMetrics.find(
(m) => m.name == "flops_any" && m.scope == "node",
).metric,
$jobMetrics.data.jobMetrics.find(
(m) => m.name == "mem_bw" && m.scope == "node",
).metric,
)}
data={
transformDataForRoofline (
$jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric,
$jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric
)
}
/>
</Col>
{:else}
@ -302,7 +302,7 @@
</Button>
{/if}
</Col>
<!-- <Col xs="auto">
<!-- <Col xs="auto">
<Zoom timeseriesPlots={plots} />
</Col> -->
</Row>
@ -310,9 +310,7 @@
<Col>
{#if $jobMetrics.error}
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
<Card body color="warning"
>Not monitored or archiving failed</Card
>
<Card body color="warning">Not monitored or archiving failed</Card>
<br />
{/if}
<Card body color="danger">{$jobMetrics.error.message}</Card>
@ -325,15 +323,14 @@
renderFor="job"
items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics
selectedMetrics,
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
{#if item.data}
<Metric
bind:this={plots[item.metric]}
on:more-loaded={({ detail }) =>
statsTable.moreLoaded(detail)}
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
job={$initq.data.job}
metricName={item.metric}
rawData={item.data.map((x) => x.metric)}
@ -344,8 +341,7 @@
/>
{:else}
<Card body color="warning"
>No dataset returned for <code>{item.metric}</code
></Card
>No dataset returned for <code>{item.metric}</code></Card
>
{/if}
</PlotTable>
@ -357,36 +353,26 @@
{#if $initq.data}
<TabContent>
{#if somethingMissing}
<TabPane
tabId="resources"
tab="Resources"
active={somethingMissing}
>
<TabPane tabId="resources" tab="Resources" active={somethingMissing}>
<div style="margin: 10px;">
<Card color="warning">
<CardHeader>
<CardTitle
>Missing Metrics/Reseources</CardTitle
>
<CardTitle>Missing Metrics/Reseources</CardTitle>
</CardHeader>
<CardBody>
{#if missingMetrics.length > 0}
<p>
No data at all is available for the
metrics: {missingMetrics.join(", ")}
No data at all is available for the metrics: {missingMetrics.join(
", ",
)}
</p>
{/if}
{#if missingHosts.length > 0}
<p>
Some metrics are missing for the
following hosts:
</p>
<p>Some metrics are missing for the following hosts:</p>
<ul>
{#each missingHosts as missing}
<li>
{missing.hostname}: {missing.metrics.join(
", "
)}
{missing.hostname}: {missing.metrics.join(", ")}
</li>
{/each}
</ul>
@ -414,22 +400,16 @@
<TabPane tabId="job-script" tab="Job Script">
<div class="pre-wrapper">
{#if $initq.data.job.metaData?.jobScript}
<pre><code
>{$initq.data.job.metaData?.jobScript}</code
></pre>
<pre><code>{$initq.data.job.metaData?.jobScript}</code></pre>
{:else}
<Card body color="warning"
>No job script available</Card
>
<Card body color="warning">No job script available</Card>
{/if}
</div>
</TabPane>
<TabPane tabId="slurm-info" tab="Slurm Info">
<div class="pre-wrapper">
{#if $initq.data.job.metaData?.slurmInfo}
<pre><code
>{$initq.data.job.metaData?.slurmInfo}</code
></pre>
<pre><code>{$initq.data.job.metaData?.slurmInfo}</code></pre>
{:else}
<Card body color="warning"
>No additional slurm information available</Card

View File

@ -1,5 +1,65 @@
<script context="module">
export function findJobThresholds(job, metricConfig, subClusterConfig) {
if (!job || !metricConfig || !subClusterConfig) {
console.warn("Argument missing for findJobThresholds!");
return null;
}
const subclusterThresholds = metricConfig.subClusters.find(
(sc) => sc.name == subClusterConfig.name,
);
const defaultThresholds = {
peak: subclusterThresholds
? subclusterThresholds.peak
: metricConfig.peak,
normal: subclusterThresholds
? subclusterThresholds.normal
: metricConfig.normal,
caution: subclusterThresholds
? subclusterThresholds.caution
: metricConfig.caution,
alert: subclusterThresholds
? subclusterThresholds.alert
: metricConfig.alert,
};
if (job.exclusive === 1) {
// Exclusive: Use as defined
return defaultThresholds;
} else {
// Shared: Handle specifically
if (metricConfig.name === "cpu_load") {
// Special: Avg Aggregation BUT scaled based on #hwthreads
return {
peak: job.numHWThreads,
normal: job.numHWThreads,
caution: defaultThresholds.caution,
alert: defaultThresholds.alert,
};
} else if (metricConfig.aggregation === "avg") {
return defaultThresholds;
} else if (metricConfig.aggregation === "sum") {
const jobFraction =
job.numHWThreads / subClusterConfig.topology.node.length;
return {
peak: round(defaultThresholds.peak * jobFraction, 0),
normal: round(defaultThresholds.normal * jobFraction, 0),
caution: round(defaultThresholds.caution * jobFraction, 0),
alert: round(defaultThresholds.alert * jobFraction, 0),
};
} else {
console.warn(
"Missing or unkown aggregation mode (sum/avg) for metric:",
metricConfig,
);
return null;
}
} // Other job.exclusive cases?
}
</script>
<script>
import { getContext } from 'svelte'
import { getContext } from "svelte";
import {
Card,
CardHeader,
@ -7,121 +67,131 @@
CardBody,
Progress,
Icon,
Tooltip
} from "sveltestrap";
import { mean, round } from 'mathjs'
Tooltip,
} from "@sveltestrap/sveltestrap";
import { mean, round } from "mathjs";
export let job
export let jobMetrics
export let view = 'job'
export let width = 'auto'
export let job;
export let jobMetrics;
export let view = "job";
export let width = "auto";
const clusters = getContext('clusters')
const subclusterConfig = clusters.find((c) => c.name == job.cluster).subClusters.find((sc) => sc.name == job.subCluster)
const clusters = getContext("clusters");
const subclusterConfig = clusters
.find((c) => c.name == job.cluster)
.subClusters.find((sc) => sc.name == job.subCluster);
const footprintMetrics = (job.numAcc !== 0)
? (job.exclusive !== 1) // GPU
? ['acc_utilization', 'acc_mem_used', 'nv_sm_clock', 'nv_mem_util'] // Shared
: ['acc_utilization', 'acc_mem_used', 'nv_sm_clock', 'nv_mem_util'] // Exclusive
: (job.exclusive !== 1) // CPU only
? ['flops_any', 'mem_used'] // Shared
: ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // Exclusive
const footprintMetrics =
job.numAcc !== 0
? job.exclusive !== 1 // GPU
? ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Shared
: ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Exclusive
: (job.exclusive !== 1) // CPU Only
? ["flops_any", "mem_used"] // Shared
: ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // Exclusive
const footprintData = footprintMetrics.map((fm) => {
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
let mv = null
if (fm === 'cpu_load' && job.loadAvg !== 0) {
mv = round(job.loadAvg, 2)
} else if (fm === 'flops_any' && job.flopsAnyAvg !== 0) {
mv = round(job.flopsAnyAvg, 2)
} else if (fm === 'mem_bw' && job.memBwAvg !== 0) {
mv = round(job.memBwAvg, 2)
} else { // Calculate from jobMetrics
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === 'node')
if (jm?.metric?.statisticsSeries) {
mv = round(mean(jm.metric.statisticsSeries.mean), 2)
} else if (jm?.metric?.series?.length > 1) {
const avgs = jm.metric.series.map(jms => jms.statistics.avg)
mv = round(mean(avgs), 2)
} else if (jm?.metric?.series) {
mv = round(jm.metric.series[0].statistics.avg, 2)
let mv = null;
if (fm === "cpu_load" && job.loadAvg !== 0) {
mv = round(job.loadAvg, 2);
} else if (fm === "flops_any" && job.flopsAnyAvg !== 0) {
mv = round(job.flopsAnyAvg, 2);
} else if (fm === "mem_bw" && job.memBwAvg !== 0) {
mv = round(job.memBwAvg, 2);
} else {
mv = 0.0
// Calculate from jobMetrics
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
if (jm?.metric?.statisticsSeries) {
mv = round(mean(jm.metric.statisticsSeries.mean), 2);
} else if (jm?.metric?.series?.length > 1) {
const avgs = jm.metric.series.map((jms) => jms.statistics.avg);
mv = round(mean(avgs), 2);
} else if (jm?.metric?.series) {
mv = round(jm.metric.series[0].statistics.avg, 2);
} else {
mv = 0.0;
}
}
// Unit
const fmc = getContext('metrics')(job.cluster, fm)
let unit = ''
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base
const fmc = getContext("metrics")(job.cluster, fm);
let unit = "";
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base;
// Threshold / -Differences
const fmt = findJobThresholds(job, fmc, subclusterConfig)
if (fm === 'flops_any') fmt.peak = round((fmt.peak * 0.85), 0)
const fmt = findJobThresholds(job, fmc, subclusterConfig);
if (fm === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0);
// Define basic data
const fmBase = {
name: fm,
unit: unit,
avg: mv,
max: fmt.peak
}
max: fmt.peak,
};
if (evalFootprint(fm, mv, fmt, 'alert')) {
if (evalFootprint(fm, mv, fmt, "alert")) {
return {
...fmBase,
color: 'danger',
message:`Metric average way ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
impact: 3
}
} else if (evalFootprint(fm, mv, fmt, 'caution')) {
color: "danger",
message: `Metric average way ${fm === "mem_used" ? "above" : "below"} expected normal thresholds.`,
impact: 3,
};
} else if (evalFootprint(fm, mv, fmt, "caution")) {
return {
...fmBase,
color: 'warning',
message: `Metric average ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
impact: 2
}
} else if (evalFootprint(fm, mv, fmt, 'normal')) {
color: "warning",
message: `Metric average ${fm === "mem_used" ? "above" : "below"} expected normal thresholds.`,
impact: 2,
};
} else if (evalFootprint(fm, mv, fmt, "normal")) {
return {
...fmBase,
color: 'success',
message: 'Metric average within expected thresholds.',
impact: 1
}
} else if (evalFootprint(fm, mv, fmt, 'peak')) {
color: "success",
message: "Metric average within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(fm, mv, fmt, "peak")) {
return {
...fmBase,
color: 'info',
message: 'Metric average above expected normal thresholds: Check for artifacts recommended.',
impact: 0
}
color: "info",
message:
"Metric average above expected normal thresholds: Check for artifacts recommended.",
impact: 0,
};
} else {
return {
...fmBase,
color: 'secondary',
message: 'Metric average above expected peak threshold: Check for artifacts!',
impact: -1
color: "secondary",
message:
"Metric average above expected peak threshold: Check for artifacts!",
impact: -1,
};
}
}
})
});
function evalFootprint(metric, mean, thresholds, level) {
// mem_used has inverse logic regarding threshold levels, notify levels triggered if mean > threshold
switch (level) {
case 'peak':
if (metric === 'mem_used') return false // mem_used over peak -> return false to trigger impact -1
else return (mean <= thresholds.peak && mean > thresholds.normal)
case 'alert':
if (metric === 'mem_used') return (mean <= thresholds.peak && mean >= thresholds.alert)
else return (mean <= thresholds.alert && mean >= 0)
case 'caution':
if (metric === 'mem_used') return (mean < thresholds.alert && mean >= thresholds.caution)
else return (mean <= thresholds.caution && mean > thresholds.alert)
case 'normal':
if (metric === 'mem_used') return (mean < thresholds.caution && mean >= 0)
else return (mean <= thresholds.normal && mean > thresholds.caution)
case "peak":
if (metric === "mem_used")
return false; // mem_used over peak -> return false to trigger impact -1
else return mean <= thresholds.peak && mean > thresholds.normal;
case "alert":
if (metric === "mem_used")
return mean <= thresholds.peak && mean >= thresholds.alert;
else return mean <= thresholds.alert && mean >= 0;
case "caution":
if (metric === "mem_used")
return mean < thresholds.alert && mean >= thresholds.caution;
else return mean <= thresholds.caution && mean > thresholds.alert;
case "normal":
if (metric === "mem_used")
return mean < thresholds.caution && mean >= 0;
else return mean <= thresholds.normal && mean > thresholds.caution;
default:
return false
return false;
}
}
</script>
@ -155,13 +225,7 @@
} else if (metricConfig.aggregation === 'avg' ){
return defaultThresholds
} else if (metricConfig.aggregation === 'sum' ){
let jobFraction = 0.0
if (job.numAcc > 0) {
jobFraction = job.numAcc / subClusterConfig.topology.accelerators.length
} else if (job.numHWThreads > 0) {
jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
}
const jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
return {
peak: round((defaultThresholds.peak * jobFraction), 0),
normal: round((defaultThresholds.normal * jobFraction), 0),
@ -177,7 +241,7 @@
</script>
<Card class="h-auto mt-1" style="width: {width}px;">
{#if view === 'job'}
{#if view === "job"}
<CardHeader>
<CardTitle class="mb-0 d-flex justify-content-center">
Core Metrics Footprint
@ -187,45 +251,50 @@
<CardBody>
{#each footprintData as fpd, index}
<div class="mb-1 d-flex justify-content-between">
<div>&nbsp;<b>{fpd.name}</b></div> <!-- For symmetry, see below ...-->
<div class="cursor-help d-inline-flex" id={`footprint-${job.jobId}-${index}`}>
<div>&nbsp;<b>{fpd.name}</b></div>
<!-- For symmetry, see below ...-->
<div
class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`}
>
<div class="mx-1">
<!-- Alerts Only -->
{#if fpd.impact === 3 || fpd.impact === -1}
<Icon name="exclamation-triangle-fill" class="text-danger"/>
<Icon name="exclamation-triangle-fill" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning"/>
<Icon name="exclamation-triangle" class="text-warning" />
{/if}
<!-- Emoji for all states-->
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger"/>
<Icon name="emoji-frown" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning"/>
<Icon name="emoji-neutral" class="text-warning" />
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success"/>
<Icon name="emoji-smile" class="text-success" />
{:else if fpd.impact === 0}
<Icon name="emoji-laughing" class="text-info"/>
<Icon name="emoji-laughing" class="text-info" />
{:else if fpd.impact === -1}
<Icon name="emoji-dizzy" class="text-danger"/>
<Icon name="emoji-dizzy" class="text-danger" />
{/if}
</div>
<div>
<!-- Print Values -->
{fpd.avg} / {fpd.max} {fpd.unit} &nbsp; <!-- To increase margin to tooltip: No other way manageable ... -->
{fpd.avg} / {fpd.max}
{fpd.unit} &nbsp; <!-- To increase margin to tooltip: No other way manageable ... -->
</div>
</div>
<Tooltip target={`footprint-${job.jobId}-${index}`} placement="right" offset={[0, 20]}>{fpd.message}</Tooltip>
<Tooltip
target={`footprint-${job.jobId}-${index}`}
placement="right"
offset={[0, 20]}>{fpd.message}</Tooltip
>
</div>
<div class="mb-2">
<Progress
value={fpd.avg}
max={fpd.max}
color={fpd.color}
/>
<Progress value={fpd.avg} max={fpd.max} color={fpd.color} />
</div>
{/each}
{#if job?.metaData?.message}
<hr class="mt-1 mb-2"/>
<hr class="mt-1 mb-2" />
{@html job.metaData.message}
{/if}
</CardBody>

View File

@ -1,43 +1,54 @@
<script>
import { onMount, getContext } from 'svelte'
import { init } from './utils.js'
import { Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte'
import Refresher from './joblist/Refresher.svelte'
import Sorting from './joblist/SortSelection.svelte'
import MetricSelection from './MetricSelection.svelte'
import UserOrProject from './filters/UserOrProject.svelte'
import { onMount, getContext } from "svelte";
import { init } from "./utils.js";
import {
Row,
Col,
Button,
Icon,
Card,
Spinner,
} from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte";
import JobList from "./joblist/JobList.svelte";
import Refresher from "./joblist/Refresher.svelte";
import Sorting from "./joblist/SortSelection.svelte";
import MetricSelection from "./MetricSelection.svelte";
import UserOrProject from "./filters/UserOrProject.svelte";
const { query: initq } = init()
const { query: initq } = init();
const ccconfig = getContext('cc-config')
const ccconfig = getContext("cc-config");
export let filterPresets = {}
export let authlevel
export let roles
export let filterPresets = {};
export let authlevel;
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 jobList, matchedJobs = null
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
let jobList,
matchedJobs = null;
let sorting = { field: "startTime", order: "DESC" },
isSortingOpen = false,
isMetricsSelectionOpen = false;
let metrics = filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics;
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
: !!ccconfig.plot_list_showFootprint;
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
// 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.update())
onMount(() => filterComponent.update());
</script>
<Row>
{#if $initq.fetching}
<Col xs="auto">
<Spinner/>
<Spinner />
</Col>
{:else if $initq.error}
<Col xs="auto">
@ -47,56 +58,64 @@
</Row>
<Row>
<Col xs="auto">
<Button
outline color="primary"
on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up"/> Sorting
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting
</Button>
<Button
outline color="primary"
on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up"/> Metrics
outline
color="primary"
on:click={() => (isMetricsSelectionOpen = true)}
>
<Icon name="graph-up" /> Metrics
</Button>
<Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button>
<Button disabled outline
>{matchedJobs == null ? "Loading..." : `${matchedJobs} jobs`}</Button
>
</Col>
<Col xs="auto">
<Filters
filterPresets={filterPresets}
{filterPresets}
bind:this={filterComponent}
on:update={({ detail }) => {
selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null
jobList.update(detail.filters)
}
} />
selectedCluster = detail.filters[0]?.cluster
? detail.filters[0].cluster.eq
: null;
jobList.update(detail.filters);
}}
/>
</Col>
<Col xs="3" style="margin-left: auto;">
<UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filterComponent.update(detail)}/>
<UserOrProject
bind:authlevel
bind:roles
on:update={({ detail }) => filterComponent.update(detail)}
/>
</Col>
<Col xs="2">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
</Row>
<br/>
<br />
<Row>
<Col>
<JobList
bind:metrics={metrics}
bind:sorting={sorting}
bind:matchedJobs={matchedJobs}
bind:metrics
bind:sorting
bind:matchedJobs
bind:this={jobList}
bind:showFootprint={showFootprint} />
bind:showFootprint
/>
</Col>
</Row>
<Sorting
bind:sorting={sorting}
bind:isOpen={isSortingOpen} />
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
<MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics={metrics}
bind:metrics
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
bind:showFootprint
view="list"
/>

View File

@ -14,7 +14,7 @@
Spinner,
InputGroup,
Input,
} from "sveltestrap";
} from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
@ -26,17 +26,23 @@
// By default, look at the jobs of the last 30 days:
if (filterPresets?.startTime == null) {
if (filterPresets == null)
filterPresets = {}
if (filterPresets == null) filterPresets = {};
const lastMonth = (new Date(Date.now() - (30*24*60*60*1000))).toISOString()
const now = (new Date(Date.now())).toISOString()
filterPresets.startTime = { from: lastMonth, to: now, text: 'Last 30 Days', url: 'last30d' }
const lastMonth = new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000,
).toISOString();
const now = new Date(Date.now()).toISOString();
filterPresets.startTime = {
from: lastMonth,
to: now,
text: "Last 30 Days",
url: "last30d",
};
}
console.assert(
type == "USER" || type == "PROJECT",
"Invalid list type provided!"
"Invalid list type provided!",
);
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
@ -58,16 +64,14 @@
totalAccHours
}
}`,
variables: { jobFilters }
variables: { jobFilters },
});
function changeSorting(event, field) {
let target = event.target;
while (target.tagName != "BUTTON") target = target.parentElement;
let direction = target.children[0].className.includes("up")
? "down"
: "up";
let direction = target.children[0].className.includes("up") ? "down" : "up";
target.children[0].className = `bi-sort-numeric-${direction}`;
sorting = { field, direction };
}
@ -119,10 +123,10 @@
<thead>
<tr>
<th scope="col">
{({
{{
USER: "Username",
PROJECT: "Project Name",
})[type]}
}[type]}
<Button
color={sorting.field == "id" ? "primary" : "light"}
size="sm"
@ -156,9 +160,7 @@
<th scope="col">
Total Walltime
<Button
color={sorting.field == "totalWalltime"
? "primary"
: "light"}
color={sorting.field == "totalWalltime" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")}
>
@ -168,9 +170,7 @@
<th scope="col">
Total Core Hours
<Button
color={sorting.field == "totalCoreHours"
? "primary"
: "light"}
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")}
>
@ -180,9 +180,7 @@
<th scope="col">
Total Accelerator Hours
<Button
color={sorting.field == "totalAccHours"
? "primary"
: "light"}
color={sorting.field == "totalAccHours" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")}
>
@ -194,15 +192,12 @@
<tbody>
{#if $stats.fetching}
<tr>
<td colspan="4" style="text-align: center;"
><Spinner secondary /></td
>
<td colspan="4" style="text-align: center;"><Spinner secondary /></td>
</tr>
{:else if $stats.error}
<tr>
<td colspan="4"
><Card body color="danger" class="mb-3"
>{$stats.error.message}</Card
><Card body color="danger" class="mb-3">{$stats.error.message}</Card
></td
>
</tr>
@ -223,7 +218,13 @@
{/if}
</td>
{#if type == "USER"}
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : 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>
@ -232,9 +233,7 @@
</tr>
{:else}
<tr>
<td colspan="4"
><i>No {type.toLowerCase()}s/jobs found</i></td
>
<td colspan="4"><i>No {type.toLowerCase()}s/jobs found</i></td>
</tr>
{/each}
{/if}

View File

@ -1,63 +1,82 @@
<script>
import { getContext, createEventDispatcher } from 'svelte'
import Timeseries from './plots/MetricPlot.svelte'
import { InputGroup, InputGroupText, Spinner, Card } from 'sveltestrap'
import { fetchMetrics, minScope } from './utils'
import { getContext, createEventDispatcher } from "svelte";
import Timeseries from "./plots/MetricPlot.svelte";
import {
InputGroup,
InputGroupText,
Spinner,
Card,
} from "@sveltestrap/sveltestrap";
import { fetchMetrics, minScope } from "./utils";
export let job
export let metricName
export let scopes
export let width
export let rawData
export let isShared = false
export let job;
export let metricName;
export let scopes;
export let width;
export let rawData;
export let isShared = false;
const dispatch = createEventDispatcher()
const cluster = getContext('clusters').find(cluster => cluster.name == job.cluster)
const subCluster = cluster.subClusters.find(subCluster => subCluster.name == job.subCluster)
const metricConfig = cluster.metricConfig.find(metricConfig => metricConfig.name == metricName)
const dispatch = createEventDispatcher();
const cluster = getContext("clusters").find(
(cluster) => cluster.name == job.cluster,
);
const subCluster = cluster.subClusters.find(
(subCluster) => subCluster.name == job.subCluster,
);
const metricConfig = cluster.metricConfig.find(
(metricConfig) => metricConfig.name == metricName,
);
let selectedHost = null, plot, fetching = false, error = null
let selectedScope = minScope(scopes)
let selectedHost = null,
plot,
fetching = false,
error = null;
let selectedScope = minScope(scopes);
$: availableScopes = scopes
$: selectedScopeIndex = scopes.findIndex(s => s == selectedScope)
$: data = rawData[selectedScopeIndex]
$: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost)
$: availableScopes = scopes;
$: selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
$: data = rawData[selectedScopeIndex];
$: series = data?.series.filter(
(series) => selectedHost == null || series.hostname == selectedHost,
);
let from = null, to = null
let from = null,
to = null;
export function setTimeRange(f, t) {
from = f, to = t
(from = f), (to = t);
}
$: if (plot != null) plot.setTimeRange(from, to)
$: if (plot != null) plot.setTimeRange(from, to);
export async function loadMore() {
fetching = true
let response = await fetchMetrics(job, [metricName], ["core"])
fetching = false
fetching = true;
let response = await fetchMetrics(job, [metricName], ["core"]);
fetching = false;
if (response.error) {
error = response.error
return
error = response.error;
return;
}
for (let jm of response.data.jobMetrics) {
if (jm.scope != "node") {
scopes = [...scopes, jm.scope]
rawData.push(jm.metric)
selectedScope = jm.scope
selectedScopeIndex = scopes.findIndex(s => s == jm.scope)
dispatch('more-loaded', jm)
scopes = [...scopes, jm.scope];
rawData.push(jm.metric);
selectedScope = jm.scope;
selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
dispatch("more-loaded", jm);
}
}
}
$: if (selectedScope == "load-more") loadMore()
$: if (selectedScope == "load-more") loadMore();
</script>
<InputGroup>
<InputGroupText style="min-width: 150px;">
{metricName} ({(metricConfig?.unit?.prefix ? metricConfig.unit.prefix : '') +
(metricConfig?.unit?.base ? metricConfig.unit.base : '')})
{metricName} ({(metricConfig?.unit?.prefix
? metricConfig.unit.prefix
: "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
</InputGroupText>
<select class="form-select" bind:value={selectedScope}>
{#each availableScopes as scope}
@ -78,18 +97,22 @@
</InputGroup>
{#key series}
{#if fetching == true}
<Spinner/>
<Spinner />
{:else if error != null}
<Card body color="danger">{error.message}</Card>
{:else if series != null}
<Timeseries
bind:this={plot}
width={width} height={300}
cluster={cluster} subCluster={subCluster}
{width}
height={300}
{cluster}
{subCluster}
timestep={data.timestep}
scope={selectedScope} metric={metricName}
series={series}
isShared={isShared}
resources={job.resources} />
scope={selectedScope}
metric={metricName}
{series}
{isShared}
resources={job.resources}
/>
{/if}
{/key}

View File

@ -8,51 +8,55 @@
-->
<script>
import { Modal, ModalBody, ModalHeader, ModalFooter, Button, ListGroup } from 'sveltestrap'
import { getContext } from 'svelte'
import { gql, getContextClient , mutationStore } from '@urql/svelte'
import {
Modal,
ModalBody,
ModalHeader,
ModalFooter,
Button,
ListGroup,
} from "@sveltestrap/sveltestrap";
import { getContext } from "svelte";
import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let metrics
export let isOpen
export let configName
export let allMetrics = null
export let cluster = null
export let showFootprint
export let view = 'job'
export let metrics;
export let isOpen;
export let configName;
export let allMetrics = null;
export let cluster = null;
export let showFootprint;
export let view = "job";
const clusters = getContext('clusters'),
onInit = getContext('on-init')
const clusters = getContext("clusters"),
onInit = getContext("on-init");
let newMetricsOrder = []
let unorderedMetrics = [...metrics]
let pendingShowFootprint = !!showFootprint
let newMetricsOrder = [];
let unorderedMetrics = [...metrics];
let pendingShowFootprint = !!showFootprint;
onInit(() => {
if (allMetrics == null) allMetrics = new Set()
if (allMetrics == null) allMetrics = new Set();
for (let c of clusters)
for (let metric of c.metricConfig)
allMetrics.add(metric.name)
})
for (let metric of c.metricConfig) allMetrics.add(metric.name);
});
$: {
if (allMetrics != null) {
if (cluster == null) {
// console.log('Reset to full metric list')
for (let c of clusters)
for (let metric of c.metricConfig)
allMetrics.add(metric.name)
for (let metric of c.metricConfig) allMetrics.add(metric.name);
} else {
// console.log('Recalculate available metrics for ' + cluster)
allMetrics.clear()
allMetrics.clear();
for (let c of clusters)
if (c.name == cluster)
for (let metric of c.metricConfig)
allMetrics.add(metric.name)
for (let metric of c.metricConfig) allMetrics.add(metric.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));
}
}
@ -61,62 +65,140 @@
return mutationStore({
client: client,
query: gql`
mutation($name: String!, $value: String!) {
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
})}
variables: { name, value },
});
};
let columnHovering = null
let columnHovering = null;
function columnsDragStart(event, i) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.dropEffect = 'move'
event.dataTransfer.setData('text/plain', i)
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.dropEffect = "move";
event.dataTransfer.setData("text/plain", i);
}
function columnsDrag(event, target) {
event.dataTransfer.dropEffect = 'move'
const start = Number.parseInt(event.dataTransfer.getData("text/plain"))
event.dataTransfer.dropEffect = "move";
const start = Number.parseInt(event.dataTransfer.getData("text/plain"));
if (start < target) {
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start])
newMetricsOrder.splice(start, 1)
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start]);
newMetricsOrder.splice(start, 1);
} else {
newMetricsOrder.splice(target, 0, newMetricsOrder[start])
newMetricsOrder.splice(start + 1, 1)
newMetricsOrder.splice(target, 0, newMetricsOrder[start]);
newMetricsOrder.splice(start + 1, 1);
}
columnHovering = null
columnHovering = null;
}
function closeAndApply() {
metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m))
isOpen = false
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
isOpen = false;
showFootprint = !!pendingShowFootprint
showFootprint = !!pendingShowFootprint;
updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`,
value: JSON.stringify(metrics)
}).subscribe(res => {
value: JSON.stringify(metrics),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
});
updateConfigurationMutation({
name: cluster == null ? 'plot_list_showFootprint' : `plot_list_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint)
}).subscribe(res => {
name:
cluster == null
? "plot_list_showFootprint"
: `plot_list_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
console.log('Error on footprint subscription: ' + res.error)
throw res.error
console.log("Error on footprint subscription: " + res.error);
throw res.error;
}
})
});
}
</script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader>
<ModalBody>
<ListGroup>
{#if view === "list"}
<li class="list-group-item">
<input type="checkbox" bind:checked={pendingShowFootprint} /> Show Footprint
</li>
<hr />
{/if}
{#each newMetricsOrder as metric, index (metric)}
<li
class="cc-config-column list-group-item"
draggable={true}
ondragover="return false"
on:dragstart={(event) => columnsDragStart(event, index)}
on:drop|preventDefault={(event) => columnsDrag(event, index)}
on:dragenter={() => (columnHovering = index)}
class:is-active={columnHovering === index}
>
{#if unorderedMetrics.includes(metric)}
<input
type="checkbox"
bind:group={unorderedMetrics}
value={metric}
checked
/>
{:else}
<input
type="checkbox"
bind:group={unorderedMetrics}
value={metric}
/>
{/if}
{metric}
<span style="float: right;">
{cluster == null
? clusters // No single cluster specified: List Clusters with Metric
.filter(
(c) => c.metricConfig.find((m) => m.name == metric) != null,
)
.map((c) => c.name)
.join(", ")
: clusters // Single cluster requested: List Subclusters with do not have metric remove flag
.filter((c) => c.name == cluster)
.filter(
(c) => c.metricConfig.find((m) => m.name == metric) != null,
)
.map(function (c) {
let scNames = c.subClusters.map((sc) => sc.name);
scNames.forEach(function (scName) {
let met = c.metricConfig.find((m) => m.name == metric);
let msc = met.subClusters.find(
(msc) => msc.name == scName,
);
if (msc != null) {
if (msc.remove == true) {
scNames = scNames.filter((scn) => scn != msc.name);
}
}
});
return scNames;
})
.join(", ")}
</span>
</li>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
</ModalFooter>
</Modal>
<style>
li.cc-config-column {
display: block;
@ -129,60 +211,3 @@
cursor: grabbing;
}
</style>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Configure columns (Metric availability shown)
</ModalHeader>
<ModalBody>
<ListGroup>
{#if view === 'list'}
<li class="list-group-item">
<input type="checkbox" bind:checked={pendingShowFootprint}> Show Footprint
</li>
<hr/>
{/if}
{#each newMetricsOrder as metric, index (metric)}
<li class="cc-config-column list-group-item"
draggable={true} ondragover="return false"
on:dragstart={event => columnsDragStart(event, index)}
on:drop|preventDefault={event => columnsDrag(event, index)}
on:dragenter={() => columnHovering = index}
class:is-active={columnHovering === index}>
{#if unorderedMetrics.includes(metric)}
<input type="checkbox" bind:group={unorderedMetrics} value={metric} checked>
{:else}
<input type="checkbox" bind:group={unorderedMetrics} value={metric}>
{/if}
{metric}
<span style="float: right;">
{cluster == null ?
clusters // No single cluster specified: List Clusters with Metric
.filter(c => c.metricConfig.find(m => m.name == metric) != null)
.map(c => c.name).join(', ') :
clusters // Single cluster requested: List Subclusters with do not have metric remove flag
.filter(c => c.name == cluster)
.filter(c => c.metricConfig.find(m => m.name == metric) != null)
.map(function(c) {
let scNames = c.subClusters.map(sc => sc.name)
scNames.forEach(function(scName){
let met = c.metricConfig.find(m => m.name == metric)
let msc = met.subClusters.find(msc => msc.name == scName)
if (msc != null) {
if (msc.remove == true) {
scNames = scNames.filter(scn => scn != msc.name)
}
}
})
return scNames
})
.join(', ')}
</span>
</li>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
</ModalFooter>
</Modal>

View File

@ -6,7 +6,7 @@
DropdownToggle,
DropdownMenu,
DropdownItem,
} from "sveltestrap";
} from "@sveltestrap/sveltestrap";
export let clusters; // array of names
export let links; // array of nav links
@ -27,8 +27,7 @@
{#each clusters as cluster}
<DropdownItem
href={item.href + cluster.name}
active={window.location.pathname ==
item.href + cluster.name}
active={window.location.pathname == item.href + cluster.name}
>
{cluster.name}
</DropdownItem>

View File

@ -10,7 +10,7 @@
Container,
Row,
Col,
} from "sveltestrap";
} from "@sveltestrap/sveltestrap";
export let username; // empty string if auth. is disabled, otherwise the username as string
export let authlevel; // Integer
@ -30,7 +30,8 @@
style="margin-left: 10px;"
/>
<!-- bootstrap classes w/o effect -->
<Button outline type="submit" title="Search"><Icon name="search" /></Button
<Button outline type="submit" title="Search"
><Icon name="search" /></Button
>
<InputGroupText
style="cursor:help;"
@ -43,7 +44,12 @@
</form>
</NavItem>
<NavItem>
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
<a
href="https://www.clustercockpit.org/docs/webinterface/"
title="Documentation"
rel="nofollow"
target="_blank"
>
<Button outline style="margin-left: 10px;">
<Icon name="book" />
</Button>
@ -70,9 +76,9 @@
title="Logout {username}"
>
{#if screenSize > 1630}
<Icon name="box-arrow-right"/> Logout {username}
<Icon name="box-arrow-right" /> Logout {username}
{:else}
<Icon name="box-arrow-right"/>
<Icon name="box-arrow-right" />
{/if}
</Button>
</form>
@ -83,7 +89,12 @@
<Container>
<Row cols={3}>
<Col xs="4">
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
<a
href="https://www.clustercockpit.org/docs/webinterface/"
title="Documentation"
rel="nofollow"
target="_blank"
>
<Button outline size="sm" class="my-2 w-100">
<Icon name="box-arrow-up-right" /> Documentation
</Button>
@ -127,8 +138,7 @@
placeholder="Search 'type:<query>' ..."
name="searchId"
/>
<Button outline type="submit"><Icon name="search" /></Button
>
<Button outline type="submit"><Icon name="search" /></Button>
<InputGroupText
style="cursor:help;"
title={authlevel >= roles.support

View File

@ -8,10 +8,10 @@
Icon,
Spinner,
Card,
} from "sveltestrap";
} from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import TimeSelection from "./filters/TimeSelection.svelte";
import Refresher from './joblist/Refresher.svelte';
import Refresher from "./joblist/Refresher.svelte";
import PlotTable from "./PlotTable.svelte";
import MetricPlot from "./plots/MetricPlot.svelte";
import { getContext } from "svelte";
@ -34,12 +34,7 @@
const client = getContextClient();
const nodeMetricsQuery = gql`
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
nodes: $nodes
from: $from
to: $to
) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
host
subCluster
metrics {
@ -150,8 +145,7 @@
{#if $nodeJobsData.fetching}
<Spinner />
{:else if $nodeJobsData.data}
Currently running jobs on this node: {$nodeJobsData.data.jobs
.count}
Currently running jobs on this node: {$nodeJobsData.data.jobs.count}
[
<a
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
@ -162,11 +156,13 @@
{/if}
</Col>
<Col>
<Refresher on:reload={() => {
const diff = Date.now() - to
from = new Date(from.getTime() + diff)
to = new Date(to.getTime() + diff)
}} />
<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 />
@ -192,7 +188,7 @@
disabled: checkMetricDisabled(
m.name,
cluster,
$nodeMetricsData.data.nodeMetrics[0].subCluster
$nodeMetricsData.data.nodeMetrics[0].subCluster,
),
}))
.sort((a, b) => a.name.localeCompare(b.name))}
@ -208,17 +204,13 @@
metric={item.name}
timestep={item.metric.timestep}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={$nodeMetricsData.data.nodeMetrics[0]
.subCluster}
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
series={item.metric.series}
resources={[{hostname: hostname}]}
resources={[{ hostname: hostname }]}
forNode={true}
/>
{:else if item.disabled === true && item.metric}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="info"
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
.subCluster}</code

View File

@ -1,66 +1,81 @@
<script>
import { Modal, ModalBody, ModalHeader, ModalFooter, InputGroup,
Button, ListGroup, ListGroupItem, Icon } from 'sveltestrap'
import { gql, getContextClient , mutationStore } from '@urql/svelte'
import {
Modal,
ModalBody,
ModalHeader,
ModalFooter,
InputGroup,
Button,
ListGroup,
ListGroupItem,
Icon,
} from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let availableMetrics
export let metricsInHistograms
export let metricsInScatterplots
export let availableMetrics;
export let metricsInHistograms;
export let metricsInScatterplots;
const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`mutation($name: String!, $value: String!) {
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}`,
variables: { name, value }
})
}
`,
variables: { name, value },
});
};
let isHistogramConfigOpen = false, isScatterPlotConfigOpen = false
let selectedMetric1 = null, selectedMetric2 = null
let isHistogramConfigOpen = false,
isScatterPlotConfigOpen = false;
let selectedMetric1 = null,
selectedMetric2 = null;
function updateConfiguration(data) {
updateConfigurationMutation({
name: data.name,
value: JSON.stringify(data.value)
}).subscribe(res => {
value: JSON.stringify(data.value),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
});
}
</script>
<Button outline
on:click={() => (isHistogramConfigOpen = true)}>
<Icon name=""/>
<Button outline on:click={() => (isHistogramConfigOpen = true)}>
<Icon name="" />
Select Plots for Histograms
</Button>
<Button outline
on:click={() => (isScatterPlotConfigOpen = true)}>
<Icon name=""/>
<Button outline on:click={() => (isScatterPlotConfigOpen = true)}>
<Icon name="" />
Select Plots in Scatter Plots
</Button>
<Modal isOpen={isHistogramConfigOpen}
toggle={() => (isHistogramConfigOpen = !isHistogramConfigOpen)}>
<ModalHeader>
Select metrics presented in histograms
</ModalHeader>
<Modal
isOpen={isHistogramConfigOpen}
toggle={() => (isHistogramConfigOpen = !isHistogramConfigOpen)}
>
<ModalHeader>Select metrics presented in histograms</ModalHeader>
<ModalBody>
<ListGroup>
{#each availableMetrics as metric (metric)}
<ListGroupItem>
<input type="checkbox" bind:group={metricsInHistograms}
<input
type="checkbox"
bind:group={metricsInHistograms}
value={metric}
on:change={() => updateConfiguration({
name: 'analysis_view_histogramMetrics',
value: metricsInHistograms
})} />
on:change={() =>
updateConfiguration({
name: "analysis_view_histogramMetrics",
value: metricsInHistograms,
})}
/>
{metric}
</ListGroupItem>
@ -68,39 +83,44 @@
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary"
on:click={() => (isHistogramConfigOpen = false)}>
<Button color="primary" on:click={() => (isHistogramConfigOpen = false)}>
Close
</Button>
</ModalFooter>
</Modal>
<Modal isOpen={isScatterPlotConfigOpen}
toggle={() => (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}>
<ModalHeader>
Select metric pairs presented in scatter plots
</ModalHeader>
<Modal
isOpen={isScatterPlotConfigOpen}
toggle={() => (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}
>
<ModalHeader>Select metric pairs presented in scatter plots</ModalHeader>
<ModalBody>
<ListGroup>
{#each metricsInScatterplots as pair}
<ListGroupItem>
<b>{pair[0]}</b> / <b>{pair[1]}</b>
<Button style="float: right;" outline color="danger"
<Button
style="float: right;"
outline
color="danger"
on:click={() => {
metricsInScatterplots = metricsInScatterplots.filter(p => pair != p)
metricsInScatterplots = metricsInScatterplots.filter(
(p) => pair != p,
);
updateConfiguration({
name: 'analysis_view_scatterPlotMetrics',
value: metricsInScatterplots
name: "analysis_view_scatterPlotMetrics",
value: metricsInScatterplots,
});
}}>
}}
>
<Icon name="x" />
</Button>
</ListGroupItem>
{/each}
</ListGroup>
<br/>
<br />
<InputGroup>
<select bind:value={selectedMetric1} class="form-group form-select">
@ -115,24 +135,28 @@
<option value={metric}>{metric}</option>
{/each}
</select>
<Button outline disabled={selectedMetric1 == null || selectedMetric2 == null}
<Button
outline
disabled={selectedMetric1 == null || selectedMetric2 == null}
on:click={() => {
metricsInScatterplots = [...metricsInScatterplots, [selectedMetric1, selectedMetric2]]
selectedMetric1 = null
selectedMetric2 = null
metricsInScatterplots = [
...metricsInScatterplots,
[selectedMetric1, selectedMetric2],
];
selectedMetric1 = null;
selectedMetric2 = null;
updateConfiguration({
name: 'analysis_view_scatterPlotMetrics',
value: metricsInScatterplots
})
}}>
name: "analysis_view_scatterPlotMetrics",
value: metricsInScatterplots,
});
}}
>
Add Plot
</Button>
</InputGroup>
</ModalBody>
<ModalFooter>
<Button color="primary"
on:click={() => (isScatterPlotConfigOpen = false)}>
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}>
Close
</Button>
</ModalFooter>

View File

@ -1,75 +1,82 @@
<script>
import { getContext } from 'svelte'
import { Button, Table, InputGroup, InputGroupText, Icon } from 'sveltestrap'
import MetricSelection from './MetricSelection.svelte'
import StatsTableEntry from './StatsTableEntry.svelte'
import { maxScope } from './utils.js'
import { getContext } from "svelte";
import {
Button,
Table,
InputGroup,
InputGroupText,
Icon,
} from "@sveltestrap/sveltestrap";
import MetricSelection from "./MetricSelection.svelte";
import StatsTableEntry from "./StatsTableEntry.svelte";
import { maxScope } from "./utils.js";
export let job
export let jobMetrics
export let job;
export let jobMetrics;
const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(),
scopesForMetric = (metric) => jobMetrics
.filter(jm => jm.name == metric)
.map(jm => jm.scope)
const allMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort(),
scopesForMetric = (metric) =>
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
let hosts = job.resources.map(r => r.hostname).sort(),
let hosts = job.resources.map((r) => r.hostname).sort(),
selectedScopes = {},
sorting = {},
isMetricSelectionOpen = false,
selectedMetrics = getContext('cc-config')[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|| getContext('cc-config')['job_view_nodestats_selectedMetrics']
selectedMetrics =
getContext("cc-config")[
`job_view_nodestats_selectedMetrics:${job.cluster}`
] || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
for (let metric of allMetrics) {
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
// -> Else: Load smallest available granularity as default as per availability
const availableScopes = scopesForMetric(metric)
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'
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'
selectedScopes[metric] = "node";
}
} else {
selectedScopes[metric] = maxScope(availableScopes)
selectedScopes[metric] = maxScope(availableScopes);
}
sorting[metric] = {
min: { dir: 'up', active: false },
avg: { dir: 'up', active: false },
max: { dir: 'up', active: false },
}
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
}
export function sortBy(metric, stat) {
let s = sorting[metric][stat]
let s = sorting[metric][stat];
if (s.active) {
s.dir = s.dir == 'up' ? 'down' : 'up'
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
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}
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
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]
})
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
});
}
export function moreLoaded(jobMetric) {
jobMetrics = [...jobMetrics, jobMetric]
jobMetrics = [...jobMetrics, jobMetric];
}
</script>
@ -77,18 +84,18 @@
<thead>
<tr>
<th>
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' -->
<Button outline on:click={() => (isMetricSelectionOpen = true)}>
<!-- log to click ', console.log(isMetricSelectionOpen)' -->
Metrics
</Button>
</th>
{#each selectedMetrics as metric}
<th colspan={selectedScopes[metric] == 'node' ? 3 : 4}>
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
<InputGroup>
<InputGroupText>
{metric}
</InputGroupText>
<select class="form-select"
bind:value={selectedScopes[metric]}>
<select class="form-select" bind:value={selectedScopes[metric]}>
{#each scopesForMetric(metric, jobMetrics) as scope}
<option value={scope}>{scope}</option>
{/each}
@ -100,14 +107,19 @@
<tr>
<th>Node</th>
{#each selectedMetrics as metric}
{#if selectedScopes[metric] != 'node'}
{#if selectedScopes[metric] != "node"}
<th>Id</th>
{/if}
{#each ['min', 'avg', 'max'] as stat}
{#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 selectedScopes[metric] == "node"}
<Icon
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
.active
? '-fill'
: ''}"
/>
{/if}
</th>
{/each}
@ -120,20 +132,23 @@
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
host={host} metric={metric}
{host}
{metric}
scope={selectedScopes[metric]}
jobMetrics={jobMetrics} />
{jobMetrics}
/>
{/each}
</tr>
{/each}
</tbody>
</Table>
<br/>
<br />
<MetricSelection
cluster={job.cluster}
configName='job_view_nodestats_selectedMetrics'
configName="job_view_nodestats_selectedMetrics"
allMetrics={new Set(allMetrics)}
bind:metrics={selectedMetrics}
bind:isOpen={isMetricSelectionOpen} />
bind:isOpen={isMetricSelectionOpen}
/>

View File

@ -1,54 +1,54 @@
<script>
import { Icon } from 'sveltestrap'
import { Icon } from "@sveltestrap/sveltestrap";
export let host
export let metric
export let scope
export let jobMetrics
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]
let s = sorting[field];
if (s.active) {
s.dir = s.dir == 'up' ? 'down' : 'up'
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let field in sorting)
sorting[field].active = false
s.active = true
for (let field in sorting) sorting[field].active = false;
s.active = true;
}
sorting = {...sorting}
sorting = { ...sorting };
series = series.sort((a, b) => {
if (a == null || b == null)
return -1
if (a == null || b == null) return -1;
if (field === 'id') {
return s.dir != 'up' ? a[field] - b[field] : b[field] - a[field]
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]
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 },
}
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)
.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}
<td colspan={scope == 'node' ? 3 : 4}><i>No data</i></td>
{:else if series.length == 1 && scope == 'node'}
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
{:else if series.length == 1 && scope == "node"}
<td>
{series[0].statistics.min}
</td>
@ -62,10 +62,14 @@
<td colspan="4">
<table style="width: 100%;">
<tr>
{#each ['id', 'min', 'avg', 'max'] as field}
{#each ["id", "min", "avg", "max"] as field}
<th on:click={() => sortByField(field)}>
Sort
<Icon name="caret-{sorting[field].dir}{sorting[field].active ? '-fill' : ''}" />
<Icon
name="caret-{sorting[field].dir}{sorting[field].active
? '-fill'
: ''}"
/>
</th>
{/each}
</tr>

View File

@ -15,9 +15,13 @@
Table,
Progress,
Icon,
Button
} from "sveltestrap";
import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js";
Button,
} from "@sveltestrap/sveltestrap";
import {
init,
convert2uplot,
transformPerNodeDataForRoofline,
} from "./utils.js";
import { scaleNumbers } from "./units.js";
import {
queryStore,
@ -25,8 +29,8 @@
getContextClient,
mutationStore,
} from "@urql/svelte";
import PlotTable from './PlotTable.svelte'
import HistogramSelection from './HistogramSelection.svelte'
import PlotTable from "./PlotTable.svelte";
import HistogramSelection from "./HistogramSelection.svelte";
const { query: initq } = init();
const ccconfig = getContext("cc-config");
@ -35,7 +39,7 @@
let plotWidths = [],
colWidth1,
colWidth2
colWidth2;
let from = new Date(Date.now() - 5 * 60 * 1000),
to = new Date(Date.now());
const topOptions = [
@ -49,25 +53,25 @@
topOptions.find(
(option) =>
option.key ==
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`]
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`],
) ||
topOptions.find(
(option) =>
option.key == ccconfig.status_view_selectedTopProjectCategory
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
);
let topUserSelection =
topOptions.find(
(option) =>
option.key ==
ccconfig[`status_view_selectedTopUserCategory:${cluster}`]
ccconfig[`status_view_selectedTopUserCategory:${cluster}`],
) ||
topOptions.find(
(option) =>
option.key == ccconfig.status_view_selectedTopUserCategory
(option) => option.key == ccconfig.status_view_selectedTopUserCategory,
);
let isHistogramSelectionOpen = false
$: metricsInHistograms = cluster ? (ccconfig[`user_view_histogramMetrics:${cluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
let isHistogramSelectionOpen = false;
$: metricsInHistograms = cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || []
: ccconfig.user_view_histogramMetrics || [];
const client = getContextClient();
$: mainQuery = queryStore({
@ -146,7 +150,7 @@
from: from.toISOString(),
to: to.toISOString(),
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
metricsInHistograms: metricsInHistograms
metricsInHistograms: metricsInHistograms,
},
});
@ -217,12 +221,11 @@
(node.metrics
.find((m) => m.name == metric)
?.metric.series.reduce(
(sum, series) =>
sum + series.data[series.data.length - 1],
0
(sum, series) => sum + series.data[series.data.length - 1],
0,
) || 0)
: sum,
0
0,
);
let allocatedNodes = {},
@ -234,37 +237,27 @@
memBwRateUnitBase = {};
$: if ($initq.data && $mainQuery.data) {
let subClusters = $initq.data.clusters.find(
(c) => c.name == cluster
(c) => c.name == cluster,
).subClusters;
for (let subCluster of subClusters) {
allocatedNodes[subCluster.name] =
$mainQuery.data.allocatedNodes.find(
({ name }) => name == subCluster.name
({ name }) => name == subCluster.name,
)?.count || 0;
flopRate[subCluster.name] =
Math.floor(
sumUp(
$mainQuery.data.nodeMetrics,
subCluster.name,
"flops_any"
) * 100
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "flops_any") *
100,
) / 100;
flopRateUnitPrefix[subCluster.name] =
subCluster.flopRateSimd.unit.prefix;
flopRateUnitBase[subCluster.name] =
subCluster.flopRateSimd.unit.base;
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix;
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base;
memBwRate[subCluster.name] =
Math.floor(
sumUp(
$mainQuery.data.nodeMetrics,
subCluster.name,
"mem_bw"
) * 100
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "mem_bw") * 100,
) / 100;
memBwRateUnitPrefix[subCluster.name] =
subCluster.memoryBandwidth.unit.prefix;
memBwRateUnitBase[subCluster.name] =
subCluster.memoryBandwidth.unit.base;
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base;
}
}
@ -281,9 +274,7 @@
};
function updateTopUserConfiguration(select) {
if (
ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select
) {
if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) {
updateConfigurationMutation({
name: `status_view_selectedTopUserCategory:${cluster}`,
value: JSON.stringify(select),
@ -301,8 +292,7 @@
function updateTopProjectConfiguration(select) {
if (
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`] !=
select
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`] != select
) {
updateConfigurationMutation({
name: `status_view_selectedTopProjectCategory:${cluster}`,
@ -340,9 +330,11 @@
</Col>
<Col xs="auto" style="margin-left: auto;">
<Button
outline color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}>
<Icon name="bar-chart-line"/> Select Histograms
outline
color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}
>
<Icon name="bar-chart-line" /> Select Histograms
</Button>
</Col>
<Col xs="auto" style="margin-left: 0.25rem;">
@ -373,9 +365,7 @@
<Col md="4" class="px-3">
<Card class="h-auto mt-1">
<CardHeader>
<CardTitle class="mb-0"
>SubCluster "{subCluster.name}"</CardTitle
>
<CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
</CardHeader>
<CardBody>
<Table borderless>
@ -384,9 +374,7 @@
<td style="min-width: 100px;"
><div class="col">
<Progress
value={allocatedNodes[
subCluster.name
]}
value={allocatedNodes[subCluster.name]}
max={subCluster.numberOfNodes}
/>
</div></td
@ -417,9 +405,8 @@
<td>
{scaleNumbers(
flopRate[subCluster.name],
subCluster.flopRateSimd.value *
subCluster.numberOfNodes,
flopRateUnitPrefix[subCluster.name]
subCluster.flopRateSimd.value * subCluster.numberOfNodes,
flopRateUnitPrefix[subCluster.name],
)}{flopRateUnitBase[subCluster.name]} [Max]
</td>
</tr>
@ -429,8 +416,7 @@
><div class="col">
<Progress
value={memBwRate[subCluster.name]}
max={subCluster.memoryBandwidth
.value *
max={subCluster.memoryBandwidth.value *
subCluster.numberOfNodes}
/>
</div></td
@ -438,9 +424,8 @@
<td>
{scaleNumbers(
memBwRate[subCluster.name],
subCluster.memoryBandwidth.value *
subCluster.numberOfNodes,
memBwRateUnitPrefix[subCluster.name]
subCluster.memoryBandwidth.value * subCluster.numberOfNodes,
memBwRateUnitPrefix[subCluster.name],
)}{memBwRateUnitBase[subCluster.name]} [Max]
</td>
</tr>
@ -456,13 +441,11 @@
width={plotWidths[i] - 10}
height={300}
cluster={subCluster}
data={
transformPerNodeDataForRoofline(
data={transformPerNodeDataForRoofline(
$mainQuery.data.nodeMetrics.filter(
(data) => data.subCluster == subCluster.name
)
)
}
(data) => data.subCluster == subCluster.name,
),
)}
/>
{/key}
</div>
@ -470,7 +453,7 @@
</Row>
{/each}
<hr/>
<hr />
<!-- Usage Stats as Histograms -->
@ -478,26 +461,21 @@
<Col class="p-2">
<div bind:clientWidth={colWidth1}>
<h4 class="text-center">
Top Users on {cluster.charAt(0).toUpperCase() +
cluster.slice(1)}
Top Users on {cluster.charAt(0).toUpperCase() + cluster.slice(1)}
</h4>
{#key $topUserQuery.data}
{#if $topUserQuery.fetching}
<Spinner />
{:else if $topUserQuery.error}
<Card body color="danger"
>{$topUserQuery.error.message}</Card
>
<Card body color="danger">{$topUserQuery.error.message}</Card>
{:else}
<Pie
size={colWidth1}
sliceLabel={topUserSelection.label}
quantities={$topUserQuery.data.topUser.map(
(tu) => tu[topUserSelection.key]
)}
entities={$topUserQuery.data.topUser.map(
(tu) => tu.id
(tu) => tu[topUserSelection.key],
)}
entities={$topUserQuery.data.topUser.map((tu) => tu.id)}
/>
{/if}
{/key}
@ -508,9 +486,7 @@
{#if $topUserQuery.fetching}
<Spinner />
{:else if $topUserQuery.error}
<Card body color="danger"
>{$topUserQuery.error.message}</Card
>
<Card body color="danger">{$topUserQuery.error.message}</Card>
{:else}
<Table>
<tr class="mb-2">
@ -518,10 +494,7 @@
<th>User Name</th>
<th
>Number of
<select
class="p-0"
bind:value={topUserSelection}
>
<select class="p-0" bind:value={topUserSelection}>
{#each topOptions as option}
<option value={option}>
{option.label}
@ -532,12 +505,7 @@
</tr>
{#each $topUserQuery.data.topUser as tu, i}
<tr>
<td
><Icon
name="circle-fill"
style="color: {colors[i]};"
/></td
>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
<th scope="col"
><a
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
@ -553,26 +521,21 @@
</Col>
<Col class="p-2">
<h4 class="text-center">
Top Projects on {cluster.charAt(0).toUpperCase() +
cluster.slice(1)}
Top Projects on {cluster.charAt(0).toUpperCase() + cluster.slice(1)}
</h4>
{#key $topProjectQuery.data}
{#if $topProjectQuery.fetching}
<Spinner />
{:else if $topProjectQuery.error}
<Card body color="danger"
>{$topProjectQuery.error.message}</Card
>
<Card body color="danger">{$topProjectQuery.error.message}</Card>
{:else}
<Pie
size={colWidth1}
sliceLabel={topProjectSelection.label}
quantities={$topProjectQuery.data.topProjects.map(
(tp) => tp[topProjectSelection.key]
)}
entities={$topProjectQuery.data.topProjects.map(
(tp) => tp.id
(tp) => tp[topProjectSelection.key],
)}
entities={$topProjectQuery.data.topProjects.map((tp) => tp.id)}
/>
{/if}
{/key}
@ -582,9 +545,7 @@
{#if $topProjectQuery.fetching}
<Spinner />
{:else if $topProjectQuery.error}
<Card body color="danger"
>{$topProjectQuery.error.message}</Card
>
<Card body color="danger">{$topProjectQuery.error.message}</Card>
{:else}
<Table>
<tr class="mb-2">
@ -592,10 +553,7 @@
<th>Project Code</th>
<th
>Number of
<select
class="p-0"
bind:value={topProjectSelection}
>
<select class="p-0" bind:value={topProjectSelection}>
{#each topOptions as option}
<option value={option}>
{option.label}
@ -606,12 +564,7 @@
</tr>
{#each $topProjectQuery.data.topProjects as tp, i}
<tr>
<td
><Icon
name="circle-fill"
style="color: {colors[i]};"
/></td
>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
<th scope="col"
><a
href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
@ -632,9 +585,7 @@
<div bind:clientWidth={colWidth2}>
{#key $mainQuery.data.stats}
<Histogram
data={convert2uplot(
$mainQuery.data.stats[0].histDuration
)}
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
width={colWidth2 - 25}
title="Duration Distribution"
xlabel="Current Runtimes"
@ -664,9 +615,7 @@
<div bind:clientWidth={colWidth2}>
{#key $mainQuery.data.stats}
<Histogram
data={convert2uplot(
$mainQuery.data.stats[0].histNumCores
)}
data={convert2uplot($mainQuery.data.stats[0].histNumCores)}
width={colWidth2 - 25}
title="Number of Cores Distribution"
xlabel="Allocated Cores"
@ -701,17 +650,19 @@
let:width
renderFor="user"
items={$mainQuery.data.stats[0].histMetrics}
itemsPerRow={3}>
itemsPerRow={3}
>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
width={width} height={250}
{width}
height={250}
title="Distribution of '{item.metric}' averages"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit}
ylabel="Number of Jobs"
yunit="Jobs"/>
yunit="Jobs"
/>
</PlotTable>
{/key}
</Col>
@ -720,6 +671,7 @@
{/if}
<HistogramSelection
bind:cluster={cluster}
bind:metricsInHistograms={metricsInHistograms}
bind:isOpen={isHistogramSelectionOpen} />
bind:cluster
bind:metricsInHistograms
bind:isOpen={isHistogramSelectionOpen}
/>

View File

@ -1,38 +1,53 @@
<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'
import PlotTable from './PlotTable.svelte'
import MetricPlot from './plots/MetricPlot.svelte'
import { getContext } from 'svelte'
import { init, checkMetricDisabled } from "./utils.js";
import Refresher from "./joblist/Refresher.svelte";
import {
Row,
Col,
Input,
InputGroup,
InputGroupText,
Icon,
Spinner,
Card,
} from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import TimeSelection from "./filters/TimeSelection.svelte";
import PlotTable from "./PlotTable.svelte";
import MetricPlot from "./plots/MetricPlot.svelte";
import { getContext } from "svelte";
export let cluster
export let from = null
export let to = null
export let cluster;
export let from = null;
export let to = null;
const { query: initq } = init()
const { query: initq } = init();
if (from == null || to == null) {
to = new Date(Date.now())
from = new Date(to.getTime())
from.setMinutes(from.getMinutes() - 30)
to = new Date(Date.now());
from = new Date(to.getTime());
from.setMinutes(from.getMinutes() - 30);
}
const clusters = getContext('clusters')
const ccconfig = getContext('cc-config')
const metricConfig = getContext('metrics')
const clusters = getContext("clusters");
const ccconfig = getContext("cc-config");
const metricConfig = getContext("metrics");
let plotHeight = 300
let hostnameFilter = ''
let selectedMetric = ccconfig.system_view_selectedMetric
let plotHeight = 300;
let hostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric;
const client = getContextClient();
$: nodesQuery = queryStore({
client: client,
query: gql`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
query: gql`
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
metrics: $metrics
from: $from
to: $to
) {
host
subCluster
metrics {
@ -40,64 +55,78 @@
scope
metric {
timestep
unit { base, prefix }
unit {
base
prefix
}
series {
statistics { min, avg, max }
statistics {
min
avg
max
}
data
}
}
}
}
}`,
}
`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString()
}
})
to: to.toISOString(),
},
});
let metricUnits = {}
let metricUnits = {};
$: if ($nodesQuery.data) {
let thisCluster = clusters.find(c => c.name == cluster)
let thisCluster = clusters.find((c) => c.name == cluster);
if (thisCluster) {
for (let metric of thisCluster.metricConfig) {
if (metric.unit.prefix || metric.unit.base) {
metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
} else { // If no unit defined: Omit Unit Display
metricUnits[metric.name] = ''
metricUnits[metric.name] =
"(" +
(metric.unit.prefix ? metric.unit.prefix : "") +
(metric.unit.base ? metric.unit.base : "") +
")";
} else {
// If no unit defined: Omit Unit Display
metricUnits[metric.name] = "";
}
}
}
}
</script>
<Row>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner/>
<Spinner />
{:else}
<Col>
<Refresher on:reload={() => {
const diff = Date.now() - to
from = new Date(from.getTime() + diff)
to = new Date(to.getTime() + diff)
}} />
<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}
bind:to={to} />
<TimeSelection bind:from bind:to />
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}>
{#each clusters.find(c => c.name == cluster).metricConfig as metric}
<option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
{#each clusters.find((c) => c.name == cluster).metricConfig as metric}
<option value={metric.name}
>{metric.name} {metricUnits[metric.name]}</option
>
{/each}
</select>
</InputGroup>
@ -106,18 +135,22 @@
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
<Input
placeholder="hostname..."
type="text"
bind:value={hostnameFilter}
/>
</InputGroup>
</Col>
{/if}
</Row>
<br/>
<br />
<Row>
<Col>
{#if $nodesQuery.error}
<Card body color="danger">{$nodesQuery.error.message}</Card>
{:else if $nodesQuery.fetching || $initq.fetching}
<Spinner/>
<Spinner />
{:else}
<PlotTable
let:item
@ -125,35 +158,61 @@
renderFor="systems"
itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics
.filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
.map(h => ({
.filter(
(h) =>
h.host.includes(hostnameFilter) &&
h.metrics.some(
(m) => m.name == selectedMetric && m.scope == "node",
),
)
.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node'),
disabled: checkMetricDisabled(selectedMetric, cluster, h.subCluster)
data: h.metrics.find(
(m) => m.name == selectedMetric && m.scope == "node",
),
disabled: checkMetricDisabled(
selectedMetric,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))
}>
<h4 style="width: 100%; text-align: center;"><a style="display: block;padding-top: 15px;" href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>
.sort((a, b) => a.host.localeCompare(b.host))}
>
<h4 style="width: 100%; text-align: center;">
<a
style="display: block;padding-top: 15px;"
href="/monitoring/node/{cluster}/{item.host}"
>{item.host} ({item.subCluster})</a
>
</h4>
{#if item.disabled === false && item.data}
<MetricPlot
width={width}
{width}
height={plotHeight}
timestep={item.data.metric.timestep}
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find(c => c.name == cluster)}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={item.subCluster}
resources={[{hostname: item.host}]}
forNode={true}/>
resources={[{ hostname: item.host }]}
forNode={true}
/>
{: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>
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{selectedMetric}</code></Card
>
{/if}
</PlotTable>
{/if}
</Col>
</Row>

View File

@ -1,126 +1,146 @@
<script>
import { getContext } from 'svelte'
import { gql, getContextClient , mutationStore } from '@urql/svelte'
import { Icon, Button, ListGroupItem, Spinner, Modal, Input,
ModalBody, ModalHeader, ModalFooter, Alert } from 'sveltestrap'
import { fuzzySearchTags } from './utils.js'
import Tag from './Tag.svelte'
import { getContext } from "svelte";
import { gql, getContextClient, mutationStore } from "@urql/svelte";
import {
Icon,
Button,
ListGroupItem,
Spinner,
Modal,
Input,
ModalBody,
ModalHeader,
ModalFooter,
Alert,
} from "@sveltestrap/sveltestrap";
import { fuzzySearchTags } from "./utils.js";
import Tag from "./Tag.svelte";
export let job
export let jobTags = job.tags
export let job;
export let jobTags = job.tags;
let allTags = getContext('tags'), initialized = getContext('initialized')
let newTagType = '', newTagName = ''
let filterTerm = ''
let pendingChange = false
let isOpen = false
let allTags = getContext("tags"),
initialized = getContext("initialized");
let newTagType = "",
newTagName = "";
let filterTerm = "";
let pendingChange = false;
let isOpen = false;
const client = getContextClient();
const createTagMutation = ({ type, name }) => {
return mutationStore({
client: client,
query: gql`mutation($type: String!, $name: String!) {
createTag(type: $type, name: $name) { id, type, name }
}`,
variables: { type, name}
})
query: gql`
mutation ($type: String!, $name: String!) {
createTag(type: $type, name: $name) {
id
type
name
}
}
`,
variables: { type, name },
});
};
const addTagsToJobMutation = ({ job, tagIds }) => {
return mutationStore({
client: client,
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
addTagsToJob(job: $job, tagIds: $tagIds) { id, type, name }
}`,
variables: {job, tagIds}
})
query: gql`
mutation ($job: ID!, $tagIds: [ID!]!) {
addTagsToJob(job: $job, tagIds: $tagIds) {
id
type
name
}
}
`,
variables: { job, tagIds },
});
};
const removeTagsFromJobMutation = ({ job, tagIds }) => {
return mutationStore({
client: client,
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
removeTagsFromJob(job: $job, tagIds: $tagIds) { id, type, name }
}`,
variables: {job, tagIds}
})
query: gql`
mutation ($job: ID!, $tagIds: [ID!]!) {
removeTagsFromJob(job: $job, tagIds: $tagIds) {
id
type
name
}
}
`,
variables: { job, tagIds },
});
};
let allTagsFiltered // $initialized is in there because when it becomes true, allTags is initailzed.
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags))
let allTagsFiltered; // $initialized is in there because when it becomes true, allTags is initailzed.
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
$: {
newTagType = '';
newTagName = '';
let parts = filterTerm.split(':').map(s => s.trim())
if (parts.length == 2 && parts.every(s => s.length > 0)) {
newTagType = parts[0]
newTagName = parts[1]
newTagType = "";
newTagName = "";
let parts = filterTerm.split(":").map((s) => s.trim());
if (parts.length == 2 && parts.every((s) => s.length > 0)) {
newTagType = parts[0];
newTagName = parts[1];
}
}
function isNewTag(type, name) {
for (let tag of allTagsFiltered)
if (tag.type == type && tag.name == name)
return false
return true
if (tag.type == type && tag.name == name) return false;
return true;
}
function createTag(type, name) {
pendingChange = true
createTagMutation({ type: type, name: name })
.subscribe(res => {
pendingChange = true;
createTagMutation({ type: type, name: name }).subscribe((res) => {
if (res.fetching === false && !res.error) {
pendingChange = false
allTags = [...allTags, res.data.createTag]
newTagType = ''
newTagName = ''
addTagToJob(res.data.createTag)
pendingChange = false;
allTags = [...allTags, res.data.createTag];
newTagType = "";
newTagName = "";
addTagToJob(res.data.createTag);
} else if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
});
}
function addTagToJob(tag) {
pendingChange = tag.id
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] })
.subscribe(res => {
pendingChange = tag.id;
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe((res) => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.addTagsToJob;
pendingChange = false;
} else if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
});
}
function removeTagFromJob(tag) {
pendingChange = tag.id
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] })
.subscribe(res => {
pendingChange = tag.id;
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe(
(res) => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.removeTagsFromJob
pendingChange = false
jobTags = job.tags = res.data.removeTagsFromJob;
pendingChange = false;
} else if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
},
);
}
</script>
<style>
ul.list-group {
max-height: 450px;
margin-bottom: 10px;
overflow: scroll;
}
</style>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Manage Tags
@ -131,33 +151,44 @@
{/if}
</ModalHeader>
<ModalBody>
<Input style="width: 100%;"
type="text" placeholder="Search Tags"
bind:value={filterTerm} />
<Input
style="width: 100%;"
type="text"
placeholder="Search Tags"
bind:value={filterTerm}
/>
<br/>
<br />
<Alert color="info">
Search using "<code>type: name</code>". If no tag matches your search,
a button for creating a new one will appear.
Search using "<code>type: name</code>". If no tag matches your search, a
button for creating a new one will appear.
</Alert>
<ul class="list-group">
{#each allTagsFiltered as tag}
<ListGroupItem>
<Tag tag={tag}/>
<Tag {tag} />
<span style="float: right;">
{#if pendingChange === tag.id}
<Spinner size="sm" secondary />
{:else if job.tags.find(t => t.id == tag.id)}
<Button size="sm" outline color="danger"
on:click={() => removeTagFromJob(tag)}>
{:else if job.tags.find((t) => t.id == tag.id)}
<Button
size="sm"
outline
color="danger"
on:click={() => removeTagFromJob(tag)}
>
<Icon name="x" />
</Button>
{:else}
<Button size="sm" outline color="success"
on:click={() => addTagToJob(tag)}>
<Button
size="sm"
outline
color="success"
on:click={() => addTagToJob(tag)}
>
<Icon name="plus" />
</Button>
{/if}
@ -169,12 +200,17 @@
</ListGroupItem>
{/each}
</ul>
<br/>
<br />
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<Button outline color="success"
on:click={e => (e.preventDefault(), createTag(newTagType, newTagName))}>
<Button
outline
color="success"
on:click={(e) => (
e.preventDefault(), createTag(newTagType, newTagName)
)}
>
Create & Add Tag:
<Tag tag={({ type: newTagType, name: newTagName })} clickable={false}/>
<Tag tag={{ type: newTagType, name: newTagName }} clickable={false} />
</Button>
{:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
@ -188,3 +224,11 @@
<Button outline on:click={() => (isOpen = true)}>
Manage Tags <Icon name="tags" />
</Button>
<style>
ul.list-group {
max-height: 450px;
margin-bottom: 10px;
overflow: scroll;
}
</style>

View File

@ -1,62 +1,94 @@
<script>
import { onMount, getContext } from 'svelte'
import { init, convert2uplot } from './utils.js'
import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte'
import Sorting from './joblist/SortSelection.svelte'
import Refresher from './joblist/Refresher.svelte'
import Histogram from './plots/Histogram.svelte'
import MetricSelection from './MetricSelection.svelte'
import HistogramSelection from './HistogramSelection.svelte'
import PlotTable from './PlotTable.svelte'
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
import { onMount, getContext } from "svelte";
import { init, convert2uplot } from "./utils.js";
import {
Table,
Row,
Col,
Button,
Icon,
Card,
Spinner,
} from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import Filters from "./filters/Filters.svelte";
import JobList from "./joblist/JobList.svelte";
import Sorting from "./joblist/SortSelection.svelte";
import Refresher from "./joblist/Refresher.svelte";
import Histogram from "./plots/Histogram.svelte";
import MetricSelection from "./MetricSelection.svelte";
import HistogramSelection from "./HistogramSelection.svelte";
import PlotTable from "./PlotTable.svelte";
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
const { query: initq } = init()
const { query: initq } = init();
const ccconfig = getContext('cc-config')
const ccconfig = getContext("cc-config");
export let user
export let filterPresets
export let user;
export let filterPresets;
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 jobList;
let jobFilters = [];
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
let w1, w2, histogramHeight = 250, isHistogramSelectionOpen = false
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
let sorting = { field: "startTime", order: "DESC" },
isSortingOpen = false;
let metrics = ccconfig.plot_list_selectedMetrics,
isMetricsSelectionOpen = false;
let w1,
w2,
histogramHeight = 250,
isHistogramSelectionOpen = false;
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
: !!ccconfig.plot_list_showFootprint;
$: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
$: metricsInHistograms = selectedCluster
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []
: ccconfig.user_view_histogramMetrics || [];
const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
totalJobs
shortJobs
totalWalltime
totalCoreHours
histDuration { count, value }
histNumNodes { count, value }
histMetrics { metric, unit, data { min, max, count, bin } }
}}`,
variables: { jobFilters, metricsInHistograms }
})
histDuration {
count
value
}
histNumNodes {
count
value
}
histMetrics {
metric
unit
data {
min
max
count
bin
}
}
}
}
`,
variables: { jobFilters, metricsInHistograms },
});
onMount(() => filterComponent.update())
onMount(() => filterComponent.update());
</script>
<Row>
{#if $initq.fetching}
<Col>
<Spinner/>
<Spinner />
</Col>
{:else if $initq.error}
<Col xs="auto">
@ -65,40 +97,45 @@
{/if}
<Col xs="auto">
<Button
outline color="primary"
on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up"/> Sorting
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting
</Button>
<Button
outline color="primary"
on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up"/> Metrics
outline
color="primary"
on:click={() => (isMetricsSelectionOpen = true)}
>
<Icon name="graph-up" /> Metrics
</Button>
<Button
outline color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}>
<Icon name="bar-chart-line"/> Select Histograms
outline
color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}
>
<Icon name="bar-chart-line" /> Select Histograms
</Button>
</Col>
<Col xs="auto">
<Filters
filterPresets={filterPresets}
{filterPresets}
startTimeQuickSelect={true}
bind:this={filterComponent}
on:update={({ detail }) => {
jobFilters = [...detail.filters, { user: { eq: user.username } }]
selectedCluster = jobFilters[0]?.cluster ? jobFilters[0].cluster.eq : null
jobList.update(jobFilters)
}} />
jobFilters = [...detail.filters, { user: { eq: user.username } }];
selectedCluster = jobFilters[0]?.cluster
? jobFilters[0].cluster.eq
: null;
jobList.update(jobFilters);
}}
/>
</Col>
<Col xs="auto" style="margin-left: auto;">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
</Row>
<br/>
<br />
<Row>
{#if $stats.error}
<Col>
@ -151,24 +188,28 @@
{#key $stats.data.jobsStatistics[0].histDuration}
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
width={w1 - 25} height={histogramHeight}
width={w1 - 25}
height={histogramHeight}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
yunit="Jobs"
/>
{/key}
</div>
<div class="col-4 text-center" bind:clientWidth={w2}>
{#key $stats.data.jobsStatistics[0].histNumNodes}
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
width={w2 - 25} height={histogramHeight}
width={w2 - 25}
height={histogramHeight}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
yunit="Jobs"
/>
{/key}
</div>
{/if}
@ -191,47 +232,45 @@
let:width
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}>
itemsPerRow={3}
>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
width={width} height={250}
{width}
height={250}
title="Distribution of '{item.metric}' averages"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit}
ylabel="Number of Jobs"
yunit="Jobs"/>
yunit="Jobs"
/>
</PlotTable>
{/key}
</Col>
{/if}
</Row>
{/if}
<br/>
<br />
<Row>
<Col>
<JobList
bind:metrics={metrics}
bind:sorting={sorting}
bind:this={jobList}
bind:showFootprint={showFootprint} />
<JobList bind:metrics bind:sorting bind:this={jobList} bind:showFootprint />
</Col>
</Row>
<Sorting
bind:sorting={sorting}
bind:isOpen={isSortingOpen} />
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
<MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics={metrics}
bind:metrics
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
bind:showFootprint
view="list"
/>
<HistogramSelection
bind:cluster={selectedCluster}
bind:metricsInHistograms={metricsInHistograms}
bind:isOpen={isHistogramSelectionOpen} />
bind:metricsInHistograms
bind:isOpen={isHistogramSelectionOpen}
/>

View File

@ -1,5 +1,5 @@
<script>
import { Icon, InputGroup, InputGroupText } from 'sveltestrap';
import { Icon, InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
export let timeseriesPlots;
@ -9,19 +9,18 @@
function updatePlots() {
let ws = windowSize / (100 * 2),
wp = windowPosition / 100;
let from = (wp - ws),
to = (wp + ws);
Object
.values(timeseriesPlots)
.forEach(plot => plot.setTimeRange(from, to));
let from = wp - ws,
to = wp + ws;
Object.values(timeseriesPlots).forEach((plot) =>
plot.setTimeRange(from, to),
);
}
// Rendering a big job can take a long time, so we
// throttle the rerenders to every 100ms here.
let timeoutId = null;
function requestUpdatePlots() {
if (timeoutId != null)
window.cancelAnimationFrame(timeoutId);
if (timeoutId != null) window.cancelAnimationFrame(timeoutId);
timeoutId = window.requestAnimationFrame(() => {
updatePlots();
@ -35,7 +34,7 @@
<div>
<InputGroup>
<InputGroupText>
<Icon name="zoom-in"/>
<Icon name="zoom-in" />
</InputGroupText>
<InputGroupText>
Window Size:
@ -43,7 +42,10 @@
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowSize}
min=1 max=100 step=1 />
min="1"
max="100"
step="1"
/>
<span style="width: 5em;">
({windowSize}%)
</span>
@ -54,7 +56,10 @@
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowPosition}
min=0 max=100 step=1 />
min="0"
max="100"
step="1"
/>
</InputGroupText>
</InputGroup>
</div>

View File

@ -1,54 +1,53 @@
<script>
import { Row, Col } from 'sveltestrap'
import { onMount } from 'svelte'
import EditRole from './admin/EditRole.svelte'
import EditProject from './admin/EditProject.svelte'
import AddUser from './admin/AddUser.svelte'
import ShowUsers from './admin/ShowUsers.svelte'
import Options from './admin/Options.svelte'
import { Row, Col } from "@sveltestrap/sveltestrap";
import { onMount } from "svelte";
import EditRole from "./admin/EditRole.svelte";
import EditProject from "./admin/EditProject.svelte";
import AddUser from "./admin/AddUser.svelte";
import ShowUsers from "./admin/ShowUsers.svelte";
import Options from "./admin/Options.svelte";
let users = []
let roles = []
let users = [];
let roles = [];
function getUserList() {
fetch('/api/users/?via-ldap=false&not-just-user=true')
.then(res => res.json())
.then(usersRaw => {
users = usersRaw
})
fetch("/api/users/?via-ldap=false&not-just-user=true")
.then((res) => res.json())
.then((usersRaw) => {
users = usersRaw;
});
}
function getValidRoles() {
fetch('/api/roles/')
.then(res => res.json())
.then(rolesRaw => {
roles = rolesRaw
})
fetch("/api/roles/")
.then((res) => res.json())
.then((rolesRaw) => {
roles = rolesRaw;
});
}
function initAdmin() {
getUserList()
getValidRoles()
getUserList();
getValidRoles();
}
onMount(() => initAdmin())
onMount(() => initAdmin());
</script>
<Row cols={2} class="p-2 g-2" >
<Row cols={2} class="p-2 g-2">
<Col class="mb-1">
<AddUser roles={roles} on:reload={getUserList}/>
<AddUser {roles} on:reload={getUserList} />
</Col>
<Col class="mb-1">
<ShowUsers on:reload={getUserList} bind:users={users}/>
<ShowUsers on:reload={getUserList} bind:users />
</Col>
<Col>
<EditRole roles={roles} on:reload={getUserList}/>
<EditRole {roles} on:reload={getUserList} />
</Col>
<Col>
<EditProject on:reload={getUserList}/>
<EditProject on:reload={getUserList} />
</Col>
<Col>
<Options/>
<Options />
</Col>
</Row>

View File

@ -1,64 +1,300 @@
<script>
import { Button, Table, Row, Col, Card, CardBody, CardTitle } from 'sveltestrap'
import { fade } from 'svelte/transition'
import {
Button,
Table,
Row,
Col,
Card,
CardTitle,
} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition";
export let config
export let config;
let message = {msg: '', target: '', color: '#d63384'}
let displayMessage = false
let message = { msg: "", target: "", color: "#d63384" };
let displayMessage = false;
const colorschemes = {
'Default': ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"],
'Autumn': ['rgb(255,0,0)','rgb(255,11,0)','rgb(255,20,0)','rgb(255,30,0)','rgb(255,41,0)','rgb(255,50,0)','rgb(255,60,0)','rgb(255,71,0)','rgb(255,80,0)','rgb(255,90,0)','rgb(255,101,0)','rgb(255,111,0)','rgb(255,120,0)','rgb(255,131,0)','rgb(255,141,0)','rgb(255,150,0)','rgb(255,161,0)','rgb(255,171,0)','rgb(255,180,0)','rgb(255,190,0)','rgb(255,201,0)','rgb(255,210,0)','rgb(255,220,0)','rgb(255,231,0)','rgb(255,240,0)','rgb(255,250,0)'],
'Beach': ['rgb(0,252,0)','rgb(0,233,0)','rgb(0,212,0)','rgb(0,189,0)','rgb(0,169,0)','rgb(0,148,0)','rgb(0,129,4)','rgb(0,145,46)','rgb(0,162,90)','rgb(0,180,132)','rgb(29,143,136)','rgb(73,88,136)','rgb(115,32,136)','rgb(81,9,64)','rgb(124,51,23)','rgb(162,90,0)','rgb(194,132,0)','rgb(220,171,0)','rgb(231,213,0)','rgb(0,0,13)','rgb(0,0,55)','rgb(0,0,92)','rgb(0,0,127)','rgb(0,0,159)','rgb(0,0,196)','rgb(0,0,233)'],
'BlueRed': ['rgb(0,0,131)','rgb(0,0,168)','rgb(0,0,208)','rgb(0,0,247)','rgb(0,27,255)','rgb(0,67,255)','rgb(0,108,255)','rgb(0,148,255)','rgb(0,187,255)','rgb(0,227,255)','rgb(8,255,247)','rgb(48,255,208)','rgb(87,255,168)','rgb(127,255,127)','rgb(168,255,87)','rgb(208,255,48)','rgb(247,255,8)','rgb(255,224,0)','rgb(255,183,0)','rgb(255,143,0)','rgb(255,104,0)','rgb(255,64,0)','rgb(255,23,0)','rgb(238,0,0)','rgb(194,0,0)','rgb(150,0,0)'],
'Rainbow': ['rgb(125,0,255)','rgb(85,0,255)','rgb(39,0,255)','rgb(0,6,255)','rgb(0,51,255)','rgb(0,97,255)','rgb(0,141,255)','rgb(0,187,255)','rgb(0,231,255)','rgb(0,255,233)','rgb(0,255,189)','rgb(0,255,143)','rgb(0,255,99)','rgb(0,255,53)','rgb(0,255,9)','rgb(37,255,0)','rgb(83,255,0)','rgb(127,255,0)','rgb(173,255,0)','rgb(217,255,0)','rgb(255,248,0)','rgb(255,203,0)','rgb(255,159,0)','rgb(255,113,0)','rgb(255,69,0)','rgb(255,23,0)'],
'Binary': ['rgb(215,215,215)','rgb(206,206,206)','rgb(196,196,196)','rgb(185,185,185)','rgb(176,176,176)','rgb(166,166,166)','rgb(155,155,155)','rgb(145,145,145)','rgb(136,136,136)','rgb(125,125,125)','rgb(115,115,115)','rgb(106,106,106)','rgb(95,95,95)','rgb(85,85,85)','rgb(76,76,76)','rgb(66,66,66)','rgb(55,55,55)','rgb(46,46,46)','rgb(36,36,36)','rgb(25,25,25)','rgb(16,16,16)','rgb(6,6,6)'],
'GistEarth': ['rgb(0,0,0)','rgb(2,7,117)','rgb(9,30,118)','rgb(16,53,120)','rgb(23,73,122)','rgb(31,93,124)','rgb(39,110,125)','rgb(47,126,127)','rgb(51,133,119)','rgb(57,138,106)','rgb(62,145,94)','rgb(66,150,82)','rgb(74,157,71)','rgb(97,162,77)','rgb(121,168,83)','rgb(136,173,85)','rgb(153,176,88)','rgb(170,180,92)','rgb(185,182,94)','rgb(189,173,99)','rgb(192,164,101)','rgb(203,169,124)','rgb(215,178,149)','rgb(226,192,176)','rgb(238,212,204)','rgb(248,236,236)'],
'BlueWaves': ['rgb(83,0,215)','rgb(43,6,108)','rgb(9,16,16)','rgb(8,32,25)','rgb(0,50,8)','rgb(27,64,66)','rgb(69,67,178)','rgb(115,62,210)','rgb(155,50,104)','rgb(178,43,41)','rgb(180,51,34)','rgb(161,78,87)','rgb(124,117,187)','rgb(78,155,203)','rgb(34,178,85)','rgb(4,176,2)','rgb(9,152,27)','rgb(4,118,2)','rgb(34,92,85)','rgb(78,92,203)','rgb(124,127,187)','rgb(161,187,87)','rgb(180,248,34)','rgb(178,220,41)','rgb(155,217,104)','rgb(115,254,210)'],
'BlueGreenRedYellow': ['rgb(0,0,0)','rgb(0,0,20)','rgb(0,0,41)','rgb(0,0,62)','rgb(0,25,83)','rgb(0,57,101)','rgb(0,87,101)','rgb(0,118,101)','rgb(0,150,101)','rgb(0,150,69)','rgb(0,148,37)','rgb(0,141,6)','rgb(60,120,0)','rgb(131,87,0)','rgb(180,25,0)','rgb(203,13,0)','rgb(208,36,0)','rgb(213,60,0)','rgb(219,83,0)','rgb(224,106,0)','rgb(229,129,0)','rgb(233,152,0)','rgb(238,176,0)','rgb(243,199,0)','rgb(248,222,0)','rgb(254,245,0)']
Default: [
"#00bfff",
"#0000ff",
"#ff00ff",
"#ff0000",
"#ff8000",
"#ffff00",
"#80ff00",
],
Autumn: [
"rgb(255,0,0)",
"rgb(255,11,0)",
"rgb(255,20,0)",
"rgb(255,30,0)",
"rgb(255,41,0)",
"rgb(255,50,0)",
"rgb(255,60,0)",
"rgb(255,71,0)",
"rgb(255,80,0)",
"rgb(255,90,0)",
"rgb(255,101,0)",
"rgb(255,111,0)",
"rgb(255,120,0)",
"rgb(255,131,0)",
"rgb(255,141,0)",
"rgb(255,150,0)",
"rgb(255,161,0)",
"rgb(255,171,0)",
"rgb(255,180,0)",
"rgb(255,190,0)",
"rgb(255,201,0)",
"rgb(255,210,0)",
"rgb(255,220,0)",
"rgb(255,231,0)",
"rgb(255,240,0)",
"rgb(255,250,0)",
],
Beach: [
"rgb(0,252,0)",
"rgb(0,233,0)",
"rgb(0,212,0)",
"rgb(0,189,0)",
"rgb(0,169,0)",
"rgb(0,148,0)",
"rgb(0,129,4)",
"rgb(0,145,46)",
"rgb(0,162,90)",
"rgb(0,180,132)",
"rgb(29,143,136)",
"rgb(73,88,136)",
"rgb(115,32,136)",
"rgb(81,9,64)",
"rgb(124,51,23)",
"rgb(162,90,0)",
"rgb(194,132,0)",
"rgb(220,171,0)",
"rgb(231,213,0)",
"rgb(0,0,13)",
"rgb(0,0,55)",
"rgb(0,0,92)",
"rgb(0,0,127)",
"rgb(0,0,159)",
"rgb(0,0,196)",
"rgb(0,0,233)",
],
BlueRed: [
"rgb(0,0,131)",
"rgb(0,0,168)",
"rgb(0,0,208)",
"rgb(0,0,247)",
"rgb(0,27,255)",
"rgb(0,67,255)",
"rgb(0,108,255)",
"rgb(0,148,255)",
"rgb(0,187,255)",
"rgb(0,227,255)",
"rgb(8,255,247)",
"rgb(48,255,208)",
"rgb(87,255,168)",
"rgb(127,255,127)",
"rgb(168,255,87)",
"rgb(208,255,48)",
"rgb(247,255,8)",
"rgb(255,224,0)",
"rgb(255,183,0)",
"rgb(255,143,0)",
"rgb(255,104,0)",
"rgb(255,64,0)",
"rgb(255,23,0)",
"rgb(238,0,0)",
"rgb(194,0,0)",
"rgb(150,0,0)",
],
Rainbow: [
"rgb(125,0,255)",
"rgb(85,0,255)",
"rgb(39,0,255)",
"rgb(0,6,255)",
"rgb(0,51,255)",
"rgb(0,97,255)",
"rgb(0,141,255)",
"rgb(0,187,255)",
"rgb(0,231,255)",
"rgb(0,255,233)",
"rgb(0,255,189)",
"rgb(0,255,143)",
"rgb(0,255,99)",
"rgb(0,255,53)",
"rgb(0,255,9)",
"rgb(37,255,0)",
"rgb(83,255,0)",
"rgb(127,255,0)",
"rgb(173,255,0)",
"rgb(217,255,0)",
"rgb(255,248,0)",
"rgb(255,203,0)",
"rgb(255,159,0)",
"rgb(255,113,0)",
"rgb(255,69,0)",
"rgb(255,23,0)",
],
Binary: [
"rgb(215,215,215)",
"rgb(206,206,206)",
"rgb(196,196,196)",
"rgb(185,185,185)",
"rgb(176,176,176)",
"rgb(166,166,166)",
"rgb(155,155,155)",
"rgb(145,145,145)",
"rgb(136,136,136)",
"rgb(125,125,125)",
"rgb(115,115,115)",
"rgb(106,106,106)",
"rgb(95,95,95)",
"rgb(85,85,85)",
"rgb(76,76,76)",
"rgb(66,66,66)",
"rgb(55,55,55)",
"rgb(46,46,46)",
"rgb(36,36,36)",
"rgb(25,25,25)",
"rgb(16,16,16)",
"rgb(6,6,6)",
],
GistEarth: [
"rgb(0,0,0)",
"rgb(2,7,117)",
"rgb(9,30,118)",
"rgb(16,53,120)",
"rgb(23,73,122)",
"rgb(31,93,124)",
"rgb(39,110,125)",
"rgb(47,126,127)",
"rgb(51,133,119)",
"rgb(57,138,106)",
"rgb(62,145,94)",
"rgb(66,150,82)",
"rgb(74,157,71)",
"rgb(97,162,77)",
"rgb(121,168,83)",
"rgb(136,173,85)",
"rgb(153,176,88)",
"rgb(170,180,92)",
"rgb(185,182,94)",
"rgb(189,173,99)",
"rgb(192,164,101)",
"rgb(203,169,124)",
"rgb(215,178,149)",
"rgb(226,192,176)",
"rgb(238,212,204)",
"rgb(248,236,236)",
],
BlueWaves: [
"rgb(83,0,215)",
"rgb(43,6,108)",
"rgb(9,16,16)",
"rgb(8,32,25)",
"rgb(0,50,8)",
"rgb(27,64,66)",
"rgb(69,67,178)",
"rgb(115,62,210)",
"rgb(155,50,104)",
"rgb(178,43,41)",
"rgb(180,51,34)",
"rgb(161,78,87)",
"rgb(124,117,187)",
"rgb(78,155,203)",
"rgb(34,178,85)",
"rgb(4,176,2)",
"rgb(9,152,27)",
"rgb(4,118,2)",
"rgb(34,92,85)",
"rgb(78,92,203)",
"rgb(124,127,187)",
"rgb(161,187,87)",
"rgb(180,248,34)",
"rgb(178,220,41)",
"rgb(155,217,104)",
"rgb(115,254,210)",
],
BlueGreenRedYellow: [
"rgb(0,0,0)",
"rgb(0,0,20)",
"rgb(0,0,41)",
"rgb(0,0,62)",
"rgb(0,25,83)",
"rgb(0,57,101)",
"rgb(0,87,101)",
"rgb(0,118,101)",
"rgb(0,150,101)",
"rgb(0,150,69)",
"rgb(0,148,37)",
"rgb(0,141,6)",
"rgb(60,120,0)",
"rgb(131,87,0)",
"rgb(180,25,0)",
"rgb(203,13,0)",
"rgb(208,36,0)",
"rgb(213,60,0)",
"rgb(219,83,0)",
"rgb(224,106,0)",
"rgb(229,129,0)",
"rgb(233,152,0)",
"rgb(238,176,0)",
"rgb(243,199,0)",
"rgb(248,222,0)",
"rgb(254,245,0)",
],
};
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector)
let formData = new FormData(form)
let form = document.querySelector(selector);
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: 'POST', body: formData });
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text()
popMessage(text, target, '#048109')
let text = await res.text();
popMessage(text, target, "#048109");
} else {
let text = await res.text()
let text = await res.text();
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, target, '#d63384')
popMessage(err, target, "#d63384");
}
return false
return false;
}
function popMessage(response, restarget, rescolor) {
message = {msg: response, target: restarget, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
message = { msg: response, target: restarget, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
</script>
<Row cols={3} class="p-2 g-2">
<!-- LINE WIDTH -->
<Col><Card class="h-100">
<Col
><Card class="h-100">
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#line-width-form', 'lw')}>
<form
id="line-width-form"
method="post"
action="/api/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#line-width-form", "lw")}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Line Width</div>
<!-- Expand If-Clause for clarity once -->
{#if displayMessage && message.target == 'lw'}
{#if displayMessage && message.target == "lw"}
<div style="margin-left: auto; font-size: 0.9em;">
<code style="color: {message.color};" out:fade>
Update: {message.msg}
@ -66,47 +302,102 @@
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_lineWidth"/>
<input type="hidden" name="key" value="plot_general_lineWidth" />
<div class="mb-3">
<label for="value" class="form-label">Line Width</label>
<input type="number" class="form-control" id="lwvalue" name="value" aria-describedby="lineWidthHelp" value="{config.plot_general_lineWidth}" min="1"/>
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div>
<input
type="number"
class="form-control"
id="lwvalue"
name="value"
aria-describedby="lineWidthHelp"
value={config.plot_general_lineWidth}
min="1"
/>
<div id="lineWidthHelp" class="form-text">
Width of the lines in the timeseries plots.
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col>
</Card></Col
>
<!-- PLOTS PER ROW -->
<Col><Card class="h-100">
<form id="plots-per-row-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#plots-per-row-form', 'ppr')}>
<Col
><Card class="h-100">
<form
id="plots-per-row-form"
method="post"
action="/api/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#plots-per-row-form", "ppr")}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Plots per Row</div>
{#if displayMessage && message.target == 'ppr'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
{#if displayMessage && message.target == "ppr"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_view_plotsPerRow"/>
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
<div class="mb-3">
<label for="value" class="form-label">Plots per Row</label>
<input type="number" class="form-control" id="pprvalue" name="value" aria-describedby="plotsperrowHelp" value="{config.plot_view_plotsPerRow }" min="1"/>
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div>
<input
type="number"
class="form-control"
id="pprvalue"
name="value"
aria-describedby="plotsperrowHelp"
value={config.plot_view_plotsPerRow}
min="1"
/>
<div id="plotsperrowHelp" class="form-text">
How many plots to show next to each other on pages such as
/monitoring/job/, /monitoring/system/...
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col>
</Card></Col
>
<!-- BACKGROUND -->
<Col><Card class="h-100">
<form id="backgrounds-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#backgrounds-form', 'bg')}>
<Col
><Card class="h-100">
<form
id="backgrounds-form"
method="post"
action="/api/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#backgrounds-form", "bg")}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Colored Backgrounds</div>
{#if displayMessage && message.target == 'bg'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
{#if displayMessage && message.target == "bg"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorBackground"/>
<input type="hidden" name="key" value="plot_general_colorBackground" />
<div class="mb-3">
<div>
{#if config.plot_general_colorBackground}
<input type="radio" id="true" name="value" value="true" checked/>
<input type="radio" id="true" name="value" value="true" checked />
{:else}
<input type="radio" id="true" name="value" value="true" />
{/if}
@ -116,41 +407,76 @@
{#if config.plot_general_colorBackground}
<input type="radio" id="false" name="value" value="false" />
{:else}
<input type="radio" id="false" name="value" value="false" checked/>
<input
type="radio"
id="false"
name="value"
value="false"
checked
/>
{/if}
<label for="false">No</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col>
</Card></Col
>
</Row>
<Row cols={1} class="p-2 g-2">
<!-- COLORSCHEME -->
<Col><Card>
<form id="colorscheme-form" method="post" action="/api/configuration/" class="card-body">
<Col
><Card>
<form
id="colorscheme-form"
method="post"
action="/api/configuration/"
class="card-body"
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Color Scheme for Timeseries Plots</div>
{#if displayMessage && message.target == 'cs'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
{#if displayMessage && message.target == "cs"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorscheme"/>
<input type="hidden" name="key" value="plot_general_colorscheme" />
<Table hover>
<tbody>
{#each Object.entries(colorschemes) as [name, rgbrow]}
<tr>
<th scope="col">{name}</th>
<td>
{#if rgbrow.join(',') == config.plot_general_colorscheme}
<input type="radio" name="value" value={JSON.stringify(rgbrow)} checked on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
{#if rgbrow.join(",") == config.plot_general_colorscheme}
<input
type="radio"
name="value"
value={JSON.stringify(rgbrow)}
checked
on:click={() =>
handleSettingSubmit("#colorscheme-form", "cs")}
/>
{:else}
<input type="radio" name="value" value={JSON.stringify(rgbrow)} on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
<input
type="radio"
name="value"
value={JSON.stringify(rgbrow)}
on:click={() =>
handleSettingSubmit("#colorscheme-form", "cs")}
/>
{/if}
</td>
<td>
{#each rgbrow as rgb}
<span class="color-dot" style="background-color: {rgb};"></span>
<span class="color-dot" style="background-color: {rgb};"
></span>
{/each}
</td>
</tr>
@ -158,7 +484,8 @@
</tbody>
</Table>
</form>
</Card></Col>
</Card></Col
>
</Row>
<style>

View File

@ -1,103 +1,156 @@
<script>
import { Button, Card, CardTitle } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
let message = {msg: '', color: '#d63384'}
let displayMessage = false
let message = { msg: "", color: "#d63384" };
let displayMessage = false;
export let roles = []
export let roles = [];
async function handleUserSubmit() {
let form = document.querySelector('#create-user-form')
let formData = new FormData(form)
let form = document.querySelector("#create-user-form");
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: 'POST', body: formData });
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
form.reset()
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
form.reset();
} else {
let text = await res.text()
let text = await res.text();
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, '#d63384')
popMessage(err, "#d63384");
}
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch('reload')
dispatch("reload");
}
</script>
<Card>
<form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}>
<form
id="create-user-form"
method="post"
action="/api/users/"
class="card-body"
on:submit|preventDefault={handleUserSubmit}
>
<CardTitle class="mb-3">Create User</CardTitle>
<div class="mb-3">
<label for="username" class="form-label">Username (ID)</label>
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
<input
type="text"
class="form-control"
id="username"
name="username"
aria-describedby="usernameHelp"
/>
<div id="usernameHelp" class="form-text">Must be unique.</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
<input
type="password"
class="form-control"
id="password"
name="password"
aria-describedby="passwordHelp"
/>
<div id="passwordHelp" class="form-text">
Only API users are allowed to have a blank password. Users with a blank
password can only authenticate via Tokens.
</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Project</label>
<input type="text" class="form-control" id="project" name="project" aria-describedby="projectHelp"/>
<div id="projectHelp" class="form-text">Only Manager users can have a project. Allows to inspect jobs and users of given project.</div>
<input
type="text"
class="form-control"
id="project"
name="project"
aria-describedby="projectHelp"
/>
<div id="projectHelp" class="form-text">
Only Manager users can have a project. Allows to inspect jobs and users
of given project.
</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
<input
type="text"
class="form-control"
id="name"
name="name"
aria-describedby="nameHelp"
/>
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
<input
type="email"
class="form-control"
id="email"
name="email"
aria-describedby="emailHelp"
/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<p>Role:</p>
{#each roles as role, i}
{#if i == 0}
<div>
<input type="radio" id={role} name="role" value={role} checked/>
<label for={role}>{role.toUpperCase()} (Allowed to interact with REST API.)</label>
<input type="radio" id={role} name="role" value={role} checked />
<label for={role}
>{role.toUpperCase()} (Allowed to interact with REST API.)</label
>
</div>
{:else if i == 1}
<div>
<input type="radio" id={role} name="role" value={role} checked/>
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created via LDAP sync.)</label>
<input type="radio" id={role} name="role" value={role} checked />
<label for={role}
>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created
via LDAP sync.)</label
>
</div>
{:else}
<div>
<input type="radio" id={role} name="role" value={role}/>
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label>
<input type="radio" id={role} name="role" value={role} />
<label for={role}
>{role.charAt(0).toUpperCase() + role.slice(1)}</label
>
</div>
{/if}
{/each}
</div>
<p style="display: flex; align-items: center;">
<Button type="submit" color="primary">Submit</Button>
{#if displayMessage}<div style="margin-left: 1.5em;"><b><code style="color: {message.color};" out:fade>{message.msg}</code></b></div>{/if}
{#if displayMessage}<div style="margin-left: 1.5em;">
<b
><code style="color: {message.color};" out:fade>{message.msg}</code
></b
>
</div>{/if}
</p>
</form>
</Card>

View File

@ -1,97 +1,129 @@
<script>
import { Card, CardTitle, CardBody } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
let message = {msg: '', color: '#d63384'}
let displayMessage = false
let message = { msg: "", color: "#d63384" };
let displayMessage = false;
async function handleAddProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
const username = document.querySelector("#project-username").value;
const project = document.querySelector("#project-id").value;
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
alert("Please fill in a username and select a project.");
return;
}
let formData = new FormData()
formData.append('username', username)
formData.append('add-project', project)
let formData = new FormData();
formData.append("username", username);
formData.append("add-project", project);
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text()
let text = await res.text();
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, '#d63384')
popMessage(err, "#d63384");
}
}
async function handleRemoveProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
const username = document.querySelector("#project-username").value;
const project = document.querySelector("#project-id").value;
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
alert("Please fill in a username and select a project.");
return;
}
let formData = new FormData()
formData.append('username', username)
formData.append('remove-project', project)
let formData = new FormData();
formData.append("username", username);
formData.append("remove-project", project);
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text()
let text = await res.text();
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, '#d63384')
popMessage(err, "#d63384");
}
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch('reload')
dispatch("reload");
}
</script>
<Card>
<CardBody>
<CardTitle class="mb-3">Edit Project Managed By User (Manager Only)</CardTitle>
<CardTitle class="mb-3"
>Edit Project Managed By User (Manager Only)</CardTitle
>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="username" id="project-username"/>
<input type="text" class="form-control" placeholder="project-id" id="project-id"/>
<input
type="text"
class="form-control"
placeholder="username"
id="project-username"
/>
<input
type="text"
class="form-control"
placeholder="project-id"
id="project-id"
/>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button class="btn btn-primary" type="button" id="add-project-button" on:click|preventDefault={handleAddProject}>Add</button>
<button class="btn btn-danger" type="button" id="remove-project-button" on:click|preventDefault={handleRemoveProject}>Remove</button>
<button
class="btn btn-primary"
type="button"
id="add-project-button"
on:click|preventDefault={handleAddProject}>Add</button
>
<button
class="btn btn-danger"
type="button"
id="remove-project-button"
on:click|preventDefault={handleRemoveProject}>Remove</button
>
</div>
<p>
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
{#if displayMessage}<b
><code style="color: {message.color};" out:fade
>Update: {message.msg}</code
></b
>{/if}
</p>
</CardBody>
</Card>

View File

@ -1,83 +1,89 @@
<script>
import { Card, CardTitle, CardBody } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
let message = {msg: '', color: '#d63384'}
let displayMessage = false
let message = { msg: "", color: "#d63384" };
let displayMessage = false;
export let roles = []
export let roles = [];
async function handleAddRole() {
const username = document.querySelector('#role-username').value
const role = document.querySelector('#role-select').value
const username = document.querySelector("#role-username").value;
const role = document.querySelector("#role-select").value;
if (username == "" || role == "") {
alert('Please fill in a username and select a role.')
return
alert("Please fill in a username and select a role.");
return;
}
let formData = new FormData()
formData.append('username', username)
formData.append('add-role', role)
let formData = new FormData();
formData.append("username", username);
formData.append("add-role", role);
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text()
let text = await res.text();
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, '#d63384')
popMessage(err, "#d63384");
}
}
async function handleRemoveRole() {
const username = document.querySelector('#role-username').value
const role = document.querySelector('#role-select').value
const username = document.querySelector("#role-username").value;
const role = document.querySelector("#role-select").value;
if (username == "" || role == "") {
alert('Please fill in a username and select a role.')
return
alert("Please fill in a username and select a role.");
return;
}
let formData = new FormData()
formData.append('username', username)
formData.append('remove-role', role)
let formData = new FormData();
formData.append("username", username);
formData.append("remove-role", role);
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text()
let text = await res.text();
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, '#d63384')
popMessage(err, "#d63384");
}
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch('reload')
dispatch("reload");
}
</script>
@ -85,20 +91,41 @@
<CardBody>
<CardTitle class="mb-3">Edit User Roles</CardTitle>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="username" id="role-username"/>
<input
type="text"
class="form-control"
placeholder="username"
id="role-username"
/>
<select class="form-select" id="role-select">
<option selected value="">Role...</option>
{#each roles as role}
<option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option>
<option value={role}
>{role.charAt(0).toUpperCase() + role.slice(1)}</option
>
{/each}
</select>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button class="btn btn-primary" type="button" id="add-role-button" on:click|preventDefault={handleAddRole}>Add</button>
<button class="btn btn-danger" type="button" id="remove-role-button" on:click|preventDefault={handleRemoveRole}>Remove</button>
<button
class="btn btn-primary"
type="button"
id="add-role-button"
on:click|preventDefault={handleAddRole}>Add</button
>
<button
class="btn btn-danger"
type="button"
id="remove-role-button"
on:click|preventDefault={handleRemoveRole}>Remove</button
>
</div>
<p>
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
{#if displayMessage}<b
><code style="color: {message.color};" out:fade
>Update: {message.msg}</code
></b
>{/if}
</p>
</CardBody>
</Card>

View File

@ -1,29 +1,34 @@
<script>
import { onMount } from 'svelte'
import { Card, CardBody, CardTitle } from 'sveltestrap'
import { onMount } from "svelte";
import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
let scrambled
let scrambled;
onMount(() => {
scrambled = window.localStorage.getItem("cc-scramble-names") != null
})
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
});
function handleScramble() {
if (!scrambled) {
scrambled = true
window.localStorage.setItem("cc-scramble-names", "true")
scrambled = true;
window.localStorage.setItem("cc-scramble-names", "true");
} else {
scrambled = false
window.localStorage.removeItem("cc-scramble-names")
scrambled = false;
window.localStorage.removeItem("cc-scramble-names");
}
}
</script>
<Card class="h-100">
<CardBody>
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
<input type="checkbox" id="scramble-names-checkbox" style="margin-right: 1em;" on:click={handleScramble} bind:checked={scrambled}/>
<input
type="checkbox"
id="scramble-names-checkbox"
style="margin-right: 1em;"
on:click={handleScramble}
bind:checked={scrambled}
/>
Active?
</CardBody>
</Card>

View File

@ -1,39 +1,51 @@
<script>
import { Button, Table, Card, CardTitle, CardBody } from 'sveltestrap'
import { onMount, createEventDispatcher } from "svelte";
import ShowUsersRow from './ShowUsersRow.svelte'
import {
Button,
Table,
Card,
CardTitle,
CardBody,
} from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import ShowUsersRow from "./ShowUsersRow.svelte";
export let users = []
export let users = [];
const dispatch = createEventDispatcher();
function reloadUserList() {
dispatch('reload')
dispatch("reload");
}
function deleteUser(username) {
if (confirm('Are you sure?')) {
let formData = new FormData()
formData.append('username', username)
fetch('/api/users/', { method: 'DELETE', body: formData }).then(res => {
if (confirm("Are you sure?")) {
let formData = new FormData();
formData.append("username", username);
fetch("/api/users/", { method: "DELETE", body: formData }).then((res) => {
if (res.status == 200) {
reloadUserList()
reloadUserList();
} else {
confirm(res.statusText)
confirm(res.statusText);
}
})
});
}
}
$: userList = users
$: userList = users;
</script>
<Card class="h-100">
<CardBody>
<CardTitle class="mb-3">Special Users</CardTitle>
<p>
Not created by an LDAP sync and/or having a role other than <code>user</code>
<Button color="secondary" size="sm" on:click={reloadUserList} style="float: right;">Reload</Button>
Not created by an LDAP sync and/or having a role other than <code
>user</code
>
<Button
color="secondary"
size="sm"
on:click={reloadUserList}
style="float: right;">Reload</Button
>
</p>
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
<Table hover>
@ -51,13 +63,20 @@
<tbody id="users-list">
{#each userList as user}
<tr id="user-{user.username}">
<ShowUsersRow {user}/>
<td><button class="btn btn-danger del-user" on:click={deleteUser(user.username)}>Delete</button></td>
<ShowUsersRow {user} />
<td
><button
class="btn btn-danger del-user"
on:click={deleteUser(user.username)}>Delete</button
></td
>
</tr>
{:else}
<tr>
<td colspan="4">
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</td>
</tr>
{/each}

View File

@ -1,16 +1,18 @@
<script>
import { Button } from 'sveltestrap'
import { Button } from "@sveltestrap/sveltestrap";
export let user
let jwt = ""
export let user;
let jwt = "";
function getUserJwt(username) {
fetch(`/api/jwt/?username=${username}`)
.then(res => res.text())
.then(text => {
jwt = text
navigator.clipboard.writeText(text).catch(reason => console.error(reason))
})
.then((res) => res.text())
.then((text) => {
jwt = text;
navigator.clipboard
.writeText(text)
.catch((reason) => console.error(reason));
});
}
</script>
@ -18,10 +20,12 @@
<td>{user.name}</td>
<td>{user.projects}</td>
<td>{user.email}</td>
<td><code>{user.roles.join(', ')}</code></td>
<td><code>{user.roles.join(", ")}</code></td>
<td>
{#if ! jwt}
<Button color="success" on:click={getUserJwt(user.username)}>Gen. JWT</Button>
{#if !jwt}
<Button color="success" on:click={getUserJwt(user.username)}
>Gen. JWT</Button
>
{:else}
<textarea rows="3" cols="20">{jwt}</textarea>
{/if}

View File

@ -1,25 +1,31 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, ListGroup, ListGroupItem,
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import { createEventDispatcher, getContext } from "svelte";
import {
Button,
ListGroup,
ListGroupItem,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
const clusters = getContext('clusters'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
const clusters = getContext("clusters"),
initialized = getContext("initialized"),
dispatch = createEventDispatcher();
export let disableClusterSelection = false
export let isModified = false
export let isOpen = false
export let cluster = null
export let partition = null
let pendingCluster = cluster, pendingPartition = partition
$: isModified = pendingCluster != cluster || pendingPartition != partition
export let disableClusterSelection = false;
export let isModified = false;
export let isOpen = false;
export let cluster = null;
export let partition = null;
let pendingCluster = cluster,
pendingPartition = partition;
$: isModified = pendingCluster != cluster || pendingPartition != partition;
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Cluster & Slurm Partition
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Cluster & Slurm Partition</ModalHeader>
<ModalBody>
{#if $initialized}
<h4>Cluster</h4>
@ -27,32 +33,38 @@
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == null}
on:click={() => (pendingCluster = null, pendingPartition = null)}>
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
>
Any Cluster
</ListGroupItem>
{#each clusters as cluster}
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == cluster.name}
on:click={() => (pendingCluster = cluster.name, pendingPartition = null)}>
on:click={() => (
(pendingCluster = cluster.name), (pendingPartition = null)
)}
>
{cluster.name}
</ListGroupItem>
{/each}
</ListGroup>
{/if}
{#if $initialized && pendingCluster != null}
<br/>
<br />
<h4>Partiton</h4>
<ListGroup>
<ListGroupItem
active={pendingPartition == null}
on:click={() => (pendingPartition = null)}>
on:click={() => (pendingPartition = null)}
>
Any Partition
</ListGroupItem>
{#each clusters.find(c => c.name == pendingCluster).partitions as partition}
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition}
<ListGroupItem
active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)}>
on:click={() => (pendingPartition = partition)}
>
{partition}
</ListGroupItem>
{/each}
@ -60,18 +72,24 @@
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => {
isOpen = false
cluster = pendingCluster
partition = pendingPartition
dispatch('update', { cluster, partition })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
cluster = pendingCluster = null
partition = pendingPartition = null
dispatch('update', { cluster, partition })
}}>Reset</Button>
<Button
color="primary"
on:click={() => {
isOpen = false;
cluster = pendingCluster;
partition = pendingPartition;
dispatch("update", { cluster, partition });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
cluster = pendingCluster = null;
partition = pendingPartition = null;
dispatch("update", { cluster, partition });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,54 +1,86 @@
<script>
import { createEventDispatcher } from 'svelte'
import { Row, Col, Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import { createEventDispatcher } from "svelte";
import {
Row,
Col,
Button,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
export let isOpen = false
export let lessThan = null
export let moreThan = null
export let from = null
export let to = null
export let isOpen = false;
export let lessThan = null;
export let moreThan = null;
export let from = null;
export let to = null;
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo
let lessDisabled = false, moreDisabled = false, betweenDisabled = false
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo;
let lessDisabled = false,
moreDisabled = false,
betweenDisabled = false;
function reset() {
pendingLessThan = lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan)
pendingMoreThan = moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan)
pendingFrom = from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from)
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to)
pendingLessThan =
lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan);
pendingMoreThan =
moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan);
pendingFrom =
from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from);
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to);
}
reset()
reset();
function secsToHoursAndMins(duration) {
const hours = Math.floor(duration / 3600)
duration -= hours * 3600
const mins = Math.floor(duration / 60)
return { hours, mins }
const hours = Math.floor(duration / 3600);
duration -= hours * 3600;
const mins = Math.floor(duration / 60);
return { hours, mins };
}
function hoursAndMinsToSecs({ hours, mins }) {
return hours * 3600 + mins * 60
return hours * 3600 + mins * 60;
}
$: lessDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
$: moreDisabled = pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
$: betweenDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0
$: lessDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: moreDisabled =
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: betweenDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0;
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Job Duration
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Job Duration</ModalHeader>
<ModalBody>
<h4>Duration more than</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" class="form-control" bind:value={pendingMoreThan.hours} disabled={moreDisabled}>
<input
type="number"
min="0"
class="form-control"
bind:value={pendingMoreThan.hours}
disabled={moreDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
@ -56,20 +88,33 @@
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingMoreThan.mins} disabled={moreDisabled}>
<input
type="number"
min="0"
max="59"
class="form-control"
bind:value={pendingMoreThan.mins}
disabled={moreDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr/>
<hr />
<h4>Duration less than</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" class="form-control" bind:value={pendingLessThan.hours} disabled={lessDisabled}>
<input
type="number"
min="0"
class="form-control"
bind:value={pendingLessThan.hours}
disabled={lessDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
@ -77,20 +122,33 @@
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingLessThan.mins} disabled={lessDisabled}>
<input
type="number"
min="0"
max="59"
class="form-control"
bind:value={pendingLessThan.mins}
disabled={lessDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr/>
<hr />
<h4>Duration between</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" class="form-control" bind:value={pendingFrom.hours} disabled={betweenDisabled}>
<input
type="number"
min="0"
class="form-control"
bind:value={pendingFrom.hours}
disabled={betweenDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
@ -98,7 +156,14 @@
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingFrom.mins} disabled={betweenDisabled}>
<input
type="number"
min="0"
max="59"
class="form-control"
bind:value={pendingFrom.mins}
disabled={betweenDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
@ -109,7 +174,13 @@
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" class="form-control" bind:value={pendingTo.hours} disabled={betweenDisabled}>
<input
type="number"
min="0"
class="form-control"
bind:value={pendingTo.hours}
disabled={betweenDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
@ -117,7 +188,14 @@
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingTo.mins} disabled={betweenDisabled}>
<input
type="number"
min="0"
max="59"
class="form-control"
bind:value={pendingTo.mins}
disabled={betweenDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
@ -126,33 +204,41 @@
</Row>
</ModalBody>
<ModalFooter>
<Button color="primary"
<Button
color="primary"
on:click={() => {
isOpen = false
lessThan = hoursAndMinsToSecs(pendingLessThan)
moreThan = hoursAndMinsToSecs(pendingMoreThan)
from = hoursAndMinsToSecs(pendingFrom)
to = hoursAndMinsToSecs(pendingTo)
dispatch('update', { lessThan, moreThan, from, to })
}}>
isOpen = false;
lessThan = hoursAndMinsToSecs(pendingLessThan);
moreThan = hoursAndMinsToSecs(pendingMoreThan);
from = hoursAndMinsToSecs(pendingFrom);
to = hoursAndMinsToSecs(pendingTo);
dispatch("update", { lessThan, moreThan, from, to });
}}
>
Close & Apply
</Button>
<Button color="warning" on:click={() => {
lessThan = null
moreThan = null
from = null
to = null
reset()
}}>Reset Values</Button>
<Button color="danger" on:click={() => {
isOpen = false
lessThan = null
moreThan = null
from = null
to = null
reset()
dispatch('update', { lessThan, moreThan, from, to })
}}>Reset Filter</Button>
<Button
color="warning"
on:click={() => {
lessThan = null;
moreThan = null;
from = null;
to = null;
reset();
}}>Reset Values</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
lessThan = null;
moreThan = null;
from = null;
to = null;
reset();
dispatch("update", { lessThan, moreThan, from, to });
}}>Reset Filter</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -10,43 +10,58 @@
- void update(additionalFilters: Object?): Triggers an update
-->
<script>
import { Row, Col, DropdownItem, DropdownMenu,
DropdownToggle, ButtonDropdown, Icon } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import Info from './InfoBox.svelte'
import Cluster from './Cluster.svelte'
import JobStates, { allJobStates } from './JobStates.svelte'
import StartTime from './StartTime.svelte'
import Tags from './Tags.svelte'
import Tag from '../Tag.svelte'
import Duration from './Duration.svelte'
import Resources from './Resources.svelte'
import Statistics from './Stats.svelte'
import {
Row,
Col,
DropdownItem,
DropdownMenu,
DropdownToggle,
ButtonDropdown,
Icon,
} from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import Info from "./InfoBox.svelte";
import Cluster from "./Cluster.svelte";
import JobStates, { allJobStates } from "./JobStates.svelte";
import StartTime from "./StartTime.svelte";
import Tags from "./Tags.svelte";
import Tag from "../Tag.svelte";
import Duration from "./Duration.svelte";
import Resources from "./Resources.svelte";
import Statistics from "./Stats.svelte";
// import TimeSelection from './TimeSelection.svelte'
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
export let menuText = null
export let filterPresets = {}
export let disableClusterSelection = false
export let startTimeQuickSelect = false
export let menuText = null;
export let filterPresets = {};
export let disableClusterSelection = false;
export let startTimeQuickSelect = false;
let filters = {
projectMatch: filterPresets.projectMatch || 'contains',
userMatch: filterPresets.userMatch || 'contains',
jobIdMatch: filterPresets.jobIdMatch || 'eq',
projectMatch: filterPresets.projectMatch || "contains",
userMatch: filterPresets.userMatch || "contains",
jobIdMatch: filterPresets.jobIdMatch || "eq",
cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null,
states: filterPresets.states || filterPresets.state ? [filterPresets.state].flat() : allJobStates,
states:
filterPresets.states || filterPresets.state
? [filterPresets.state].flat()
: allJobStates,
startTime: filterPresets.startTime || { from: null, to: null },
tags: filterPresets.tags || [],
duration: filterPresets.duration || { lessThan: null, moreThan: null, from: null, to: null },
jobId: filterPresets.jobId || '',
duration: filterPresets.duration || {
lessThan: null,
moreThan: null,
from: null,
to: null,
},
jobId: filterPresets.jobId || "",
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || '',
project: filterPresets.project || '',
jobName: filterPresets.jobName || '',
user: filterPresets.user || "",
project: filterPresets.project || "",
jobName: filterPresets.jobName || "",
node: filterPresets.node || null,
numNodes: filterPresets.numNodes || { from: null, to: null },
@ -54,7 +69,7 @@
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
stats: [],
}
};
let isClusterOpen = false,
isJobStatesOpen = false,
@ -65,118 +80,126 @@
isStatsOpen = false,
isNodesModified = false,
isHwthreadsModified = false,
isAccsModified = false
isAccsModified = false;
// Can be called from the outside to trigger a 'update' event from this component.
export function update(additionalFilters = null) {
if (additionalFilters != null)
for (let key in additionalFilters)
filters[key] = additionalFilters[key]
for (let key in additionalFilters) filters[key] = additionalFilters[key];
let items = []
if (filters.cluster)
items.push({ cluster: { eq: filters.cluster } })
if (filters.node)
items.push({ node: { contains: filters.node } })
if (filters.partition)
items.push({ partition: { eq: filters.partition } })
let items = [];
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
if (filters.node) items.push({ node: { contains: filters.node } });
if (filters.partition) items.push({ partition: { eq: filters.partition } });
if (filters.states.length != allJobStates.length)
items.push({ state: filters.states })
items.push({ state: filters.states });
if (filters.startTime.from || filters.startTime.to)
items.push({ startTime: { from: filters.startTime.from, to: filters.startTime.to } })
if (filters.tags.length != 0)
items.push({ tags: filters.tags })
items.push({
startTime: { from: filters.startTime.from, to: filters.startTime.to },
});
if (filters.tags.length != 0) items.push({ tags: filters.tags });
if (filters.duration.from || filters.duration.to)
items.push({ duration: { from: filters.duration.from, to: filters.duration.to } })
items.push({
duration: { from: filters.duration.from, to: filters.duration.to },
});
if (filters.duration.lessThan)
items.push({ duration: { from: 0, to: filters.duration.lessThan } })
items.push({ duration: { from: 0, to: filters.duration.lessThan } });
if (filters.duration.moreThan)
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }) // 7 days to include special jobs with long runtimes
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } })
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId })
items.push({ arrayJobId: filters.arrayJobId });
if (filters.numNodes.from != null || filters.numNodes.to != null)
items.push({ numNodes: { from: filters.numNodes.from, to: filters.numNodes.to } })
items.push({
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
});
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
items.push({ numHWThreads: { from: filters.numHWThreads.from, to: filters.numHWThreads.to } })
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null)
items.push({ numAccelerators: { from: filters.numAccelerators.from, to: filters.numAccelerators.to } })
items.push({
numHWThreads: {
from: filters.numHWThreads.from,
to: filters.numHWThreads.to,
},
});
if (
filters.numAccelerators.from != null ||
filters.numAccelerators.to != null
)
items.push({
numAccelerators: {
from: filters.numAccelerators.from,
to: filters.numAccelerators.to,
},
});
if (filters.user)
items.push({ user: { [filters.userMatch]: filters.user } })
items.push({ user: { [filters.userMatch]: filters.user } });
if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } })
if (filters.jobName)
items.push({ jobName: { contains: filters.jobName } })
items.push({ project: { [filters.projectMatch]: filters.project } });
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
for (let stat of filters.stats)
items.push({ [stat.field]: { from: stat.from, to: stat.to } })
items.push({ [stat.field]: { from: stat.from, to: stat.to } });
dispatch('update', { filters: items })
changeURL()
return items
dispatch("update", { filters: items });
changeURL();
return items;
}
function changeURL() {
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000)
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.partition)
opts.push(`partition=${filters.partition}`)
let opts = [];
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
if (filters.node) opts.push(`node=${filters.node}`);
if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.states.length != allJobStates.length)
for (let state of filters.states)
opts.push(`state=${state}`)
for (let state of filters.states) opts.push(`state=${state}`);
if (filters.startTime.from && filters.startTime.to)
// if (filters.startTime.url) {
// opts.push(`startTime=${filters.startTime.url}`)
// } else {
opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`)
opts.push(
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
);
// }
if (filters.jobId.length != 0)
if (filters.jobIdMatch != 'in') {
opts.push(`jobId=${filters.jobId}`)
if (filters.jobIdMatch != "in") {
opts.push(`jobId=${filters.jobId}`);
} else {
for (let singleJobId of filters.jobId)
opts.push(`jobId=${singleJobId}`)
opts.push(`jobId=${singleJobId}`);
}
if (filters.jobIdMatch != 'eq')
opts.push(`jobIdMatch=${filters.jobIdMatch}`)
for (let tag of filters.tags)
opts.push(`tag=${tag}`)
if (filters.jobIdMatch != "eq")
opts.push(`jobIdMatch=${filters.jobIdMatch}`);
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}`)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
if (filters.duration.lessThan)
opts.push(`duration=0-${filters.duration.lessThan}`)
opts.push(`duration=0-${filters.duration.lessThan}`);
if (filters.duration.moreThan)
opts.push(`duration=${filters.duration.moreThan}-604800`)
opts.push(`duration=${filters.duration.moreThan}-604800`);
if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`)
opts.push(
`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`,
);
if (filters.user.length != 0)
if (filters.userMatch != 'in') {
opts.push(`user=${filters.user}`)
if (filters.userMatch != "in") {
opts.push(`user=${filters.user}`);
} else {
for (let singleUser of filters.user)
opts.push(`user=${singleUser}`)
for (let singleUser of filters.user) opts.push(`user=${singleUser}`);
}
if (filters.userMatch != 'contains')
opts.push(`userMatch=${filters.userMatch}`)
if (filters.project)
opts.push(`project=${filters.project}`)
if (filters.jobName)
opts.push(`jobName=${filters.jobName}`)
if (filters.projectMatch != 'contains')
opts.push(`projectMatch=${filters.projectMatch}`)
if (filters.userMatch != "contains")
opts.push(`userMatch=${filters.userMatch}`);
if (filters.project) opts.push(`project=${filters.project}`);
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
if (filters.projectMatch != "contains")
opts.push(`projectMatch=${filters.projectMatch}`);
if (opts.length == 0 && window.location.search.length <= 1)
return
if (opts.length == 0 && window.location.search.length <= 1) return;
let newurl = `${window.location.pathname}?${opts.join('&')}`
window.history.replaceState(null, '', newurl)
let newurl = `${window.location.pathname}?${opts.join("&")}`;
window.history.replaceState(null, "", newurl);
}
</script>
@ -184,57 +207,52 @@
<Col xs="auto">
<ButtonDropdown class="cc-dropdown-on-hover">
<DropdownToggle outline caret color="success">
<Icon name="sliders"/>
<Icon name="sliders" />
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>
Manage Filters
</DropdownItem>
<DropdownItem header>Manage Filters</DropdownItem>
{#if menuText}
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu"/> Cluster/Partition
<Icon name="cpu" /> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill"/> Job States
<Icon name="gear-fill" /> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range"/> Start Time
<Icon name="calendar-range" /> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch"/> Duration
<Icon name="stopwatch" /> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags"/> Tags
<Icon name="tags" /> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack"/> Resources
<Icon name="hdd-stack" /> Resources
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider/>
<DropdownItem divider />
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
{#each [
{ text: 'Last 6hrs', url: 'last6h', seconds: 6*60*60 },
// { text: 'Last 12hrs', seconds: 12*60*60 },
{ text: 'Last 24hrs', url: 'last24h', seconds: 24*60*60 },
// { text: 'Last 48hrs', seconds: 48*60*60 },
{ text: 'Last 7 days', url: 'last7d', seconds: 7*24*60*60 },
{ text: 'Last 30 days', url: 'last30d', seconds: 30*24*60*60 }
] as {text, url, seconds}}
<DropdownItem on:click={() => {
filters.startTime.from = (new Date(Date.now() - seconds * 1000)).toISOString()
filters.startTime.to = (new Date(Date.now())).toISOString()
filters.startTime.text = text,
filters.startTime.url = url
update()
}}>
<Icon name="calendar-range"/> {text}
{#each [{ text: "Last 6hrs", url: "last6h", seconds: 6 * 60 * 60 }, { text: "Last 24hrs", url: "last24h", seconds: 24 * 60 * 60 }, { text: "Last 7 days", url: "last7d", seconds: 7 * 24 * 60 * 60 }, { text: "Last 30 days", url: "last30d", seconds: 30 * 24 * 60 * 60 }] as { text, url, seconds }}
<DropdownItem
on:click={() => {
filters.startTime.from = new Date(
Date.now() - seconds * 1000,
).toISOString();
filters.startTime.to = new Date(Date.now()).toISOString();
(filters.startTime.text = text), (filters.startTime.url = url);
update();
}}
>
<Icon name="calendar-range" />
{text}
</DropdownItem>
{/each}
{/if}
@ -253,7 +271,7 @@
{#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(', ')}
{filters.states.join(", ")}
</Info>
{/if}
@ -262,28 +280,37 @@
{#if filters.startTime.text}
{filters.startTime.text}
{:else}
{new Date(filters.startTime.from).toLocaleString()} - {new Date(filters.startTime.to).toLocaleString()}
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
filters.startTime.to,
).toLocaleString()}
{/if}
</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
{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
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
Duration more than {Math.floor(
filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
</Info>
{/if}
@ -295,19 +322,26 @@
</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 }
{#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}
{#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 }
{#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node}
</Info>
@ -315,33 +349,38 @@
{#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')}
{filters.stats
.map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
.join(", ")}
</Info>
{/if}
</Col>
</Row>
<Cluster
disableClusterSelection={disableClusterSelection}
{disableClusterSelection}
bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster}
bind:partition={filters.partition}
on:update={() => update()} />
on:update={() => update()}
/>
<JobStates
bind:isOpen={isJobStatesOpen}
bind:states={filters.states}
on:update={() => update()} />
on:update={() => update()}
/>
<StartTime
bind:isOpen={isStartTimeOpen}
bind:from={filters.startTime.from}
bind:to={filters.startTime.to}
on:update={() => {
delete filters.startTime['text']
delete filters.startTime['url']
update()
}} />
delete filters.startTime["text"];
delete filters.startTime["url"];
update();
}}
/>
<Duration
bind:isOpen={isDurationOpen}
@ -349,28 +388,34 @@
bind:moreThan={filters.duration.moreThan}
bind:from={filters.duration.from}
bind:to={filters.duration.to}
on:update={() => update()} />
on:update={() => update()}
/>
<Tags
bind:isOpen={isTagsOpen}
bind:tags={filters.tags}
on:update={() => update()} />
on:update={() => update()}
/>
<Resources cluster={filters.cluster}
<Resources
cluster={filters.cluster}
bind:isOpen={isResourcesOpen}
bind:numNodes={filters.numNodes}
bind:numHWThreads={filters.numHWThreads}
bind:numAccelerators={filters.numAccelerators}
bind:namedNode={filters.node}
bind:isNodesModified={isNodesModified}
bind:isHwthreadsModified={isHwthreadsModified}
bind:isAccsModified={isAccsModified}
on:update={() => update()} />
bind:isNodesModified
bind:isHwthreadsModified
bind:isAccsModified
on:update={() => update()}
/>
<Statistics cluster={filters.cluster}
<Statistics
cluster={filters.cluster}
bind:isOpen={isStatsOpen}
bind:stats={filters.stats}
on:update={() => update()} />
on:update={() => update()}
/>
<style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {

View File

@ -1,11 +1,11 @@
<script>
import { Button, Icon } from 'sveltestrap'
import { Button, Icon } from "@sveltestrap/sveltestrap";
export let icon
export let modified = false
export let icon;
export let modified = false;
</script>
<Button outline color={modified ? 'warning' : 'primary'} on:click>
<Icon name={icon}/>
<Button outline color={modified ? "warning" : "primary"} on:click>
<Icon name={icon} />
<slot />
</Button>

View File

@ -1,47 +1,76 @@
<script context="module">
export const allJobStates = [ 'running', 'completed', 'failed', 'cancelled', 'stopped', 'timeout', 'preempted', 'out_of_memory' ]
export const allJobStates = [
"running",
"completed",
"failed",
"cancelled",
"stopped",
"timeout",
"preempted",
"out_of_memory",
];
</script>
<script>
import { createEventDispatcher } from 'svelte'
import { Button, ListGroup, ListGroupItem,
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import { createEventDispatcher } from "svelte";
import {
Button,
ListGroup,
ListGroupItem,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
export let isModified = false
export let isOpen = false
export let states = [...allJobStates]
export let isModified = false;
export let isOpen = false;
export let states = [...allJobStates];
let pendingStates = [...states]
$: isModified = states.length != pendingStates.length || !states.every(state => pendingStates.includes(state))
let pendingStates = [...states];
$: isModified =
states.length != pendingStates.length ||
!states.every((state) => pendingStates.includes(state));
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Job States
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Job States</ModalHeader>
<ModalBody>
<ListGroup>
{#each allJobStates as state}
<ListGroupItem>
<input type=checkbox bind:group={pendingStates} name="flavours" value={state}>
<input
type="checkbox"
bind:group={pendingStates}
name="flavours"
value={state}
/>
{state}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" disabled={pendingStates.length == 0} on:click={() => {
isOpen = false
states = [...pendingStates]
dispatch('update', { states })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
states = [...allJobStates]
pendingStates = [...allJobStates]
dispatch('update', { states })
}}>Reset</Button>
<Button
color="primary"
disabled={pendingStates.length == 0}
on:click={() => {
isOpen = false;
states = [...pendingStates];
dispatch("update", { states });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
states = [...allJobStates];
pendingStates = [...allJobStates];
dispatch("update", { states });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,145 +1,242 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import Header from '../Header.svelte';
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
import { createEventDispatcher, getContext } from "svelte";
import {
Button,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
const clusters = getContext('clusters'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
const clusters = getContext("clusters"),
initialized = getContext("initialized"),
dispatch = createEventDispatcher();
export let cluster = null
export let isOpen = false
export let numNodes = { from: null, to: null }
export let numHWThreads = { from: null, to: null }
export let numAccelerators = { from: null, to: null }
export let isNodesModified = false
export let isHwthreadsModified = false
export let isAccsModified = false
export let namedNode = null
export let cluster = null;
export let isOpen = false;
export let numNodes = { from: null, to: null };
export let numHWThreads = { from: null, to: null };
export let numAccelerators = { from: null, to: null };
export let isNodesModified = false;
export let isHwthreadsModified = false;
export let isAccsModified = false;
export let namedNode = null;
let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators, pendingNamedNode = namedNode
let pendingNumNodes = numNodes,
pendingNumHWThreads = numHWThreads,
pendingNumAccelerators = numAccelerators,
pendingNamedNode = namedNode;
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
const findMaxNumAccels = (clusters) =>
clusters.reduce(
(max, cluster) =>
Math.max(
max,
cluster.subClusters.reduce(
(max, sc) => Math.max(max, sc.topology.accelerators?.length || 0),
0,
),
),
0,
);
// Limited to Single-Node Thread Count
const findMaxNumHWTreadsPerNode = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, (sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode) || 0), 0)), 0)
const findMaxNumHWTreadsPerNode = (clusters) =>
clusters.reduce(
(max, cluster) =>
Math.max(
max,
cluster.subClusters.reduce(
(max, sc) =>
Math.max(
max,
sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode || 0,
),
0,
),
),
0,
);
// console.log(header)
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
let minNumNodes = 1,
maxNumNodes = 0,
minNumHWThreads = 1,
maxNumHWThreads = 0,
minNumAccelerators = 0,
maxNumAccelerators = 0;
$: {
if ($initialized) {
if (cluster != null) {
const { subClusters } = clusters.find(c => c.name == cluster)
const { filterRanges } = header.clusters.find(c => c.name == cluster)
minNumNodes = filterRanges.numNodes.from
maxNumNodes = filterRanges.numNodes.to
maxNumAccelerators = findMaxNumAccels([{ subClusters }])
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }])
const { subClusters } = clusters.find((c) => c.name == cluster);
const { filterRanges } = header.clusters.find((c) => c.name == cluster);
minNumNodes = filterRanges.numNodes.from;
maxNumNodes = filterRanges.numNodes.to;
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]);
} else if (clusters.length > 0) {
const { filterRanges } = header.clusters[0]
minNumNodes = filterRanges.numNodes.from
maxNumNodes = filterRanges.numNodes.to
maxNumAccelerators = findMaxNumAccels(clusters)
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters)
const { filterRanges } = header.clusters[0];
minNumNodes = filterRanges.numNodes.from;
maxNumNodes = filterRanges.numNodes.to;
maxNumAccelerators = findMaxNumAccels(clusters);
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters);
for (let cluster of header.clusters) {
const { filterRanges } = cluster
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from)
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to)
const { filterRanges } = cluster;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
}
}
}
}
$: {
if (isOpen && $initialized && pendingNumNodes.from == null && pendingNumNodes.to == null) {
pendingNumNodes = { from: 0, to: maxNumNodes }
if (
isOpen &&
$initialized &&
pendingNumNodes.from == null &&
pendingNumNodes.to == null
) {
pendingNumNodes = { from: 0, to: maxNumNodes };
}
}
$: {
if (isOpen && $initialized && ((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) || (isHwthreadsModified == false))) {
pendingNumHWThreads = { from: 0, to: maxNumHWThreads }
if (
isOpen &&
$initialized &&
((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) ||
isHwthreadsModified == false)
) {
pendingNumHWThreads = { from: 0, to: maxNumHWThreads };
}
}
$: if ( maxNumAccelerators != null && maxNumAccelerators > 1 ) {
if (isOpen && $initialized && pendingNumAccelerators.from == null && pendingNumAccelerators.to == null) {
pendingNumAccelerators = { from: 0, to: maxNumAccelerators }
$: if (maxNumAccelerators != null && maxNumAccelerators > 1) {
if (
isOpen &&
$initialized &&
pendingNumAccelerators.from == null &&
pendingNumAccelerators.to == null
) {
pendingNumAccelerators = { from: 0, to: maxNumAccelerators };
}
}
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select number of utilized Resources
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select number of utilized Resources</ModalHeader>
<ModalBody>
<h6>Named Node</h6>
<input type="text" class="form-control" bind:value={pendingNamedNode}>
<input type="text" class="form-control" bind:value={pendingNamedNode} />
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
pendingNumNodes = { from: detail[0], to: detail[1] }
isNodesModified = true
pendingNumNodes = { from: detail[0], to: detail[1] };
isNodesModified = true;
}}
min={minNumNodes} max={maxNumNodes}
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to}
inputFieldFrom={pendingNumNodes.from} inputFieldTo={pendingNumNodes.to}/>
<h6 style="margin-top: 1rem;">Number of HWThreads (Use for Single-Node Jobs)</h6>
min={minNumNodes}
max={maxNumNodes}
firstSlider={pendingNumNodes.from}
secondSlider={pendingNumNodes.to}
inputFieldFrom={pendingNumNodes.from}
inputFieldTo={pendingNumNodes.to}
/>
<h6 style="margin-top: 1rem;">
Number of HWThreads (Use for Single-Node Jobs)
</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
pendingNumHWThreads = { from: detail[0], to: detail[1] }
isHwthreadsModified = true
pendingNumHWThreads = { from: detail[0], to: detail[1] };
isHwthreadsModified = true;
}}
min={minNumHWThreads} max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to}
inputFieldFrom={pendingNumHWThreads.from} inputFieldTo={pendingNumHWThreads.to}/>
min={minNumHWThreads}
max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from}
secondSlider={pendingNumHWThreads.to}
inputFieldFrom={pendingNumHWThreads.from}
inputFieldTo={pendingNumHWThreads.to}
/>
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
pendingNumAccelerators = { from: detail[0], to: detail[1] }
isAccsModified = true
pendingNumAccelerators = { from: detail[0], to: detail[1] };
isAccsModified = true;
}}
min={minNumAccelerators} max={maxNumAccelerators}
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to}
inputFieldFrom={pendingNumAccelerators.from} inputFieldTo={pendingNumAccelerators.to}/>
min={minNumAccelerators}
max={maxNumAccelerators}
firstSlider={pendingNumAccelerators.from}
secondSlider={pendingNumAccelerators.to}
inputFieldFrom={pendingNumAccelerators.from}
inputFieldTo={pendingNumAccelerators.to}
/>
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary"
<Button
color="primary"
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
on:click={() => {
isOpen = false
pendingNumNodes = isNodesModified ? pendingNumNodes : { from: null, to: null }
pendingNumHWThreads = isHwthreadsModified ? pendingNumHWThreads : { from: null, to: null }
pendingNumAccelerators = isAccsModified ? pendingNumAccelerators : { from: null, to: null }
numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
namedNode = pendingNamedNode
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode })
}}>
isOpen = false;
pendingNumNodes = isNodesModified
? pendingNumNodes
: { from: null, to: null };
pendingNumHWThreads = isHwthreadsModified
? pendingNumHWThreads
: { from: null, to: null };
pendingNumAccelerators = isAccsModified
? pendingNumAccelerators
: { from: null, to: null };
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = {
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
namedNode = pendingNamedNode;
dispatch("update", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
});
}}
>
Close & Apply
</Button>
<Button color="danger" on:click={() => {
isOpen = false
pendingNumNodes = { from: null, to: null }
pendingNumHWThreads = { from: null, to: null }
pendingNumAccelerators = { from: null, to: null }
pendingNamedNode = null
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
isNodesModified = false
isHwthreadsModified = false
isAccsModified = false
namedNode = pendingNamedNode
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode})
}}>Reset</Button>
<Button
color="danger"
on:click={() => {
isOpen = false;
pendingNumNodes = { from: null, to: null };
pendingNumHWThreads = { from: null, to: null };
pendingNumAccelerators = { from: null, to: null };
pendingNamedNode = null;
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = {
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
isNodesModified = false;
isHwthreadsModified = false;
isAccsModified = false;
namedNode = pendingNamedNode;
dispatch("update", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
});
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,86 +1,121 @@
<script>
import { createEventDispatcher } from 'svelte'
import { parse, format, sub } from 'date-fns'
import { Row, Button, Input, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap'
import { createEventDispatcher } from "svelte";
import { parse, format, sub } from "date-fns";
import {
Row,
Button,
Input,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
FormGroup,
} from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
export let isModified = false
export let isOpen = false
export let from = null
export let to = null
export let isModified = false;
export let isOpen = false;
export let from = null;
export let to = null;
let pendingFrom, pendingTo
let pendingFrom, pendingTo;
const now = new Date(Date.now())
const ago = sub(now, {months: 1})
const defaultFrom = {date: format(ago, 'yyyy-MM-dd'), time: format(ago, 'HH:mm')}
const defaultTo = {date: format(now, 'yyyy-MM-dd'), time: format(now, 'HH:mm')}
const now = new Date(Date.now());
const ago = sub(now, { months: 1 });
const defaultFrom = {
date: format(ago, "yyyy-MM-dd"),
time: format(ago, "HH:mm"),
};
const defaultTo = {
date: format(now, "yyyy-MM-dd"),
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);
}
reset()
reset();
function toRFC3339({ date, time }, secs = '00') {
const parsedDate = parse(date+' '+time+':'+secs, 'yyyy-MM-dd HH:mm:ss', new Date())
return parsedDate.toISOString()
function toRFC3339({ date, time }, secs = "00") {
const parsedDate = parse(
date + " " + time + ":" + secs,
"yyyy-MM-dd HH:mm:ss",
new Date(),
);
return parsedDate.toISOString();
}
function fromRFC3339(rfc3339) {
const parsedDate = new Date(rfc3339)
return { date: format(parsedDate, 'yyyy-MM-dd'), time: format(parsedDate, 'HH:mm') }
const parsedDate = new Date(rfc3339);
return {
date: format(parsedDate, "yyyy-MM-dd"),
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')
$: 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={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Start Time
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Start Time</ModalHeader>
<ModalBody>
<h4>From</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date}/>
<Input type="date" bind:value={pendingFrom.date} />
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time}/>
<Input type="time" bind:value={pendingFrom.time} />
</FormGroup>
</Row>
<h4>To</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingTo.date}/>
<Input type="date" bind:value={pendingTo.date} />
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingTo.time}/>
<Input type="time" bind:value={pendingTo.time} />
</FormGroup>
</Row>
</ModalBody>
<ModalFooter>
<Button color="primary"
disabled={pendingFrom.date == '0000-00-00' || pendingTo.date == '0000-00-00'}
<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('update', { from, to })
}}>
isOpen = false;
from = toRFC3339(pendingFrom);
to = toRFC3339(pendingTo, "59");
dispatch("update", { from, to });
}}
>
Close & Apply
</Button>
<Button color="danger" on:click={() => {
isOpen = false
from = null
to = null
reset()
dispatch('update', { from, to })
}}>Reset</Button>
<Button
color="danger"
on:click={() => {
isOpen = false;
from = null;
to = null;
reset();
dispatch("update", { from, to });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,115 +1,137 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
import { createEventDispatcher, getContext } from "svelte";
import {
Button,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
const clusters = getContext('clusters'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
const clusters = getContext("clusters"),
initialized = getContext("initialized"),
dispatch = createEventDispatcher();
export let cluster = null
export let isModified = false
export let isOpen = false
export let stats = []
export let cluster = null;
export let isModified = false;
export let isOpen = false;
export let stats = [];
let statistics = [
{
field: 'flopsAnyAvg',
text: 'FLOPs (Avg.)',
metric: 'flops_any',
from: 0, to: 0, peak: 0,
enabled: false
field: "flopsAnyAvg",
text: "FLOPs (Avg.)",
metric: "flops_any",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
{
field: 'memBwAvg',
text: 'Mem. Bw. (Avg.)',
metric: 'mem_bw',
from: 0, to: 0, peak: 0,
enabled: false
field: "memBwAvg",
text: "Mem. Bw. (Avg.)",
metric: "mem_bw",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
{
field: 'loadAvg',
text: 'Load (Avg.)',
metric: 'cpu_load',
from: 0, to: 0, peak: 0,
enabled: false
field: "loadAvg",
text: "Load (Avg.)",
metric: "cpu_load",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
{
field: 'memUsedMax',
text: 'Mem. used (Max.)',
metric: 'mem_used',
from: 0, to: 0, peak: 0,
enabled: false
}
]
field: "memUsedMax",
text: "Mem. used (Max.)",
metric: "mem_used",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
];
$: isModified = !statistics.every(a => {
let b = stats.find(s => s.field == a.field)
if (b == null)
return !a.enabled
$: isModified = !statistics.every((a) => {
let b = stats.find((s) => s.field == a.field);
if (b == null) return !a.enabled;
return a.from == b.from && a.to == b.to
})
return a.from == b.from && a.to == b.to;
});
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find(mc => mc.name == metric)
return mc ? mc.peak : 0
const mc = cluster.metricConfig.find((mc) => mc.name == metric);
return mc ? mc.peak : 0;
}
function resetRange(isInitialized, cluster) {
if (!isInitialized)
return
if (!isInitialized) return;
if (cluster != null) {
let c = clusters.find(c => c.name == cluster)
let c = clusters.find((c) => c.name == cluster);
for (let stat of statistics) {
stat.peak = getPeak(c, stat.metric)
stat.from = 0
stat.to = stat.peak
stat.peak = getPeak(c, stat.metric);
stat.from = 0;
stat.to = stat.peak;
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric))
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric));
}
stat.from = 0
stat.to = stat.peak
stat.from = 0;
stat.to = stat.peak;
}
}
statistics = [...statistics]
statistics = [...statistics];
}
$: resetRange($initialized, cluster)
$: resetRange($initialized, cluster);
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Filter based on statistics (of non-running jobs)
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on statistics (of non-running jobs)</ModalHeader>
<ModalBody>
{#each statistics as stat}
<h4>{stat.text}</h4>
<DoubleRangeSlider
on:change={({ detail }) => (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)}
min={0} max={stat.peak}
firstSlider={stat.from} secondSlider={stat.to}
inputFieldFrom={stat.from} inputFieldTo={stat.to}/>
on:change={({ detail }) => (
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
)}
min={0}
max={stat.peak}
firstSlider={stat.from}
secondSlider={stat.to}
inputFieldFrom={stat.from}
inputFieldTo={stat.to}
/>
{/each}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => {
isOpen = false
stats = statistics.filter(stat => stat.enabled)
dispatch('update', { stats })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
resetRange($initialized, cluster)
statistics.forEach(stat => (stat.enabled = false))
stats = []
dispatch('update', { stats })
}}>Reset</Button>
<Button
color="primary"
on:click={() => {
isOpen = false;
stats = statistics.filter((stat) => stat.enabled);
dispatch("update", { stats });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
resetRange($initialized, cluster);
statistics.forEach((stat) => (stat.enabled = false));
stats = [];
dispatch("update", { stats });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,48 +1,64 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, ListGroup, ListGroupItem, Input,
Modal, ModalBody, ModalHeader, ModalFooter, Icon } from 'sveltestrap'
import { fuzzySearchTags } from '../utils.js'
import Tag from '../Tag.svelte'
import { createEventDispatcher, getContext } from "svelte";
import {
Button,
ListGroup,
ListGroupItem,
Input,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
Icon,
} from "@sveltestrap/sveltestrap";
import { fuzzySearchTags } from "../utils.js";
import Tag from "../Tag.svelte";
const allTags = getContext('tags'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
const allTags = getContext("tags"),
initialized = getContext("initialized"),
dispatch = createEventDispatcher();
export let isModified = false
export let isOpen = false
export let tags = []
export let isModified = false;
export let isOpen = false;
export let tags = [];
let pendingTags = [...tags]
$: isModified = tags.length != pendingTags.length || !tags.every(tagId => pendingTags.includes(tagId))
let pendingTags = [...tags];
$: isModified =
tags.length != pendingTags.length ||
!tags.every((tagId) => pendingTags.includes(tagId));
let searchTerm = ''
let searchTerm = "";
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Tags
</ModalHeader>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Tags</ModalHeader>
<ModalBody>
<Input type="text" placeholder="Search" bind:value={searchTerm} />
<br/>
<br />
<ListGroup>
{#if $initialized}
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
<ListGroupItem>
{#if pendingTags.includes(tag.id)}
<Button outline color="danger"
on:click={() => (pendingTags = pendingTags.filter(id => id != tag.id))}>
<Button
outline
color="danger"
on:click={() =>
(pendingTags = pendingTags.filter((id) => id != tag.id))}
>
<Icon name="dash-circle" />
</Button>
{:else}
<Button outline color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])}>
<Button
outline
color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])}
>
<Icon name="plus-circle" />
</Button>
{/if}
<Tag tag={tag} />
<Tag {tag} />
</ListGroupItem>
{:else}
<ListGroupItem disabled>No Tags</ListGroupItem>
@ -51,17 +67,23 @@
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => {
isOpen = false
tags = [...pendingTags]
dispatch('update', { tags })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
tags = []
pendingTags = []
dispatch('update', { tags })
}}>Reset</Button>
<Button
color="primary"
on:click={() => {
isOpen = false;
tags = [...pendingTags];
dispatch("update", { tags });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
tags = [];
pendingTags = [];
dispatch("update", { tags });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,67 +1,76 @@
<script>
import { Icon, Input, InputGroup, InputGroupText } from 'sveltestrap'
import { createEventDispatcher } from "svelte"
import {
Icon,
Input,
InputGroup,
InputGroupText,
} from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
export let from
export let to
export let customEnabled = true
export let anyEnabled = false
export let from;
export let to;
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,
'Last 4hrs': 4*60*60,
'Last 12hrs': 12*60*60,
'Last 24hrs': 24*60*60
}
"Last quarter hour": 15 * 60,
"Last half hour": 30 * 60,
"Last hour": 60 * 60,
"Last 2hrs": 2 * 60 * 60,
"Last 4hrs": 4 * 60 * 60,
"Last 12hrs": 12 * 60 * 60,
"Last 24hrs": 24 * 60 * 60,
};
$: pendingFrom = from
$: pendingTo = to
$: pendingFrom = from;
$: pendingTo = to;
const dispatch = createEventDispatcher()
let timeRange = to && from
? (to.getTime() - from.getTime()) / 1000
: (anyEnabled ? -2 : -1)
const dispatch = createEventDispatcher();
let timeRange =
to && from ? (to.getTime() - from.getTime()) / 1000 : anyEnabled ? -2 : -1;
function updateTimeRange(event) {
if (timeRange == -1) {
pendingFrom = null
pendingTo = null
return
pendingFrom = null;
pendingTo = null;
return;
}
if (timeRange == -2) {
from = pendingFrom = null
to = pendingTo = null
dispatch('change', { from, to })
return
from = pendingFrom = null;
to = pendingTo = null;
dispatch("change", { from, to });
return;
}
let now = Date.now(), t = timeRange * 1000
from = pendingFrom = new Date(now - t)
to = pendingTo = new Date(now)
dispatch('change', { from, to })
let now = Date.now(),
t = timeRange * 1000;
from = pendingFrom = new Date(now - t);
to = pendingTo = new Date(now);
dispatch("change", { from, to });
}
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == 'from') pendingFrom = d
else pendingTo = d
if (type == "from") pendingFrom = d;
else pendingTo = d;
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom
to = pendingTo
dispatch('change', { from, to })
from = pendingFrom;
to = pendingTo;
dispatch("change", { from, to });
}
}
</script>
<InputGroup class="inline-from">
<InputGroupText><Icon name="clock-history"/></InputGroupText>
<InputGroupText><Icon name="clock-history" /></InputGroupText>
<!-- <InputGroupText>
Time
</InputGroupText> -->
<select class="form-select" bind:value={timeRange} on:change={updateTimeRange}>
<select
class="form-select"
bind:value={timeRange}
on:change={updateTimeRange}
>
{#if customEnabled}
<option value={-1}>Custom</option>
{/if}
@ -74,8 +83,14 @@
</select>
{#if timeRange == -1}
<InputGroupText>from</InputGroupText>
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('from', event)}></Input>
<Input
type="datetime-local"
on:change={(event) => updateExplicitTimeRange("from", event)}
></Input>
<InputGroupText>to</InputGroupText>
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('to', event)}></Input>
<Input
type="datetime-local"
on:change={(event) => updateExplicitTimeRange("to", event)}
></Input>
{/if}
</InputGroup>

View File

@ -1,75 +1,84 @@
<script>
import { InputGroup, Input } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import { InputGroup, Input } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
export let user = ''
export let project = ''
export let authlevel
export let roles
let mode = 'user', term = ''
const throttle = 500
export let user = "";
export let project = "";
export let authlevel;
export let roles;
let mode = "user",
term = "";
const throttle = 500;
function modeChanged() {
if (mode == 'user') {
project = term
term = user
if (mode == "user") {
project = term;
term = user;
} else {
user = term
term = project
user = term;
term = project;
}
termChanged(0)
termChanged(0);
}
let timeoutId = null
let timeoutId = null;
// Compatibility: Handle "user role" and "no role" identically
function termChanged(sleep = throttle) {
if (authlevel >= roles.manager) {
if (mode == 'user')
user = term
else
project = term
if (mode == "user") user = term;
else project = term;
if (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
dispatch('update', {
dispatch("update", {
user,
project
})
}, sleep)
project,
});
}, sleep);
} else {
project = term
if (timeoutId != null)
clearTimeout(timeoutId)
project = term;
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
dispatch('update', {
project
})
}, sleep)
dispatch("update", {
project,
});
}, sleep);
}
}
</script>
{#if authlevel >= roles.manager}
<InputGroup>
<select style="max-width: 175px;" class="form-select"
bind:value={mode} on:change={modeChanged}>
<option value={'user'}>Search User</option>
<option value={'project'}>Search Project</option>
<select
style="max-width: 175px;"
class="form-select"
bind:value={mode}
on:change={modeChanged}
>
<option value={"user"}>Search User</option>
<option value={"project"}>Search Project</option>
</select>
<Input
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
type="text"
bind:value={term}
on:change={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder={mode == "user" ? "filter username..." : "filter project..."}
/>
</InputGroup>
{:else}
<!-- Compatibility: Handle "user role" and "no role" identically-->
<InputGroup>
<Input
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...'
type="text"
bind:value={term}
on:change={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder="filter project..."
/>
</InputGroup>
{/if}

View File

@ -6,15 +6,20 @@
- jobTags: Defaults to job.tags, usefull for dynamically updating the tags.
-->
<script context="module">
export const scrambleNames = window.localStorage.getItem("cc-scramble-names")
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)
}
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
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';
import { Badge, Icon } from 'sveltestrap';
import Tag from "../Tag.svelte";
import { Badge, Icon } from "@sveltestrap/sveltestrap";
export let job;
export let jobTags = job.tags;
@ -25,50 +30,65 @@
const minutes = Math.floor(duration / 60);
duration -= minutes * 60;
const seconds = duration;
return `${hours}:${('0' + minutes).slice(-2)}:${('0' + seconds).slice(-2)}`;
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`;
}
function getStateColor(state) {
switch (state) {
case 'running':
return 'success'
case 'completed':
return 'primary'
case "running":
return "success";
case "completed":
return "primary";
default:
return 'danger'
return "danger";
}
}
</script>
<div>
<p>
<span class="fw-bold"><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> ({job.cluster})</span>
<span class="fw-bold"
><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
({job.cluster})</span
>
{#if job.metaData?.jobName}
<br/>
<br />
{#if job.metaData?.jobName.length <= 25}
<div>{job.metaData.jobName}</div>
{:else}
<div class="truncate" style="cursor:help; width:230px;" title={job.metaData.jobName}>{job.metaData.jobName}</div>
<div
class="truncate"
style="cursor:help; width:230px;"
title={job.metaData.jobName}
>
{job.metaData.jobName}
</div>
{/if}
{/if}
{#if job.arrayJobId}
Array Job: <a href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" target="_blank">#{job.arrayJobId}</a>
Array Job: <a
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
target="_blank">#{job.arrayJobId}</a
>
{/if}
</p>
<p>
<Icon name="person-fill"/>
<Icon name="person-fill" />
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
{scrambleNames ? scramble(job.user) : job.user}
</a>
{#if job.userData && job.userData.name}
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
{/if}
{#if job.project && job.project != 'no project'}
<br/>
<Icon name="people-fill"/>
<a class="fst-italic" href="/monitoring/jobs/?project={job.project}&projectMatch=eq" target="_blank">
{#if job.project && job.project != "no project"}
<br />
<Icon name="people-fill" />
<a
class="fst-italic"
href="/monitoring/jobs/?project={job.project}&projectMatch=eq"
target="_blank"
>
{scrambleNames ? scramble(job.project) : job.project}
</a>
{/if}
@ -80,33 +100,36 @@
{:else}
{job.numNodes}
{/if}
<Icon name="pc-horizontal"/>
<Icon name="pc-horizontal" />
{#if job.exclusive != 1}
(shared)
{/if}
{#if job.numAcc > 0}
, {job.numAcc} <Icon name="gpu-card"/>
, {job.numAcc} <Icon name="gpu-card" />
{/if}
{#if job.numHWThreads > 0}
, {job.numHWThreads} <Icon name="cpu"/>
, {job.numHWThreads} <Icon name="cpu" />
{/if}
<br/>
<br />
{job.subCluster}
</p>
<p>
Start: <span class="fw-bold">{(new Date(job.startTime)).toLocaleString()}</span>
<br/>
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span> <Badge color="{getStateColor(job.state)}">{job.state}</Badge>
Start: <span class="fw-bold"
>{new Date(job.startTime).toLocaleString()}</span
>
<br />
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span>
<Badge color={getStateColor(job.state)}>{job.state}</Badge>
{#if job.walltime}
<br/>
<br />
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
{/if}
</p>
<p>
{#each jobTags as tag}
<Tag tag={tag}/>
<Tag {tag} />
{/each}
</p>
</div>

View File

@ -16,7 +16,7 @@
mutationStore,
} from "@urql/svelte";
import { getContext } from "svelte";
import { Row, Table, Card, Spinner } from "sveltestrap";
import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap";
import Pagination from "./Pagination.svelte";
import JobListRow from "./Row.svelte";
import { stickyHeader } from "../utils.js";
@ -86,7 +86,7 @@
$: jobs = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter }
variables: { paging, sorting, filter },
});
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
@ -97,7 +97,7 @@
client: client,
query: query,
variables: { paging, sorting, filter },
requestPolicy: 'network-only'
requestPolicy: "network-only",
});
}
@ -122,21 +122,23 @@
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
variables: { name, value },
});
}
};
function updateConfiguration(value, page) {
updateConfigurationMutation({ name: 'plot_list_jobsPerPage', value: value })
.subscribe(res => {
updateConfigurationMutation({
name: "plot_list_jobsPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) {
throw res.error
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
})
};
});
}
let plotWidth = null;
let tableWidth = null;
@ -144,18 +146,18 @@
$: if (showFootprint) {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10
)
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
);
} else {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / metrics.length - 10
)
(tableWidth - jobInfoColumnWidth) / metrics.length - 10,
);
}
let headerPaddingTop = 0;
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x)
(x) => (headerPaddingTop = x),
);
</script>
@ -190,24 +192,18 @@
{#if $initialized}
({clusters
.map((cluster) =>
cluster.metricConfig.find(
(m) => m.name == metric
)
cluster.metricConfig.find((m) => m.name == metric),
)
.filter((m) => m != null)
.map(
(m) =>
(m.unit?.prefix
? m.unit?.prefix
: "") +
(m.unit?.base ? m.unit?.base : "")
(m.unit?.prefix ? m.unit?.prefix : "") +
(m.unit?.base ? m.unit?.base : ""),
) // Build unitStr
.reduce(
(arr, unitStr) =>
arr.includes(unitStr)
? arr
: [...arr, unitStr],
[]
arr.includes(unitStr) ? arr : [...arr, unitStr],
[],
) // w/o this, output would be [unitStr, unitStr]
.join(", ")})
{/if}
@ -232,12 +228,10 @@
</tr>
{:else if $jobs.data && $initialized}
{#each $jobs.data.jobs.items as job (job)}
<JobListRow {job} {metrics} {plotWidth} {showFootprint}/>
<JobListRow {job} {metrics} {plotWidth} {showFootprint} />
{:else}
<tr>
<td colspan={metrics.length + 1}>
No jobs found
</td>
<td colspan={metrics.length + 1}> No jobs found </td>
</tr>
{/each}
{/if}
@ -253,12 +247,9 @@
totalItems={matchedJobs}
on:update={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(
detail.itemsPerPage.toString(),
detail.page
)
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else {
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
}
}}
/>

View File

@ -5,39 +5,46 @@
- 'reload': When fired, the parent component shoud refresh its contents
-->
<script>
import { createEventDispatcher } from 'svelte'
import { Button, Icon, InputGroup } from 'sveltestrap'
import { createEventDispatcher } from "svelte";
import { Button, Icon, InputGroup } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
let refreshInterval = null;
let refreshIntervalId = null;
function refreshIntervalChanged() {
if (refreshIntervalId != null)
clearInterval(refreshIntervalId);
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
if (refreshInterval == null)
return;
if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
}
export let initially = null
export let initially = null;
if (initially != null) {
refreshInterval = initially * 1000
refreshIntervalChanged()
refreshInterval = initially * 1000;
refreshIntervalChanged();
}
</script>
<InputGroup>
<Button outline on:click={() => dispatch("reload")} disabled={refreshInterval != null}>
<Button
outline
on:click={() => dispatch("reload")}
disabled={refreshInterval != null}
>
<Icon name="arrow-clockwise" /> Reload
</Button>
<select class="form-select" bind:value={refreshInterval} on:change={refreshIntervalChanged}>
<select
class="form-select"
bind:value={refreshInterval}
on:change={refreshIntervalChanged}
>
<option value={null}>No periodic reload</option>
<option value={ 30 * 1000}>Update every 30 seconds</option>
<option value={ 60 * 1000}>Update every minute</option>
<option value={30 * 1000}>Update every 30 seconds</option>
<option value={60 * 1000}>Update every minute</option>
<option value={2 * 60 * 1000}>Update every two minutes</option>
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
</select>
</InputGroup>

View File

@ -11,7 +11,7 @@
<script>
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte";
import { Card, Spinner } from "sveltestrap";
import { Card, Spinner } from "@sveltestrap/sveltestrap";
import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from "./JobInfo.svelte";
import JobFootprint from "../JobFootprint.svelte";
@ -67,16 +67,23 @@
$: metricsQuery = queryStore({
client: client,
query: query,
variables: { id, queryMetrics, scopes }
variables: { id, queryMetrics, scopes },
});
let queryMetrics = null
let queryMetrics = null;
$: if (showFootprint) {
queryMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw', 'acc_utilization', ...metrics].filter(distinct)
scopes = ["node"]
queryMetrics = [
"cpu_load",
"flops_any",
"mem_used",
"mem_bw",
"acc_utilization",
...metrics,
].filter(distinct);
scopes = ["node"];
} else {
queryMetrics = [...metrics]
scopes = [job.numNodes == 1 ? "core" : "node"]
queryMetrics = [...metrics];
scopes = [job.numNodes == 1 ? "core" : "node"];
}
export function refresh() {
@ -98,20 +105,30 @@
: job.numNodes > 1
? b
: a,
jobMetrics[0]
jobMetrics[0],
);
const sortAndSelectScope = (jobMetrics) => metrics
.map(name => jobMetrics.filter(jobMetric => jobMetric.name == name))
.map(jobMetrics => ({ disabled: false, data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null }))
.map(jobMetric => {
const sortAndSelectScope = (jobMetrics) =>
metrics
.map((name) => jobMetrics.filter((jobMetric) => jobMetric.name == name))
.map((jobMetrics) => ({
disabled: false,
data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null,
}))
.map((jobMetric) => {
if (jobMetric.data) {
return { disabled: checkMetricDisabled(jobMetric.data.name, job.cluster, job.subCluster), data: jobMetric.data }
return {
disabled: checkMetricDisabled(
jobMetric.data.name,
job.cluster,
job.subCluster,
),
data: jobMetric.data,
};
} else {
return jobMetric
return jobMetric;
}
})
});
if (job.monitoringStatus) refresh();
</script>
@ -140,7 +157,7 @@
{#if showFootprint}
<td>
<JobFootprint
job={job}
{job}
jobMetrics={$metricsQuery.data.jobMetrics}
width={plotWidth}
view="list"
@ -161,13 +178,17 @@
metric={metric.data.name}
{cluster}
subCluster={job.subCluster}
isShared={(job.exclusive != 1)}
isShared={job.exclusive != 1}
resources={job.resources}
numhwthreads={job.numHWThreads}
numaccs={job.numAcc}
/>
{:else if metric.disabled == true && metric.data}
<Card body color="info">Metric disabled for subcluster <code>{metric.data.name}:{job.subCluster}</code></Card>
<Card body color="info"
>Metric disabled for subcluster <code
>{metric.data.name}:{job.subCluster}</code
></Card
>
{:else}
<Card body color="warning">No dataset returned</Card>
{/if}

View File

@ -7,47 +7,70 @@
-->
<script>
import { Icon, Button, ListGroup, ListGroupItem,
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import {
Icon,
Button,
ListGroup,
ListGroupItem,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
export let isOpen = false
export let sorting = { field: 'startTime', order: 'DESC' }
export let isOpen = false;
export let sorting = { field: "startTime", order: "DESC" };
let sortableColumns = [
{ field: 'startTime', text: 'Start Time', order: 'DESC' },
{ field: 'duration', text: 'Duration', order: 'DESC' },
{ field: 'numNodes', text: 'Number of Nodes', order: 'DESC' },
{ field: 'memUsedMax', text: 'Max. Memory Used', order: 'DESC' },
{ field: 'flopsAnyAvg', text: 'Avg. FLOPs', order: 'DESC' },
{ field: 'memBwAvg', text: 'Avg. Memory Bandwidth', order: 'DESC' },
{ field: 'netBwAvg', text: 'Avg. Network Bandwidth', order: 'DESC' }
]
{ field: "startTime", text: "Start Time", order: "DESC" },
{ field: "duration", text: "Duration", order: "DESC" },
{ field: "numNodes", text: "Number of Nodes", order: "DESC" },
{ field: "memUsedMax", text: "Max. Memory Used", order: "DESC" },
{ field: "flopsAnyAvg", text: "Avg. FLOPs", order: "DESC" },
{ field: "memBwAvg", text: "Avg. Memory Bandwidth", order: "DESC" },
{ field: "netBwAvg", text: "Avg. Network Bandwidth", order: "DESC" },
];
let activeColumnIdx = sortableColumns.findIndex(col => col.field == sorting.field)
sortableColumns[activeColumnIdx].order = sorting.order
let activeColumnIdx = sortableColumns.findIndex(
(col) => col.field == sorting.field,
);
sortableColumns[activeColumnIdx].order = sorting.order;
</script>
<Modal isOpen={isOpen} toggle={() => { isOpen = !isOpen }}>
<ModalHeader>
Sort rows
</ModalHeader>
<Modal
{isOpen}
toggle={() => {
isOpen = !isOpen;
}}
>
<ModalHeader>Sort rows</ModalHeader>
<ModalBody>
<ListGroup>
{#each sortableColumns as col, i (col)}
<ListGroupItem>
<button class="sort" on:click={() => {
<button
class="sort"
on:click={() => {
if (activeColumnIdx == i) {
col.order = col.order == 'DESC' ? 'ASC' : 'DESC'
col.order = col.order == "DESC" ? "ASC" : "DESC";
} else {
sortableColumns[activeColumnIdx] = { ...sortableColumns[activeColumnIdx] }
sortableColumns[activeColumnIdx] = {
...sortableColumns[activeColumnIdx],
};
}
sortableColumns[i] = { ...sortableColumns[i] }
activeColumnIdx = i
sortableColumns = [...sortableColumns]
sorting = { field: col.field, order: col.order }
}}>
<Icon name="arrow-{col.order == 'DESC' ? 'down' : 'up'}-circle{i == activeColumnIdx ? '-fill' : ''}"/>
sortableColumns[i] = { ...sortableColumns[i] };
activeColumnIdx = i;
sortableColumns = [...sortableColumns];
sorting = { field: col.field, order: col.order };
}}
>
<Icon
name="arrow-{col.order == 'DESC' ? 'down' : 'up'}-circle{i ==
activeColumnIdx
? '-fill'
: ''}"
/>
</button>
{col.text}
@ -56,7 +79,12 @@
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => { isOpen = false }}>Close</Button>
<Button
color="primary"
on:click={() => {
isOpen = false;
}}>Close</Button
>
</ModalFooter>
</Modal>
@ -69,3 +97,4 @@
transition: all 70ms;
}
</style>

View File

@ -5,22 +5,22 @@
-->
<script>
import uPlot from 'uplot'
import { formatNumber } from '../units.js'
import { onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap'
import uPlot from "uplot";
import { formatNumber } from "../units.js";
import { onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap";
export let data
export let usesBins = false
export let width = 500
export let height = 300
export let title = ''
export let xlabel = ''
export let xunit = 'X'
export let ylabel = ''
export let yunit = 'Y'
export let data;
export let usesBins = false;
export let width = 500;
export let height = 300;
export let title = "";
export let xlabel = "";
export let xunit = "X";
export let ylabel = "";
export let yunit = "Y";
const { bars } = uPlot.paths
const { bars } = uPlot.paths;
const drawStyles = {
bars: 1,
@ -31,18 +31,17 @@
let s = u.series[seriesIdx];
let style = s.drawStyle;
let renderer = ( // If bars to wide, change here
style == drawStyles.bars ? (
bars({size: [0.75, 100]})
) :
() => null
)
let renderer = // If bars to wide, change here
style == drawStyles.bars ? bars({ size: [0.75, 100] }) : () => null;
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" } } = {}) {
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
@ -60,14 +59,13 @@
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style
...style,
});
// hide series color markers
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
for (let i = 0; i < idents.length; i++) idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
@ -76,8 +74,12 @@
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
// let tooltip exit plot
// overEl.style.overflow = "visible";
@ -85,40 +87,40 @@
function update(u) {
const { left, top } = u.cursor;
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
legendEl.style.transform =
"translate(" + (left + 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
}
},
};
}
let plotWrapper = null
let uplot = null
let timeoutId = null
let plotWrapper = null;
let uplot = null;
let timeoutId = null;
function render() {
let opts = {
width: width,
height: height,
title: title,
plugins: [
legendAsTooltipPlugin()
],
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',
stroke: (u, seriesIdx) =>
u.series[seriesIdx].points.stroke(u, seriesIdx) + "90",
fill: (u, seriesIdx) => "#fff",
}
},
},
scales: {
x: {
time: false
time: false,
},
},
axes: [
@ -138,7 +140,7 @@
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map(v => formatNumber(v)),
values: (_, t) => t.map((v) => formatNumber(v)),
},
{
stroke: "#000000",
@ -155,71 +157,70 @@
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map(v => formatNumber(v)),
values: (_, t) => t.map((v) => formatNumber(v)),
},
],
series: [
{
label: xunit !== '' ? xunit : null,
label: xunit !== "" ? xunit : null,
value: (u, ts, sidx, didx) => {
if (usesBins) {
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0
const max = u.data[sidx][didx]
ts = min + '-' + max // narrow spaces
}
return ts
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0;
const max = u.data[sidx][didx];
ts = min + "-" + max; // narrow spaces
}
return ts;
},
Object.assign({
label: yunit !== '' ? yunit : 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
}),
]
},
),
],
};
uplot = new uPlot(opts, data, plotWrapper)
uplot = new uPlot(opts, data, plotWrapper);
}
onMount(() => {
render()
})
render();
});
onDestroy(() => {
if (uplot)
uplot.destroy()
if (uplot) uplot.destroy();
if (timeoutId != null)
clearTimeout(timeoutId)
})
if (timeoutId != null) clearTimeout(timeoutId);
});
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null
if (uplot)
uplot.destroy()
timeoutId = null;
if (uplot) uplot.destroy();
render()
}, 200)
render();
}, 200);
}
$: sizeChanged(width, height)
$: sizeChanged(width, height);
</script>
{#if data.length > 0}
<div bind:this={plotWrapper}/>
<div bind:this={plotWrapper} />
{:else}
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
<Card class="mx-4" body color="warning"
>Cannot render histogram: No data!</Card
>
{/if}

View File

@ -1,3 +1,121 @@
<script context="module">
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 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;
}
}
export function findThresholds(
metricConfig,
scope,
subCluster,
isShared,
hwthreads,
) {
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
if (!metricConfig || !scope || !subCluster) {
console.warn("Argument missing for findThresholds!");
return null;
}
if (
(scope == "node" && isShared == false) ||
metricConfig.aggregation == "avg"
) {
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
// console.log('subClusterConfigs array empty, use metricConfig defaults')
return {
normal: metricConfig.normal,
caution: metricConfig.caution,
alert: metricConfig.alert,
peak: metricConfig.peak,
};
} else if (
metricConfig.subClusters &&
metricConfig.subClusters.length > 0
) {
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
let forSubCluster = metricConfig.subClusters.find(
(sc) => sc.name == subCluster.name,
);
if (
forSubCluster &&
forSubCluster.normal &&
forSubCluster.caution &&
forSubCluster.alert &&
forSubCluster.peak
)
return forSubCluster;
else
return {
normal: metricConfig.normal,
caution: metricConfig.caution,
alert: metricConfig.alert,
peak: metricConfig.peak,
};
} else {
console.warn("metricConfig.subClusters not found!");
return null;
}
}
if (metricConfig.aggregation != "sum") {
console.warn(
"Missing or unkown aggregation mode (sum/avg) for metric:",
metricConfig,
);
return null;
}
let divisor = 1
if (isShared == true) { // Shared
if (numaccs > 0) divisor = subCluster.topology.accelerators.length / numaccs;
else if (numhwthreads > 0) divisor = subCluster.topology.node.length / numhwthreads;
}
else if (scope == 'socket') divisor = subCluster.topology.socket.length;
else if (scope == "core") divisor = subCluster.topology.core.length;
else if (scope == "accelerator")
divisor = subCluster.topology.accelerators.length;
else if (scope == "hwthread") divisor = subCluster.topology.node.length;
else {
// console.log('TODO: how to calc thresholds for ', scope)
return null;
}
let mc =
metricConfig?.subClusters?.find((sc) => sc.name == subCluster.name) ||
metricConfig;
return {
peak: mc.peak / divisor,
normal: mc.normal / divisor,
caution: mc.caution / divisor,
alert: mc.alert / divisor,
};
}
</script>
<!--
@component
@ -21,46 +139,59 @@
// TODO: Move helper functions to module context?
-->
<script>
import uPlot from 'uplot'
import { formatNumber } from '../units.js'
import { getContext, onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap'
import uPlot from "uplot";
import { formatNumber } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/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 isShared = false
export let forNode = false
export let numhwthreads = 0
export let numaccs = 0
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 isShared = false;
export let forNode = false;
export let hwthreads = 0;
if (useStatsSeries == null)
useStatsSeries = statisticsSeries != null
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
if (useStatsSeries == false && series == null)
useStatsSeries = true
if (useStatsSeries == false && series == null) useStatsSeries = true;
const metricConfig = getContext('metrics')(cluster, metric)
const clusterCockpitConfig = getContext('cc-config')
const resizeSleepTime = 250
const normalLineColor = '#000000'
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio
const lineColors = clusterCockpitConfig.plot_general_colorscheme
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, isShared, numhwthreads, numaccs)
const metricConfig = getContext("metrics")(cluster, metric);
const clusterCockpitConfig = getContext("cc-config");
const resizeSleepTime = 250;
const normalLineColor = "#000000";
const lineWidth =
clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
const lineColors = clusterCockpitConfig.plot_general_colorscheme;
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,
isShared,
hwthreads,
);
// converts the legend into a simple tooltip
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
const dataSize = series.length
const dataSize = series.length;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
@ -77,13 +208,16 @@
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style
...style,
});
// conditional hide series color markers:
if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory
if (
useStatsSeries === true || // Min/Max/Avg Self-Explanatory
dataSize === 1 || // Only one Y-Dataseries
dataSize > 6 ){ // More than 6 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";
@ -96,8 +230,12 @@
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
// let tooltip exit plot
// overEl.style.overflow = "visible";
@ -106,7 +244,8 @@
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)";
legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
if (dataSize <= 12 || useStatsSeries === true) {
@ -114,336 +253,289 @@
hooks: {
init: init,
setCursor: update,
}
}
} else { // Setting legend-opts show/live as object with false here will not work ...
return {}
},
};
} else {
// Setting legend-opts show/live as object with false here will not work ...
return {};
}
}
function backgroundColor() {
if (clusterCockpitConfig.plot_general_colorBackground == false
|| !thresholds
|| !(series && series.every(s => s.statistics != null)))
return backgroundColors.normal
if (
clusterCockpitConfig.plot_general_colorBackground == false ||
!thresholds ||
!(series && series.every((s) => s.statistics != null))
)
return backgroundColors.normal;
let cond = thresholds.alert < thresholds.caution
let cond =
thresholds.alert < thresholds.caution
? (a, b) => a <= b
: (a, b) => a >= b
: (a, b) => a >= b;
let avg = series.reduce((sum, series) => sum + series.statistics.avg, 0) / series.length
let avg =
series.reduce((sum, series) => sum + series.statistics.avg, 0) /
series.length;
if (Number.isNaN(avg))
return backgroundColors.normal
if (Number.isNaN(avg)) return backgroundColors.normal;
if (cond(avg, thresholds.alert))
return backgroundColors.alert
if (cond(avg, thresholds.alert)) return backgroundColors.alert;
if (cond(avg, thresholds.caution))
return backgroundColors.caution
if (cond(avg, thresholds.caution)) return backgroundColors.caution;
return backgroundColors.normal
return backgroundColors.normal;
}
function lineColor(i, n) {
if (n >= lineColors.length)
return lineColors[i % lineColors.length];
else
return lineColors[Math.floor((i / n) * lineColors.length)];
if (n >= lineColors.length) return lineColors[i % lineColors.length];
else return lineColors[Math.floor((i / n) * lineColors.length)];
}
const longestSeries = useStatsSeries
? statisticsSeries.mean.length
: series.reduce((n, series) => Math.max(n, series.data.length), 0)
const maxX = longestSeries * timestep
let maxY = null
: series.reduce((n, series) => Math.max(n, series.data.length), 0);
const maxX = longestSeries * timestep;
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)
? 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;
if (maxY >= (10 * thresholds.peak)) { // Hard y-range render limit if outliers in series data
maxY = (10 * thresholds.peak)
if (maxY >= 10 * thresholds.peak) {
// Hard y-range render limit if outliers in series data
maxY = 10 * thresholds.peak;
}
}
const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts, forNode)}]
const plotData = [new Array(longestSeries)]
const plotSeries = [
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
didx == null ? null : formatTime(ts, forNode),
},
];
const plotData = [new Array(longestSeries)];
if (forNode === true) {
// Negative Timestamp Buildup
for (let i = 0; i <= longestSeries; i++) {
plotData[0][i] = (longestSeries - i) * timestep * -1
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
for (
let j = 0;
j < longestSeries;
j++ // TODO: Cache/Reuse this array?
)
plotData[0][j] = j * timestep;
}
let plotBands = undefined
let plotBands = undefined;
if (useStatsSeries) {
plotData.push(statisticsSeries.min)
plotData.push(statisticsSeries.max)
plotData.push(statisticsSeries.mean)
plotData.push(statisticsSeries.min);
plotData.push(statisticsSeries.max);
plotData.push(statisticsSeries.mean);
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)
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' })
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)' }
{ series: [2, 3], fill: "rgba(0,255,0,0.1)" },
{ series: [3, 1], fill: "rgba(255,0,0,0.1)" },
];
} 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
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',
label:
scope === "node"
? resources[i].hostname
: // scope === 'accelerator' ? resources[0].accelerators[i] :
scope + " #" + (i + 1),
scale: "y",
width: lineWidth,
stroke: lineColor(i, series.length)
})
stroke: lineColor(i, series.length),
});
}
}
const opts = {
width,
height,
plugins: [
legendAsTooltipPlugin()
],
plugins: [legendAsTooltipPlugin()],
series: plotSeries,
axes: [
{
scale: 'x',
scale: "x",
space: 35,
incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map(v => formatTime(v, forNode))
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
},
{
scale: 'y',
scale: "y",
grid: { show: true },
labelFont: 'sans-serif',
values: (u, vals) => vals.map(v => formatNumber(v))
}
labelFont: "sans-serif",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {
draw: [(u) => {
draw: [
(u) => {
// Draw plot type label:
let textl = `${scope}${plotSeries.length > 2 ? 's' : ''}${
useStatsSeries ? ': min/avg/max' : (metricConfig != null && scope != metricConfig.scope ? ` (${metricConfig.aggregation})` : '')}`
let textr = `${(isShared && (scope != 'core' && scope != 'accelerator')) ? '[Shared]' : '' }`
u.ctx.save()
u.ctx.textAlign = 'start' // 'end'
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)
let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
useStatsSeries
? ": min/avg/max"
: metricConfig != null && scope != metricConfig.scope
? ` (${metricConfig.aggregation})`
: ""
}`;
let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
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.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
if (!thresholds) {
u.ctx.restore()
return
u.ctx.restore();
return;
}
let y = u.valToPos(thresholds.normal, 'y', true)
u.ctx.save()
u.ctx.lineWidth = lineWidth
u.ctx.strokeStyle = normalLineColor
u.ctx.setLineDash([5, 5])
u.ctx.beginPath()
u.ctx.moveTo(u.bbox.left, y)
u.ctx.lineTo(u.bbox.left + u.bbox.width, y)
u.ctx.stroke()
u.ctx.restore()
}]
let y = u.valToPos(thresholds.normal, "y", true);
u.ctx.save();
u.ctx.lineWidth = lineWidth;
u.ctx.strokeStyle = normalLineColor;
u.ctx.setLineDash([5, 5]);
u.ctx.beginPath();
u.ctx.moveTo(u.bbox.left, y);
u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
u.ctx.stroke();
u.ctx.restore();
},
],
},
scales: {
x: { time: false },
y: maxY ? { range: [0., maxY * 1.1] } : {}
y: maxY ? { range: [0, maxY * 1.1] } : {},
},
legend : { // Display legend until max 12 Y-dataseries
show: (series.length <= 12 || useStatsSeries === true) ? true : false,
live: (series.length <= 12 || useStatsSeries === true) ? true : 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 } }
}
cursor: { drag: { x: true, y: true } },
};
// console.log(opts)
let plotWrapper = null
let uplot = null
let timeoutId = null
let prevWidth = null, prevHeight = null
let plotWrapper = null;
let uplot = null;
let timeoutId = null;
let prevWidth = null,
prevHeight = null;
function render() {
if (!width || Number.isNaN(width) || width < 0)
return
if (!width || Number.isNaN(width) || width < 0) return;
if (prevWidth != null && Math.abs(prevWidth - width) < 10)
return
if (prevWidth != null && Math.abs(prevWidth - width) < 10) return;
prevWidth = width
prevHeight = height
prevWidth = width;
prevHeight = height;
if (!uplot) {
opts.width = width
opts.height = height
uplot = new uPlot(opts, plotData, plotWrapper)
opts.width = width;
opts.height = height;
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width, height })
uplot.setSize({ width, height });
}
}
function onSizeChange() {
if (!uplot)
return
if (!uplot) return;
if (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null
render()
}, resizeSleepTime)
timeoutId = null;
render();
}, resizeSleepTime);
}
$: if (series[0].data.length > 0) {
onSizeChange(width, height)
onSizeChange(width, height);
}
onMount(() => {
if (series[0].data.length > 0) {
plotWrapper.style.backgroundColor = backgroundColor()
render()
plotWrapper.style.backgroundColor = backgroundColor();
render();
}
})
});
onDestroy(() => {
if (uplot)
uplot.destroy()
if (uplot) uplot.destroy();
if (timeoutId != null)
clearTimeout(timeoutId)
})
if (timeoutId != null) clearTimeout(timeoutId);
});
// `from` and `to` must be numbers between 0 and 1.
export function setTimeRange(from, to) {
if (!uplot || from > to)
return false
if (!uplot || from > to) return false;
uplot.setScale('x', { min: from * maxX, max: to * maxX })
return true
uplot.setScale("x", { min: from * maxX, max: to * maxX });
return true;
}
</script>
<script context="module">
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 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
}
}
export function findThresholds(metricConfig, scope, subCluster, isShared, numhwthreads, numaccs) {
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
if (!metricConfig || !scope || !subCluster) {
console.warn('Argument missing for findThresholds!')
return null
}
if ((scope == 'node' && isShared == false) || metricConfig.aggregation == 'avg') {
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
// console.log('subClusterConfigs array empty, use metricConfig defaults')
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak }
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert && forSubCluster.peak) return forSubCluster
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak}
} else {
console.warn('metricConfig.subClusters not found!')
return null
}
}
if (metricConfig.aggregation != 'sum') {
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
return null
}
let divisor = 1
if (isShared == true) { // Shared
if (numaccs > 0) {
divisor = subCluster.topology.accelerators.length / numaccs
} else if (numhwthreads > 0) {
divisor = subCluster.topology.node.length / numhwthreads
}
else if (scope == 'socket')
divisor = subCluster.topology.socket.length
else if (scope == 'core')
divisor = subCluster.topology.core.length
else if (scope == 'accelerator')
divisor = subCluster.topology.accelerators.length
else if (scope == 'hwthread')
divisor = subCluster.topology.node.length
else
// console.log('TODO: how to calc thresholds for ', scope)
return null
}
let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig
return {
peak: mc.peak / divisor,
normal: mc.normal / divisor,
caution: mc.caution / divisor,
alert: mc.alert / divisor
}
}
</script>
{#if series[0].data.length > 0}
<div bind:this={plotWrapper} class="cc-plot"></div>
{:else}
<Card class="mx-4" 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>

View File

@ -1,21 +1,21 @@
<script>
import uPlot from 'uplot'
import { formatNumber } from '../units.js'
import { onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap'
import uPlot from "uplot";
import { formatNumber } from "../units.js";
import { onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap";
export let data = null
export let renderTime = false
export let allowSizeChange = false
export let cluster = null
export let width = 600
export let height = 350
export let data = null;
export let renderTime = false;
export let allowSizeChange = false;
export let cluster = null;
export let width = 600;
export let height = 350;
let plotWrapper = null
let uplot = null
let timeoutId = null
let plotWrapper = null;
let uplot = null;
let timeoutId = null;
const lineWidth = clusterCockpitConfig.plot_general_lineWidth
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
/* Data Format
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
@ -26,67 +26,111 @@
// Helpers
function getGradientR(x) {
if (x < 0.5) return 0
if (x > 0.75) return 255
x = (x - 0.5) * 4.0
return Math.floor(x * 255.0)
if (x < 0.5) return 0;
if (x > 0.75) return 255;
x = (x - 0.5) * 4.0;
return Math.floor(x * 255.0);
}
function getGradientG(x) {
if (x > 0.25 && x < 0.75) return 255
if (x < 0.25) x = x * 4.0
else x = 1.0 - (x - 0.75) * 4.0
return Math.floor(x * 255.0)
if (x > 0.25 && x < 0.75) return 255;
if (x < 0.25) x = x * 4.0;
else x = 1.0 - (x - 0.75) * 4.0;
return Math.floor(x * 255.0);
}
function getGradientB(x) {
if (x < 0.25) return 255
if (x > 0.5) return 0
x = 1.0 - (x - 0.25) * 4.0
return Math.floor(x * 255.0)
if (x < 0.25) return 255;
if (x > 0.5) return 0;
x = 1.0 - (x - 0.25) * 4.0;
return Math.floor(x * 255.0);
}
function getRGB(c) {
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`;
}
function nearestThousand (num) {
return Math.ceil(num/1000) * 1000
function nearestThousand(num) {
return Math.ceil(num / 1000) * 1000;
}
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l;
return {
x: x1 + a * (x2 - x1),
y: y1 + a * (y2 - y1)
}
y: y1 + a * (y2 - y1),
};
}
// End Helpers
// Dot Renderers
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
const size = 5 * devicePixelRatio;
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
uPlot.orient(
u,
seriesIdx,
(
series,
dataX,
dataY,
scaleX,
scaleY,
valToPosX,
valToPosY,
xOff,
yOff,
xDim,
yDim,
moveTo,
lineTo,
rect,
arc,
) => {
let d = u.data[seriesIdx];
let deg360 = 2 * Math.PI;
for (let i = 0; i < d[0].length; i++) {
let p = new Path2D();
let xVal = d[0][i];
let yVal = d[1][i];
u.ctx.strokeStyle = getRGB(u.data[2][i])
u.ctx.fillStyle = getRGB(u.data[2][i])
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
u.ctx.strokeStyle = getRGB(u.data[2][i]);
u.ctx.fillStyle = getRGB(u.data[2][i]);
if (
xVal >= scaleX.min &&
xVal <= scaleX.max &&
yVal >= scaleY.min &&
yVal <= scaleY.max
) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size/2, cy);
arc(p, cx, cy, size/2, 0, deg360);
p.moveTo(cx + size / 2, cy);
arc(p, cx, cy, size / 2, 0, deg360);
}
u.ctx.fill(p);
}
});
},
);
return null;
};
const drawPoints = (u, seriesIdx, idx0, idx1) => {
const size = 5 * devicePixelRatio;
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
uPlot.orient(
u,
seriesIdx,
(
series,
dataX,
dataY,
scaleX,
scaleY,
valToPosX,
valToPosY,
xOff,
yOff,
xDim,
yDim,
moveTo,
lineTo,
rect,
arc,
) => {
let d = u.data[seriesIdx];
u.ctx.strokeStyle = getRGB(0);
u.ctx.fillStyle = getRGB(0);
@ -95,15 +139,21 @@
for (let i = 0; i < d[0].length; i++) {
let xVal = d[0][i];
let yVal = d[1][i];
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
if (
xVal >= scaleX.min &&
xVal <= scaleX.max &&
yVal >= scaleY.min &&
yVal <= scaleY.max
) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size/2, cy);
arc(p, cx, cy, size/2, 0, deg360);
p.moveTo(cx + size / 2, cy);
arc(p, cx, cy, size / 2, 0, deg360);
}
}
u.ctx.fill(p);
});
},
);
return null;
};
@ -116,18 +166,18 @@
width: width,
height: height,
legend: {
show: false
show: false,
},
cursor: { drag: { x: false, y: false } },
axes: [
{
label: 'Intensity [FLOPS/Byte]',
values: (u, vals) => vals.map(v => formatNumber(v))
label: "Intensity [FLOPS/Byte]",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
label: 'Performace [GFLOPS]',
values: (u, vals) => vals.map(v => formatNumber(v))
}
label: "Performace [GFLOPS]",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
scales: {
x: {
@ -137,118 +187,153 @@
log: 10, // log exp
},
y: {
range: [1.0, cluster?.flopRateSimd?.value ? nearestThousand(cluster.flopRateSimd.value) : 10000],
range: [
1.0,
cluster?.flopRateSimd?.value
? nearestThousand(cluster.flopRateSimd.value)
: 10000,
],
distr: 3, // Render as log
log: 10, // log exp
},
},
series: [
{},
{ paths: renderTime ? drawColorPoints : drawPoints }
],
series: [{}, { paths: renderTime ? drawColorPoints : drawPoints }],
hooks: {
drawClear: [
u => {
(u) => {
u.series.forEach((s, i) => {
if (i > 0)
s._paths = null;
if (i > 0) s._paths = null;
});
},
],
draw: [
u => { // draw roofs when cluster set
(u) => {
// draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding // [top, right, bottom, left]
const padding = u._padding; // [top, right, bottom, left]
u.ctx.strokeStyle = 'black'
u.ctx.lineWidth = lineWidth
u.ctx.beginPath()
u.ctx.strokeStyle = "black";
u.ctx.lineWidth = lineWidth;
u.ctx.beginPath();
const ycut = 0.01 * cluster.memoryBandwidth.value
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, 'x', true),
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
const ycut = 0.01 * cluster.memoryBandwidth.value;
const scalarKnee =
(cluster.flopRateScalar.value - ycut) /
cluster.memoryBandwidth.value;
const simdKnee =
(cluster.flopRateSimd.value - ycut) /
cluster.memoryBandwidth.value;
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, "x", true),
flopRateScalarY = u.valToPos(
cluster.flopRateScalar.value,
"y",
true,
),
flopRateSimdY = u.valToPos(
cluster.flopRateSimd.value,
"y",
true,
);
// Debug get zoomLevel from browser
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
if (scalarKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Lower horizontal roofline
u.ctx.moveTo(scalarKneeX, flopRateScalarY)
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateScalarY)
if (
scalarKneeX <
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio
) {
// Lower horizontal roofline
u.ctx.moveTo(scalarKneeX, flopRateScalarY);
u.ctx.lineTo(
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio,
flopRateScalarY,
);
}
if (simdKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Top horitontal roofline
u.ctx.moveTo(simdKneeX, flopRateSimdY)
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateSimdY)
if (
simdKneeX <
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio
) {
// Top horitontal roofline
u.ctx.moveTo(simdKneeX, flopRateSimdY);
u.ctx.lineTo(
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio,
flopRateSimdY,
);
}
let x1 = u.valToPos(0.01, 'x', true),
y1 = u.valToPos(ycut, 'y', true)
let x1 = u.valToPos(0.01, "x", true),
y1 = u.valToPos(ycut, "y", true);
let x2 = u.valToPos(simdKnee, 'x', true),
y2 = flopRateSimdY
let x2 = u.valToPos(simdKnee, "x", true),
y2 = flopRateSimdY;
let xAxisIntersect = lineIntersect(
x1, y1, x2, y2,
u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
)
x1,
y1,
x2,
y2,
u.valToPos(0.01, "x", true),
u.valToPos(1.0, "y", true), // X-Axis Start Coords
u.valToPos(1000, "x", true),
u.valToPos(1.0, "y", true), // X-Axis End Coords
);
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x
y1 = xAxisIntersect.y
x1 = xAxisIntersect.x;
y1 = xAxisIntersect.y;
}
// Diagonal
u.ctx.moveTo(x1, y1)
u.ctx.lineTo(x2, y2)
u.ctx.moveTo(x1, y1);
u.ctx.lineTo(x2, y2);
u.ctx.stroke()
u.ctx.stroke();
// Reset grid lineWidth
u.ctx.lineWidth = 0.15
u.ctx.lineWidth = 0.15;
}
}
]
},
],
},
// cursor: { drag: { x: true, y: true } } // Activate zoom
};
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
console.log('No data for roofline!')
console.log("No data for roofline!");
}
}
// Svelte and Sizechange
onMount(() => {
render(data)
})
render(data);
});
onDestroy(() => {
if (uplot)
uplot.destroy()
if (uplot) uplot.destroy();
if (timeoutId != null)
clearTimeout(timeoutId)
})
if (timeoutId != null) clearTimeout(timeoutId);
});
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null
if (uplot)
uplot.destroy()
render(data)
}, 200)
timeoutId = null;
if (uplot) uplot.destroy();
render(data);
}, 200);
}
$: if (allowSizeChange) sizeChanged(width, height)
$: if (allowSizeChange) sizeChanged(width, height);
</script>
{#if data != null}
<div bind:this={plotWrapper}/>
<div bind:this={plotWrapper} />
{:else}
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
>
{/if}