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"
},
"peerDependencies": {
"svelte": "^3.53.1"
"@types/estree": "^1.0.0"
}
},
"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"
}
}

File diff suppressed because it is too large Load Diff

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>
<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}/>
<CardHeader>
<CardTitle class="mb-1">Plotting Options</CardTitle>
</CardHeader>
<PlotSettings config={ccconfig} />
</Card>

View File

@ -1,178 +1,169 @@
<script>
import {
Icon,
Collapse,
Navbar,
NavbarBrand,
Nav,
NavbarToggler,
Dropdown,
DropdownToggle,
DropdownMenu,
} from "sveltestrap";
import NavbarLinks from "./NavbarLinks.svelte";
import NavbarTools from "./NavbarTools.svelte";
import {
Icon,
Collapse,
Navbar,
NavbarBrand,
Nav,
NavbarToggler,
Dropdown,
DropdownToggle,
DropdownMenu,
} from "@sveltestrap/sveltestrap";
import NavbarLinks from "./NavbarLinks.svelte";
import NavbarTools from "./NavbarTools.svelte";
export let username; // empty string if auth. is disabled, otherwise the username as string
export let authlevel; // Integer
export let clusters; // array of names
export let roles; // Role Enum-Like
export let username; // empty string if auth. is disabled, otherwise the username as string
export let authlevel; // Integer
export let clusters; // array of names
export let roles; // Role Enum-Like
let isOpen = false;
let screenSize;
let isOpen = false;
let screenSize;
const jobsTitle = new Map();
jobsTitle.set(2, "Job Search");
jobsTitle.set(3, "Managed Jobs");
jobsTitle.set(4, "Jobs");
jobsTitle.set(5, "Jobs");
const usersTitle = new Map();
usersTitle.set(3, "Managed Users");
usersTitle.set(4, "Users");
usersTitle.set(5, "Users");
const jobsTitle = new Map();
jobsTitle.set(2, "Job Search");
jobsTitle.set(3, "Managed Jobs");
jobsTitle.set(4, "Jobs");
jobsTitle.set(5, "Jobs");
const usersTitle = new Map();
usersTitle.set(3, "Managed Users");
usersTitle.set(4, "Users");
usersTitle.set(5, "Users");
const views = [
{
title: "My Jobs",
requiredRole: roles.user,
href: `/monitoring/user/${username}`,
icon: "bar-chart-line-fill",
perCluster: false,
menu: "none",
},
{
title: jobsTitle.get(authlevel),
requiredRole: roles.user,
href: `/monitoring/jobs/`,
icon: "card-list",
perCluster: false,
menu: "none",
},
{
title: usersTitle.get(authlevel),
requiredRole: roles.manager,
href: "/monitoring/users/",
icon: "people-fill",
perCluster: false,
menu: "Groups",
},
{
title: "Projects",
requiredRole: roles.support,
href: "/monitoring/projects/",
icon: "folder",
perCluster: false,
menu: "Groups",
},
{
title: "Tags",
requiredRole: roles.user,
href: "/monitoring/tags/",
icon: "tags",
perCluster: false,
menu: "Groups",
},
{
title: "Analysis",
requiredRole: roles.support,
href: "/monitoring/analysis/",
icon: "graph-up",
perCluster: true,
menu: "Stats",
},
{
title: "Nodes",
requiredRole: roles.admin,
href: "/monitoring/systems/",
icon: "cpu",
perCluster: true,
menu: "Groups",
},
{
title: "Status",
requiredRole: roles.admin,
href: "/monitoring/status/",
icon: "cpu",
perCluster: true,
menu: "Stats",
},
];
const views = [
{
title: "My Jobs",
requiredRole: roles.user,
href: `/monitoring/user/${username}`,
icon: "bar-chart-line-fill",
perCluster: false,
menu: "none",
},
{
title: jobsTitle.get(authlevel),
requiredRole: roles.user,
href: `/monitoring/jobs/`,
icon: "card-list",
perCluster: false,
menu: "none",
},
{
title: usersTitle.get(authlevel),
requiredRole: roles.manager,
href: "/monitoring/users/",
icon: "people-fill",
perCluster: false,
menu: "Groups",
},
{
title: "Projects",
requiredRole: roles.support,
href: "/monitoring/projects/",
icon: "folder",
perCluster: false,
menu: "Groups",
},
{
title: "Tags",
requiredRole: roles.user,
href: "/monitoring/tags/",
icon: "tags",
perCluster: false,
menu: "Groups",
},
{
title: "Analysis",
requiredRole: roles.support,
href: "/monitoring/analysis/",
icon: "graph-up",
perCluster: true,
menu: "Stats",
},
{
title: "Nodes",
requiredRole: roles.admin,
href: "/monitoring/systems/",
icon: "cpu",
perCluster: true,
menu: "Groups",
},
{
title: "Status",
requiredRole: roles.admin,
href: "/monitoring/status/",
icon: "cpu",
perCluster: true,
menu: "Stats",
},
];
</script>
<svelte:window bind:innerWidth={screenSize} />
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
</NavbarBrand>
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
<Collapse
style="justify-content: space-between"
{isOpen}
navbar
expand="md"
on:update={({ detail }) => (isOpen = detail.isOpen)}
>
<Nav navbar>
{#if screenSize > 1500 || screenSize < 768}
<NavbarLinks
{clusters}
links={views.filter(
(item) => item.requiredRole <= authlevel
)}
/>
{:else if screenSize > 1300}
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu != "Stats"
)}
/>
<Dropdown nav>
<DropdownToggle nav caret>
<Icon name="graph-up" />
Stats
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu == "Stats"
)}
/>
</DropdownMenu>
</Dropdown>
{:else}
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu == "none"
)}
/>
{#each Array("Groups", "Stats") as menu}
<Dropdown nav>
<DropdownToggle nav caret>
{menu}
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel &&
item.menu == menu
)}
/>
</DropdownMenu>
</Dropdown>
{/each}
{/if}
</Nav>
<NavbarTools {username} {authlevel} {roles} {screenSize} />
</Collapse>
<NavbarBrand href="/">
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
</NavbarBrand>
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
<Collapse
style="justify-content: space-between"
{isOpen}
navbar
expand="md"
on:update={({ detail }) => (isOpen = detail.isOpen)}
>
<Nav navbar>
{#if screenSize > 1500 || screenSize < 768}
<NavbarLinks
{clusters}
links={views.filter((item) => item.requiredRole <= authlevel)}
/>
{:else if screenSize > 1300}
<NavbarLinks
{clusters}
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu != "Stats",
)}
/>
<Dropdown nav>
<DropdownToggle nav caret>
<Icon name="graph-up" />
Stats
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
links={views.filter(
(item) =>
item.requiredRole <= authlevel && item.menu == "Stats",
)}
/>
</DropdownMenu>
</Dropdown>
{:else}
<NavbarLinks
{clusters}
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == "none",
)}
/>
{#each Array("Groups", "Stats") as menu}
<Dropdown nav>
<DropdownToggle nav caret>
{menu}
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks
{clusters}
links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == menu,
)}
/>
</DropdownMenu>
</Dropdown>
{/each}
{/if}
</Nav>
<NavbarTools {username} {authlevel} {roles} {screenSize} />
</Collapse>
</Navbar>

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!) {
updateConfiguration(name: $name, value: $value)
}`,
variables: { name, value }
})
}
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
function updateConfiguration(data) {
updateConfigurationMutation({
name: data.name,
value: JSON.stringify(data.value)
}).subscribe(res => {
if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
}
function updateConfiguration(data) {
updateConfigurationMutation({
name: data.name,
value: JSON.stringify(data.value),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
});
}
function closeAndApply() {
metricsInHistograms = [...pendingMetrics] // Set for parent
isOpen = !isOpen
updateConfiguration({
name: cluster ? `user_view_histogramMetrics:${cluster}` : 'user_view_histogramMetrics',
value: metricsInHistograms
})
}
function closeAndApply() {
metricsInHistograms = [...pendingMetrics]; // Set for parent
isOpen = !isOpen;
updateConfiguration({
name: cluster
? `user_view_histogramMetrics:${cluster}`
: "user_view_histogramMetrics",
value: metricsInHistograms,
});
}
</script>
<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}>
{metric}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={closeAndApply}> Close & Apply </Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}> Close </Button>
</ModalFooter>
<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} />
{metric}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,43 +1,50 @@
<script>
import {
init,
groupByScope,
fetchMetricsStore,
checkMetricDisabled,
transformDataForRoofline
} from "./utils.js";
import {
Row,
Col,
Card,
Spinner,
TabContent,
TabPane,
CardBody,
CardHeader,
CardTitle,
Button,
Icon,
} from "sveltestrap";
import PlotTable from "./PlotTable.svelte";
import Metric from "./Metric.svelte";
import Polar from "./plots/Polar.svelte";
import Roofline from "./plots/Roofline.svelte";
import JobInfo from "./joblist/JobInfo.svelte";
import TagManagement from "./TagManagement.svelte";
import MetricSelection from "./MetricSelection.svelte";
import StatsTable from "./StatsTable.svelte";
import JobFootprint from "./JobFootprint.svelte";
import { getContext } from "svelte";
import {
init,
groupByScope,
fetchMetricsStore,
checkMetricDisabled,
transformDataForRoofline,
} from "./utils.js";
import {
Row,
Col,
Card,
Spinner,
TabContent,
TabPane,
CardBody,
CardHeader,
CardTitle,
Button,
Icon,
} from "@sveltestrap/sveltestrap";
import PlotTable from "./PlotTable.svelte";
import Metric from "./Metric.svelte";
import Polar from "./plots/Polar.svelte";
import Roofline from "./plots/Roofline.svelte";
import JobInfo from "./joblist/JobInfo.svelte";
import TagManagement from "./TagManagement.svelte";
import MetricSelection from "./MetricSelection.svelte";
import StatsTable from "./StatsTable.svelte";
import JobFootprint from "./JobFootprint.svelte";
import { getContext } from "svelte";
export let dbid;
export let authlevel;
export let roles;
export let dbid;
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(`
const { query: initq } = init(`
job(id: "${dbid}") {
id, jobId, user, project, cluster, startTime,
duration, numNodes, numHWThreads, numAcc,
@ -52,417 +59,390 @@
}
`);
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
metrics = getContext("metrics")
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
metrics = getContext("metrics");
let isMetricsSelectionOpen = false,
selectedMetrics = [],
isFetched = new Set();
const [jobMetrics, startFetching] = fetchMetricsStore();
getContext("on-init")(() => {
let job = $initq.data.job;
if (!job) return;
let isMetricsSelectionOpen = false,
selectedMetrics = [],
isFetched = new Set();
const [jobMetrics, startFetching] = fetchMetricsStore();
getContext("on-init")(() => {
let job = $initq.data.job;
if (!job) return;
selectedMetrics =
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
selectedMetrics =
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
let toFetch = new Set([
"flops_any",
"mem_bw",
...selectedMetrics,
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
ccconfig[`job_view_polarPlotMetrics`]),
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
ccconfig[`job_view_nodestats_selectedMetrics`]),
]);
let toFetch = new Set([
"flops_any",
"mem_bw",
...selectedMetrics,
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
ccconfig[`job_view_polarPlotMetrics`]),
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
ccconfig[`job_view_nodestats_selectedMetrics`]),
]);
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
accNodeOnly = [...toFetch].some(function(m) {
if (accMetrics.includes(m)) {
const mc = metrics(job.cluster, m)
return mc.scope !== 'accelerator'
} else {
return false
}
})
if (job.numAcc === 0 || accNodeOnly === true) {
// No Accels or Accels on Node Scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node"]
: ["node", "socket", "core"]
);
} else {
// Accels and not on node scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node", "accelerator"]
: ["node", "accelerator", "socket", "core"]
);
}
isFetched = toFetch;
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
accNodeOnly = [...toFetch].some(function (m) {
if (accMetrics.includes(m)) {
const mc = metrics(job.cluster, m);
return mc.scope !== "accelerator";
} else {
return false;
}
});
const lazyFetchMoreMetrics = () => {
let notYetFetched = new Set();
for (let m of selectedMetrics) {
if (!isFetched.has(m)) {
notYetFetched.add(m);
isFetched.add(m);
}
}
if (notYetFetched.size > 0)
startFetching(
$initq.data.job,
[...notYetFetched],
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"]
);
};
// Fetch more data once required:
$: if ($initq.data && $jobMetrics.data && selectedMetrics)
lazyFetchMoreMetrics();
let plots = {},
jobTags,
statsTable,
jobFootprint;
$: document.title = $initq.fetching
? "Loading..."
: $initq.error
? "Error"
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
// Find out what metrics or hosts are missing:
let missingMetrics = [],
missingHosts = [],
somethingMissing = false;
$: if ($initq.data && $jobMetrics.data) {
let job = $initq.data.job,
metrics = $jobMetrics.data.jobMetrics,
metricNames = clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
// Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
missingMetrics = metricNames.filter(
(metric) =>
!metrics.some((jm) => jm.name == metric) &&
!checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster
)
);
missingHosts = job.resources
.map(({ hostname }) => ({
hostname: hostname,
metrics: metricNames.filter(
(metric) =>
!metrics.some(
(jm) =>
jm.scope == "node" &&
jm.metric.series.some(
(series) => series.hostname == hostname
)
)
),
}))
.filter(({ metrics }) => metrics.length > 0);
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
if (job.numAcc === 0 || accNodeOnly === true) {
// No Accels or Accels on Node Scope
startFetching(
job,
[...toFetch],
job.numNodes > 2 ? ["node"] : ["node", "socket", "core"],
);
} else {
// Accels and not on node scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node", "accelerator"]
: ["node", "accelerator", "socket", "core"],
);
}
const orderAndMap = (grouped, selectedMetrics) =>
selectedMetrics.map((metric) => ({
metric: metric,
data: grouped.find((group) => group[0].name == metric),
disabled: checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster
isFetched = toFetch;
});
const lazyFetchMoreMetrics = () => {
let notYetFetched = new Set();
for (let m of selectedMetrics) {
if (!isFetched.has(m)) {
notYetFetched.add(m);
isFetched.add(m);
}
}
if (notYetFetched.size > 0)
startFetching(
$initq.data.job,
[...notYetFetched],
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"],
);
};
// Fetch more data once required:
$: if ($initq.data && $jobMetrics.data && selectedMetrics)
lazyFetchMoreMetrics();
let plots = {},
jobTags,
statsTable,
jobFootprint;
$: document.title = $initq.fetching
? "Loading..."
: $initq.error
? "Error"
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
// Find out what metrics or hosts are missing:
let missingMetrics = [],
missingHosts = [],
somethingMissing = false;
$: if ($initq.data && $jobMetrics.data) {
let job = $initq.data.job,
metrics = $jobMetrics.data.jobMetrics,
metricNames = clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
// Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
missingMetrics = metricNames.filter(
(metric) =>
!metrics.some((jm) => jm.name == metric) &&
!checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster,
),
);
missingHosts = job.resources
.map(({ hostname }) => ({
hostname: hostname,
metrics: metricNames.filter(
(metric) =>
!metrics.some(
(jm) =>
jm.scope == "node" &&
jm.metric.series.some((series) => series.hostname == hostname),
),
}));
),
}))
.filter(({ metrics }) => metrics.length > 0);
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
}
const orderAndMap = (grouped, selectedMetrics) =>
selectedMetrics.map((metric) => ({
metric: metric,
data: grouped.find((group) => group[0].name == metric),
disabled: checkMetricDisabled(
metric,
$initq.data.job.cluster,
$initq.data.job.subCluster,
),
}));
</script>
<Row>
<Col>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.data}
<JobInfo job={$initq.data.job} {jobTags} />
{:else}
<Spinner secondary />
{/if}
</Col>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<Col>
<JobFootprint
bind:this={jobFootprint}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
{/key}
{/if}
{#if $jobMetrics.data && $initq.data}
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
{#if authlevel > roles.manager}
<Col>
<h5>
Concurrent Jobs <Icon
name="info-circle"
style="cursor:help;"
title="Shared jobs running on the same node with overlapping runtimes"
/>
</h5>
<ul>
<li>
<a
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
>
</li>
{/each}
</ul>
</Col>
{:else}
<Col>
<h5>
{$initq.data.job.concurrentJobs.items.length} Concurrent
Jobs
</h5>
<p>
Number of shared jobs on the same node with overlapping
runtimes.
</p>
</Col>
{/if}
{/if}
<Col>
<Polar
metrics={ccconfig[
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
] || ccconfig[`job_view_polarPlotMetrics`]}
cluster={$initq.data.job.cluster}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
<Col>
<Roofline
renderTime={true}
cluster={clusters
.find((c) => c.name == $initq.data.job.cluster)
.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
)
}
/>
</Col>
<Col>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.data}
<JobInfo job={$initq.data.job} {jobTags} />
{:else}
<Col />
<Col />
<Spinner secondary />
{/if}
</Col>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<Col>
<JobFootprint
bind:this={jobFootprint}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
{/key}
{/if}
{#if $jobMetrics.data && $initq.data}
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
{#if authlevel > roles.manager}
<Col>
<h5>
Concurrent Jobs <Icon
name="info-circle"
style="cursor:help;"
title="Shared jobs running on the same node with overlapping runtimes"
/>
</h5>
<ul>
<li>
<a
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
>
</li>
{/each}
</ul>
</Col>
{:else}
<Col>
<h5>
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
</h5>
<p>
Number of shared jobs on the same node with overlapping runtimes.
</p>
</Col>
{/if}
{/if}
<Col>
<Polar
metrics={ccconfig[
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
] || ccconfig[`job_view_polarPlotMetrics`]}
cluster={$initq.data.job.cluster}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
<Col>
<Roofline
renderTime={true}
cluster={clusters
.find((c) => c.name == $initq.data.job.cluster)
.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,
)}
/>
</Col>
{:else}
<Col />
<Col />
{/if}
</Row>
<Row class="mb-3">
<Col xs="auto">
{#if $initq.data}
<TagManagement job={$initq.data.job} bind:jobTags />
{/if}
</Col>
<Col xs="auto">
{#if $initq.data}
<Button outline on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up" /> Metrics
</Button>
{/if}
</Col>
<!-- <Col xs="auto">
<Col xs="auto">
{#if $initq.data}
<TagManagement job={$initq.data.job} bind:jobTags />
{/if}
</Col>
<Col xs="auto">
{#if $initq.data}
<Button outline on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up" /> Metrics
</Button>
{/if}
</Col>
<!-- <Col xs="auto">
<Zoom timeseriesPlots={plots} />
</Col> -->
</Row>
<Row>
<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
>
<br />
{/if}
<Card body color="danger">{$jobMetrics.error.message}</Card>
{:else if $jobMetrics.fetching}
<Spinner secondary />
{:else if $jobMetrics.data && $initq.data}
<PlotTable
let:item
let:width
renderFor="job"
items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
{#if item.data}
<Metric
bind:this={plots[item.metric]}
on:more-loaded={({ detail }) =>
statsTable.moreLoaded(detail)}
job={$initq.data.job}
metricName={item.metric}
rawData={item.data.map((x) => x.metric)}
scopes={item.data.map((x) => x.scope)}
{width}
isShared={$initq.data.job.exclusive != 1}
resources={$initq.data.job.resources}
/>
{:else}
<Card body color="warning"
>No dataset returned for <code>{item.metric}</code
></Card
>
{/if}
</PlotTable>
<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>
<br />
{/if}
<Card body color="danger">{$jobMetrics.error.message}</Card>
{:else if $jobMetrics.fetching}
<Spinner secondary />
{:else if $jobMetrics.data && $initq.data}
<PlotTable
let:item
let:width
renderFor="job"
items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics,
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
{#if item.data}
<Metric
bind:this={plots[item.metric]}
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
job={$initq.data.job}
metricName={item.metric}
rawData={item.data.map((x) => x.metric)}
scopes={item.data.map((x) => x.scope)}
{width}
isShared={$initq.data.job.exclusive != 1}
resources={$initq.data.job.resources}
/>
{:else}
<Card body color="warning"
>No dataset returned for <code>{item.metric}</code></Card
>
{/if}
</Col>
</PlotTable>
{/if}
</Col>
</Row>
<Row class="mt-2">
<Col>
{#if $initq.data}
<TabContent>
{#if somethingMissing}
<TabPane
tabId="resources"
tab="Resources"
active={somethingMissing}
>
<div style="margin: 10px;">
<Card color="warning">
<CardHeader>
<CardTitle
>Missing Metrics/Reseources</CardTitle
>
</CardHeader>
<CardBody>
{#if missingMetrics.length > 0}
<p>
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>
<ul>
{#each missingHosts as missing}
<li>
{missing.hostname}: {missing.metrics.join(
", "
)}
</li>
{/each}
</ul>
{/if}
</CardBody>
</Card>
</div>
</TabPane>
{/if}
<TabPane
tabId="stats"
tab="Statistics Table"
active={!somethingMissing}
>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<StatsTable
bind:this={statsTable}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
{/key}
{/if}
</TabPane>
<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>
{:else}
<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>
{:else}
<Card body color="warning"
>No additional slurm information available</Card
>
{/if}
</div>
</TabPane>
</TabContent>
<Col>
{#if $initq.data}
<TabContent>
{#if somethingMissing}
<TabPane tabId="resources" tab="Resources" active={somethingMissing}>
<div style="margin: 10px;">
<Card color="warning">
<CardHeader>
<CardTitle>Missing Metrics/Reseources</CardTitle>
</CardHeader>
<CardBody>
{#if missingMetrics.length > 0}
<p>
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>
<ul>
{#each missingHosts as missing}
<li>
{missing.hostname}: {missing.metrics.join(", ")}
</li>
{/each}
</ul>
{/if}
</CardBody>
</Card>
</div>
</TabPane>
{/if}
</Col>
<TabPane
tabId="stats"
tab="Statistics Table"
active={!somethingMissing}
>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<StatsTable
bind:this={statsTable}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
{/key}
{/if}
</TabPane>
<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>
{:else}
<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>
{:else}
<Card body color="warning"
>No additional slurm information available</Card
>
{/if}
</div>
</TabPane>
</TabContent>
{/if}
</Col>
</Row>
{#if $initq.data}
<MetricSelection
cluster={$initq.data.job.cluster}
configName="job_view_selectedMetrics"
bind:metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}
/>
<MetricSelection
cluster={$initq.data.job.cluster}
configName="job_view_selectedMetrics"
bind:metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}
/>
{/if}
<style>
.pre-wrapper {
font-size: 1.1rem;
margin: 10px;
border: 1px solid #bbb;
border-radius: 5px;
padding: 5px;
}
.pre-wrapper {
font-size: 1.1rem;
margin: 10px;
border: 1px solid #bbb;
border-radius: 5px;
padding: 5px;
}
ul {
columns: 2;
-webkit-columns: 2;
-moz-columns: 2;
}
ul {
columns: 2;
-webkit-columns: 2;
-moz-columns: 2;
}
</style>

View File

@ -1,129 +1,199 @@
<script>
import { getContext } from 'svelte'
import {
Card,
CardHeader,
CardTitle,
CardBody,
Progress,
Icon,
Tooltip
} from "sveltestrap";
import { mean, round } from 'mathjs'
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 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)
} 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
// Threshold / -Differences
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
}
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')) {
return {
...fmBase,
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')) {
return {
...fmBase,
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
}
}
})
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)
default:
return false
}
<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 {
Card,
CardHeader,
CardTitle,
CardBody,
Progress,
Icon,
Tooltip,
} from "@sveltestrap/sveltestrap";
import { mean, round } from "mathjs";
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 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);
} 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;
// Threshold / -Differences
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,
};
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")) {
return {
...fmBase,
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")) {
return {
...fmBase,
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,
};
}
});
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;
default:
return false;
}
}
</script>
<script context="module">
@ -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,62 +241,67 @@
</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
</CardTitle>
<CardTitle class="mb-0 d-flex justify-content-center">
Core Metrics Footprint
</CardTitle>
</CardHeader>
{/if}
<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 class="mx-1">
<!-- Alerts Only -->
{#if fpd.impact === 3 || fpd.impact === -1}
<Icon name="exclamation-triangle-fill" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning" />
{/if}
<!-- Emoji for all states-->
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger" />
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning" />
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success" />
{:else if fpd.impact === 0}
<Icon name="emoji-laughing" class="text-info" />
{:else if fpd.impact === -1}
<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 ... -->
</div>
</div>
<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} />
</div>
{/each}
{#if job?.metaData?.message}
<hr class="mt-1 mb-2" />
{@html job.metaData.message}
{/if}
<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 class="mx-1">
<!-- Alerts Only -->
{#if fpd.impact === 3 || fpd.impact === -1}
<Icon name="exclamation-triangle-fill" class="text-danger"/>
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning"/>
{/if}
<!-- Emoji for all states-->
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger"/>
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning"/>
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success"/>
{:else if fpd.impact === 0}
<Icon name="emoji-laughing" class="text-info"/>
{:else if fpd.impact === -1}
<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 ... -->
</div>
</div>
<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}
/>
</div>
{/each}
{#if job?.metaData?.message}
<hr class="mt-1 mb-2"/>
{@html job.metaData.message}
{/if}
</CardBody>
</CardBody>
</Card>
<style>
.cursor-help {
cursor: help;
}
.cursor-help {
cursor: help;
}
</style>

View File

@ -1,102 +1,121 @@
<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 metrics = filterPresets.cluster
? 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
// 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())
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 metrics = filterPresets.cluster
? 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;
// 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());
</script>
<Row>
{#if $initq.fetching}
<Col xs="auto">
<Spinner/>
</Col>
{:else if $initq.error}
<Col xs="auto">
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
{#if $initq.fetching}
<Col xs="auto">
<Spinner />
</Col>
{:else if $initq.error}
<Col xs="auto">
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
</Row>
<Row>
<Col xs="auto">
<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
</Button>
<Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button>
</Col>
<Col xs="auto">
<Filters
filterPresets={filterPresets}
bind:this={filterComponent}
on:update={({ detail }) => {
selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null
jobList.update(detail.filters)
}
} />
</Col>
<Col xs="auto">
<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
</Button>
<Button disabled outline
>{matchedJobs == null ? "Loading..." : `${matchedJobs} jobs`}</Button
>
</Col>
<Col xs="auto">
<Filters
{filterPresets}
bind:this={filterComponent}
on:update={({ detail }) => {
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)}/>
</Col>
<Col xs="2">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
<Col xs="3" style="margin-left: auto;">
<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:this={jobList}
bind:showFootprint={showFootprint} />
</Col>
<Col>
<JobList
bind:metrics
bind:sorting
bind:matchedJobs
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:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint
view="list"
/>

View File

@ -2,52 +2,58 @@
@component List of users or projects
-->
<script>
import { onMount } from "svelte";
import { init } from "./utils.js";
import {
Row,
Col,
Button,
Icon,
Table,
Card,
Spinner,
InputGroup,
Input,
} from "sveltestrap";
import Filters from "./filters/Filters.svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
import { onMount } from "svelte";
import { init } from "./utils.js";
import {
Row,
Col,
Button,
Icon,
Table,
Card,
Spinner,
InputGroup,
Input,
} from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
const {} = init();
const {} = init();
export let type;
export let filterPresets;
export let type;
export let filterPresets;
// By default, look at the jobs of the last 30 days:
if (filterPresets?.startTime == null) {
if (filterPresets == null)
filterPresets = {}
// By default, look at the jobs of the last 30 days:
if (filterPresets?.startTime == null) {
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!"
);
console.assert(
type == "USER" || type == "PROJECT",
"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
let jobFilters = [];
let nameFilter = "";
let sorting = { field: "totalJobs", direction: "down" };
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobFilters = [];
let nameFilter = "";
let sorting = { field: "totalJobs", direction: "down" };
const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
id
@ -58,185 +64,178 @@
totalAccHours
}
}`,
variables: { jobFilters }
});
variables: { jobFilters },
});
function changeSorting(event, field) {
let target = event.target;
while (target.tagName != "BUTTON") target = target.parentElement;
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";
target.children[0].className = `bi-sort-numeric-${direction}`;
sorting = { field, direction };
}
let direction = target.children[0].className.includes("up") ? "down" : "up";
target.children[0].className = `bi-sort-numeric-${direction}`;
sorting = { field, direction };
}
function sort(stats, sorting, nameFilter) {
const cmp =
sorting.field == "id"
? sorting.direction == "up"
? (a, b) => a.id < b.id
: (a, b) => a.id > b.id
: sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field];
function sort(stats, sorting, nameFilter) {
const cmp =
sorting.field == "id"
? sorting.direction == "up"
? (a, b) => a.id < b.id
: (a, b) => a.id > b.id
: sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field];
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
}
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
}
onMount(() => filterComponent.update());
onMount(() => filterComponent.update());
</script>
<Row>
<Col xs="auto">
<InputGroup>
<Button disabled outline>
Search {type.toLowerCase()}s
</Button>
<Input
bind:value={nameFilter}
placeholder="Filter by {{
USER: 'username',
PROJECT: 'project',
}[type]}"
/>
</InputGroup>
</Col>
<Col xs="auto">
<Filters
bind:this={filterComponent}
{filterPresets}
startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update={({ detail }) => {
jobFilters = detail.filters;
}}
/>
</Col>
<Col xs="auto">
<InputGroup>
<Button disabled outline>
Search {type.toLowerCase()}s
</Button>
<Input
bind:value={nameFilter}
placeholder="Filter by {{
USER: 'username',
PROJECT: 'project',
}[type]}"
/>
</InputGroup>
</Col>
<Col xs="auto">
<Filters
bind:this={filterComponent}
{filterPresets}
startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update={({ detail }) => {
jobFilters = detail.filters;
}}
/>
</Col>
</Row>
<Table>
<thead>
<thead>
<tr>
<th scope="col">
{{
USER: "Username",
PROJECT: "Project Name",
}[type]}
<Button
color={sorting.field == "id" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "id")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{#if type == "USER"}
<th scope="col">
Name
<Button
color={sorting.field == "name" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "name")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col">
Total Jobs
<Button
color={sorting.field == "totalJobs" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalJobs")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Walltime
<Button
color={sorting.field == "totalWalltime" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Core Hours
<Button
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Accelerator Hours
<Button
color={sorting.field == "totalAccHours" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
</tr>
</thead>
<tbody>
{#if $stats.fetching}
<tr>
<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
></td
>
</tr>
{:else if $stats.data}
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
<tr>
<th scope="col">
{({
USER: "Username",
PROJECT: "Project Name",
})[type]}
<Button
color={sorting.field == "id" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "id")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<td>
{#if type == "USER"}
<th scope="col">
Name
<Button
color={sorting.field == "name" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "name")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col">
Total Jobs
<Button
color={sorting.field == "totalJobs" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalJobs")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Walltime
<Button
color={sorting.field == "totalWalltime"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Core Hours
<Button
color={sorting.field == "totalCoreHours"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Accelerator Hours
<Button
color={sorting.field == "totalAccHours"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
</tr>
</thead>
<tbody>
{#if $stats.fetching}
<tr>
<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
></td
>
</tr>
{:else if $stats.data}
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
<tr>
<td>
{#if type == "USER"}
<a href="/monitoring/user/{row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else if type == "PROJECT"}
<a href="/monitoring/jobs/?project={row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else}
{row.id}
{/if}
</td>
{#if type == "USER"}
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
{/if}
<td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td>
<td>{row.totalAccHours}</td>
</tr>
<a href="/monitoring/user/{row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else if type == "PROJECT"}
<a href="/monitoring/jobs/?project={row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else}
<tr>
<td colspan="4"
><i>No {type.toLowerCase()}s/jobs found</i></td
>
</tr>
{/each}
{/if}
</tbody>
{row.id}
{/if}
</td>
{#if type == "USER"}
<td
>{scrambleNames
? scramble(row?.name ? row.name : "-")
: row?.name
? row.name
: "-"}</td
>
{/if}
<td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td>
<td>{row.totalAccHours}</td>
</tr>
{:else}
<tr>
<td colspan="4"><i>No {type.toLowerCase()}s/jobs found</i></td>
</tr>
{/each}
{/if}
</tbody>
</Table>

View File

@ -1,95 +1,118 @@
<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)
let selectedHost = null, plot, fetching = false, error = null
let selectedScope = minScope(scopes)
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,
);
$: availableScopes = scopes
$: selectedScopeIndex = scopes.findIndex(s => s == selectedScope)
$: data = rawData[selectedScopeIndex]
$: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost)
let selectedHost = null,
plot,
fetching = false,
error = null;
let selectedScope = minScope(scopes);
let from = null, to = null
export function setTimeRange(f, t) {
from = f, to = t
$: 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;
export function setTimeRange(f, t) {
(from = f), (to = t);
}
$: if (plot != null) plot.setTimeRange(from, to);
export async function loadMore() {
fetching = true;
let response = await fetchMetrics(job, [metricName], ["core"]);
fetching = false;
if (response.error) {
error = response.error;
return;
}
$: if (plot != null) plot.setTimeRange(from, to)
export async function loadMore() {
fetching = true
let response = await fetchMetrics(job, [metricName], ["core"])
fetching = false
if (response.error) {
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)
}
}
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);
}
}
}
$: 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 : '')})
</InputGroupText>
<select class="form-select" bind:value={selectedScope}>
{#each availableScopes as scope}
<option value={scope}>{scope}</option>
{/each}
{#if availableScopes.length == 1 && metricConfig?.scope != "node"}
<option value={"load-more"}>Load more...</option>
{/if}
</select>
{#if job.resources.length > 1}
<select class="form-select" bind:value={selectedHost}>
<option value={null}>All Hosts</option>
{#each job.resources as { hostname }}
<option value={hostname}>{hostname}</option>
{/each}
</select>
<InputGroupText style="min-width: 150px;">
{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}
<option value={scope}>{scope}</option>
{/each}
{#if availableScopes.length == 1 && metricConfig?.scope != "node"}
<option value={"load-more"}>Load more...</option>
{/if}
</select>
{#if job.resources.length > 1}
<select class="form-select" bind:value={selectedHost}>
<option value={null}>All Hosts</option>
{#each job.resources as { hostname }}
<option value={hostname}>{hostname}</option>
{/each}
</select>
{/if}
</InputGroup>
{#key series}
{#if fetching == true}
<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}
timestep={data.timestep}
scope={selectedScope} metric={metricName}
series={series}
isShared={isShared}
resources={job.resources} />
{/if}
{#if fetching == true}
<Spinner />
{:else if error != null}
<Card body color="danger">{error.message}</Card>
{:else if series != null}
<Timeseries
bind:this={plot}
{width}
height={300}
{cluster}
{subCluster}
timestep={data.timestep}
scope={selectedScope}
metric={metricName}
{series}
{isShared}
resources={job.resources}
/>
{/if}
{/key}

View File

@ -8,181 +8,206 @@
-->
<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()
for (let c of clusters)
for (let metric of c.metricConfig)
allMetrics.add(metric.name)
})
onInit(() => {
if (allMetrics == null) allMetrics = new Set();
for (let c of clusters)
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)
} else {
// console.log('Recalculate available metrics for ' + cluster)
allMetrics.clear()
for (let c of clusters)
if (c.name == cluster)
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);
} else {
// console.log('Recalculate available metrics for ' + cluster)
allMetrics.clear();
for (let c of clusters)
if (c.name == cluster)
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));
}
}
const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
let columnHovering = null;
function columnsDragStart(event, 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"));
if (start < target) {
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start]);
newMetricsOrder.splice(start, 1);
} else {
newMetricsOrder.splice(target, 0, newMetricsOrder[start]);
newMetricsOrder.splice(start + 1, 1);
}
columnHovering = null;
}
const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
})}
function closeAndApply() {
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
isOpen = false;
let columnHovering = null
showFootprint = !!pendingShowFootprint;
function columnsDragStart(event, i) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.dropEffect = 'move'
event.dataTransfer.setData('text/plain', i)
}
updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`,
value: JSON.stringify(metrics),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
});
function columnsDrag(event, target) {
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)
} else {
newMetricsOrder.splice(target, 0, newMetricsOrder[start])
newMetricsOrder.splice(start + 1, 1)
}
columnHovering = null
}
function closeAndApply() {
metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m))
isOpen = false
showFootprint = !!pendingShowFootprint
updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`,
value: JSON.stringify(metrics)
}).subscribe(res => {
if (res.fetching === false && 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 => {
if (res.fetching === false && res.error) {
console.log('Error on footprint subscription: ' + res.error)
throw res.error
}
})
}
updateConfigurationMutation({
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;
}
});
}
</script>
<style>
li.cc-config-column {
display: block;
cursor: grab;
}
li.cc-config-column.is-active {
background-color: #3273dc;
color: #fff;
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 {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;
cursor: grab;
}
li.cc-config-column.is-active {
background-color: #3273dc;
color: #fff;
cursor: grabbing;
}
</style>

View File

@ -1,39 +1,38 @@
<script>
import {
Icon,
NavLink,
Dropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from "sveltestrap";
import {
Icon,
NavLink,
Dropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from "@sveltestrap/sveltestrap";
export let clusters; // array of names
export let links; // array of nav links
export let clusters; // array of names
export let links; // array of nav links
</script>
{#each links as item}
{#if !item.perCluster}
<NavLink href={item.href} active={window.location.pathname == item.href}
><Icon name={item.icon} /> {item.title}</NavLink
>
{:else}
<Dropdown nav inNavbar>
<DropdownToggle nav caret>
<Icon name={item.icon} />
{item.title}
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
{#each clusters as cluster}
<DropdownItem
href={item.href + cluster.name}
active={window.location.pathname ==
item.href + cluster.name}
>
{cluster.name}
</DropdownItem>
{/each}
</DropdownMenu>
</Dropdown>
{/if}
{#if !item.perCluster}
<NavLink href={item.href} active={window.location.pathname == item.href}
><Icon name={item.icon} /> {item.title}</NavLink
>
{:else}
<Dropdown nav inNavbar>
<DropdownToggle nav caret>
<Icon name={item.icon} />
{item.title}
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
{#each clusters as cluster}
<DropdownItem
href={item.href + cluster.name}
active={window.location.pathname == item.href + cluster.name}
>
{cluster.name}
</DropdownItem>
{/each}
</DropdownMenu>
</Dropdown>
{/if}
{/each}

View File

@ -1,143 +1,153 @@
<script>
import {
Icon,
Nav,
NavItem,
InputGroup,
Input,
Button,
InputGroupText,
Container,
Row,
Col,
} from "sveltestrap";
import {
Icon,
Nav,
NavItem,
InputGroup,
Input,
Button,
InputGroupText,
Container,
Row,
Col,
} from "@sveltestrap/sveltestrap";
export let username; // empty string if auth. is disabled, otherwise the username as string
export let authlevel; // Integer
export let roles; // Role Enum-Like
export let screenSize; // screensize
export let username; // empty string if auth. is disabled, otherwise the username as string
export let authlevel; // Integer
export let roles; // Role Enum-Like
export let screenSize; // screensize
</script>
<Nav navbar>
{#if screenSize >= 768}
<NavItem>
<form method="GET" action="/search">
<InputGroup>
<Input
type="text"
placeholder="Search 'type:<query>' ..."
name="searchId"
style="margin-left: 10px;"
/>
<!-- bootstrap classes w/o effect -->
<Button outline type="submit" title="Search"><Icon name="search" /></Button
>
<InputGroupText
style="cursor:help;"
title={authlevel >= roles.support
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
><Icon name="info-circle" /></InputGroupText
>
</InputGroup>
</form>
</NavItem>
<NavItem>
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
<Button outline style="margin-left: 10px;">
<Icon name="book" />
</Button>
</a>
</NavItem>
<NavItem>
<Button
outline
on:click={() => (window.location.href = "/config")}
style="margin-left: 10px;"
title="Settings"
>
<Icon name="gear" />
</Button>
</NavItem>
{#if username}
<NavItem>
<form method="POST" action="/logout">
<Button
outline
color="success"
type="submit"
style="margin-left: 10px;"
title="Logout {username}"
>
{#if screenSize > 1630}
<Icon name="box-arrow-right"/> Logout {username}
{:else}
<Icon name="box-arrow-right"/>
{/if}
</Button>
</form>
</NavItem>
{/if}
{:else}
<NavItem>
<Container>
<Row cols={3}>
<Col xs="4">
<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>
</a>
</Col>
<Col xs="4">
<Button
outline
on:click={() => (window.location.href = "/config")}
size="sm"
class="my-2 w-100"
>
{#if authlevel >= roles.admin}
<Icon name="gear" /> Admin Settings
{:else}
<Icon name="gear" /> Plotting Options
{/if}
</Button>
</Col>
<Col xs="4">
<form method="POST" action="/logout">
<Button
outline
color="success"
type="submit"
size="sm"
class="my-2 w-100"
>
<Icon name="box-arrow-right" /> Logout {username}
</Button>
</form>
</Col>
</Row>
</Container>
</NavItem>
<NavItem style="margin-left: 10px; margin-right:10px;">
<form method="GET" action="/search">
<InputGroup>
<Input
type="text"
placeholder="Search 'type:<query>' ..."
name="searchId"
/>
<Button outline type="submit"><Icon name="search" /></Button
>
<InputGroupText
style="cursor:help;"
title={authlevel >= roles.support
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
><Icon name="info-circle" /></InputGroupText
>
</InputGroup>
</form>
</NavItem>
{#if screenSize >= 768}
<NavItem>
<form method="GET" action="/search">
<InputGroup>
<Input
type="text"
placeholder="Search 'type:<query>' ..."
name="searchId"
style="margin-left: 10px;"
/>
<!-- bootstrap classes w/o effect -->
<Button outline type="submit" title="Search"
><Icon name="search" /></Button
>
<InputGroupText
style="cursor:help;"
title={authlevel >= roles.support
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
><Icon name="info-circle" /></InputGroupText
>
</InputGroup>
</form>
</NavItem>
<NavItem>
<a
href="https://www.clustercockpit.org/docs/webinterface/"
title="Documentation"
rel="nofollow"
target="_blank"
>
<Button outline style="margin-left: 10px;">
<Icon name="book" />
</Button>
</a>
</NavItem>
<NavItem>
<Button
outline
on:click={() => (window.location.href = "/config")}
style="margin-left: 10px;"
title="Settings"
>
<Icon name="gear" />
</Button>
</NavItem>
{#if username}
<NavItem>
<form method="POST" action="/logout">
<Button
outline
color="success"
type="submit"
style="margin-left: 10px;"
title="Logout {username}"
>
{#if screenSize > 1630}
<Icon name="box-arrow-right" /> Logout {username}
{:else}
<Icon name="box-arrow-right" />
{/if}
</Button>
</form>
</NavItem>
{/if}
{:else}
<NavItem>
<Container>
<Row cols={3}>
<Col xs="4">
<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>
</a>
</Col>
<Col xs="4">
<Button
outline
on:click={() => (window.location.href = "/config")}
size="sm"
class="my-2 w-100"
>
{#if authlevel >= roles.admin}
<Icon name="gear" /> Admin Settings
{:else}
<Icon name="gear" /> Plotting Options
{/if}
</Button>
</Col>
<Col xs="4">
<form method="POST" action="/logout">
<Button
outline
color="success"
type="submit"
size="sm"
class="my-2 w-100"
>
<Icon name="box-arrow-right" /> Logout {username}
</Button>
</form>
</Col>
</Row>
</Container>
</NavItem>
<NavItem style="margin-left: 10px; margin-right:10px;">
<form method="GET" action="/search">
<InputGroup>
<Input
type="text"
placeholder="Search 'type:<query>' ..."
name="searchId"
/>
<Button outline type="submit"><Icon name="search" /></Button>
<InputGroupText
style="cursor:help;"
title={authlevel >= roles.support
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
><Icon name="info-circle" /></InputGroupText
>
</InputGroup>
</form>
</NavItem>
{/if}
</Nav>

View File

@ -1,238 +1,230 @@
<script>
import { init, checkMetricDisabled } from "./utils.js";
import {
Row,
Col,
InputGroup,
InputGroupText,
Icon,
Spinner,
Card,
} from "sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import TimeSelection from "./filters/TimeSelection.svelte";
import Refresher from './joblist/Refresher.svelte';
import PlotTable from "./PlotTable.svelte";
import MetricPlot from "./plots/MetricPlot.svelte";
import { getContext } from "svelte";
import { init, checkMetricDisabled } from "./utils.js";
import {
Row,
Col,
InputGroup,
InputGroupText,
Icon,
Spinner,
Card,
} from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import TimeSelection from "./filters/TimeSelection.svelte";
import Refresher from "./joblist/Refresher.svelte";
import PlotTable from "./PlotTable.svelte";
import MetricPlot from "./plots/MetricPlot.svelte";
import { getContext } from "svelte";
export let cluster;
export let hostname;
export let from = null;
export let to = null;
export let cluster;
export let hostname;
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);
if (from == null || to == null) {
to = new Date(Date.now());
from = new Date(to.getTime());
from.setMinutes(from.getMinutes() - 30);
}
const ccconfig = getContext("cc-config");
const clusters = getContext("clusters");
const client = getContextClient();
const nodeMetricsQuery = gql`
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
}
data
}
}
}
}
}
`;
const ccconfig = getContext("cc-config");
const clusters = getContext("clusters");
const client = getContextClient();
const nodeMetricsQuery = gql`
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
nodes: $nodes
from: $from
to: $to
) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
}
data
}
}
}
}
}
`;
$: nodeMetricsData = queryStore({
client: client,
query: nodeMetricsQuery,
variables: {
cluster: cluster,
nodes: [hostname],
from: from.toISOString(),
to: to.toISOString(),
},
});
$: nodeMetricsData = queryStore({
client: client,
query: nodeMetricsQuery,
variables: {
cluster: cluster,
nodes: [hostname],
from: from.toISOString(),
to: to.toISOString(),
},
});
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1;
let paging = { itemsPerPage, page };
let sorting = { field: "startTime", order: "DESC" };
$: filter = [
{ cluster: { eq: cluster } },
{ node: { contains: hostname } },
{ state: ["running"] },
// {startTime: {
// from: from.toISOString(),
// to: to.toISOString()
// }}
];
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1;
let paging = { itemsPerPage, page };
let sorting = { field: "startTime", order: "DESC" };
$: filter = [
{ cluster: { eq: cluster } },
{ node: { contains: hostname } },
{ state: ["running"] },
// {startTime: {
// from: from.toISOString(),
// to: to.toISOString()
// }}
];
const nodeJobsQuery = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
# items {
# id
# jobId
# }
count
}
}
`;
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
});
let metricUnits = {};
$: if ($nodeMetricsData.data) {
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] = "";
}
}
}
const nodeJobsQuery = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
# items {
# id
# jobId
# }
count
}
}
`;
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
});
let metricUnits = {};
$: if ($nodeMetricsData.data) {
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] = "";
}
}
}
}
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
</script>
<Row>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner />
{:else}
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>{hostname} ({cluster})</InputGroupText>
</InputGroup>
</Col>
<Col>
{#if $nodeJobsData.fetching}
<Spinner />
{:else}
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>{hostname} ({cluster})</InputGroupText>
</InputGroup>
</Col>
<Col>
{#if $nodeJobsData.fetching}
<Spinner />
{:else if $nodeJobsData.data}
Currently running jobs on this node: {$nodeJobsData.data.jobs
.count}
[
<a
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
target="_blank">View in Job List</a
> ]
{:else}
No currently running jobs.
{/if}
</Col>
<Col>
<Refresher on:reload={() => {
const diff = Date.now() - to
from = new Date(from.getTime() + diff)
to = new Date(to.getTime() + diff)
}} />
</Col>
<Col>
<TimeSelection bind:from bind:to />
</Col>
{/if}
{:else if $nodeJobsData.data}
Currently running jobs on this node: {$nodeJobsData.data.jobs.count}
[
<a
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
target="_blank">View in Job List</a
> ]
{:else}
No currently running jobs.
{/if}
</Col>
<Col>
<Refresher
on:reload={() => {
const diff = Date.now() - to;
from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff);
}}
/>
</Col>
<Col>
<TimeSelection bind:from bind:to />
</Col>
{/if}
</Row>
<br />
<Row>
<Col>
{#if $nodeMetricsData.error}
<Card body color="danger">{$nodeMetricsData.error.message}</Card>
{:else if $nodeMetricsData.fetching || $initq.fetching}
<Spinner />
<Col>
{#if $nodeMetricsData.error}
<Card body color="danger">{$nodeMetricsData.error.message}</Card>
{:else if $nodeMetricsData.fetching || $initq.fetching}
<Spinner />
{:else}
<PlotTable
let:item
let:width
renderFor="node"
itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodeMetricsData.data.nodeMetrics[0].metrics
.map((m) => ({
...m,
disabled: checkMetricDisabled(
m.name,
cluster,
$nodeMetricsData.data.nodeMetrics[0].subCluster,
),
}))
.sort((a, b) => a.name.localeCompare(b.name))}
>
<h4 style="text-align: center; padding-top:15px;">
{item.name}
{metricUnits[item.name]}
</h4>
{#if item.disabled === false && item.metric}
<MetricPlot
{width}
height={300}
metric={item.name}
timestep={item.metric.timestep}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
series={item.metric.series}
resources={[{ hostname: hostname }]}
forNode={true}
/>
{:else if item.disabled === true && item.metric}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
.subCluster}</code
></Card
>
{:else}
<PlotTable
let:item
let:width
renderFor="node"
itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodeMetricsData.data.nodeMetrics[0].metrics
.map((m) => ({
...m,
disabled: checkMetricDisabled(
m.name,
cluster,
$nodeMetricsData.data.nodeMetrics[0].subCluster
),
}))
.sort((a, b) => a.name.localeCompare(b.name))}
>
<h4 style="text-align: center; padding-top:15px;">
{item.name}
{metricUnits[item.name]}
</h4>
{#if item.disabled === false && item.metric}
<MetricPlot
{width}
height={300}
metric={item.name}
timestep={item.metric.timestep}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={$nodeMetricsData.data.nodeMetrics[0]
.subCluster}
series={item.metric.series}
resources={[{hostname: hostname}]}
forNode={true}
/>
{:else if item.disabled === true && item.metric}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="info"
>Metric disabled for subcluster <code
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
.subCluster}</code
></Card
>
{:else}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{item.name}</code></Card
>
{/if}
</PlotTable>
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{item.name}</code></Card
>
{/if}
</Col>
</PlotTable>
{/if}
</Col>
</Row>

View File

@ -1,139 +1,163 @@
<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!) {
updateConfiguration(name: $name, value: $value)
}`,
variables: { name, value }
})
}
const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $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 => {
if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
}
function updateConfiguration(data) {
updateConfigurationMutation({
name: data.name,
value: JSON.stringify(data.value),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
});
}
</script>
<Button outline
on:click={() => (isHistogramConfigOpen = true)}>
<Icon name=""/>
Select Plots for Histograms
<Button outline on:click={() => (isHistogramConfigOpen = true)}>
<Icon name="" />
Select Plots for Histograms
</Button>
<Button outline
on:click={() => (isScatterPlotConfigOpen = true)}>
<Icon name=""/>
Select Plots in Scatter Plots
<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>
<ModalBody>
<ListGroup>
{#each availableMetrics as metric (metric)}
<ListGroupItem>
<input type="checkbox" bind:group={metricsInHistograms}
value={metric}
on:change={() => updateConfiguration({
name: 'analysis_view_histogramMetrics',
value: metricsInHistograms
})} />
<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}
value={metric}
on:change={() =>
updateConfiguration({
name: "analysis_view_histogramMetrics",
value: metricsInHistograms,
})}
/>
{metric}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary"
on:click={() => (isHistogramConfigOpen = false)}>
Close
</Button>
</ModalFooter>
{metric}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<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>
<ModalBody>
<ListGroup>
{#each metricsInScatterplots as pair}
<ListGroupItem>
<b>{pair[0]}</b> / <b>{pair[1]}</b>
<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"
on:click={() => {
metricsInScatterplots = metricsInScatterplots.filter(p => pair != p)
updateConfiguration({
name: 'analysis_view_scatterPlotMetrics',
value: metricsInScatterplots
});
}}>
<Icon name="x" />
</Button>
</ListGroupItem>
{/each}
</ListGroup>
<Button
style="float: right;"
outline
color="danger"
on:click={() => {
metricsInScatterplots = metricsInScatterplots.filter(
(p) => pair != p,
);
updateConfiguration({
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">
<option value={null}>Choose Metric for X Axis</option>
{#each availableMetrics as metric}
<option value={metric}>{metric}</option>
{/each}
</select>
<select bind:value={selectedMetric2} class="form-group form-select">
<option value={null}>Choose Metric for Y Axis</option>
{#each availableMetrics as metric}
<option value={metric}>{metric}</option>
{/each}
</select>
<Button outline disabled={selectedMetric1 == null || selectedMetric2 == null}
on:click={() => {
metricsInScatterplots = [...metricsInScatterplots, [selectedMetric1, selectedMetric2]]
selectedMetric1 = null
selectedMetric2 = null
updateConfiguration({
name: 'analysis_view_scatterPlotMetrics',
value: metricsInScatterplots
})
}}>
Add Plot
</Button>
</InputGroup>
</ModalBody>
<ModalFooter>
<Button color="primary"
on:click={() => (isScatterPlotConfigOpen = false)}>
Close
</Button>
</ModalFooter>
<InputGroup>
<select bind:value={selectedMetric1} class="form-group form-select">
<option value={null}>Choose Metric for X Axis</option>
{#each availableMetrics as metric}
<option value={metric}>{metric}</option>
{/each}
</select>
<select bind:value={selectedMetric2} class="form-group form-select">
<option value={null}>Choose Metric for Y Axis</option>
{#each availableMetrics as metric}
<option value={metric}>{metric}</option>
{/each}
</select>
<Button
outline
disabled={selectedMetric1 == null || selectedMetric2 == null}
on:click={() => {
metricsInScatterplots = [
...metricsInScatterplots,
[selectedMetric1, selectedMetric2],
];
selectedMetric1 = null;
selectedMetric2 = null;
updateConfiguration({
name: "analysis_view_scatterPlotMetrics",
value: metricsInScatterplots,
});
}}
>
Add Plot
</Button>
</InputGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}>
Close
</Button>
</ModalFooter>
</Modal>

View File

@ -1,139 +1,154 @@
<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(),
selectedScopes = {},
sorting = {},
isMetricSelectionOpen = false,
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)
if (job.exclusive != 1 || job.numNodes == 1) {
if (availableScopes.includes('accelerator')) {
selectedScopes[metric] = 'accelerator'
} else if (availableScopes.includes('core')) {
selectedScopes[metric] = 'core'
} else if (availableScopes.includes('socket')) {
selectedScopes[metric] = 'socket'
} else {
selectedScopes[metric] = 'node'
}
} else {
selectedScopes[metric] = maxScope(availableScopes)
}
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"];
sorting[metric] = {
min: { dir: 'up', active: false },
avg: { dir: 'up', active: false },
max: { dir: 'up', active: false },
}
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);
if (job.exclusive != 1 || job.numNodes == 1) {
if (availableScopes.includes("accelerator")) {
selectedScopes[metric] = "accelerator";
} else if (availableScopes.includes("core")) {
selectedScopes[metric] = "core";
} else if (availableScopes.includes("socket")) {
selectedScopes[metric] = "socket";
} else {
selectedScopes[metric] = "node";
}
} else {
selectedScopes[metric] = maxScope(availableScopes);
}
export function sortBy(metric, stat) {
let s = sorting[metric][stat]
if (s.active) {
s.dir = s.dir == 'up' ? 'down' : 'up'
} else {
for (let metric in sorting)
for (let stat in sorting[metric])
sorting[metric][stat].active = false
s.active = true
}
sorting[metric] = {
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
}
let series = jobMetrics.find(jm => jm.name == metric && jm.scope == 'node')?.metric.series
sorting = {...sorting}
hosts = hosts.sort((h1, h2) => {
let s1 = series.find(s => s.hostname == h1)?.statistics
let s2 = series.find(s => s.hostname == h2)?.statistics
if (s1 == null || s2 == null)
return -1
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
})
export function sortBy(metric, stat) {
let s = sorting[metric][stat];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let metric in sorting)
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
s.active = true;
}
export function moreLoaded(jobMetric) {
jobMetrics = [...jobMetrics, jobMetric]
}
let series = jobMetrics.find(
(jm) => jm.name == metric && jm.scope == "node",
)?.metric.series;
sorting = { ...sorting };
hosts = hosts.sort((h1, h2) => {
let s1 = series.find((s) => s.hostname == h1)?.statistics;
let s2 = series.find((s) => s.hostname == h2)?.statistics;
if (s1 == null || s2 == null) return -1;
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
});
}
export function moreLoaded(jobMetric) {
jobMetrics = [...jobMetrics, jobMetric];
}
</script>
<Table>
<thead>
<tr>
<th>
<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}>
<InputGroup>
<InputGroupText>
{metric}
</InputGroupText>
<select class="form-select"
bind:value={selectedScopes[metric]}>
{#each scopesForMetric(metric, jobMetrics) as scope}
<option value={scope}>{scope}</option>
{/each}
</select>
</InputGroup>
</th>
{/each}
</tr>
<tr>
<th>Node</th>
{#each selectedMetrics as metric}
{#if selectedScopes[metric] != 'node'}
<th>Id</th>
{/if}
{#each ['min', 'avg', 'max'] as stat}
<th on:click={() => sortBy(metric, stat)}>
{stat}
{#if selectedScopes[metric] == 'node'}
<Icon name="caret-{sorting[metric][stat].dir}{sorting[metric][stat].active ? '-fill' : ''}" />
{/if}
</th>
{/each}
{/each}
</tr>
</thead>
<tbody>
{#each hosts as host (host)}
<tr>
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
host={host} metric={metric}
scope={selectedScopes[metric]}
jobMetrics={jobMetrics} />
{/each}
</tr>
<thead>
<tr>
<th>
<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}>
<InputGroup>
<InputGroupText>
{metric}
</InputGroupText>
<select class="form-select" bind:value={selectedScopes[metric]}>
{#each scopesForMetric(metric, jobMetrics) as scope}
<option value={scope}>{scope}</option>
{/each}
</select>
</InputGroup>
</th>
{/each}
</tr>
<tr>
<th>Node</th>
{#each selectedMetrics as metric}
{#if selectedScopes[metric] != "node"}
<th>Id</th>
{/if}
{#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}>
{stat}
{#if selectedScopes[metric] == "node"}
<Icon
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
.active
? '-fill'
: ''}"
/>
{/if}
</th>
{/each}
</tbody>
{/each}
</tr>
</thead>
<tbody>
{#each hosts as host (host)}
<tr>
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
{host}
{metric}
scope={selectedScopes[metric]}
{jobMetrics}
/>
{/each}
</tr>
{/each}
</tbody>
</Table>
<br/>
<br />
<MetricSelection
cluster={job.cluster}
configName='job_view_nodestats_selectedMetrics'
allMetrics={new Set(allMetrics)}
bind:metrics={selectedMetrics}
bind:isOpen={isMetricSelectionOpen} />
cluster={job.cluster}
configName="job_view_nodestats_selectedMetrics"
allMetrics={new Set(allMetrics)}
bind:metrics={selectedMetrics}
bind:isOpen={isMetricSelectionOpen}
/>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,159 +1,218 @@
<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)
}
if (from == null || to == null) {
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) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit { base, prefix }
series {
statistics { min, avg, max }
data
}
}
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
) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
}
data
}
}
}`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString()
}
}
})
}
`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString(),
},
});
let metricUnits = {}
$: if ($nodesQuery.data) {
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] = ''
}
}
let metricUnits = {};
$: if ($nodesQuery.data) {
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] = "";
}
}
}
}
</script>
<Row>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner/>
{:else}
<Col>
<Refresher on:reload={() => {
const diff = Date.now() - to
from = new Date(from.getTime() + diff)
to = new Date(to.getTime() + diff)
}} />
</Col>
<Col>
<TimeSelection
bind:from={from}
bind:to={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}
</select>
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
</InputGroup>
</Col>
{/if}
</Row>
<br/>
<Row>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner />
{:else}
<Col>
{#if $nodesQuery.error}
<Card body color="danger">{$nodesQuery.error.message}</Card>
{:else if $nodesQuery.fetching || $initq.fetching}
<Spinner/>
{:else}
<PlotTable
let:item
let:width
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 => ({
host: h.host,
subCluster: 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>
{#if item.disabled === false && item.data}
<MetricPlot
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)}
subCluster={item.subCluster}
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>
{:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
{/if}
</PlotTable>
{/if}
<Refresher
on:reload={() => {
const diff = Date.now() - to;
from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff);
}}
/>
</Col>
<Col>
<TimeSelection bind:from bind:to />
</Col>
<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}
</select>
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input
placeholder="hostname..."
type="text"
bind:value={hostnameFilter}
/>
</InputGroup>
</Col>
{/if}
</Row>
<br />
<Row>
<Col>
{#if $nodesQuery.error}
<Card body color="danger">{$nodesQuery.error.message}</Card>
{:else if $nodesQuery.fetching || $initq.fetching}
<Spinner />
{:else}
<PlotTable
let:item
let:width
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) => ({
host: h.host,
subCluster: 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>
{#if item.disabled === false && item.data}
<MetricPlot
{width}
height={plotHeight}
timestep={item.data.metric.timestep}
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={item.subCluster}
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
>
{:else}
<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,190 +1,234 @@
<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 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}
})
}
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}
})
}
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}
})
}
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]
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 },
});
};
function isNewTag(type, name) {
for (let tag of allTagsFiltered)
if (tag.type == type && tag.name == name)
return false
return true
}
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 },
});
};
function createTag(type, name) {
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)
} else if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
}
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 },
});
};
function addTagToJob(tag) {
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
// console.log('Error on subscription: ' + res.error)
}
})
}
let allTagsFiltered; // $initialized is in there because when it becomes true, allTags is initailzed.
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
function removeTagFromJob(tag) {
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
} else if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
$: {
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;
}
function createTag(type, name) {
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);
} else if (res.fetching === false && 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) => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.addTagsToJob;
pendingChange = false;
} else if (res.fetching === false && 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) => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.removeTagsFromJob;
pendingChange = false;
} else if (res.fetching === false && 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
{#if pendingChange !== false}
<Spinner size="sm" secondary />
{:else}
<Icon name="tags" />
{/if}
</ModalHeader>
<ModalBody>
<Input style="width: 100%;"
type="text" placeholder="Search Tags"
bind:value={filterTerm} />
<ModalHeader>
Manage Tags
{#if pendingChange !== false}
<Spinner size="sm" secondary />
{:else}
<Icon name="tags" />
{/if}
</ModalHeader>
<ModalBody>
<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.
</Alert>
<Alert color="info">
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}/>
<ul class="list-group">
{#each allTagsFiltered as tag}
<ListGroupItem>
<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)}>
<Icon name="x" />
</Button>
{:else}
<Button size="sm" outline color="success"
on:click={() => addTagToJob(tag)}>
<Icon name="plus" />
</Button>
{/if}
</span>
</ListGroupItem>
<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)}
>
<Icon name="x" />
</Button>
{:else}
<ListGroupItem disabled>
<i>No tags matching</i>
</ListGroupItem>
{/each}
</ul>
<br/>
{#if newTagType && newTagName && isNewTag(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}/>
</Button>
{:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<Button
size="sm"
outline
color="success"
on:click={() => addTagToJob(tag)}
>
<Icon name="plus" />
</Button>
{/if}
</span>
</ListGroupItem>
{:else}
<ListGroupItem disabled>
<i>No tags matching</i>
</ListGroupItem>
{/each}
</ul>
<br />
{#if newTagType && newTagName && isNewTag(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} />
</Button>
{:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>
<Button outline on:click={() => (isOpen = true)}>
Manage Tags <Icon name="tags" />
Manage Tags <Icon name="tags" />
</Button>
<style>
ul.list-group {
max-height: 450px;
margin-bottom: 10px;
overflow: scroll;
}
</style>

View File

@ -1,237 +1,276 @@
<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 showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
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 showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!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!]) {
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 }
})
const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
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 },
});
onMount(() => filterComponent.update())
onMount(() => filterComponent.update());
</script>
<Row>
{#if $initq.fetching}
<Col>
<Spinner/>
</Col>
{:else if $initq.error}
<Col xs="auto">
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
{#if $initq.fetching}
<Col>
<Spinner />
</Col>
{:else if $initq.error}
<Col xs="auto">
<Button
outline color="primary"
on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up"/> Sorting
</Button>
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
<Button
outline color="primary"
on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up"/> Metrics
</Button>
<Col xs="auto">
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting
</Button>
<Button
outline color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}>
<Icon name="bar-chart-line"/> Select Histograms
</Button>
</Col>
<Col xs="auto">
<Filters
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)
}} />
</Col>
<Col xs="auto" style="margin-left: auto;">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
<Button
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
</Button>
</Col>
<Col xs="auto">
<Filters
{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);
}}
/>
</Col>
<Col xs="auto" style="margin-left: auto;">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
</Row>
<br/>
<br />
<Row>
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col xs="4">
<Table>
<tbody>
<tr>
<th scope="row">Username</th>
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
</tr>
{#if user.name}
<tr>
<th scope="row">Name</th>
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
</tr>
{/if}
{#if user.email}
<tr>
<th scope="row">Email</th>
<td>{user.email}</td>
</tr>
{/if}
<tr>
<th scope="row">Total Jobs</th>
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
</tr>
<tr>
<th scope="row">Short Jobs</th>
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
</tr>
<tr>
<th scope="row">Total Walltime</th>
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
</tr>
<tr>
<th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr>
</tbody>
</Table>
</Col>
<div class="col-4 text-center" bind:clientWidth={w1}>
{#key $stats.data.jobsStatistics[0].histDuration}
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
width={w1 - 25} height={histogramHeight}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of 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}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
{/if}
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col xs="4">
<Table>
<tbody>
<tr>
<th scope="row">Username</th>
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
</tr>
{#if user.name}
<tr>
<th scope="row">Name</th>
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
</tr>
{/if}
{#if user.email}
<tr>
<th scope="row">Email</th>
<td>{user.email}</td>
</tr>
{/if}
<tr>
<th scope="row">Total Jobs</th>
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
</tr>
<tr>
<th scope="row">Short Jobs</th>
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
</tr>
<tr>
<th scope="row">Total Walltime</th>
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
</tr>
<tr>
<th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr>
</tbody>
</Table>
</Col>
<div class="col-4 text-center" bind:clientWidth={w1}>
{#key $stats.data.jobsStatistics[0].histDuration}
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
width={w1 - 25}
height={histogramHeight}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of 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}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"
/>
{/key}
</div>
{/if}
</Row>
{#if metricsInHistograms}
<Row>
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col>
{#key $stats.data.jobsStatistics[0].histMetrics}
<PlotTable
let:item
let:width
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
width={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"/>
</PlotTable>
{/key}
</Col>
{/if}
</Row>
<Row>
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col>
{#key $stats.data.jobsStatistics[0].histMetrics}
<PlotTable
let:item
let:width
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}
>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
{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"
/>
</PlotTable>
{/key}
</Col>
{/if}
</Row>
{/if}
<br/>
<br />
<Row>
<Col>
<JobList
bind:metrics={metrics}
bind:sorting={sorting}
bind:this={jobList}
bind:showFootprint={showFootprint} />
</Col>
<Col>
<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
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint
view="list"
/>
<MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics={metrics}
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
<HistogramSelection
bind:cluster={selectedCluster}
bind:metricsInHistograms={metricsInHistograms}
bind:isOpen={isHistogramSelectionOpen} />
bind:cluster={selectedCluster}
bind:metricsInHistograms
bind:isOpen={isHistogramSelectionOpen}
/>

View File

@ -1,60 +1,65 @@
<script>
import { Icon, InputGroup, InputGroupText } from 'sveltestrap';
import { Icon, InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
export let timeseriesPlots;
export let timeseriesPlots;
let windowSize = 100; // Goes from 0 to 100
let windowPosition = 50; // Goes from 0 to 100
let windowSize = 100; // Goes from 0 to 100
let windowPosition = 50; // Goes from 0 to 100
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));
}
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),
);
}
// 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);
// 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);
timeoutId = window.requestAnimationFrame(() => {
updatePlots();
timeoutId = null;
}, 100);
}
timeoutId = window.requestAnimationFrame(() => {
updatePlots();
timeoutId = null;
}, 100);
}
$: requestUpdatePlots(windowSize, windowPosition);
$: requestUpdatePlots(windowSize, windowPosition);
</script>
<div>
<InputGroup>
<InputGroupText>
<Icon name="zoom-in"/>
</InputGroupText>
<InputGroupText>
Window Size:
<input
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowSize}
min=1 max=100 step=1 />
<span style="width: 5em;">
({windowSize}%)
</span>
</InputGroupText>
<InputGroupText>
Window Position:
<input
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowPosition}
min=0 max=100 step=1 />
</InputGroupText>
</InputGroup>
<InputGroup>
<InputGroupText>
<Icon name="zoom-in" />
</InputGroupText>
<InputGroupText>
Window Size:
<input
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowSize}
min="1"
max="100"
step="1"
/>
<span style="width: 5em;">
({windowSize}%)
</span>
</InputGroupText>
<InputGroupText>
Window Position:
<input
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowPosition}
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
})
}
function getUserList() {
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
})
}
function getValidRoles() {
fetch("/api/roles/")
.then((res) => res.json())
.then((rolesRaw) => {
roles = rolesRaw;
});
}
function initAdmin() {
getUserList()
getValidRoles()
}
onMount(() => initAdmin())
function initAdmin() {
getUserList();
getValidRoles();
}
onMount(() => initAdmin());
</script>
<Row cols={2} class="p-2 g-2" >
<Col class="mb-1">
<AddUser roles={roles} on:reload={getUserList}/>
</Col>
<Col class="mb-1">
<ShowUsers on:reload={getUserList} bind:users={users}/>
</Col>
<Col>
<EditRole roles={roles} on:reload={getUserList}/>
</Col>
<Col>
<EditProject on:reload={getUserList}/>
</Col>
<Col>
<Options/>
</Col>
<Row cols={2} class="p-2 g-2">
<Col class="mb-1">
<AddUser {roles} on:reload={getUserList} />
</Col>
<Col class="mb-1">
<ShowUsers on:reload={getUserList} bind:users />
</Col>
<Col>
<EditRole {roles} on:reload={getUserList} />
</Col>
<Col>
<EditProject on:reload={getUserList} />
</Col>
<Col>
<Options />
</Col>
</Row>

View File

@ -1,171 +1,498 @@
<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)']
};
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)",
],
};
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector)
let formData = new FormData(form)
try {
const res = await fetch(form.action, { method: 'POST', body: formData });
if (res.ok) {
let text = await res.text()
popMessage(text, target, '#048109')
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, target, '#d63384')
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector);
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text();
popMessage(text, target, "#048109");
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
return false
} catch (err) {
popMessage(err, target, "#d63384");
}
function popMessage(response, restarget, rescolor) {
message = {msg: response, target: restarget, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
return false;
}
function popMessage(response, restarget, rescolor) {
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">
<!-- 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')}>
<!-- 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;">
<div>Line Width</div>
<!-- Expand If-Clause for clarity once -->
{#if displayMessage && message.target == 'lw'}
<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_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>
<!-- LINE WIDTH -->
<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")}
>
<!-- 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;"
>
<div>Line Width</div>
<!-- Expand If-Clause for clarity once -->
{#if displayMessage && message.target == "lw"}
<div style="margin-left: auto; font-size: 0.9em;">
<code style="color: {message.color};" out:fade>
Update: {message.msg}
</code>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col>
{/if}
</CardTitle>
<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>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</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')}>
<!-- 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;">
<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}
</CardTitle>
<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>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</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")}
>
<!-- 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;"
>
<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}
</CardTitle>
<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>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</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')}>
<!-- 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;">
<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}
</CardTitle>
<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/>
{:else}
<input type="radio" id="true" name="value" value="true" />
{/if}
<label for="true">Yes</label>
</div>
<div>
{#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/>
{/if}
<label for="false">No</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</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")}
>
<!-- 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;"
>
<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}
</CardTitle>
<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 />
{:else}
<input type="radio" id="true" name="value" value="true" />
{/if}
<label for="true">Yes</label>
</div>
<div>
{#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
/>
{/if}
<label for="false">No</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</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">
<!-- 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;">
<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}
</CardTitle>
<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")}/>
{:else}
<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>
{/each}
</td>
</tr>
{/each}
</tbody>
</Table>
</form>
</Card></Col>
<!-- COLORSCHEME -->
<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;"
>
<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}
</CardTitle>
<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")}
/>
{:else}
<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>
{/each}
</td>
</tr>
{/each}
</tbody>
</Table>
</form>
</Card></Col
>
</Row>
<style>
.color-dot {
height: 10px;
width: 10px;
border-radius: 50%;
display: inline-block;
}
.color-dot {
height: 10px;
width: 10px;
border-radius: 50%;
display: inline-block;
}
</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)
async function handleUserSubmit() {
let form = document.querySelector("#create-user-form");
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: 'POST', body: formData });
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
form.reset()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
try {
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
form.reset();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
function popMessage(response, rescolor) {
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch('reload')
}
function reloadUserList() {
dispatch("reload");
}
</script>
<Card>
<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"/>
<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>
</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>
</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"/>
<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"/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<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"
/>
<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>
</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>
</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"
/>
<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"
/>
<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>
</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>
</div>
{:else}
<div>
<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}
</p>
</form>
<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
>
</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
>
</div>
{:else}
<div>
<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}
</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
async function handleAddProject() {
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
}
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 })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
if (username == "" || project == "") {
alert("Please fill in a username and select a project.");
return;
}
async function handleRemoveProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
let formData = new FormData();
formData.append("username", username);
formData.append("add-project", project);
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
}
try {
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
let formData = new FormData()
formData.append('username', username)
formData.append('remove-project', project)
async function handleRemoveProject() {
const username = document.querySelector("#project-username").value;
const project = document.querySelector("#project-id").value;
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
if (username == "" || project == "") {
alert("Please fill in a username and select a project.");
return;
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
let formData = new FormData();
formData.append("username", username);
formData.append("remove-project", project);
function reloadUserList() {
dispatch('reload')
try {
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
function popMessage(response, rescolor) {
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch("reload");
}
</script>
<Card>
<CardBody>
<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"/>
<!-- 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>
</div>
<p>
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
</p>
</CardBody>
<CardBody>
<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"
/>
<!-- 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
>
</div>
<p>
{#if displayMessage}<b
><code style="color: {message.color};" out:fade
>Update: {message.msg}</code
></b
>{/if}
</p>
</CardBody>
</Card>

View File

@ -1,104 +1,131 @@
<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
async function handleAddRole() {
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
}
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 })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
if (username == "" || role == "") {
alert("Please fill in a username and select a role.");
return;
}
async function handleRemoveRole() {
const username = document.querySelector('#role-username').value
const role = document.querySelector('#role-select').value
let formData = new FormData();
formData.append("username", username);
formData.append("add-role", role);
if (username == "" || role == "") {
alert('Please fill in a username and select a role.')
return
}
try {
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
let formData = new FormData()
formData.append('username', username)
formData.append('remove-role', role)
async function handleRemoveRole() {
const username = document.querySelector("#role-username").value;
const role = document.querySelector("#role-select").value;
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
if (username == "" || role == "") {
alert("Please fill in a username and select a role.");
return;
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
let formData = new FormData();
formData.append("username", username);
formData.append("remove-role", role);
function reloadUserList() {
dispatch('reload')
try {
const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
function popMessage(response, rescolor) {
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch("reload");
}
</script>
<Card>
<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"/>
<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>
{/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>
</div>
<p>
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
</p>
</CardBody>
<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"
/>
<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
>
{/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
>
</div>
<p>
{#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
})
onMount(() => {
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
});
function handleScramble() {
if (!scrambled) {
scrambled = true
window.localStorage.setItem("cc-scramble-names", "true")
} else {
scrambled = false
window.localStorage.removeItem("cc-scramble-names")
}
function handleScramble() {
if (!scrambled) {
scrambled = true;
window.localStorage.setItem("cc-scramble-names", "true");
} else {
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}/>
Active?
</CardBody>
<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}
/>
Active?
</CardBody>
</Card>

View File

@ -1,68 +1,87 @@
<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')
}
const dispatch = createEventDispatcher();
function reloadUserList() {
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 (res.status == 200) {
reloadUserList()
} else {
confirm(res.statusText)
}
})
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 (res.status == 200) {
reloadUserList();
} else {
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>
</p>
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
<Table hover>
<thead>
<tr>
<th>Username</th>
<th>Name</th>
<th>Project(s)</th>
<th>Email</th>
<th>Roles</th>
<th>JWT</th>
<th>Delete</th>
</tr>
</thead>
<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>
</tr>
{:else}
<tr>
<td colspan="4">
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>
</td>
</tr>
{/each}
</tbody>
</Table>
</div>
</CardBody>
<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
>
</p>
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
<Table hover>
<thead>
<tr>
<th>Username</th>
<th>Name</th>
<th>Project(s)</th>
<th>Email</th>
<th>Roles</th>
<th>JWT</th>
<th>Delete</th>
</tr>
</thead>
<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
>
</tr>
{:else}
<tr>
<td colspan="4">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</td>
</tr>
{/each}
</tbody>
</Table>
</div>
</CardBody>
</Card>

View File

@ -1,28 +1,32 @@
<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))
})
}
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));
});
}
</script>
<td>{user.username}</td>
<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>
{:else}
<textarea rows="3" cols="20">{jwt}</textarea>
{/if}
{#if !jwt}
<Button color="success" on:click={getUserJwt(user.username)}
>Gen. JWT</Button
>
{:else}
<textarea rows="3" cols="20">{jwt}</textarea>
{/if}
</td>

View File

@ -1,77 +1,95 @@
<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>
<ModalBody>
{#if $initialized}
<h4>Cluster</h4>
<ListGroup>
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == 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)}>
{cluster.name}
</ListGroupItem>
{/each}
</ListGroup>
{/if}
{#if $initialized && pendingCluster != null}
<br/>
<h4>Partiton</h4>
<ListGroup>
<ListGroupItem
active={pendingPartition == null}
on:click={() => (pendingPartition = null)}>
Any Partition
</ListGroupItem>
{#each clusters.find(c => c.name == pendingCluster).partitions as partition}
<ListGroupItem
active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)}>
{partition}
</ListGroupItem>
{/each}
</ListGroup>
{/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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Cluster & Slurm Partition</ModalHeader>
<ModalBody>
{#if $initialized}
<h4>Cluster</h4>
<ListGroup>
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == 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)
)}
>
{cluster.name}
</ListGroupItem>
{/each}
</ListGroup>
{/if}
{#if $initialized && pendingCluster != null}
<br />
<h4>Partiton</h4>
<ListGroup>
<ListGroupItem
active={pendingPartition == null}
on:click={() => (pendingPartition = null)}
>
Any Partition
</ListGroupItem>
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition}
<ListGroupItem
active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)}
>
{partition}
</ListGroupItem>
{/each}
</ListGroup>
{/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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,158 +1,244 @@
<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)
}
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);
}
reset()
reset();
function secsToHoursAndMins(duration) {
const hours = Math.floor(duration / 3600)
duration -= hours * 3600
const mins = Math.floor(duration / 60)
return { hours, mins }
}
function secsToHoursAndMins(duration) {
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
}
$: 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
function hoursAndMinsToSecs({ hours, mins }) {
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;
</script>
<Modal isOpen={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}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr/>
<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}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<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}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<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}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<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}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<h4>and</h4>
<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}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<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 })
}}>
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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<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}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<h4>and</h4>
<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}
/>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</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}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<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 });
}}
>
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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -10,373 +10,418 @@
- 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 TimeSelection from './TimeSelection.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',
let filters = {
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,
startTime: filterPresets.startTime || { from: null, to: null },
tags: filterPresets.tags || [],
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 || '',
cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null,
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 || "",
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || "",
project: filterPresets.project || "",
jobName: filterPresets.jobName || "",
node: filterPresets.node || null,
numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
node: filterPresets.node || null,
numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
stats: [],
}
stats: [],
};
let isClusterOpen = false,
isJobStatesOpen = false,
isStartTimeOpen = false,
isTagsOpen = false,
isDurationOpen = false,
isResourcesOpen = false,
isStatsOpen = false,
isNodesModified = false,
isHwthreadsModified = false,
isAccsModified = false
let isClusterOpen = false,
isJobStatesOpen = false,
isStartTimeOpen = false,
isTagsOpen = false,
isDurationOpen = false,
isResourcesOpen = false,
isStatsOpen = false,
isNodesModified = false,
isHwthreadsModified = 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]
// 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];
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 })
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 })
if (filters.duration.from || 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 } })
if (filters.duration.moreThan)
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 } })
if (filters.arrayJobId != null)
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 } })
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 } })
if (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 } })
for (let stat of filters.stats)
items.push({ [stat.field]: { from: stat.from, to: stat.to } })
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 });
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 });
if (filters.duration.from || 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 } });
if (filters.duration.moreThan)
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 } });
if (filters.arrayJobId != null)
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 },
});
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,
},
});
if (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 } });
for (let stat of filters.stats)
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)
function changeURL() {
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
let opts = []
if (filters.cluster)
opts.push(`cluster=${filters.cluster}`)
if (filters.node)
opts.push(`node=${filters.node}`)
if (filters.partition)
opts.push(`partition=${filters.partition}`)
if (filters.states.length != allJobStates.length)
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)}`)
// }
if (filters.jobId.length != 0)
if (filters.jobIdMatch != 'in') {
opts.push(`jobId=${filters.jobId}`)
} else {
for (let singleJobId of filters.jobId)
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.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}`)
if (filters.duration.moreThan)
opts.push(`duration=${filters.duration.moreThan}-604800`)
if (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}`)
if (filters.user.length != 0)
if (filters.userMatch != 'in') {
opts.push(`user=${filters.user}`)
} else {
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}`)
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}`);
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)}`,
);
// }
if (filters.jobId.length != 0)
if (filters.jobIdMatch != "in") {
opts.push(`jobId=${filters.jobId}`);
} else {
for (let singleJobId of filters.jobId)
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.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}`);
if (filters.duration.moreThan)
opts.push(`duration=${filters.duration.moreThan}-604800`);
if (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}`,
);
if (filters.user.length != 0)
if (filters.userMatch != "in") {
opts.push(`user=${filters.user}`);
} else {
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 (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>
<Row>
<Col xs="auto">
<ButtonDropdown class="cc-dropdown-on-hover">
<DropdownToggle outline caret color="success">
<Icon name="sliders"/>
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>
Manage Filters
</DropdownItem>
{#if menuText}
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu"/> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill"/> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range"/> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch"/> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags"/> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack"/> Resources
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<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}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</ButtonDropdown>
</Col>
<Col xs="auto">
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
<Col xs="auto">
<ButtonDropdown class="cc-dropdown-on-hover">
<DropdownToggle outline caret color="success">
<Icon name="sliders" />
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>Manage Filters</DropdownItem>
{#if menuText}
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range" /> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch" /> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags" /> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack" /> Resources
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider />
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
{#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}
</DropdownMenu>
</ButtonDropdown>
</Col>
<Col xs="auto">
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
{/if}
{#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(', ')}
</Info>
{/if}
{#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(", ")}
</Info>
{/if}
{#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{#if filters.startTime.text}
{filters.startTime.text}
{:else}
{new Date(filters.startTime.from).toLocaleString()} - {new Date(filters.startTime.to).toLocaleString()}
{/if}
</Info>
{#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{#if filters.startTime.text}
{filters.startTime.text}
{:else}
{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
</Info>
{/if}
{#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
(filters.duration.from % 3600) / 60,
)}m -
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
(filters.duration.to % 3600) / 60,
)}m
</Info>
{/if}
{#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration less than {Math.floor(filters.duration.lessThan / 3600)}h:{Math.floor(filters.duration.lessThan % 3600 / 60)}m
</Info>
{/if}
{#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration less than {Math.floor(
filters.duration.lessThan / 3600,
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration more than {Math.floor(filters.duration.moreThan / 3600)}h:{Math.floor(filters.duration.moreThan % 3600 / 60)}m
</Info>
{/if}
{#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration more than {Math.floor(
filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId}
<Tag id={tagId} clickable={false} />
{/each}
</Info>
{/if}
{#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId}
<Tag id={tagId} clickable={false} />
{/each}
</Info>
{/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null ||
filters.numHWThreads.from != null || filters.numHWThreads.to != null ||
filters.numAccelerators.from != null || filters.numAccelerators.to != null }
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
{#if isNodesModified } Nodes: {filters.numNodes.from} - {filters.numNodes.to} {/if}
{#if isNodesModified && isHwthreadsModified }, {/if}
{#if isHwthreadsModified } HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} {/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified }, {/if}
{#if isAccsModified } Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} {/if}
</Info>
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
{#if isNodesModified}
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{/if}
{#if isNodesModified && isHwthreadsModified},
{/if}
{#if isHwthreadsModified}
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
{/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
{/if}
{#if isAccsModified}
Accelerators: {filters.numAccelerators.from} - {filters
.numAccelerators.to}
{/if}
</Info>
{/if}
{#if filters.node != null }
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node}
</Info>
{/if}
{#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node}
</Info>
{/if}
{#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')}
</Info>
{/if}
</Col>
{#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats
.map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
.join(", ")}
</Info>
{/if}
</Col>
</Row>
<Cluster
disableClusterSelection={disableClusterSelection}
bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster}
bind:partition={filters.partition}
on:update={() => update()} />
{disableClusterSelection}
bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster}
bind:partition={filters.partition}
on:update={() => update()}
/>
<JobStates
bind:isOpen={isJobStatesOpen}
bind:states={filters.states}
on:update={() => update()} />
bind:isOpen={isJobStatesOpen}
bind:states={filters.states}
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()
}} />
bind:isOpen={isStartTimeOpen}
bind:from={filters.startTime.from}
bind:to={filters.startTime.to}
on:update={() => {
delete filters.startTime["text"];
delete filters.startTime["url"];
update();
}}
/>
<Duration
bind:isOpen={isDurationOpen}
bind:lessThan={filters.duration.lessThan}
bind:moreThan={filters.duration.moreThan}
bind:from={filters.duration.from}
bind:to={filters.duration.to}
on:update={() => update()} />
bind:isOpen={isDurationOpen}
bind:lessThan={filters.duration.lessThan}
bind:moreThan={filters.duration.moreThan}
bind:from={filters.duration.from}
bind:to={filters.duration.to}
on:update={() => update()}
/>
<Tags
bind:isOpen={isTagsOpen}
bind:tags={filters.tags}
on:update={() => update()} />
bind:isOpen={isTagsOpen}
bind:tags={filters.tags}
on:update={() => update()}
/>
<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()} />
<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
bind:isHwthreadsModified
bind:isAccsModified
on:update={() => update()}
/>
<Statistics cluster={filters.cluster}
bind:isOpen={isStatsOpen}
bind:stats={filters.stats}
on:update={() => update()} />
<Statistics
cluster={filters.cluster}
bind:isOpen={isStatsOpen}
bind:stats={filters.stats}
on:update={() => update()}
/>
<style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block;
margin-top: 0px;
padding-top: 0px;
transform: none !important;
}
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block;
margin-top: 0px;
padding-top: 0px;
transform: none !important;
}
</style>

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}/>
<slot />
<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>
<ModalBody>
<ListGroup>
{#each allJobStates as state}
<ListGroupItem>
<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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<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}
/>
{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 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)
// 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,
);
// console.log(header)
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 }])
} else if (clusters.length > 0) {
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)
}
}
// console.log(header)
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 }]);
} else if (clusters.length > 0) {
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);
}
}
}
}
$: {
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>
<ModalBody>
<h6>Named Node</h6>
<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
}}
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
}}
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
}}
min={minNumAccelerators} max={maxNumAccelerators}
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to}
inputFieldFrom={pendingNumAccelerators.from} inputFieldTo={pendingNumAccelerators.to}/>
{/if}
</ModalBody>
<ModalFooter>
<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 })
}}>
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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<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} />
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
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>
<DoubleRangeSlider
on:change={({ detail }) => {
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}
/>
{#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;
}}
min={minNumAccelerators}
max={maxNumAccelerators}
firstSlider={pendingNumAccelerators.from}
secondSlider={pendingNumAccelerators.to}
inputFieldFrom={pendingNumAccelerators.from}
inputFieldTo={pendingNumAccelerators.to}
/>
{/if}
</ModalBody>
<ModalFooter>
<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,
});
}}
>
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 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)
}
function reset() {
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') }
}
function fromRFC3339(rfc3339) {
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>
<ModalBody>
<h4>From</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time}/>
</FormGroup>
</Row>
<h4>To</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingTo.date}/>
</FormGroup>
<FormGroup class="col">
<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'}
on:click={() => {
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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<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} />
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time} />
</FormGroup>
</Row>
<h4>To</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingTo.date} />
</FormGroup>
<FormGroup class="col">
<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"}
on:click={() => {
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 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: '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: 'memUsedMax',
text: 'Mem. used (Max.)',
metric: 'mem_used',
from: 0, to: 0, peak: 0,
enabled: false
let statistics = [
{
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: "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,
},
];
$: 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;
});
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find((mc) => mc.name == metric);
return mc ? mc.peak : 0;
}
function resetRange(isInitialized, cluster) {
if (!isInitialized) return;
if (cluster != null) {
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;
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric));
}
]
$: 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
})
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find(mc => mc.name == metric)
return mc ? mc.peak : 0
stat.from = 0;
stat.to = stat.peak;
}
}
function resetRange(isInitialized, cluster) {
if (!isInitialized)
return
statistics = [...statistics];
}
if (cluster != null) {
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
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric))
}
stat.from = 0
stat.to = stat.peak
}
}
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>
<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}/>
{/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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<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}
/>
{/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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,67 +1,89 @@
<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>
<ModalBody>
<Input type="text" placeholder="Search" bind:value={searchTerm} />
<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))}>
<Icon name="dash-circle" />
</Button>
{:else}
<Button outline color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])}>
<Icon name="plus-circle" />
</Button>
{/if}
<Tag tag={tag} />
</ListGroupItem>
{:else}
<ListGroupItem disabled>No Tags</ListGroupItem>
{/each}
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Tags</ModalHeader>
<ModalBody>
<Input type="text" placeholder="Search" bind:value={searchTerm} />
<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))}
>
<Icon name="dash-circle" />
</Button>
{:else}
<Button
outline
color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])}
>
<Icon name="plus-circle" />
</Button>
{/if}
</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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
<Tag {tag} />
</ListGroupItem>
{:else}
<ListGroupItem disabled>No Tags</ListGroupItem>
{/each}
{/if}
</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 on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,81 +1,96 @@
<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 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
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,
};
$: pendingFrom = from;
$: pendingTo = to;
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;
}
if (timeRange == -2) {
from = pendingFrom = null;
to = pendingTo = null;
dispatch("change", { from, to });
return;
}
$: pendingFrom = from
$: pendingTo = to
let now = Date.now(),
t = timeRange * 1000;
from = pendingFrom = new Date(now - t);
to = pendingTo = new Date(now);
dispatch("change", { from, to });
}
const dispatch = createEventDispatcher()
let timeRange = to && from
? (to.getTime() - from.getTime()) / 1000
: (anyEnabled ? -2 : -1)
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == "from") pendingFrom = d;
else pendingTo = d;
function updateTimeRange(event) {
if (timeRange == -1) {
pendingFrom = null
pendingTo = null
return
}
if (timeRange == -2) {
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 })
}
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == 'from') pendingFrom = d
else pendingTo = d
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom
to = pendingTo
dispatch('change', { from, to })
}
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom;
to = pendingTo;
dispatch("change", { from, to });
}
}
</script>
<InputGroup class="inline-from">
<InputGroupText><Icon name="clock-history"/></InputGroupText>
<!-- <InputGroupText>
<InputGroupText><Icon name="clock-history" /></InputGroupText>
<!-- <InputGroupText>
Time
</InputGroupText> -->
<select class="form-select" bind:value={timeRange} on:change={updateTimeRange}>
{#if customEnabled}
<option value={-1}>Custom</option>
{/if}
{#if anyEnabled}
<option value={-2}>Any</option>
{/if}
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</select>
{#if timeRange == -1}
<InputGroupText>from</InputGroupText>
<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>
<select
class="form-select"
bind:value={timeRange}
on:change={updateTimeRange}
>
{#if customEnabled}
<option value={-1}>Custom</option>
{/if}
{#if anyEnabled}
<option value={-2}>Any</option>
{/if}
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</select>
{#if timeRange == -1}
<InputGroupText>from</InputGroupText>
<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>
{/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
} else {
user = term
term = project
}
termChanged(0)
function modeChanged() {
if (mode == "user") {
project = term;
term = user;
} else {
user = term;
term = project;
}
termChanged(0);
}
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
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 (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
dispatch('update', {
user,
project
})
}, sleep)
} else {
project = term
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
dispatch("update", {
user,
project,
});
}, sleep);
} else {
project = term;
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
dispatch('update', {
project
})
}, sleep)
}
timeoutId = setTimeout(() => {
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>
<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...'} />
</InputGroup>
<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>
<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..."}
/>
</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...'
/>
</InputGroup>
<!-- 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..."
/>
</InputGroup>
{/if}

View File

@ -6,115 +6,138 @@
- 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;
export let job;
export let jobTags = job.tags;
function formatDuration(duration) {
const hours = Math.floor(duration / 3600);
duration -= hours * 3600;
const minutes = Math.floor(duration / 60);
duration -= minutes * 60;
const seconds = duration;
return `${hours}:${('0' + minutes).slice(-2)}:${('0' + seconds).slice(-2)}`;
function formatDuration(duration) {
const hours = Math.floor(duration / 3600);
duration -= hours * 3600;
const minutes = Math.floor(duration / 60);
duration -= minutes * 60;
const seconds = duration;
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`;
}
function getStateColor(state) {
switch (state) {
case "running":
return "success";
case "completed":
return "primary";
default:
return "danger";
}
function getStateColor(state) {
switch (state) {
case 'running':
return 'success'
case 'completed':
return 'primary'
default:
return 'danger'
}
}
}
</script>
<div>
<p>
<span class="fw-bold"><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> ({job.cluster})</span>
{#if job.metaData?.jobName}
<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>
{/if}
{/if}
{#if job.arrayJobId}
Array Job: <a href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" target="_blank">#{job.arrayJobId}</a>
{/if}
</p>
<p>
<span class="fw-bold"
><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
({job.cluster})</span
>
{#if job.metaData?.jobName}
<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>
{/if}
{/if}
{#if job.arrayJobId}
Array Job: <a
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
target="_blank">#{job.arrayJobId}</a
>
{/if}
</p>
<p>
<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">
{scrambleNames ? scramble(job.project) : job.project}
</a>
{/if}
</p>
<p>
<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"
>
{scrambleNames ? scramble(job.project) : job.project}
</a>
{/if}
</p>
<p>
{#if job.numNodes == 1}
{job.resources[0].hostname}
{:else}
{job.numNodes}
{/if}
<Icon name="pc-horizontal"/>
{#if job.exclusive != 1}
(shared)
{/if}
{#if job.numAcc > 0}
, {job.numAcc} <Icon name="gpu-card"/>
{/if}
{#if job.numHWThreads > 0}
, {job.numHWThreads} <Icon name="cpu"/>
{/if}
<br/>
{job.subCluster}
</p>
<p>
{#if job.numNodes == 1}
{job.resources[0].hostname}
{:else}
{job.numNodes}
{/if}
<Icon name="pc-horizontal" />
{#if job.exclusive != 1}
(shared)
{/if}
{#if job.numAcc > 0}
, {job.numAcc} <Icon name="gpu-card" />
{/if}
{#if job.numHWThreads > 0}
, {job.numHWThreads} <Icon name="cpu" />
{/if}
<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>
{#if job.walltime}
<br/>
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
{/if}
</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>
{#if job.walltime}
<br />
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
{/if}
</p>
<p>
{#each jobTags as tag}
<Tag tag={tag}/>
{/each}
</p>
<p>
{#each jobTags as tag}
<Tag {tag} />
{/each}
</p>
</div>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -9,284 +9,275 @@
- update(filters?: [JobFilter])
-->
<script>
import {
queryStore,
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import { getContext } from "svelte";
import { Row, Table, Card, Spinner } from "sveltestrap";
import Pagination from "./Pagination.svelte";
import JobListRow from "./Row.svelte";
import { stickyHeader } from "../utils.js";
import {
queryStore,
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import { getContext } from "svelte";
import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap";
import Pagination from "./Pagination.svelte";
import JobListRow from "./Row.svelte";
import { stickyHeader } from "../utils.js";
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
initialized = getContext("initialized");
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
initialized = getContext("initialized");
export let sorting = { field: "startTime", order: "DESC" };
export let matchedJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
export let showFootprint;
export let sorting = { field: "startTime", order: "DESC" };
export let matchedJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
export let showFootprint;
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1;
let paging = { itemsPerPage, page };
let filter = [];
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1;
let paging = { itemsPerPage, page };
let filter = [];
const client = getContextClient();
const query = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
id
jobId
user
project
cluster
subCluster
startTime
duration
numNodes
numHWThreads
numAcc
walltime
resources {
hostname
}
SMT
exclusive
partition
arrayJobId
monitoringStatus
state
tags {
id
type
name
}
userData {
name
}
metaData
flopsAnyAvg
memBwAvg
loadAvg
}
count
}
const client = getContextClient();
const query = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
id
jobId
user
project
cluster
subCluster
startTime
duration
numNodes
numHWThreads
numAcc
walltime
resources {
hostname
}
SMT
exclusive
partition
arrayJobId
monitoringStatus
state
tags {
id
type
name
}
userData {
name
}
metaData
flopsAnyAvg
memBwAvg
loadAvg
}
`;
count
}
}
`;
$: jobs = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter }
$: jobs = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
});
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refresh() {
jobs = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
requestPolicy: "network-only",
});
}
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refresh() {
jobs = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
requestPolicy: 'network-only'
});
// (Re-)query and optionally set new filters.
export function update(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor });
}
filter = filters;
}
page = 1;
paging = paging = { page, itemsPerPage };
}
// (Re-)query and optionally set new filters.
export function update(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor });
}
filter = filters;
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
page = 1;
paging = paging = { page, itemsPerPage };
}
`,
variables: { name, value },
});
};
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
});
}
function updateConfiguration(value, page) {
updateConfigurationMutation({
name: "plot_list_jobsPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
});
}
function updateConfiguration(value, page) {
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
// console.log('Error on subscription: ' + res.error)
}
})
};
let plotWidth = null;
let tableWidth = null;
let jobInfoColumnWidth = 250;
let plotWidth = null;
let tableWidth = null;
let jobInfoColumnWidth = 250;
$: if (showFootprint) {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10
)
} else {
plotWidth = Math.floor(
(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)
$: if (showFootprint) {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
);
} else {
plotWidth = Math.floor(
(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),
);
</script>
<Row>
<div class="col cc-table-wrapper" bind:clientWidth={tableWidth}>
<Table cellspacing="0px" cellpadding="0px">
<thead>
<tr>
<th
class="position-sticky top-0"
scope="col"
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"
>
Job Info
</th>
{#if showFootprint}
<th
class="position-sticky top-0"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
Job Footprint
</th>
{/if}
{#each metrics as metric (metric)}
<th
class="position-sticky top-0 text-center"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
{metric}
{#if $initialized}
({clusters
.map((cluster) =>
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 : "")
) // Build unitStr
.reduce(
(arr, unitStr) =>
arr.includes(unitStr)
? arr
: [...arr, unitStr],
[]
) // w/o this, output would be [unitStr, unitStr]
.join(", ")})
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#if $jobs.error}
<tr>
<td colspan={metrics.length + 1}>
<Card body color="danger" class="mb-3"
><h2>{$jobs.error.message}</h2></Card
>
</td>
</tr>
{:else if $jobs.fetching || !$jobs.data}
<tr>
<td colspan={metrics.length + 1}>
<Spinner secondary />
</td>
</tr>
{:else if $jobs.data && $initialized}
{#each $jobs.data.jobs.items as job (job)}
<JobListRow {job} {metrics} {plotWidth} {showFootprint}/>
{:else}
<tr>
<td colspan={metrics.length + 1}>
No jobs found
</td>
</tr>
{/each}
{/if}
</tbody>
</Table>
</div>
<div class="col cc-table-wrapper" bind:clientWidth={tableWidth}>
<Table cellspacing="0px" cellpadding="0px">
<thead>
<tr>
<th
class="position-sticky top-0"
scope="col"
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"
>
Job Info
</th>
{#if showFootprint}
<th
class="position-sticky top-0"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
Job Footprint
</th>
{/if}
{#each metrics as metric (metric)}
<th
class="position-sticky top-0 text-center"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
{metric}
{#if $initialized}
({clusters
.map((cluster) =>
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 : ""),
) // Build unitStr
.reduce(
(arr, unitStr) =>
arr.includes(unitStr) ? arr : [...arr, unitStr],
[],
) // w/o this, output would be [unitStr, unitStr]
.join(", ")})
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#if $jobs.error}
<tr>
<td colspan={metrics.length + 1}>
<Card body color="danger" class="mb-3"
><h2>{$jobs.error.message}</h2></Card
>
</td>
</tr>
{:else if $jobs.fetching || !$jobs.data}
<tr>
<td colspan={metrics.length + 1}>
<Spinner secondary />
</td>
</tr>
{:else if $jobs.data && $initialized}
{#each $jobs.data.jobs.items as job (job)}
<JobListRow {job} {metrics} {plotWidth} {showFootprint} />
{:else}
<tr>
<td colspan={metrics.length + 1}> No jobs found </td>
</tr>
{/each}
{/if}
</tbody>
</Table>
</div>
</Row>
<Pagination
bind:page
{itemsPerPage}
itemText="Jobs"
totalItems={matchedJobs}
on:update={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(
detail.itemsPerPage.toString(),
detail.page
)
} else {
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
}
}}
bind:page
{itemsPerPage}
itemText="Jobs"
totalItems={matchedJobs}
on:update={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else {
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
}
}}
/>
<style>
.cc-table-wrapper {
overflow: initial;
}
.cc-table-wrapper {
overflow: initial;
}
.cc-table-wrapper > :global(table) {
border-collapse: separate;
border-spacing: 0px;
table-layout: fixed;
}
.cc-table-wrapper > :global(table) {
border-collapse: separate;
border-spacing: 0px;
table-layout: fixed;
}
.cc-table-wrapper :global(button) {
margin-bottom: 0px;
}
.cc-table-wrapper :global(button) {
margin-bottom: 0px;
}
.cc-table-wrapper > :global(table > tbody > tr > td) {
margin: 0px;
padding-left: 5px;
padding-right: 0px;
}
.cc-table-wrapper > :global(table > tbody > tr > td) {
margin: 0px;
padding-left: 5px;
padding-right: 0px;
}
th.position-sticky.top-0 {
background-color: white;
z-index: 10;
border-bottom: 1px solid black;
}
th.position-sticky.top-0 {
background-color: white;
z-index: 10;
border-bottom: 1px solid black;
}
</style>

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);
let refreshInterval = null;
let refreshIntervalId = null;
function refreshIntervalChanged() {
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
if (refreshInterval == null)
return;
if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
}
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
}
export let initially = null
if (initially != null) {
refreshInterval = initially * 1000
refreshIntervalChanged()
}
export let initially = null;
if (initially != null) {
refreshInterval = initially * 1000;
refreshIntervalChanged();
}
</script>
<InputGroup>
<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}>
<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={2 * 60 * 1000}>Update every two minutes</option>
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
</select>
</InputGroup>
<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}
>
<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={2 * 60 * 1000}>Update every two minutes</option>
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
</select>
</InputGroup>

View File

@ -9,169 +9,190 @@
-->
<script>
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte";
import { Card, Spinner } from "sveltestrap";
import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from "./JobInfo.svelte";
import JobFootprint from "../JobFootprint.svelte";
import { maxScope, checkMetricDisabled } from "../utils.js";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap";
import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from "./JobInfo.svelte";
import JobFootprint from "../JobFootprint.svelte";
import { maxScope, checkMetricDisabled } from "../utils.js";
export let job;
export let metrics;
export let plotWidth;
export let plotHeight = 275;
export let showFootprint;
export let job;
export let metrics;
export let plotWidth;
export let plotHeight = 275;
export let showFootprint;
let { id } = job;
let scopes = [job.numNodes == 1 ? "core" : "node"];
let { id } = job;
let scopes = [job.numNodes == 1 ? "core" : "node"];
function distinct(value, index, array) {
return array.indexOf(value) === index;
}
function distinct(value, index, array) {
return array.indexOf(value) === index;
}
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
const client = getContextClient();
const query = gql`
query ($id: ID!, $queryMetrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $queryMetrics, scopes: $scopes) {
name
scope
metric {
unit {
prefix
base
}
timestep
statisticsSeries {
min
mean
max
}
series {
hostname
id
data
statistics {
min
avg
max
}
}
}
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
const client = getContextClient();
const query = gql`
query ($id: ID!, $queryMetrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $queryMetrics, scopes: $scopes) {
name
scope
metric {
unit {
prefix
base
}
timestep
statisticsSeries {
min
mean
max
}
series {
hostname
id
data
statistics {
min
avg
max
}
}
}
`;
}
}
`;
$: metricsQuery = queryStore({
client: client,
query: query,
variables: { id, queryMetrics, scopes }
$: metricsQuery = queryStore({
client: client,
query: query,
variables: { id, queryMetrics, scopes },
});
let queryMetrics = null;
$: if (showFootprint) {
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"];
}
export function refresh() {
metricsQuery = queryStore({
client: client,
query: query,
variables: { id, queryMetrics, scopes },
// requestPolicy: 'network-only' // use default cache-first for refresh
});
}
let queryMetrics = null
$: if (showFootprint) {
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"]
}
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope
? job.numNodes > 1
? a
: b
: job.numNodes > 1
? b
: a,
jobMetrics[0],
);
export function refresh() {
metricsQuery = queryStore({
client: client,
query: query,
variables: { id, queryMetrics, scopes },
// requestPolicy: 'network-only' // use default cache-first for refresh
});
}
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,
};
} else {
return jobMetric;
}
});
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope
? job.numNodes > 1
? a
: b
: job.numNodes > 1
? b
: a,
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 => {
if (jobMetric.data) {
return { disabled: checkMetricDisabled(jobMetric.data.name, job.cluster, job.subCluster), data: jobMetric.data }
} else {
return jobMetric
}
})
if (job.monitoringStatus) refresh();
if (job.monitoringStatus) refresh();
</script>
<tr>
<td>
<JobInfo {job} />
<td>
<JobInfo {job} />
</td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan={metrics.length}>
<Card body color="warning">Not monitored or archiving failed</Card>
</td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan={metrics.length}>
<Card body color="warning">Not monitored or archiving failed</Card>
</td>
{:else if $metricsQuery.fetching}
<td colspan={metrics.length} style="text-align: center;">
<Spinner secondary />
</td>
{:else if $metricsQuery.error}
<td colspan={metrics.length}>
<Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message}
</Card>
</td>
{:else}
{#if showFootprint}
<td>
<JobFootprint
job={job}
jobMetrics={$metricsQuery.data.jobMetrics}
width={plotWidth}
view="list"
/>
</td>
{/if}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.disabled == false && metric.data}
<MetricPlot
width={plotWidth}
height={plotHeight}
timestep={metric.data.metric.timestep}
scope={metric.data.scope}
series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name}
{cluster}
subCluster={job.subCluster}
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>
{:else}
<Card body color="warning">No dataset returned</Card>
{/if}
</td>
{/each}
{:else if $metricsQuery.fetching}
<td colspan={metrics.length} style="text-align: center;">
<Spinner secondary />
</td>
{:else if $metricsQuery.error}
<td colspan={metrics.length}>
<Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message}
</Card>
</td>
{:else}
{#if showFootprint}
<td>
<JobFootprint
{job}
jobMetrics={$metricsQuery.data.jobMetrics}
width={plotWidth}
view="list"
/>
</td>
{/if}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.disabled == false && metric.data}
<MetricPlot
width={plotWidth}
height={plotHeight}
timestep={metric.data.metric.timestep}
scope={metric.data.scope}
series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name}
{cluster}
subCluster={job.subCluster}
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
>
{:else}
<Card body color="warning">No dataset returned</Card>
{/if}
</td>
{/each}
{/if}
</tr>

View File

@ -7,65 +7,94 @@
-->
<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' }
]
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" },
];
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>
<ModalBody>
<ListGroup>
{#each sortableColumns as col, i (col)}
<ListGroupItem>
<button class="sort" on:click={() => {
if (activeColumnIdx == i) {
col.order = col.order == 'DESC' ? 'ASC' : 'DESC'
} else {
sortableColumns[activeColumnIdx] = { ...sortableColumns[activeColumnIdx] }
}
<Modal
{isOpen}
toggle={() => {
isOpen = !isOpen;
}}
>
<ModalHeader>Sort rows</ModalHeader>
<ModalBody>
<ListGroup>
{#each sortableColumns as col, i (col)}
<ListGroupItem>
<button
class="sort"
on:click={() => {
if (activeColumnIdx == i) {
col.order = col.order == "DESC" ? "ASC" : "DESC";
} else {
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' : ''}"/>
</button>
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}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => { isOpen = false }}>Close</Button>
</ModalFooter>
{col.text}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button
color="primary"
on:click={() => {
isOpen = false;
}}>Close</Button
>
</ModalFooter>
</Modal>
<style>
.sort {
border: none;
margin: 0;
padding: 0;
background: 0 0;
transition: all 70ms;
}
</style>
.sort {
border: none;
margin: 0;
padding: 0;
background: 0 0;
transition: all 70ms;
}
</style>

View File

@ -5,221 +5,222 @@
-->
<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,
points: 2,
const drawStyles = {
bars: 1,
points: 2,
};
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
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;
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
}
// converts the legend into a simple tooltip
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// hide series color markers
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++) idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
// let tooltip exit plot
// overEl.style.overflow = "visible";
}
function update(u) {
const { left, top } = u.cursor;
legendEl.style.transform =
"translate(" + (left + 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
},
};
}
let plotWrapper = null;
let uplot = null;
let timeoutId = null;
function render() {
let opts = {
width: width,
height: height,
title: title,
plugins: [legendAsTooltipPlugin()],
cursor: {
points: {
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
width: (u, seriesIdx, size) => size / 4,
stroke: (u, seriesIdx) =>
u.series[seriesIdx].points.stroke(u, seriesIdx) + "90",
fill: (u, seriesIdx) => "#fff",
},
},
scales: {
x: {
time: false,
},
},
axes: [
{
stroke: "#000000",
// scale: 'x',
label: xlabel,
labelGap: 10,
size: 25,
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
border: {
show: true,
stroke: "#000000",
},
ticks: {
width: 1 / devicePixelRatio,
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map((v) => formatNumber(v)),
},
{
stroke: "#000000",
// scale: 'y',
label: ylabel,
labelGap: 10,
size: 35,
border: {
show: true,
stroke: "#000000",
},
ticks: {
width: 1 / devicePixelRatio,
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map((v) => formatNumber(v)),
},
],
series: [
{
label: xunit !== "" ? xunit : null,
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;
},
},
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
},
),
],
};
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
let s = u.series[seriesIdx];
let style = s.drawStyle;
uplot = new uPlot(opts, data, plotWrapper);
}
let renderer = ( // If bars to wide, change here
style == drawStyles.bars ? (
bars({size: [0.75, 100]})
) :
() => null
)
onMount(() => {
render();
});
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
}
onDestroy(() => {
if (uplot) uplot.destroy();
// converts the legend into a simple tooltip
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
let legendEl;
if (timeoutId != null) clearTimeout(timeoutId);
});
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
function sizeChanged() {
if (timeoutId != null) clearTimeout(timeoutId);
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
timeoutId = setTimeout(() => {
timeoutId = null;
if (uplot) uplot.destroy();
uPlot.assign(legendEl.style, {
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style
});
render();
}, 200);
}
// hide series color markers
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
// let tooltip exit plot
// overEl.style.overflow = "visible";
}
function update(u) {
const { left, top } = u.cursor;
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
}
};
}
let plotWrapper = null
let uplot = null
let timeoutId = null
function render() {
let opts = {
width: width,
height: height,
title: title,
plugins: [
legendAsTooltipPlugin()
],
cursor: {
points: {
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
width: (u, seriesIdx, size) => size / 4,
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90',
fill: (u, seriesIdx) => "#fff",
}
},
scales: {
x: {
time: false
},
},
axes: [
{
stroke: "#000000",
// scale: 'x',
label: xlabel,
labelGap: 10,
size: 25,
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
border: {
show: true,
stroke: "#000000",
},
ticks: {
width: 1 / devicePixelRatio,
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map(v => formatNumber(v)),
},
{
stroke: "#000000",
// scale: 'y',
label: ylabel,
labelGap: 10,
size: 35,
border: {
show: true,
stroke: "#000000",
},
ticks: {
width: 1 / devicePixelRatio,
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map(v => formatNumber(v)),
},
],
series: [
{
label: xunit !== '' ? xunit : null,
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
}
},
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)
}
onMount(() => {
render()
})
onDestroy(() => {
if (uplot)
uplot.destroy()
if (timeoutId != null)
clearTimeout(timeoutId)
})
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
if (uplot)
uplot.destroy()
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,433 +139,407 @@
// 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" } } = {}) {
let legendEl;
const dataSize = series.length
// converts the legend into a simple tooltip
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
const dataSize = series.length;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style
});
uPlot.assign(legendEl.style, {
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// conditional hide series color markers:
if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory
dataSize === 1 || // Only one Y-Dataseries
dataSize > 6 ){ // More than 6 Y-Dataseries
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
}
// conditional hide series color markers:
if (
useStatsSeries === true || // Min/Max/Avg Self-Explanatory
dataSize === 1 || // Only one Y-Dataseries
dataSize > 6
) {
// More than 6 Y-Dataseries
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
}
const overEl = u.over;
overEl.style.overflow = "visible";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
// let tooltip exit plot
// overEl.style.overflow = "visible";
}
function update(u) {
const { left, top } = u.cursor;
const width = u.over.querySelector(".u-legend").offsetWidth;
legendEl.style.transform = "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
if (dataSize <= 12 || useStatsSeries === true) {
return {
hooks: {
init: init,
setCursor: update,
}
}
} else { // Setting legend-opts show/live as object with false here will not work ...
return {}
}
// let tooltip exit plot
// overEl.style.overflow = "visible";
}
function backgroundColor() {
if (clusterCockpitConfig.plot_general_colorBackground == false
|| !thresholds
|| !(series && series.every(s => s.statistics != null)))
return backgroundColors.normal
let cond = thresholds.alert < thresholds.caution
? (a, b) => a <= b
: (a, b) => a >= b
let avg = series.reduce((sum, series) => sum + series.statistics.avg, 0) / series.length
if (Number.isNaN(avg))
return backgroundColors.normal
if (cond(avg, thresholds.alert))
return backgroundColors.alert
if (cond(avg, thresholds.caution))
return backgroundColors.caution
return backgroundColors.normal
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)";
}
function lineColor(i, n) {
if (n >= lineColors.length)
return lineColors[i % lineColors.length];
else
return lineColors[Math.floor((i / n) * lineColors.length)];
if (dataSize <= 12 || useStatsSeries === true) {
return {
hooks: {
init: init,
setCursor: update,
},
};
} else {
// Setting legend-opts show/live as object with false here will not work ...
return {};
}
}
const longestSeries = useStatsSeries
? statisticsSeries.mean.length
: 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)
function backgroundColor() {
if (
clusterCockpitConfig.plot_general_colorBackground == false ||
!thresholds ||
!(series && series.every((s) => s.statistics != null))
)
return backgroundColors.normal;
if (maxY >= (10 * thresholds.peak)) { // Hard y-range render limit if outliers in series data
maxY = (10 * thresholds.peak)
}
let cond =
thresholds.alert < thresholds.caution
? (a, b) => a <= b
: (a, b) => a >= b;
let avg =
series.reduce((sum, series) => sum + series.statistics.avg, 0) /
series.length;
if (Number.isNaN(avg)) return backgroundColors.normal;
if (cond(avg, thresholds.alert)) return backgroundColors.alert;
if (cond(avg, thresholds.caution)) return backgroundColors.caution;
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)];
}
const longestSeries = useStatsSeries
? statisticsSeries.mean.length
: 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;
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;
}
} else {
// Positive Timestamp Buildup
for (
let j = 0;
j < longestSeries;
j++ // TODO: Cache/Reuse this array?
)
plotData[0][j] = j * timestep;
}
let plotBands = undefined;
if (useStatsSeries) {
plotData.push(statisticsSeries.min);
plotData.push(statisticsSeries.max);
plotData.push(statisticsSeries.mean);
if (forNode === true) {
// Negative Timestamp Buildup
for (let i = 0; i <= longestSeries; i++) {
plotData[0][i] = (longestSeries - i) * timestep * -1
}
// timestamp 0 with null value for reversed time axis
if (plotData[1].length != 0) plotData[1].push(null);
if (plotData[2].length != 0) plotData[2].push(null);
if (plotData[3].length != 0) plotData[3].push(null);
}
plotSeries.push({
label: "min",
scale: "y",
width: lineWidth,
stroke: "red",
});
plotSeries.push({
label: "max",
scale: "y",
width: lineWidth,
stroke: "green",
});
plotSeries.push({
label: "mean",
scale: "y",
width: lineWidth,
stroke: "black",
});
plotBands = [
{ series: [2, 3], fill: "rgba(0,255,0,0.1)" },
{ series: [3, 1], fill: "rgba(255,0,0,0.1)" },
];
} else {
for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data);
if (forNode === true && plotData[1].length != 0) plotData[1].push(null); // timestamp 0 with null value for reversed time axis
plotSeries.push({
label:
scope === "node"
? resources[i].hostname
: // scope === 'accelerator' ? resources[0].accelerators[i] :
scope + " #" + (i + 1),
scale: "y",
width: lineWidth,
stroke: lineColor(i, series.length),
});
}
}
const opts = {
width,
height,
plugins: [legendAsTooltipPlugin()],
series: plotSeries,
axes: [
{
scale: "x",
space: 35,
incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
},
{
scale: "y",
grid: { show: true },
labelFont: "sans-serif",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {
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,
);
// 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;
}
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] } : {},
},
legend: {
// Display legend until max 12 Y-dataseries
show: series.length <= 12 || useStatsSeries === true ? true : false,
live: series.length <= 12 || useStatsSeries === true ? true : false,
},
cursor: { drag: { x: true, y: true } },
};
// console.log(opts)
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 (prevWidth != null && Math.abs(prevWidth - width) < 10) return;
prevWidth = width;
prevHeight = height;
if (!uplot) {
opts.width = width;
opts.height = height;
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
// Positive Timestamp Buildup
for (let j = 0; j < longestSeries; j++) // TODO: Cache/Reuse this array?
plotData[0][j] = j * timestep
uplot.setSize({ width, height });
}
}
let plotBands = undefined
if (useStatsSeries) {
plotData.push(statisticsSeries.min)
plotData.push(statisticsSeries.max)
plotData.push(statisticsSeries.mean)
function onSizeChange() {
if (!uplot) return;
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 (timeoutId != null) clearTimeout(timeoutId);
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)' }
];
} else {
for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data)
if (forNode === true && plotData[1].length != 0) plotData[1].push(null) // timestamp 0 with null value for reversed time axis
plotSeries.push({
label: scope === 'node' ? resources[i].hostname :
// scope === 'accelerator' ? resources[0].accelerators[i] :
scope + ' #' + (i+1),
scale: 'y',
width: lineWidth,
stroke: lineColor(i, series.length)
})
}
timeoutId = setTimeout(() => {
timeoutId = null;
render();
}, resizeSleepTime);
}
$: if (series[0].data.length > 0) {
onSizeChange(width, height);
}
onMount(() => {
if (series[0].data.length > 0) {
plotWrapper.style.backgroundColor = backgroundColor();
render();
}
});
const opts = {
width,
height,
plugins: [
legendAsTooltipPlugin()
],
series: plotSeries,
axes: [
{
scale: 'x',
space: 35,
incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map(v => formatTime(v, forNode))
},
{
scale: 'y',
grid: { show: true },
labelFont: 'sans-serif',
values: (u, vals) => vals.map(v => formatNumber(v))
}
],
bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {
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)
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
onDestroy(() => {
if (uplot) uplot.destroy();
if (!thresholds) {
u.ctx.restore()
return
}
if (timeoutId != null) clearTimeout(timeoutId);
});
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] } : {}
},
legend : { // Display legend until max 12 Y-dataseries
show: (series.length <= 12 || useStatsSeries === true) ? true : false,
live: (series.length <= 12 || useStatsSeries === true) ? true : false
},
cursor: { drag: { x: true, y: true } }
}
// console.log(opts)
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 (prevWidth != null && Math.abs(prevWidth - width) < 10)
return
prevWidth = width
prevHeight = height
if (!uplot) {
opts.width = width
opts.height = height
uplot = new uPlot(opts, plotData, plotWrapper)
} else {
uplot.setSize({ width, height })
}
}
function onSizeChange() {
if (!uplot)
return
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
render()
}, resizeSleepTime)
}
$: if (series[0].data.length > 0) {
onSizeChange(width, height)
}
onMount(() => {
if (series[0].data.length > 0) {
plotWrapper.style.backgroundColor = backgroundColor()
render()
}
})
onDestroy(() => {
if (uplot)
uplot.destroy()
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
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
}
}
// `from` and `to` must be numbers between 0 and 1.
export function setTimeRange(from, to) {
if (!uplot || from > to) return false;
uplot.setScale("x", { min: from * maxX, max: to * maxX });
return true;
}
</script>
{#if series[0].data.length > 0}
<div bind:this={plotWrapper} class="cc-plot"></div>
<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>
.cc-plot {
border-radius: 5px;
}
.cc-plot {
border-radius: 5px;
}
</style>

View File

@ -1,254 +1,339 @@
<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
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
*/
/* Data Format
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
*/
// 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)
}
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)
}
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)
}
function getRGB(c) {
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
}
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
return {
x: x1 + a * (x2 - x1),
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) => {
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) {
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);
}
u.ctx.fill(p);
}
});
return null;
// 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);
}
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);
}
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);
}
function getRGB(c) {
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`;
}
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;
return {
x: x1 + a * (x2 - x1),
y: y1 + a * (y2 - y1),
};
}
// End Helpers
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) => {
let d = u.data[seriesIdx];
u.ctx.strokeStyle = getRGB(0);
u.ctx.fillStyle = getRGB(0);
let deg360 = 2 * Math.PI;
let p = new Path2D();
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) {
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);
}
}
u.ctx.fill(p);
});
return null;
};
// 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,
) => {
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
) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
// Main Function
function render(plotData) {
if (plotData) {
const opts = {
title: "",
mode: 2,
width: width,
height: height,
legend: {
show: false
},
cursor: { drag: { x: false, y: false } },
axes: [
{
label: 'Intensity [FLOPS/Byte]',
values: (u, vals) => vals.map(v => formatNumber(v))
},
{
label: 'Performace [GFLOPS]',
values: (u, vals) => vals.map(v => formatNumber(v))
}
],
scales: {
x: {
time: false,
range: [0.01, 1000],
distr: 3, // Render as log
log: 10, // log exp
},
y: {
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 }
],
hooks: {
drawClear: [
u => {
u.series.forEach((s, i) => {
if (i > 0)
s._paths = null;
});
},
],
draw: [
u => { // draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding // [top, right, bottom, left]
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)
// 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 (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 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
)
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x
y1 = xAxisIntersect.y
}
// Diagonal
u.ctx.moveTo(x1, y1)
u.ctx.lineTo(x2, y2)
u.ctx.stroke()
// Reset grid lineWidth
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!')
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,
) => {
let d = u.data[seriesIdx];
u.ctx.strokeStyle = getRGB(0);
u.ctx.fillStyle = getRGB(0);
let deg360 = 2 * Math.PI;
let p = new Path2D();
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
) {
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);
}
}
u.ctx.fill(p);
},
);
return null;
};
// Main Function
function render(plotData) {
if (plotData) {
const opts = {
title: "",
mode: 2,
width: width,
height: height,
legend: {
show: false,
},
cursor: { drag: { x: false, y: false } },
axes: [
{
label: "Intensity [FLOPS/Byte]",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
label: "Performace [GFLOPS]",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
scales: {
x: {
time: false,
range: [0.01, 1000],
distr: 3, // Render as log
log: 10, // log exp
},
y: {
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 }],
hooks: {
drawClear: [
(u) => {
u.series.forEach((s, i) => {
if (i > 0) s._paths = null;
});
},
],
draw: [
(u) => {
// draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding; // [top, right, bottom, left]
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,
);
// 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 (
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 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
);
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x;
y1 = xAxisIntersect.y;
}
// Diagonal
u.ctx.moveTo(x1, y1);
u.ctx.lineTo(x2, y2);
u.ctx.stroke();
// Reset grid lineWidth
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!");
}
}
// Svelte and Sizechange
onMount(() => {
render(data)
})
onDestroy(() => {
if (uplot)
uplot.destroy()
// Svelte and Sizechange
onMount(() => {
render(data);
});
onDestroy(() => {
if (uplot) uplot.destroy();
if (timeoutId != null)
clearTimeout(timeoutId)
})
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null) clearTimeout(timeoutId);
});
function sizeChanged() {
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null
if (uplot)
uplot.destroy()
render(data)
}, 200)
}
$: if (allowSizeChange) sizeChanged(width, height)
timeoutId = setTimeout(() => {
timeoutId = null;
if (uplot) uplot.destroy();
render(data);
}, 200);
}
$: 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>
{/if}
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
>
{/if}