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

View File

@ -9,27 +9,26 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.5",
"@urql/svelte": "^4.0.1", "@sveltestrap/sveltestrap": "^6.2.6",
"chart.js": "^4.3.3", "@urql/svelte": "^4.1.0",
"chart.js": "^4.4.2",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"date-fns": "^2.30.0", "graphql": "^16.8.1",
"graphql": "^16.6.0", "mathjs": "^12.4.0",
"mathjs": "^12.0.0", "svelte-chartjs": "^3.1.5",
"svelte-chartjs": "^3.1.2", "uplot": "^1.6.30",
"sveltestrap": "^5.11.1", "wonka": "^6.3.4"
"uplot": "^1.6.24",
"wonka": "^6.3.2"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.1", "@rollup/plugin-terser": "^0.4.4",
"@timohausmann/quadtree-js": "^1.2.5", "@timohausmann/quadtree-js": "^1.2.6",
"rollup": "^3.21.0", "rollup": "^4.12.1",
"rollup-plugin-css-only": "^4.3.0", "rollup-plugin-css-only": "^4.5.2",
"rollup-plugin-svelte": "^7.1.4", "rollup-plugin-svelte": "^7.1.6",
"svelte": "^3.58.0" "svelte": "^4.2.12"
} }
}, },
"node_modules/@0no-co/graphql.web": { "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": { "node_modules/@babel/runtime": {
"version": "7.23.5", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
"integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -57,33 +68,30 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.24"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/set-array": { "node_modules/@jridgewell/set-array": {
"version": "1.1.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
@ -104,13 +112,9 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.20", "version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"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,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
@ -131,9 +135,9 @@
} }
}, },
"node_modules/@rollup/plugin-commonjs": { "node_modules/@rollup/plugin-commonjs": {
"version": "24.1.0", "version": "25.0.7",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
"integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.1",
@ -141,13 +145,13 @@
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"glob": "^8.0.3", "glob": "^8.0.3",
"is-reference": "1.2.1", "is-reference": "1.2.1",
"magic-string": "^0.27.0" "magic-string": "^0.30.3"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.68.0||^3.0.0" "rollup": "^2.68.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -156,9 +160,6 @@
} }
}, },
"node_modules/@rollup/plugin-node-resolve": { "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", "version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
@ -176,7 +177,6 @@
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0" "rollup": "^2.78.0||^3.0.0||^4.0.0"
"rollup": "^2.78.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -185,23 +185,18 @@
} }
}, },
"node_modules/@rollup/plugin-replace": { "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", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.1",
"magic-string": "^0.30.3" "magic-string": "^0.30.3"
"magic-string": "^0.30.3"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "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"
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "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": { "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", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
@ -249,7 +219,6 @@
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.0.0||^3.0.0||^4.0.0" "rollup": "^2.0.0||^3.0.0||^4.0.0"
"rollup": "^2.0.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -271,7 +240,6 @@
}, },
"peerDependencies": { "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"
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "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": { "node_modules/@timohausmann/quadtree-js": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz",
@ -289,9 +437,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" "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": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
@ -300,20 +445,20 @@
"dev": true "dev": true
}, },
"node_modules/@urql/core": { "node_modules/@urql/core": {
"version": "4.2.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@urql/core/-/core-4.3.0.tgz",
"integrity": "sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==", "integrity": "sha512-wT+FeL8DG4x5o6RfHEnONNFVDM3616ouzATMYUClB6CB+iIu2mwfBKd7xSUxYOZmwtxna5/hDRQdMl3nbQZlnw==",
"dependencies": { "dependencies": {
"@0no-co/graphql.web": "^1.0.1", "@0no-co/graphql.web": "^1.0.1",
"wonka": "^6.3.2" "wonka": "^6.3.2"
} }
}, },
"node_modules/@urql/svelte": { "node_modules/@urql/svelte": {
"version": "4.0.4", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.1.0.tgz",
"integrity": "sha512-HYz9dHdqEcs9d82WWczQ3XG+zuup3TS01H+txaij/QfQ+KHjrlrn0EkOHQQd1S+H8+nFjFU2x9+HE3+3fuwL1A==", "integrity": "sha512-Ov3EclCjaXPPTjKNTcIDlAG3qY/jhLjl/J9yyz9FeLUQ9S2jEgsvlzNXibrY27f4ihD4gH36CNGuj1XOi5hEEQ==",
"dependencies": { "dependencies": {
"@urql/core": "^4.1.0", "@urql/core": "^4.3.0",
"wonka": "^6.3.2" "wonka": "^6.3.2"
}, },
"peerDependencies": { "peerDependencies": {
@ -321,13 +466,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.11.2", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -335,6 +476,22 @@
"node": ">=0.4.0" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -369,14 +526,34 @@
} }
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "4.4.1", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
"integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
}, },
"engines": { "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": { "node_modules/commander": {
@ -403,6 +580,18 @@
"url": "https://www.patreon.com/infusion" "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": { "node_modules/date-fns": {
"version": "2.30.0", "version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -432,6 +621,14 @@
"node": ">=0.10.0" "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": { "node_modules/escape-latex": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
@ -482,13 +679,6 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/glob": {
"version": "8.1.0", "version": "8.1.0",
@ -510,9 +700,6 @@
} }
}, },
"node_modules/graphql": { "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", "version": "16.8.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
@ -521,21 +708,15 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"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==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
"function-bind": "^1.1.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
"node": ">= 0.4"
} }
}, },
"node_modules/inflight": { "node_modules/inflight": {
@ -570,16 +751,12 @@
} }
}, },
"node_modules/is-core-module": { "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", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"hasown": "^2.0.0" "hasown": "^2.0.0"
"hasown": "^2.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "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", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==" "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": { "node_modules/magic-string": {
"version": "0.27.0", "version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"dev": true,
"dev": true,
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13" "@jridgewell/sourcemap-codec": "^1.4.15"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/mathjs": { "node_modules/mathjs": {
"version": "12.0.0", "version": "12.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.0.0.tgz", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.0.tgz",
"integrity": "sha512-Oz3swPplNPe7taoP6WrkKhQzhDE2SwvOgLzu8H3EN+hEadw2GjEJUm6Xl+hrioHoB8g2BYb3gfw1glSzhdBKYw==", "integrity": "sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.23.2", "@babel/runtime": "^7.23.9",
"complex.js": "^2.1.1", "complex.js": "^2.1.1",
"decimal.js": "^10.4.3", "decimal.js": "^10.4.3",
"escape-latex": "^1.2.0", "escape-latex": "^1.2.0",
@ -640,6 +820,11 @@
"node": ">= 18" "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": { "node_modules/minimatch": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@ -667,6 +852,32 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true "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": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -688,19 +899,11 @@
} }
}, },
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.0", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"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/resolve": { "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", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
@ -727,28 +930,38 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.4", "version": "4.12.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"devOptional": true, "devOptional": true,
"dependencies": {
"@types/estree": "1.0.5"
},
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
"engines": { "engines": {
"node": ">=14.18.0", "node": ">=18.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "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" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rollup-plugin-css-only": { "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", "version": "4.5.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==", "integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
@ -761,7 +974,6 @@
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "<5" "rollup": "<5"
"rollup": "<5"
} }
}, },
"node_modules/rollup-plugin-svelte": { "node_modules/rollup-plugin-svelte": {
@ -820,18 +1032,15 @@
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
}, },
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
}, },
"node_modules/smob": { "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", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
@ -846,6 +1055,14 @@
"node": ">=0.10.0" "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": { "node_modules/source-map-support": {
"version": "0.5.21", "version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -869,40 +1086,58 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "3.59.2", "version": "4.2.12",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==", "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": { "engines": {
"node": ">= 8" "node": ">=16"
} }
}, },
"node_modules/svelte-chartjs": { "node_modules/svelte-chartjs": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz", "resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz",
"integrity": "sha512-3+6gY2IJ9Ua8R9pk3iS1ypa7Z9OoXCJb9oPwIfTp7caJM+X+RrWnH2CTkGAq7FeSxc2nnmW08tYN88Q8Y+5M+w==", "integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==",
"peerDependencies": { "peerDependencies": {
"chart.js": "^3.5.0 || ^4.0.0", "chart.js": "^3.5.0 || ^4.0.0",
"svelte": "^3.45.0" "svelte": "^4.0.0"
} }
}, },
"node_modules/sveltestrap": { "node_modules/svelte/node_modules/estree-walker": {
"version": "5.11.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8" "@types/estree": "^1.0.0"
}
}, },
"peerDependencies": { "node_modules/svelte/node_modules/is-reference": {
"svelte": "^3.53.1" "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": { "node_modules/terser": {
"version": "5.25.0", "version": "5.29.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
"integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==", "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
@ -931,12 +1166,9 @@
} }
}, },
"node_modules/uplot": { "node_modules/uplot": {
"version": "1.6.27", "version": "1.6.30",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz", "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.30.tgz",
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg==" "integrity": "sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q=="
"version": "1.6.27",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
}, },
"node_modules/wonka": { "node_modules/wonka": {
"version": "6.3.4", "version": "6.3.4",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,64 +1,300 @@
<script> <script>
import { Button, Table, Row, Col, Card, CardBody, CardTitle } from 'sveltestrap' import {
import { fade } from 'svelte/transition' 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 message = { msg: "", target: "", color: "#d63384" };
let displayMessage = false let displayMessage = false;
const colorschemes = { const colorschemes = {
'Default': ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"], Default: [
'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)'], "#00bfff",
'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)'], "#0000ff",
'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)'], "#ff00ff",
'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)'], "#ff0000",
'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)'], "#ff8000",
'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)'], "#ffff00",
'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)'], "#80ff00",
'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)'] ],
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) { async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector) let form = document.querySelector(selector);
let formData = new FormData(form) let formData = new FormData(form);
try { try {
const res = await fetch(form.action, { method: 'POST', body: formData }); const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) { if (res.ok) {
let text = await res.text() let text = await res.text();
popMessage(text, target, '#048109') popMessage(text, target, "#048109");
} else { } else {
let text = await res.text() let text = await res.text();
// console.log(res.statusText) // console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text) throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {
popMessage(err, target, '#d63384') popMessage(err, target, "#d63384");
} }
return false return false;
} }
function popMessage(response, restarget, rescolor) { function popMessage(response, restarget, rescolor) {
message = {msg: response, target: restarget, color: rescolor} message = { msg: response, target: restarget, color: rescolor };
displayMessage = true displayMessage = true;
setTimeout(function() { setTimeout(function () {
displayMessage = false displayMessage = false;
}, 3500) }, 3500);
} }
</script> </script>
<Row cols={3} class="p-2 g-2"> <Row cols={3} class="p-2 g-2">
<!-- LINE WIDTH --> <!-- LINE WIDTH -->
<Col><Card class="h-100"> <Col
><Card class="h-100">
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} --> <!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#line-width-form', 'lw')}> <form
id="line-width-form"
method="post"
action="/api/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#line-width-form", "lw")}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> <CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Line Width</div> <div>Line Width</div>
<!-- Expand If-Clause for clarity once --> <!-- Expand If-Clause for clarity once -->
{#if displayMessage && message.target == 'lw'} {#if displayMessage && message.target == "lw"}
<div style="margin-left: auto; font-size: 0.9em;"> <div style="margin-left: auto; font-size: 0.9em;">
<code style="color: {message.color};" out:fade> <code style="color: {message.color};" out:fade>
Update: {message.msg} Update: {message.msg}
@ -66,47 +302,102 @@
</div> </div>
{/if} {/if}
</CardTitle> </CardTitle>
<input type="hidden" name="key" value="plot_general_lineWidth"/> <input type="hidden" name="key" value="plot_general_lineWidth" />
<div class="mb-3"> <div class="mb-3">
<label for="value" class="form-label">Line Width</label> <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"/> <input
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div> 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> </div>
<Button color="primary" type="submit">Submit</Button> <Button color="primary" type="submit">Submit</Button>
</form> </form>
</Card></Col> </Card></Col
>
<!-- PLOTS PER ROW --> <!-- PLOTS PER ROW -->
<Col><Card class="h-100"> <Col
<form id="plots-per-row-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#plots-per-row-form', 'ppr')}> ><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. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> <CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Plots per Row</div> <div>Plots per Row</div>
{#if displayMessage && message.target == 'ppr'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if} {#if displayMessage && message.target == "ppr"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle> </CardTitle>
<input type="hidden" name="key" value="plot_view_plotsPerRow"/> <input type="hidden" name="key" value="plot_view_plotsPerRow" />
<div class="mb-3"> <div class="mb-3">
<label for="value" class="form-label">Plots per Row</label> <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"/> <input
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div> 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> </div>
<Button color="primary" type="submit">Submit</Button> <Button color="primary" type="submit">Submit</Button>
</form> </form>
</Card></Col> </Card></Col
>
<!-- BACKGROUND --> <!-- BACKGROUND -->
<Col><Card class="h-100"> <Col
<form id="backgrounds-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#backgrounds-form', 'bg')}> ><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. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> <CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Colored Backgrounds</div> <div>Colored Backgrounds</div>
{#if displayMessage && message.target == 'bg'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if} {#if displayMessage && message.target == "bg"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle> </CardTitle>
<input type="hidden" name="key" value="plot_general_colorBackground"/> <input type="hidden" name="key" value="plot_general_colorBackground" />
<div class="mb-3"> <div class="mb-3">
<div> <div>
{#if config.plot_general_colorBackground} {#if config.plot_general_colorBackground}
<input type="radio" id="true" name="value" value="true" checked/> <input type="radio" id="true" name="value" value="true" checked />
{:else} {:else}
<input type="radio" id="true" name="value" value="true" /> <input type="radio" id="true" name="value" value="true" />
{/if} {/if}
@ -116,41 +407,76 @@
{#if config.plot_general_colorBackground} {#if config.plot_general_colorBackground}
<input type="radio" id="false" name="value" value="false" /> <input type="radio" id="false" name="value" value="false" />
{:else} {:else}
<input type="radio" id="false" name="value" value="false" checked/> <input
type="radio"
id="false"
name="value"
value="false"
checked
/>
{/if} {/if}
<label for="false">No</label> <label for="false">No</label>
</div> </div>
</div> </div>
<Button color="primary" type="submit">Submit</Button> <Button color="primary" type="submit">Submit</Button>
</form> </form>
</Card></Col> </Card></Col
>
</Row> </Row>
<Row cols={1} class="p-2 g-2"> <Row cols={1} class="p-2 g-2">
<!-- COLORSCHEME --> <!-- COLORSCHEME -->
<Col><Card> <Col
<form id="colorscheme-form" method="post" action="/api/configuration/" class="card-body"> ><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. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> <CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Color Scheme for Timeseries Plots</div> <div>Color Scheme for Timeseries Plots</div>
{#if displayMessage && message.target == 'cs'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if} {#if displayMessage && message.target == "cs"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle> </CardTitle>
<input type="hidden" name="key" value="plot_general_colorscheme"/> <input type="hidden" name="key" value="plot_general_colorscheme" />
<Table hover> <Table hover>
<tbody> <tbody>
{#each Object.entries(colorschemes) as [name, rgbrow]} {#each Object.entries(colorschemes) as [name, rgbrow]}
<tr> <tr>
<th scope="col">{name}</th> <th scope="col">{name}</th>
<td> <td>
{#if rgbrow.join(',') == config.plot_general_colorscheme} {#if rgbrow.join(",") == config.plot_general_colorscheme}
<input type="radio" name="value" value={JSON.stringify(rgbrow)} checked on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/> <input
type="radio"
name="value"
value={JSON.stringify(rgbrow)}
checked
on:click={() =>
handleSettingSubmit("#colorscheme-form", "cs")}
/>
{:else} {:else}
<input type="radio" name="value" value={JSON.stringify(rgbrow)} on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/> <input
type="radio"
name="value"
value={JSON.stringify(rgbrow)}
on:click={() =>
handleSettingSubmit("#colorscheme-form", "cs")}
/>
{/if} {/if}
</td> </td>
<td> <td>
{#each rgbrow as rgb} {#each rgbrow as rgb}
<span class="color-dot" style="background-color: {rgb};"></span> <span class="color-dot" style="background-color: {rgb};"
></span>
{/each} {/each}
</td> </td>
</tr> </tr>
@ -158,7 +484,8 @@
</tbody> </tbody>
</Table> </Table>
</form> </form>
</Card></Col> </Card></Col
>
</Row> </Row>
<style> <style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,22 +5,22 @@
--> -->
<script> <script>
import uPlot from 'uplot' import uPlot from "uplot";
import { formatNumber } from '../units.js' import { formatNumber } from "../units.js";
import { onMount, onDestroy } from 'svelte' import { onMount, onDestroy } from "svelte";
import { Card } from 'sveltestrap' import { Card } from "@sveltestrap/sveltestrap";
export let data export let data;
export let usesBins = false export let usesBins = false;
export let width = 500 export let width = 500;
export let height = 300 export let height = 300;
export let title = '' export let title = "";
export let xlabel = '' export let xlabel = "";
export let xunit = 'X' export let xunit = "X";
export let ylabel = '' export let ylabel = "";
export let yunit = 'Y' export let yunit = "Y";
const { bars } = uPlot.paths const { bars } = uPlot.paths;
const drawStyles = { const drawStyles = {
bars: 1, bars: 1,
@ -31,18 +31,17 @@
let s = u.series[seriesIdx]; let s = u.series[seriesIdx];
let style = s.drawStyle; let style = s.drawStyle;
let renderer = ( // If bars to wide, change here let renderer = // If bars to wide, change here
style == drawStyles.bars ? ( style == drawStyles.bars ? bars({ size: [0.75, 100] }) : () => null;
bars({size: [0.75, 100]})
) :
() => null
)
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip); return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
} }
// converts the legend into a simple tooltip // converts the legend into a simple tooltip
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) { function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl; let legendEl;
function init(u, opts) { function init(u, opts) {
@ -60,14 +59,13 @@
top: 0, top: 0,
zIndex: 100, zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)", boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style ...style,
}); });
// hide series color markers // hide series color markers
const idents = legendEl.querySelectorAll(".u-marker"); const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++) for (let i = 0; i < idents.length; i++) idents[i].style.display = "none";
idents[i].style.display = "none";
const overEl = u.over; const overEl = u.over;
overEl.style.overflow = "visible"; overEl.style.overflow = "visible";
@ -76,8 +74,12 @@
overEl.appendChild(legendEl); overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit // show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;}); overEl.addEventListener("mouseenter", () => {
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";}); legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
// let tooltip exit plot // let tooltip exit plot
// overEl.style.overflow = "visible"; // overEl.style.overflow = "visible";
@ -85,40 +87,40 @@
function update(u) { function update(u) {
const { left, top } = u.cursor; const { left, top } = u.cursor;
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)"; legendEl.style.transform =
"translate(" + (left + 15) + "px, " + (top + 15) + "px)";
} }
return { return {
hooks: { hooks: {
init: init, init: init,
setCursor: update, setCursor: update,
} },
}; };
} }
let plotWrapper = null let plotWrapper = null;
let uplot = null let uplot = null;
let timeoutId = null let timeoutId = null;
function render() { function render() {
let opts = { let opts = {
width: width, width: width,
height: height, height: height,
title: title, title: title,
plugins: [ plugins: [legendAsTooltipPlugin()],
legendAsTooltipPlugin()
],
cursor: { cursor: {
points: { points: {
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5, size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
width: (u, seriesIdx, size) => size / 4, width: (u, seriesIdx, size) => size / 4,
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90', stroke: (u, seriesIdx) =>
u.series[seriesIdx].points.stroke(u, seriesIdx) + "90",
fill: (u, seriesIdx) => "#fff", fill: (u, seriesIdx) => "#fff",
} },
}, },
scales: { scales: {
x: { x: {
time: false time: false,
}, },
}, },
axes: [ axes: [
@ -138,7 +140,7 @@
size: 5 / devicePixelRatio, size: 5 / devicePixelRatio,
stroke: "#000000", stroke: "#000000",
}, },
values: (_, t) => t.map(v => formatNumber(v)), values: (_, t) => t.map((v) => formatNumber(v)),
}, },
{ {
stroke: "#000000", stroke: "#000000",
@ -155,71 +157,70 @@
size: 5 / devicePixelRatio, size: 5 / devicePixelRatio,
stroke: "#000000", stroke: "#000000",
}, },
values: (_, t) => t.map(v => formatNumber(v)), values: (_, t) => t.map((v) => formatNumber(v)),
}, },
], ],
series: [ series: [
{ {
label: xunit !== '' ? xunit : null, label: xunit !== "" ? xunit : null,
value: (u, ts, sidx, didx) => { value: (u, ts, sidx, didx) => {
if (usesBins) { if (usesBins) {
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0 const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0;
const max = u.data[sidx][didx] const max = u.data[sidx][didx];
ts = min + '-' + max // narrow spaces ts = min + "-" + max; // narrow spaces
}
return ts
} }
return ts;
}, },
Object.assign({ },
label: yunit !== '' ? yunit : null, Object.assign(
{
label: yunit !== "" ? yunit : null,
width: 1 / devicePixelRatio, width: 1 / devicePixelRatio,
drawStyle: drawStyles.points, drawStyle: drawStyles.points,
lineInterpolation: null, lineInterpolation: null,
paths, paths,
}, { },
{
drawStyle: drawStyles.bars, drawStyle: drawStyles.bars,
lineInterpolation: null, lineInterpolation: null,
stroke: "#85abce", stroke: "#85abce",
fill: "#85abce", // + "1A", // Transparent Fill fill: "#85abce", // + "1A", // Transparent Fill
}), },
] ),
],
}; };
uplot = new uPlot(opts, data, plotWrapper) uplot = new uPlot(opts, data, plotWrapper);
} }
onMount(() => { onMount(() => {
render() render();
}) });
onDestroy(() => { onDestroy(() => {
if (uplot) if (uplot) uplot.destroy();
uplot.destroy()
if (timeoutId != null) if (timeoutId != null) clearTimeout(timeoutId);
clearTimeout(timeoutId) });
})
function sizeChanged() { function sizeChanged() {
if (timeoutId != null) if (timeoutId != null) clearTimeout(timeoutId);
clearTimeout(timeoutId)
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null timeoutId = null;
if (uplot) if (uplot) uplot.destroy();
uplot.destroy()
render() render();
}, 200) }, 200);
} }
$: sizeChanged(width, height) $: sizeChanged(width, height);
</script> </script>
{#if data.length > 0} {#if data.length > 0}
<div bind:this={plotWrapper}/> <div bind:this={plotWrapper} />
{:else} {: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} {/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 @component
@ -21,46 +139,59 @@
// TODO: Move helper functions to module context? // TODO: Move helper functions to module context?
--> -->
<script> <script>
import uPlot from 'uplot' import uPlot from "uplot";
import { formatNumber } from '../units.js' import { formatNumber } from "../units.js";
import { getContext, onMount, onDestroy } from 'svelte' import { getContext, onMount, onDestroy } from "svelte";
import { Card } from 'sveltestrap' import { Card } from "@sveltestrap/sveltestrap";
export let metric export let metric;
export let scope = 'node' export let scope = "node";
export let resources = [] export let resources = [];
export let width export let width;
export let height export let height;
export let timestep export let timestep;
export let series export let series;
export let useStatsSeries = null export let useStatsSeries = null;
export let statisticsSeries = null export let statisticsSeries = null;
export let cluster export let cluster;
export let subCluster export let subCluster;
export let isShared = false export let isShared = false;
export let forNode = false export let forNode = false;
export let numhwthreads = 0 export let hwthreads = 0;
export let numaccs = 0
if (useStatsSeries == null) if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
useStatsSeries = statisticsSeries != null
if (useStatsSeries == false && series == null) if (useStatsSeries == false && series == null) useStatsSeries = true;
useStatsSeries = true
const metricConfig = getContext('metrics')(cluster, metric) const metricConfig = getContext("metrics")(cluster, metric);
const clusterCockpitConfig = getContext('cc-config') const clusterCockpitConfig = getContext("cc-config");
const resizeSleepTime = 250 const resizeSleepTime = 250;
const normalLineColor = '#000000' const normalLineColor = "#000000";
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio const lineWidth =
const lineColors = clusterCockpitConfig.plot_general_colorscheme clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' } const lineColors = clusterCockpitConfig.plot_general_colorscheme;
const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster, isShared, numhwthreads, numaccs) 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 // converts the legend into a simple tooltip
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) { function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl; let legendEl;
const dataSize = series.length const dataSize = series.length;
function init(u, opts) { function init(u, opts) {
legendEl = u.root.querySelector(".u-legend"); legendEl = u.root.querySelector(".u-legend");
@ -77,13 +208,16 @@
top: 0, top: 0,
zIndex: 100, zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)", boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style ...style,
}); });
// conditional hide series color markers: // conditional hide series color markers:
if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory if (
useStatsSeries === true || // Min/Max/Avg Self-Explanatory
dataSize === 1 || // Only one Y-Dataseries dataSize === 1 || // Only one Y-Dataseries
dataSize > 6 ){ // More than 6 Y-Dataseries dataSize > 6
) {
// More than 6 Y-Dataseries
const idents = legendEl.querySelectorAll(".u-marker"); const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++) for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none"; idents[i].style.display = "none";
@ -96,8 +230,12 @@
overEl.appendChild(legendEl); overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit // show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;}); overEl.addEventListener("mouseenter", () => {
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";}); legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
// let tooltip exit plot // let tooltip exit plot
// overEl.style.overflow = "visible"; // overEl.style.overflow = "visible";
@ -106,7 +244,8 @@
function update(u) { function update(u) {
const { left, top } = u.cursor; const { left, top } = u.cursor;
const width = u.over.querySelector(".u-legend").offsetWidth; const width = u.over.querySelector(".u-legend").offsetWidth;
legendEl.style.transform = "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
} }
if (dataSize <= 12 || useStatsSeries === true) { if (dataSize <= 12 || useStatsSeries === true) {
@ -114,336 +253,289 @@
hooks: { hooks: {
init: init, init: init,
setCursor: update, setCursor: update,
} },
} };
} else { // Setting legend-opts show/live as object with false here will not work ... } else {
return {} // Setting legend-opts show/live as object with false here will not work ...
return {};
} }
} }
function backgroundColor() { function backgroundColor() {
if (clusterCockpitConfig.plot_general_colorBackground == false if (
|| !thresholds clusterCockpitConfig.plot_general_colorBackground == false ||
|| !(series && series.every(s => s.statistics != null))) !thresholds ||
return backgroundColors.normal !(series && series.every((s) => s.statistics != null))
)
return backgroundColors.normal;
let cond = thresholds.alert < thresholds.caution let cond =
thresholds.alert < thresholds.caution
? (a, b) => a <= b ? (a, b) => a <= b
: (a, b) => a >= b : (a, b) => a >= b;
let avg = series.reduce((sum, series) => sum + series.statistics.avg, 0) / series.length let avg =
series.reduce((sum, series) => sum + series.statistics.avg, 0) /
series.length;
if (Number.isNaN(avg)) if (Number.isNaN(avg)) return backgroundColors.normal;
return backgroundColors.normal
if (cond(avg, thresholds.alert)) if (cond(avg, thresholds.alert)) return backgroundColors.alert;
return backgroundColors.alert
if (cond(avg, thresholds.caution)) if (cond(avg, thresholds.caution)) return backgroundColors.caution;
return backgroundColors.caution
return backgroundColors.normal return backgroundColors.normal;
} }
function lineColor(i, n) { function lineColor(i, n) {
if (n >= lineColors.length) if (n >= lineColors.length) return lineColors[i % lineColors.length];
return lineColors[i % lineColors.length]; else return lineColors[Math.floor((i / n) * lineColors.length)];
else
return lineColors[Math.floor((i / n) * lineColors.length)];
} }
const longestSeries = useStatsSeries const longestSeries = useStatsSeries
? statisticsSeries.mean.length ? statisticsSeries.mean.length
: series.reduce((n, series) => Math.max(n, series.data.length), 0) : series.reduce((n, series) => Math.max(n, series.data.length), 0);
const maxX = longestSeries * timestep const maxX = longestSeries * timestep;
let maxY = null let maxY = null;
if (thresholds !== null) { if (thresholds !== null) {
maxY = useStatsSeries maxY = useStatsSeries
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal) ? statisticsSeries.max.reduce(
: (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal) (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 if (maxY >= 10 * thresholds.peak) {
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 plotSeries = [
const plotData = [new Array(longestSeries)] {
label: "Runtime",
value: (u, ts, sidx, didx) =>
didx == null ? null : formatTime(ts, forNode),
},
];
const plotData = [new Array(longestSeries)];
if (forNode === true) { if (forNode === true) {
// Negative Timestamp Buildup // Negative Timestamp Buildup
for (let i = 0; i <= longestSeries; i++) { for (let i = 0; i <= longestSeries; i++) {
plotData[0][i] = (longestSeries - i) * timestep * -1 plotData[0][i] = (longestSeries - i) * timestep * -1;
} }
} else { } else {
// Positive Timestamp Buildup // Positive Timestamp Buildup
for (let j = 0; j < longestSeries; j++) // TODO: Cache/Reuse this array? for (
plotData[0][j] = j * timestep let j = 0;
j < longestSeries;
j++ // TODO: Cache/Reuse this array?
)
plotData[0][j] = j * timestep;
} }
let plotBands = undefined let plotBands = undefined;
if (useStatsSeries) { if (useStatsSeries) {
plotData.push(statisticsSeries.min) plotData.push(statisticsSeries.min);
plotData.push(statisticsSeries.max) plotData.push(statisticsSeries.max);
plotData.push(statisticsSeries.mean) plotData.push(statisticsSeries.mean);
if (forNode === true) { // timestamp 0 with null value for reversed time axis if (forNode === true) {
if (plotData[1].length != 0) plotData[1].push(null) // timestamp 0 with null value for reversed time axis
if (plotData[2].length != 0) plotData[2].push(null) if (plotData[1].length != 0) plotData[1].push(null);
if (plotData[3].length != 0) plotData[3].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({
plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' }) label: "min",
plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' }) 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 = [ plotBands = [
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' }, { series: [2, 3], fill: "rgba(0,255,0,0.1)" },
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' } { series: [3, 1], fill: "rgba(255,0,0,0.1)" },
]; ];
} else { } else {
for (let i = 0; i < series.length; i++) { for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data) 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 if (forNode === true && plotData[1].length != 0) plotData[1].push(null); // timestamp 0 with null value for reversed time axis
plotSeries.push({ plotSeries.push({
label: scope === 'node' ? resources[i].hostname : label:
// scope === 'accelerator' ? resources[0].accelerators[i] : scope === "node"
scope + ' #' + (i+1), ? resources[i].hostname
scale: 'y', : // scope === 'accelerator' ? resources[0].accelerators[i] :
scope + " #" + (i + 1),
scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series.length) stroke: lineColor(i, series.length),
}) });
} }
} }
const opts = { const opts = {
width, width,
height, height,
plugins: [ plugins: [legendAsTooltipPlugin()],
legendAsTooltipPlugin()
],
series: plotSeries, series: plotSeries,
axes: [ axes: [
{ {
scale: 'x', scale: "x",
space: 35, space: 35,
incrs: timeIncrs(timestep, maxX, forNode), incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map(v => formatTime(v, forNode)) values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
}, },
{ {
scale: 'y', scale: "y",
grid: { show: true }, grid: { show: true },
labelFont: 'sans-serif', labelFont: "sans-serif",
values: (u, vals) => vals.map(v => formatNumber(v)) values: (u, vals) => vals.map((v) => formatNumber(v)),
} },
], ],
bands: plotBands, bands: plotBands,
padding: [5, 10, -20, 0], padding: [5, 10, -20, 0],
hooks: { hooks: {
draw: [(u) => { draw: [
(u) => {
// Draw plot type label: // Draw plot type label:
let textl = `${scope}${plotSeries.length > 2 ? 's' : ''}${ let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
useStatsSeries ? ': min/avg/max' : (metricConfig != null && scope != metricConfig.scope ? ` (${metricConfig.aggregation})` : '')}` useStatsSeries
let textr = `${(isShared && (scope != 'core' && scope != 'accelerator')) ? '[Shared]' : '' }` ? ": min/avg/max"
u.ctx.save() : metricConfig != null && scope != metricConfig.scope
u.ctx.textAlign = 'start' // 'end' ? ` (${metricConfig.aggregation})`
u.ctx.fillStyle = 'black' : ""
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10) }`;
u.ctx.textAlign = 'end' let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
u.ctx.fillStyle = 'black' u.ctx.save();
u.ctx.fillText(textr, u.bbox.left + u.bbox.width - 10, u.bbox.top + 10) 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 // u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
if (!thresholds) { if (!thresholds) {
u.ctx.restore() u.ctx.restore();
return return;
} }
let y = u.valToPos(thresholds.normal, 'y', true) let y = u.valToPos(thresholds.normal, "y", true);
u.ctx.save() u.ctx.save();
u.ctx.lineWidth = lineWidth u.ctx.lineWidth = lineWidth;
u.ctx.strokeStyle = normalLineColor u.ctx.strokeStyle = normalLineColor;
u.ctx.setLineDash([5, 5]) u.ctx.setLineDash([5, 5]);
u.ctx.beginPath() u.ctx.beginPath();
u.ctx.moveTo(u.bbox.left, y) u.ctx.moveTo(u.bbox.left, y);
u.ctx.lineTo(u.bbox.left + u.bbox.width, y) u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
u.ctx.stroke() u.ctx.stroke();
u.ctx.restore() u.ctx.restore();
}] },
],
}, },
scales: { scales: {
x: { time: false }, x: { time: false },
y: maxY ? { range: [0., maxY * 1.1] } : {} y: maxY ? { range: [0, maxY * 1.1] } : {},
}, },
legend : { // Display legend until max 12 Y-dataseries legend: {
show: (series.length <= 12 || useStatsSeries === true) ? true : false, // Display legend until max 12 Y-dataseries
live: (series.length <= 12 || useStatsSeries === true) ? true : false show: series.length <= 12 || useStatsSeries === true ? true : false,
live: series.length <= 12 || useStatsSeries === true ? true : false,
}, },
cursor: { drag: { x: true, y: true } } cursor: { drag: { x: true, y: true } },
} };
// console.log(opts) // console.log(opts)
let plotWrapper = null let plotWrapper = null;
let uplot = null let uplot = null;
let timeoutId = null let timeoutId = null;
let prevWidth = null, prevHeight = null let prevWidth = null,
prevHeight = null;
function render() { function render() {
if (!width || Number.isNaN(width) || width < 0) if (!width || Number.isNaN(width) || width < 0) return;
return
if (prevWidth != null && Math.abs(prevWidth - width) < 10) if (prevWidth != null && Math.abs(prevWidth - width) < 10) return;
return
prevWidth = width prevWidth = width;
prevHeight = height prevHeight = height;
if (!uplot) { if (!uplot) {
opts.width = width opts.width = width;
opts.height = height opts.height = height;
uplot = new uPlot(opts, plotData, plotWrapper) uplot = new uPlot(opts, plotData, plotWrapper);
} else { } else {
uplot.setSize({ width, height }) uplot.setSize({ width, height });
} }
} }
function onSizeChange() { function onSizeChange() {
if (!uplot) if (!uplot) return;
return
if (timeoutId != null) if (timeoutId != null) clearTimeout(timeoutId);
clearTimeout(timeoutId)
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null timeoutId = null;
render() render();
}, resizeSleepTime) }, resizeSleepTime);
} }
$: if (series[0].data.length > 0) { $: if (series[0].data.length > 0) {
onSizeChange(width, height) onSizeChange(width, height);
} }
onMount(() => { onMount(() => {
if (series[0].data.length > 0) { if (series[0].data.length > 0) {
plotWrapper.style.backgroundColor = backgroundColor() plotWrapper.style.backgroundColor = backgroundColor();
render() render();
} }
}) });
onDestroy(() => { onDestroy(() => {
if (uplot) if (uplot) uplot.destroy();
uplot.destroy()
if (timeoutId != null) if (timeoutId != null) clearTimeout(timeoutId);
clearTimeout(timeoutId) });
})
// `from` and `to` must be numbers between 0 and 1. // `from` and `to` must be numbers between 0 and 1.
export function setTimeRange(from, to) { export function setTimeRange(from, to) {
if (!uplot || from > to) if (!uplot || from > to) return false;
return false
uplot.setScale('x', { min: from * maxX, max: to * maxX }) uplot.setScale("x", { min: from * maxX, max: to * maxX });
return true return true;
} }
</script> </script>
<script context="module">
export function formatTime(t, forNode = false) {
if (t !== null) {
if (isNaN(t)) {
return t
} else {
const tAbs = Math.abs(t)
const h = Math.floor(tAbs / 3600)
const m = Math.floor((tAbs % 3600) / 60)
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
if (h == 0)
return `${forNode && m != 0 ? '-' : ''}${m}m`
else if (m == 0)
return `${forNode?'-':''}${h}h`
else
return `${forNode?'-':''}${h}:${m}h`
}
}
}
export function timeIncrs(timestep, maxX, forNode) {
if (forNode === true) {
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600] // forNode fixed increments
} else {
let incrs = []
for (let t = timestep; t < maxX; t *= 10)
incrs.push(t, t * 2, t * 3, t * 5)
return incrs
}
}
export function findThresholds(metricConfig, scope, subCluster, isShared, numhwthreads, numaccs) {
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
if (!metricConfig || !scope || !subCluster) {
console.warn('Argument missing for findThresholds!')
return null
}
if ((scope == 'node' && isShared == false) || metricConfig.aggregation == 'avg') {
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
// console.log('subClusterConfigs array empty, use metricConfig defaults')
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak }
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert && forSubCluster.peak) return forSubCluster
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak}
} else {
console.warn('metricConfig.subClusters not found!')
return null
}
}
if (metricConfig.aggregation != 'sum') {
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
return null
}
let divisor = 1
if (isShared == true) { // Shared
if (numaccs > 0) {
divisor = subCluster.topology.accelerators.length / numaccs
} else if (numhwthreads > 0) {
divisor = subCluster.topology.node.length / numhwthreads
}
else if (scope == 'socket')
divisor = subCluster.topology.socket.length
else if (scope == 'core')
divisor = subCluster.topology.core.length
else if (scope == 'accelerator')
divisor = subCluster.topology.accelerators.length
else if (scope == 'hwthread')
divisor = subCluster.topology.node.length
else
// console.log('TODO: how to calc thresholds for ', scope)
return null
}
let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig
return {
peak: mc.peak / divisor,
normal: mc.normal / divisor,
caution: mc.caution / divisor,
alert: mc.alert / divisor
}
}
</script>
{#if series[0].data.length > 0} {#if series[0].data.length > 0}
<div bind:this={plotWrapper} class="cc-plot"></div> <div bind:this={plotWrapper} class="cc-plot"></div>
{:else} {: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} {/if}
<style> <style>

View File

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