This commit is contained in:
Christoph Kluge
2024-03-14 11:08:37 +01:00
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": { },
"svelte": "^3.53.1" "node_modules/svelte/node_modules/is-reference": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dependencies": {
"@types/estree": "*"
} }
}, },
"node_modules/terser": { "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"
} }
} }

File diff suppressed because it is too large Load Diff

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

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

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`
updateConfiguration(name: $name, value: $value) mutation ($name: String!, $value: String!) {
}`, 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> <ModalBody>
Select metrics presented in histograms <ListGroup>
</ModalHeader> {#each availableMetrics as metric (metric)}
<ModalBody> <ListGroupItem>
<ListGroup> <input type="checkbox" bind:group={pendingMetrics} value={metric} />
{#each availableMetrics as metric (metric)} {metric}
<ListGroupItem> </ListGroupItem>
<input type="checkbox" bind:group={pendingMetrics} value={metric}> {/each}
{metric} </ListGroup>
</ListGroupItem> </ModalBody>
{/each} <ModalFooter>
</ListGroup> <Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
</ModalBody> <Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
<ModalFooter> </ModalFooter>
<Button color="primary" on:click={closeAndApply}> Close & Apply </Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}> Close </Button>
</ModalFooter>
</Modal> </Modal>

View File

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

View File

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

View File

@@ -1,102 +1,121 @@
<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 metrics = filterPresets.cluster let sorting = { field: "startTime", order: "DESC" },
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics isSortingOpen = false,
: ccconfig.plot_list_selectedMetrics isMetricsSelectionOpen = false;
let showFootprint = filterPresets.cluster let metrics = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`] ? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
: !!ccconfig.plot_list_showFootprint ccconfig.plot_list_selectedMetrics
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null : ccconfig.plot_list_selectedMetrics;
let showFootprint = filterPresets.cluster
// The filterPresets are handled by the Filters component, ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
// so we need to wait for it to be ready before we can start a query. : !!ccconfig.plot_list_showFootprint;
// This is also why JobList component starts out with a paused query. let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
onMount(() => filterComponent.update())
// The filterPresets are handled by the Filters component,
// so we need to wait for it to be ready before we can start a query.
// This is also why JobList component starts out with a paused query.
onMount(() => filterComponent.update());
</script> </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">
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
</Col> </Col>
{/if} {/if}
</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)}> </Button>
<Icon name="sort-up"/> Sorting <Button
</Button> outline
<Button color="primary"
outline color="primary" on:click={() => (isMetricsSelectionOpen = true)}
on:click={() => (isMetricsSelectionOpen = true)}> >
<Icon name="graph-up"/> Metrics <Icon name="graph-up" /> Metrics
</Button> </Button>
<Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button> <Button disabled outline
</Col> >{matchedJobs == null ? "Loading..." : `${matchedJobs} jobs`}</Button
<Col xs="auto"> >
<Filters </Col>
filterPresets={filterPresets} <Col xs="auto">
bind:this={filterComponent} <Filters
on:update={({ detail }) => { {filterPresets}
selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null bind:this={filterComponent}
jobList.update(detail.filters) on:update={({ detail }) => {
} selectedCluster = detail.filters[0]?.cluster
} /> ? detail.filters[0].cluster.eq
</Col> : null;
jobList.update(detail.filters);
}}
/>
</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
</Col> bind:authlevel
<Col xs="2"> bind:roles
<Refresher on:reload={() => jobList.refresh()} /> on:update={({ detail }) => filterComponent.update(detail)}
</Col> />
</Col>
<Col xs="2">
<Refresher on:reload={() => jobList.refresh()} />
</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

@@ -2,52 +2,58 @@
@component List of users or projects @component List of users or projects
--> -->
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { init } from "./utils.js"; import { init } from "./utils.js";
import { import {
Row, Row,
Col, Col,
Button, Button,
Icon, Icon,
Table, Table,
Card, Card,
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";
const {} = init(); const {} = init();
export let type; export let type;
export let filterPresets; export let filterPresets;
// 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
let jobFilters = []; let jobFilters = [];
let nameFilter = ""; let nameFilter = "";
let sorting = { field: "totalJobs", direction: "down" }; let sorting = { field: "totalJobs", direction: "down" };
const client = getContextClient(); const client = getContextClient();
$: stats = queryStore({ $: stats = queryStore({
client: client, client: client,
query: gql` query: gql`
query($jobFilters: [JobFilter!]!) { query($jobFilters: [JobFilter!]!) {
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) { rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
id id
@@ -58,185 +64,178 @@
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" target.children[0].className = `bi-sort-numeric-${direction}`;
: "up"; sorting = { field, direction };
target.children[0].className = `bi-sort-numeric-${direction}`; }
sorting = { field, direction };
}
function sort(stats, sorting, nameFilter) { function sort(stats, sorting, nameFilter) {
const cmp = const cmp =
sorting.field == "id" sorting.field == "id"
? sorting.direction == "up" ? sorting.direction == "up"
? (a, b) => a.id < b.id ? (a, b) => a.id < b.id
: (a, b) => a.id > b.id : (a, b) => a.id > b.id
: sorting.direction == "up" : sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field] ? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field]; : (a, b) => b[sorting.field] - a[sorting.field];
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp); return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
} }
onMount(() => filterComponent.update()); onMount(() => filterComponent.update());
</script> </script>
<Row> <Row>
<Col xs="auto"> <Col xs="auto">
<InputGroup> <InputGroup>
<Button disabled outline> <Button disabled outline>
Search {type.toLowerCase()}s Search {type.toLowerCase()}s
</Button> </Button>
<Input <Input
bind:value={nameFilter} bind:value={nameFilter}
placeholder="Filter by {{ placeholder="Filter by {{
USER: 'username', USER: 'username',
PROJECT: 'project', PROJECT: 'project',
}[type]}" }[type]}"
/> />
</InputGroup> </InputGroup>
</Col> </Col>
<Col xs="auto"> <Col xs="auto">
<Filters <Filters
bind:this={filterComponent} bind:this={filterComponent}
{filterPresets} {filterPresets}
startTimeQuickSelect={true} startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up" menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update={({ detail }) => { on:update={({ detail }) => {
jobFilters = detail.filters; jobFilters = detail.filters;
}} }}
/> />
</Col> </Col>
</Row> </Row>
<Table> <Table>
<thead> <thead>
<tr>
<th scope="col">
{{
USER: "Username",
PROJECT: "Project Name",
}[type]}
<Button
color={sorting.field == "id" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "id")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{#if type == "USER"}
<th scope="col">
Name
<Button
color={sorting.field == "name" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "name")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col">
Total Jobs
<Button
color={sorting.field == "totalJobs" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalJobs")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Walltime
<Button
color={sorting.field == "totalWalltime" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Core Hours
<Button
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Accelerator Hours
<Button
color={sorting.field == "totalAccHours" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
</tr>
</thead>
<tbody>
{#if $stats.fetching}
<tr>
<td colspan="4" style="text-align: center;"><Spinner secondary /></td>
</tr>
{:else if $stats.error}
<tr>
<td colspan="4"
><Card body color="danger" class="mb-3">{$stats.error.message}</Card
></td
>
</tr>
{:else if $stats.data}
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
<tr> <tr>
<th scope="col"> <td>
{({
USER: "Username",
PROJECT: "Project Name",
})[type]}
<Button
color={sorting.field == "id" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "id")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
{#if type == "USER"} {#if type == "USER"}
<th scope="col"> <a href="/monitoring/user/{row.id}"
Name >{scrambleNames ? scramble(row.id) : row.id}</a
<Button >
color={sorting.field == "name" ? "primary" : "light"} {:else if type == "PROJECT"}
size="sm" <a href="/monitoring/jobs/?project={row.id}"
on:click={(e) => changeSorting(e, "name")} >{scrambleNames ? scramble(row.id) : row.id}</a
> >
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col">
Total Jobs
<Button
color={sorting.field == "totalJobs" ? "primary" : "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalJobs")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Walltime
<Button
color={sorting.field == "totalWalltime"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Core Hours
<Button
color={sorting.field == "totalCoreHours"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
<th scope="col">
Total Accelerator Hours
<Button
color={sorting.field == "totalAccHours"
? "primary"
: "light"}
size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")}
>
<Icon name="sort-numeric-down" />
</Button>
</th>
</tr>
</thead>
<tbody>
{#if $stats.fetching}
<tr>
<td colspan="4" style="text-align: center;"
><Spinner secondary /></td
>
</tr>
{:else if $stats.error}
<tr>
<td colspan="4"
><Card body color="danger" class="mb-3"
>{$stats.error.message}</Card
></td
>
</tr>
{:else if $stats.data}
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
<tr>
<td>
{#if type == "USER"}
<a href="/monitoring/user/{row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else if type == "PROJECT"}
<a href="/monitoring/jobs/?project={row.id}"
>{scrambleNames ? scramble(row.id) : row.id}</a
>
{:else}
{row.id}
{/if}
</td>
{#if type == "USER"}
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
{/if}
<td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td>
<td>{row.totalAccHours}</td>
</tr>
{:else} {:else}
<tr> {row.id}
<td colspan="4" {/if}
><i>No {type.toLowerCase()}s/jobs found</i></td </td>
> {#if type == "USER"}
</tr> <td
{/each} >{scrambleNames
{/if} ? scramble(row?.name ? row.name : "-")
</tbody> : row?.name
? row.name
: "-"}</td
>
{/if}
<td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td>
<td>{row.totalAccHours}</td>
</tr>
{:else}
<tr>
<td colspan="4"><i>No {type.toLowerCase()}s/jobs found</i></td>
</tr>
{/each}
{/if}
</tbody>
</Table> </Table>

View File

@@ -1,95 +1,118 @@
<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(
let selectedHost = null, plot, fetching = false, error = null (subCluster) => subCluster.name == job.subCluster,
let selectedScope = minScope(scopes) );
const metricConfig = cluster.metricConfig.find(
(metricConfig) => metricConfig.name == metricName,
);
$: availableScopes = scopes let selectedHost = null,
$: selectedScopeIndex = scopes.findIndex(s => s == selectedScope) plot,
$: data = rawData[selectedScopeIndex] fetching = false,
$: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost) error = null;
let selectedScope = minScope(scopes);
let from = null, to = null $: availableScopes = scopes;
export function setTimeRange(f, t) { $: selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
from = f, to = t $: data = rawData[selectedScopeIndex];
$: series = data?.series.filter(
(series) => selectedHost == null || series.hostname == selectedHost,
);
let from = null,
to = null;
export function setTimeRange(f, t) {
(from = f), (to = t);
}
$: if (plot != null) plot.setTimeRange(from, to);
export async function loadMore() {
fetching = true;
let response = await fetchMetrics(job, [metricName], ["core"]);
fetching = false;
if (response.error) {
error = response.error;
return;
} }
$: if (plot != null) plot.setTimeRange(from, to) for (let jm of response.data.jobMetrics) {
if (jm.scope != "node") {
export async function loadMore() { scopes = [...scopes, jm.scope];
fetching = true rawData.push(jm.metric);
let response = await fetchMetrics(job, [metricName], ["core"]) selectedScope = jm.scope;
fetching = false selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
dispatch("more-loaded", jm);
if (response.error) { }
error = response.error
return
}
for (let jm of response.data.jobMetrics) {
if (jm.scope != "node") {
scopes = [...scopes, jm.scope]
rawData.push(jm.metric)
selectedScope = jm.scope
selectedScopeIndex = scopes.findIndex(s => s == jm.scope)
dispatch('more-loaded', jm)
}
}
} }
}
$: 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
</InputGroupText> : "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
<select class="form-select" bind:value={selectedScope}> </InputGroupText>
{#each availableScopes as scope} <select class="form-select" bind:value={selectedScope}>
<option value={scope}>{scope}</option> {#each availableScopes as scope}
{/each} <option value={scope}>{scope}</option>
{#if availableScopes.length == 1 && metricConfig?.scope != "node"} {/each}
<option value={"load-more"}>Load more...</option> {#if availableScopes.length == 1 && metricConfig?.scope != "node"}
{/if} <option value={"load-more"}>Load more...</option>
</select>
{#if job.resources.length > 1}
<select class="form-select" bind:value={selectedHost}>
<option value={null}>All Hosts</option>
{#each job.resources as { hostname }}
<option value={hostname}>{hostname}</option>
{/each}
</select>
{/if} {/if}
</select>
{#if job.resources.length > 1}
<select class="form-select" bind:value={selectedHost}>
<option value={null}>All Hosts</option>
{#each job.resources as { hostname }}
<option value={hostname}>{hostname}</option>
{/each}
</select>
{/if}
</InputGroup> </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}
timestep={data.timestep} {cluster}
scope={selectedScope} metric={metricName} {subCluster}
series={series} timestep={data.timestep}
isShared={isShared} scope={selectedScope}
resources={job.resources} /> metric={metricName}
{/if} {series}
{isShared}
resources={job.resources}
/>
{/if}
{/key} {/key}

View File

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

View File

@@ -1,39 +1,38 @@
<script> <script>
import { import {
Icon, Icon,
NavLink, NavLink,
Dropdown, Dropdown,
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
</script> </script>
{#each links as item} {#each links as item}
{#if !item.perCluster} {#if !item.perCluster}
<NavLink href={item.href} active={window.location.pathname == item.href} <NavLink href={item.href} active={window.location.pathname == item.href}
><Icon name={item.icon} /> {item.title}</NavLink ><Icon name={item.icon} /> {item.title}</NavLink
> >
{:else} {:else}
<Dropdown nav inNavbar> <Dropdown nav inNavbar>
<DropdownToggle nav caret> <DropdownToggle nav caret>
<Icon name={item.icon} /> <Icon name={item.icon} />
{item.title} {item.title}
</DropdownToggle> </DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
{#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> {/each}
{/each} </DropdownMenu>
</DropdownMenu> </Dropdown>
</Dropdown> {/if}
{/if}
{/each} {/each}

View File

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

View File

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

View File

@@ -1,139 +1,163 @@
<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`
updateConfiguration(name: $name, value: $value) mutation ($name: String!, $value: String!) {
}`, 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
value={metric} type="checkbox"
on:change={() => updateConfiguration({ bind:group={metricsInHistograms}
name: 'analysis_view_histogramMetrics', value={metric}
value: metricsInHistograms on:change={() =>
})} /> updateConfiguration({
name: "analysis_view_histogramMetrics",
value: metricsInHistograms,
})}
/>
{metric} {metric}
</ListGroupItem> </ListGroupItem>
{/each} {/each}
</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
on:click={() => { style="float: right;"
metricsInScatterplots = metricsInScatterplots.filter(p => pair != p) outline
updateConfiguration({ color="danger"
name: 'analysis_view_scatterPlotMetrics', on:click={() => {
value: metricsInScatterplots metricsInScatterplots = metricsInScatterplots.filter(
}); (p) => pair != p,
}}> );
<Icon name="x" /> updateConfiguration({
</Button> name: "analysis_view_scatterPlotMetrics",
</ListGroupItem> value: metricsInScatterplots,
{/each} });
</ListGroup> }}
>
<Icon name="x" />
</Button>
</ListGroupItem>
{/each}
</ListGroup>
<br/> <br />
<InputGroup> <InputGroup>
<select bind:value={selectedMetric1} class="form-group form-select"> <select bind:value={selectedMetric1} class="form-group form-select">
<option value={null}>Choose Metric for X Axis</option> <option value={null}>Choose Metric for X Axis</option>
{#each availableMetrics as metric} {#each availableMetrics as metric}
<option value={metric}>{metric}</option> <option value={metric}>{metric}</option>
{/each} {/each}
</select> </select>
<select bind:value={selectedMetric2} class="form-group form-select"> <select bind:value={selectedMetric2} class="form-group form-select">
<option value={null}>Choose Metric for Y Axis</option> <option value={null}>Choose Metric for Y Axis</option>
{#each availableMetrics as metric} {#each availableMetrics as metric}
<option value={metric}>{metric}</option> <option value={metric}>{metric}</option>
{/each} {/each}
</select> </select>
<Button outline disabled={selectedMetric1 == null || selectedMetric2 == null} <Button
on:click={() => { outline
metricsInScatterplots = [...metricsInScatterplots, [selectedMetric1, selectedMetric2]] disabled={selectedMetric1 == null || selectedMetric2 == null}
selectedMetric1 = null on:click={() => {
selectedMetric2 = null metricsInScatterplots = [
updateConfiguration({ ...metricsInScatterplots,
name: 'analysis_view_scatterPlotMetrics', [selectedMetric1, selectedMetric2],
value: metricsInScatterplots ];
}) selectedMetric1 = null;
}}> selectedMetric2 = null;
Add Plot updateConfiguration({
</Button> name: "analysis_view_scatterPlotMetrics",
</InputGroup> value: metricsInScatterplots,
});
</ModalBody> }}
<ModalFooter> >
<Button color="primary" Add Plot
on:click={() => (isScatterPlotConfigOpen = false)}> </Button>
Close </InputGroup>
</Button> </ModalBody>
</ModalFooter> <ModalFooter>
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}>
Close
</Button>
</ModalFooter>
</Modal> </Modal>

View File

@@ -1,139 +1,154 @@
<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}`
for (let metric of allMetrics) { ] || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
// -> Else: Load smallest available granularity as default as per availability
const availableScopes = scopesForMetric(metric)
if (job.exclusive != 1 || job.numNodes == 1) {
if (availableScopes.includes('accelerator')) {
selectedScopes[metric] = 'accelerator'
} else if (availableScopes.includes('core')) {
selectedScopes[metric] = 'core'
} else if (availableScopes.includes('socket')) {
selectedScopes[metric] = 'socket'
} else {
selectedScopes[metric] = 'node'
}
} else {
selectedScopes[metric] = maxScope(availableScopes)
}
sorting[metric] = { for (let metric of allMetrics) {
min: { dir: 'up', active: false }, // Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
avg: { dir: 'up', active: false }, // -> Else: Load smallest available granularity as default as per availability
max: { dir: 'up', active: false }, const availableScopes = scopesForMetric(metric);
} if (job.exclusive != 1 || job.numNodes == 1) {
if (availableScopes.includes("accelerator")) {
selectedScopes[metric] = "accelerator";
} else if (availableScopes.includes("core")) {
selectedScopes[metric] = "core";
} else if (availableScopes.includes("socket")) {
selectedScopes[metric] = "socket";
} else {
selectedScopes[metric] = "node";
}
} else {
selectedScopes[metric] = maxScope(availableScopes);
} }
export function sortBy(metric, stat) { sorting[metric] = {
let s = sorting[metric][stat] min: { dir: "up", active: false },
if (s.active) { avg: { dir: "up", active: false },
s.dir = s.dir == 'up' ? 'down' : 'up' max: { dir: "up", active: false },
} else { };
for (let metric in sorting) }
for (let stat in sorting[metric])
sorting[metric][stat].active = false
s.active = true
}
let series = jobMetrics.find(jm => jm.name == metric && jm.scope == 'node')?.metric.series export function sortBy(metric, stat) {
sorting = {...sorting} let s = sorting[metric][stat];
hosts = hosts.sort((h1, h2) => { if (s.active) {
let s1 = series.find(s => s.hostname == h1)?.statistics s.dir = s.dir == "up" ? "down" : "up";
let s2 = series.find(s => s.hostname == h2)?.statistics } else {
if (s1 == null || s2 == null) for (let metric in sorting)
return -1 for (let stat in sorting[metric]) sorting[metric][stat].active = false;
s.active = true;
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
})
} }
export function moreLoaded(jobMetric) { let series = jobMetrics.find(
jobMetrics = [...jobMetrics, jobMetric] (jm) => jm.name == metric && jm.scope == "node",
} )?.metric.series;
sorting = { ...sorting };
hosts = hosts.sort((h1, h2) => {
let s1 = series.find((s) => s.hostname == h1)?.statistics;
let s2 = series.find((s) => s.hostname == h2)?.statistics;
if (s1 == null || s2 == null) return -1;
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
});
}
export function moreLoaded(jobMetric) {
jobMetrics = [...jobMetrics, jobMetric];
}
</script> </script>
<Table> <Table>
<thead> <thead>
<tr> <tr>
<th> <th>
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' --> <Button outline on:click={() => (isMetricSelectionOpen = true)}>
Metrics <!-- log to click ', console.log(isMetricSelectionOpen)' -->
</Button> Metrics
</th> </Button>
{#each selectedMetrics as metric} </th>
<th colspan={selectedScopes[metric] == 'node' ? 3 : 4}> {#each selectedMetrics as metric}
<InputGroup> <th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
<InputGroupText> <InputGroup>
{metric} <InputGroupText>
</InputGroupText> {metric}
<select class="form-select" </InputGroupText>
bind:value={selectedScopes[metric]}> <select class="form-select" 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}
</select> </select>
</InputGroup> </InputGroup>
</th> </th>
{/each} {/each}
</tr> </tr>
<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
{/if} name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
</th> .active
{/each} ? '-fill'
{/each} : ''}"
</tr> />
</thead> {/if}
<tbody> </th>
{#each hosts as host (host)}
<tr>
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
host={host} metric={metric}
scope={selectedScopes[metric]}
jobMetrics={jobMetrics} />
{/each}
</tr>
{/each} {/each}
</tbody> {/each}
</tr>
</thead>
<tbody>
{#each hosts as host (host)}
<tr>
<th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)}
<StatsTableEntry
{host}
{metric}
scope={selectedScopes[metric]}
{jobMetrics}
/>
{/each}
</tr>
{/each}
</tbody>
</Table> </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,82 +1,86 @@
<script> <script>
import { Icon } from 'sveltestrap' import { Icon } from "@sveltestrap/sveltestrap";
export let host
export let metric
export let scope
export let jobMetrics
function compareNumbers(a, b) { export let host;
return a.id - b.id; export let metric;
export let scope;
export let jobMetrics;
function compareNumbers(a, b) {
return a.id - b.id;
}
function sortByField(field) {
let s = sorting[field];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let field in sorting) sorting[field].active = false;
s.active = true;
} }
function sortByField(field) { sorting = { ...sorting };
let s = sorting[field] series = series.sort((a, b) => {
if (s.active) { if (a == null || b == null) return -1;
s.dir = s.dir == 'up' ? 'down' : 'up'
} else {
for (let field in sorting)
sorting[field].active = false
s.active = true
}
sorting = {...sorting} if (field === "id") {
series = series.sort((a, b) => { return s.dir != "up" ? a[field] - b[field] : b[field] - a[field];
if (a == null || b == null) } else {
return -1 return s.dir != "up"
? a.statistics[field] - b.statistics[field]
: b.statistics[field] - a.statistics[field];
}
});
}
if (field === 'id') { let sorting = {
return s.dir != 'up' ? a[field] - b[field] : b[field] - a[field] id: { dir: "down", active: true },
} else { min: { dir: "up", active: false },
return s.dir != 'up' ? a.statistics[field] - b.statistics[field] : b.statistics[field] - a.statistics[field] avg: { dir: "up", active: false },
} max: { dir: "up", active: false },
}) };
}
let sorting = { $: series = jobMetrics
id: { dir: 'down', active: true }, .find((jm) => jm.name == metric && jm.scope == scope)
min: { dir: 'up', active: false }, ?.metric.series.filter((s) => s.hostname == host && s.statistics != null)
avg: { dir: 'up', active: false }, ?.sort(compareNumbers);
max: { dir: 'up', active: false },
}
$: series = jobMetrics
.find(jm => jm.name == metric && jm.scope == scope)
?.metric.series.filter(s => s.hostname == host && s.statistics != null)
?.sort(compareNumbers)
</script> </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>
<td> <td>
{series[0].statistics.avg} {series[0].statistics.avg}
</td> </td>
<td> <td>
{series[0].statistics.max} {series[0].statistics.max}
</td> </td>
{:else} {:else}
<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
</th> name="caret-{sorting[field].dir}{sorting[field].active
{/each} ? '-fill'
</tr> : ''}"
{#each series as s, i} />
<tr> </th>
<th>{s.id ?? i}</th> {/each}
<td>{s.statistics.min}</td> </tr>
<td>{s.statistics.avg}</td> {#each series as s, i}
<td>{s.statistics.max}</td> <tr>
</tr> <th>{s.id ?? i}</th>
{/each} <td>{s.statistics.min}</td>
</table> <td>{s.statistics.avg}</td>
</td> <td>{s.statistics.max}</td>
</tr>
{/each}
</table>
</td>
{/if} {/if}

File diff suppressed because it is too large Load Diff

View File

@@ -1,159 +1,218 @@
<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!) {
host nodeMetrics(
subCluster cluster: $cluster
metrics { metrics: $metrics
name from: $from
scope to: $to
metric { ) {
timestep host
unit { base, prefix } subCluster
series { metrics {
statistics { min, avg, max } name
data scope
} metric {
} timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
} }
data
}
} }
}`, }
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString()
} }
}) }
`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString(),
},
});
let metricUnits = {} 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>
<Refresher on:reload={() => {
const diff = Date.now() - to
from = new Date(from.getTime() + diff)
to = new Date(to.getTime() + diff)
}} />
</Col>
<Col>
<TimeSelection
bind:from={from}
bind:to={to} />
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}>
{#each clusters.find(c => c.name == cluster).metricConfig as metric}
<option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
{/each}
</select>
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
</InputGroup>
</Col>
{/if}
</Row>
<br/>
<Row>
<Col> <Col>
{#if $nodesQuery.error} <Refresher
<Card body color="danger">{$nodesQuery.error.message}</Card> on:reload={() => {
{:else if $nodesQuery.fetching || $initq.fetching} const diff = Date.now() - to;
<Spinner/> from = new Date(from.getTime() + diff);
{:else} to = new Date(to.getTime() + diff);
<PlotTable }}
let:item />
let:width
renderFor="systems"
itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics
.filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
.map(h => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node'),
disabled: checkMetricDisabled(selectedMetric, cluster, h.subCluster)
}))
.sort((a, b) => a.host.localeCompare(b.host))
}>
<h4 style="width: 100%; text-align: center;"><a style="display: block;padding-top: 15px;" href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>
{#if item.disabled === false && item.data}
<MetricPlot
width={width}
height={plotHeight}
timestep={item.data.metric.timestep}
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find(c => c.name == cluster)}
subCluster={item.subCluster}
resources={[{hostname: item.host}]}
forNode={true}/>
{:else if item.disabled === true && item.data}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
{:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
{/if}
</PlotTable>
{/if}
</Col> </Col>
<Col>
<TimeSelection bind:from bind:to />
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}>
{#each clusters.find((c) => c.name == cluster).metricConfig as metric}
<option value={metric.name}
>{metric.name} {metricUnits[metric.name]}</option
>
{/each}
</select>
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input
placeholder="hostname..."
type="text"
bind:value={hostnameFilter}
/>
</InputGroup>
</Col>
{/if}
</Row>
<br />
<Row>
<Col>
{#if $nodesQuery.error}
<Card body color="danger">{$nodesQuery.error.message}</Card>
{:else if $nodesQuery.fetching || $initq.fetching}
<Spinner />
{:else}
<PlotTable
let:item
let:width
renderFor="systems"
itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics
.filter(
(h) =>
h.host.includes(hostnameFilter) &&
h.metrics.some(
(m) => m.name == selectedMetric && m.scope == "node",
),
)
.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.find(
(m) => m.name == selectedMetric && m.scope == "node",
),
disabled: checkMetricDisabled(
selectedMetric,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))}
>
<h4 style="width: 100%; text-align: center;">
<a
style="display: block;padding-top: 15px;"
href="/monitoring/node/{cluster}/{item.host}"
>{item.host} ({item.subCluster})</a
>
</h4>
{#if item.disabled === false && item.data}
<MetricPlot
{width}
height={plotHeight}
timestep={item.data.metric.timestep}
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={item.subCluster}
resources={[{ hostname: item.host }]}
forNode={true}
/>
{:else if item.disabled === true && item.data}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{selectedMetric}</code></Card
>
{/if}
</PlotTable>
{/if}
</Col>
</Row> </Row>

View File

@@ -1,190 +1,234 @@
<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
}
const addTagsToJobMutation = ({ job, tagIds }) => {
return mutationStore({
client: client,
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
addTagsToJob(job: $job, tagIds: $tagIds) { id, type, name }
}`,
variables: {job, tagIds}
})
}
const removeTagsFromJobMutation = ({ job, tagIds }) => {
return mutationStore({
client: client,
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
removeTagsFromJob(job: $job, tagIds: $tagIds) { id, type, name }
}`,
variables: {job, tagIds}
})
}
let allTagsFiltered // $initialized is in there because when it becomes true, allTags is initailzed.
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags))
$: {
newTagType = '';
newTagName = '';
let parts = filterTerm.split(':').map(s => s.trim())
if (parts.length == 2 && parts.every(s => s.length > 0)) {
newTagType = parts[0]
newTagName = parts[1]
} }
} `,
variables: { type, name },
});
};
function isNewTag(type, name) { const addTagsToJobMutation = ({ job, tagIds }) => {
for (let tag of allTagsFiltered) return mutationStore({
if (tag.type == type && tag.name == name) client: client,
return false query: gql`
return true mutation ($job: ID!, $tagIds: [ID!]!) {
} addTagsToJob(job: $job, tagIds: $tagIds) {
id
type
name
}
}
`,
variables: { job, tagIds },
});
};
function createTag(type, name) { const removeTagsFromJobMutation = ({ job, tagIds }) => {
pendingChange = true return mutationStore({
createTagMutation({ type: type, name: name }) client: client,
.subscribe(res => { query: gql`
if (res.fetching === false && !res.error) { mutation ($job: ID!, $tagIds: [ID!]!) {
pendingChange = false removeTagsFromJob(job: $job, tagIds: $tagIds) {
allTags = [...allTags, res.data.createTag] id
newTagType = '' type
newTagName = '' name
addTagToJob(res.data.createTag) }
} else if (res.fetching === false && res.error) { }
throw res.error `,
// console.log('Error on subscription: ' + res.error) variables: { job, tagIds },
} });
}) };
}
function addTagToJob(tag) { let allTagsFiltered; // $initialized is in there because when it becomes true, allTags is initailzed.
pendingChange = tag.id $: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] })
.subscribe(res => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.addTagsToJob;
pendingChange = false;
} else if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
}
function removeTagFromJob(tag) { $: {
pendingChange = tag.id newTagType = "";
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] }) newTagName = "";
.subscribe(res => { let parts = filterTerm.split(":").map((s) => s.trim());
if (res.fetching === false && !res.error) { if (parts.length == 2 && parts.every((s) => s.length > 0)) {
jobTags = job.tags = res.data.removeTagsFromJob newTagType = parts[0];
pendingChange = false newTagName = parts[1];
} else if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
} }
}
function isNewTag(type, name) {
for (let tag of allTagsFiltered)
if (tag.type == type && tag.name == name) return false;
return true;
}
function createTag(type, name) {
pendingChange = true;
createTagMutation({ type: type, name: name }).subscribe((res) => {
if (res.fetching === false && !res.error) {
pendingChange = false;
allTags = [...allTags, res.data.createTag];
newTagType = "";
newTagName = "";
addTagToJob(res.data.createTag);
} else if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
});
}
function addTagToJob(tag) {
pendingChange = tag.id;
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe((res) => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.addTagsToJob;
pendingChange = false;
} else if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
});
}
function removeTagFromJob(tag) {
pendingChange = tag.id;
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe(
(res) => {
if (res.fetching === false && !res.error) {
jobTags = job.tags = res.data.removeTagsFromJob;
pendingChange = false;
} else if (res.fetching === false && res.error) {
throw res.error;
// console.log('Error on subscription: ' + res.error)
}
},
);
}
</script> </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
{#if pendingChange !== false} {#if pendingChange !== false}
<Spinner size="sm" secondary /> <Spinner size="sm" secondary />
{:else} {:else}
<Icon name="tags" /> <Icon name="tags" />
{/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"
<Icon name="x" /> outline
</Button> color="danger"
{:else} on:click={() => removeTagFromJob(tag)}
<Button size="sm" outline color="success" >
on:click={() => addTagToJob(tag)}> <Icon name="x" />
<Icon name="plus" /> </Button>
</Button>
{/if}
</span>
</ListGroupItem>
{:else} {:else}
<ListGroupItem disabled> <Button
<i>No tags matching</i> size="sm"
</ListGroupItem> outline
{/each} color="success"
</ul> on:click={() => addTagToJob(tag)}
<br/> >
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)} <Icon name="plus" />
<Button outline color="success" </Button>
on:click={e => (e.preventDefault(), createTag(newTagType, newTagName))}> {/if}
Create & Add Tag: </span>
<Tag tag={({ type: newTagType, name: newTagName })} clickable={false}/> </ListGroupItem>
</Button> {:else}
{:else if allTagsFiltered.length == 0} <ListGroupItem disabled>
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert> <i>No tags matching</i>
{/if} </ListGroupItem>
</ModalBody> {/each}
<ModalFooter> </ul>
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button> <br />
</ModalFooter> {#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<Button
outline
color="success"
on:click={(e) => (
e.preventDefault(), createTag(newTagType, newTagName)
)}
>
Create & Add Tag:
<Tag tag={{ type: newTagType, name: newTagName }} clickable={false} />
</Button>
{:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal> </Modal>
<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,237 +1,276 @@
<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 showFootprint = filterPresets.cluster let w1,
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`] w2,
: !!ccconfig.plot_list_showFootprint histogramHeight = 250,
isHistogramSelectionOpen = false;
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint;
$: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || []) $: metricsInHistograms = selectedCluster
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []
: ccconfig.user_view_histogramMetrics || [];
const client = getContextClient(); 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">
<Card body color="danger">{$initq.error.message}</Card>
</Col>
{/if}
<Col xs="auto"> <Col xs="auto">
<Button <Card body color="danger">{$initq.error.message}</Card>
outline color="primary" </Col>
on:click={() => (isSortingOpen = true)}> {/if}
<Icon name="sort-up"/> Sorting
</Button>
<Button <Col xs="auto">
outline color="primary" <Button outline color="primary" on:click={() => (isSortingOpen = true)}>
on:click={() => (isMetricsSelectionOpen = true)}> <Icon name="sort-up" /> Sorting
<Icon name="graph-up"/> Metrics </Button>
</Button>
<Button <Button
outline color="secondary" outline
on:click={() => (isHistogramSelectionOpen = true)}> color="primary"
<Icon name="bar-chart-line"/> Select Histograms on:click={() => (isMetricsSelectionOpen = true)}
</Button> >
</Col> <Icon name="graph-up" /> Metrics
<Col xs="auto"> </Button>
<Filters
filterPresets={filterPresets} <Button
startTimeQuickSelect={true} outline
bind:this={filterComponent} color="secondary"
on:update={({ detail }) => { on:click={() => (isHistogramSelectionOpen = true)}
jobFilters = [...detail.filters, { user: { eq: user.username } }] >
selectedCluster = jobFilters[0]?.cluster ? jobFilters[0].cluster.eq : null <Icon name="bar-chart-line" /> Select Histograms
jobList.update(jobFilters) </Button>
}} /> </Col>
</Col> <Col xs="auto">
<Col xs="auto" style="margin-left: auto;"> <Filters
<Refresher on:reload={() => jobList.refresh()} /> {filterPresets}
</Col> startTimeQuickSelect={true}
bind:this={filterComponent}
on:update={({ detail }) => {
jobFilters = [...detail.filters, { user: { eq: user.username } }];
selectedCluster = jobFilters[0]?.cluster
? jobFilters[0].cluster.eq
: null;
jobList.update(jobFilters);
}}
/>
</Col>
<Col xs="auto" style="margin-left: auto;">
<Refresher on:reload={() => jobList.refresh()} />
</Col>
</Row> </Row>
<br/> <br />
<Row> <Row>
{#if $stats.error} {#if $stats.error}
<Col> <Col>
<Card body color="danger">{$stats.error.message}</Card> <Card body color="danger">{$stats.error.message}</Card>
</Col> </Col>
{:else if !$stats.data} {:else if !$stats.data}
<Col> <Col>
<Spinner secondary /> <Spinner secondary />
</Col> </Col>
{:else} {:else}
<Col xs="4"> <Col xs="4">
<Table> <Table>
<tbody> <tbody>
<tr> <tr>
<th scope="row">Username</th> <th scope="row">Username</th>
<td>{scrambleNames ? scramble(user.username) : user.username}</td> <td>{scrambleNames ? scramble(user.username) : user.username}</td>
</tr> </tr>
{#if user.name} {#if user.name}
<tr> <tr>
<th scope="row">Name</th> <th scope="row">Name</th>
<td>{scrambleNames ? scramble(user.name) : user.name}</td> <td>{scrambleNames ? scramble(user.name) : user.name}</td>
</tr> </tr>
{/if} {/if}
{#if user.email} {#if user.email}
<tr> <tr>
<th scope="row">Email</th> <th scope="row">Email</th>
<td>{user.email}</td> <td>{user.email}</td>
</tr> </tr>
{/if} {/if}
<tr> <tr>
<th scope="row">Total Jobs</th> <th scope="row">Total Jobs</th>
<td>{$stats.data.jobsStatistics[0].totalJobs}</td> <td>{$stats.data.jobsStatistics[0].totalJobs}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Short Jobs</th> <th scope="row">Short Jobs</th>
<td>{$stats.data.jobsStatistics[0].shortJobs}</td> <td>{$stats.data.jobsStatistics[0].shortJobs}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Total Walltime</th> <th scope="row">Total Walltime</th>
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td> <td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Total Core Hours</th> <th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td> <td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr> </tr>
</tbody> </tbody>
</Table> </Table>
</Col> </Col>
<div class="col-4 text-center" bind:clientWidth={w1}> <div class="col-4 text-center" bind:clientWidth={w1}>
{#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}
title="Duration Distribution" height={histogramHeight}
xlabel="Current Runtimes" title="Duration Distribution"
xunit="Hours" xlabel="Current Runtimes"
ylabel="Number of Jobs" xunit="Hours"
yunit="Jobs"/> ylabel="Number of Jobs"
{/key} yunit="Jobs"
</div> />
<div class="col-4 text-center" bind:clientWidth={w2}> {/key}
{#key $stats.data.jobsStatistics[0].histNumNodes} </div>
<Histogram <div class="col-4 text-center" bind:clientWidth={w2}>
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)} {#key $stats.data.jobsStatistics[0].histNumNodes}
width={w2 - 25} height={histogramHeight} <Histogram
title="Number of Nodes Distribution" data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
xlabel="Allocated Nodes" width={w2 - 25}
xunit="Nodes" height={histogramHeight}
ylabel="Number of Jobs" title="Number of Nodes Distribution"
yunit="Jobs"/> xlabel="Allocated Nodes"
{/key} xunit="Nodes"
</div> ylabel="Number of Jobs"
{/if} yunit="Jobs"
/>
{/key}
</div>
{/if}
</Row> </Row>
{#if metricsInHistograms} {#if metricsInHistograms}
<Row> <Row>
{#if $stats.error} {#if $stats.error}
<Col> <Col>
<Card body color="danger">{$stats.error.message}</Card> <Card body color="danger">{$stats.error.message}</Card>
</Col> </Col>
{:else if !$stats.data} {:else if !$stats.data}
<Col> <Col>
<Spinner secondary /> <Spinner secondary />
</Col> </Col>
{:else} {:else}
<Col> <Col>
{#key $stats.data.jobsStatistics[0].histMetrics} {#key $stats.data.jobsStatistics[0].histMetrics}
<PlotTable <PlotTable
let:item let:item
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}
title="Distribution of '{item.metric}' averages" height={250}
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`} title="Distribution of '{item.metric}' averages"
xunit={item.unit} xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
ylabel="Number of Jobs" xunit={item.unit}
yunit="Jobs"/> ylabel="Number of Jobs"
</PlotTable> yunit="Jobs"
{/key} />
</Col> </PlotTable>
{/if} {/key}
</Row> </Col>
{/if}
</Row>
{/if} {/if}
<br/> <br />
<Row> <Row>
<Col> <Col>
<JobList <JobList bind:metrics bind:sorting bind:this={jobList} bind:showFootprint />
bind:metrics={metrics} </Col>
bind:sorting={sorting}
bind:this={jobList}
bind:showFootprint={showFootprint} />
</Col>
</Row> </Row>
<Sorting <Sorting bind:sorting bind:isOpen={isSortingOpen} />
bind:sorting={sorting}
bind:isOpen={isSortingOpen} /> <MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint
view="list"
/>
<MetricSelection
bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics"
bind:metrics={metrics}
bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
<HistogramSelection <HistogramSelection
bind:cluster={selectedCluster} bind:cluster={selectedCluster}
bind:metricsInHistograms={metricsInHistograms} bind:metricsInHistograms
bind:isOpen={isHistogramSelectionOpen} /> bind:isOpen={isHistogramSelectionOpen}
/>

View File

@@ -1,60 +1,65 @@
<script> <script>
import { Icon, InputGroup, InputGroupText } from 'sveltestrap'; import { Icon, InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
export let timeseriesPlots; export let timeseriesPlots;
let windowSize = 100; // Goes from 0 to 100 let windowSize = 100; // Goes from 0 to 100
let windowPosition = 50; // Goes from 0 to 100 let windowPosition = 50; // Goes from 0 to 100
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();
timeoutId = null; timeoutId = null;
}, 100); }, 100);
} }
$: requestUpdatePlots(windowSize, windowPosition); $: requestUpdatePlots(windowSize, windowPosition);
</script> </script>
<div> <div>
<InputGroup> <InputGroup>
<InputGroupText> <InputGroupText>
<Icon name="zoom-in"/> <Icon name="zoom-in" />
</InputGroupText> </InputGroupText>
<InputGroupText> <InputGroupText>
Window Size: Window Size:
<input <input
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"
<span style="width: 5em;"> max="100"
({windowSize}%) step="1"
</span> />
</InputGroupText> <span style="width: 5em;">
<InputGroupText> ({windowSize}%)
Window Position: </span>
<input </InputGroupText>
style="margin: 0em 0em 0em 1em" <InputGroupText>
type="range" Window Position:
bind:value={windowPosition} <input
min=0 max=100 step=1 /> style="margin: 0em 0em 0em 1em"
</InputGroupText> type="range"
</InputGroup> bind:value={windowPosition}
min="0"
max="100"
step="1"
/>
</InputGroupText>
</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,171 +1,498 @@
<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) {
popMessage(err, target, '#d63384')
} }
} catch (err) {
return false popMessage(err, target, "#d63384");
} }
function popMessage(response, restarget, rescolor) { return false;
message = {msg: response, target: restarget, color: rescolor} }
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
function popMessage(response, restarget, rescolor) {
message = { msg: response, target: restarget, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 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
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} --> ><Card class="h-100">
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#line-width-form', 'lw')}> <!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <form
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> id="line-width-form"
<div>Line Width</div> method="post"
<!-- Expand If-Clause for clarity once --> action="/api/configuration/"
{#if displayMessage && message.target == 'lw'} class="card-body"
<div style="margin-left: auto; font-size: 0.9em;"> on:submit|preventDefault={() =>
<code style="color: {message.color};" out:fade> handleSettingSubmit("#line-width-form", "lw")}
Update: {message.msg} >
</code> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
</div> <CardTitle
{/if} style="margin-bottom: 1em; display: flex; align-items: center;"
</CardTitle> >
<input type="hidden" name="key" value="plot_general_lineWidth"/> <div>Line Width</div>
<div class="mb-3"> <!-- Expand If-Clause for clarity once -->
<label for="value" class="form-label">Line Width</label> {#if displayMessage && message.target == "lw"}
<input type="number" class="form-control" id="lwvalue" name="value" aria-describedby="lineWidthHelp" value="{config.plot_general_lineWidth}" min="1"/> <div style="margin-left: auto; font-size: 0.9em;">
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div> <code style="color: {message.color};" out:fade>
Update: {message.msg}
</code>
</div> </div>
<Button color="primary" type="submit">Submit</Button> {/if}
</form> </CardTitle>
</Card></Col> <input type="hidden" name="key" value="plot_general_lineWidth" />
<div class="mb-3">
<label for="value" class="form-label">Line Width</label>
<input
type="number"
class="form-control"
id="lwvalue"
name="value"
aria-describedby="lineWidthHelp"
value={config.plot_general_lineWidth}
min="1"
/>
<div id="lineWidthHelp" class="form-text">
Width of the lines in the timeseries plots.
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col
>
<!-- PLOTS PER ROW --> <!-- 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">
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <form
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> id="plots-per-row-form"
<div>Plots per Row</div> method="post"
{#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} action="/api/configuration/"
</CardTitle> class="card-body"
<input type="hidden" name="key" value="plot_view_plotsPerRow"/> on:submit|preventDefault={() =>
<div class="mb-3"> handleSettingSubmit("#plots-per-row-form", "ppr")}
<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"/> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div> <CardTitle
</div> style="margin-bottom: 1em; display: flex; align-items: center;"
<Button color="primary" type="submit">Submit</Button> >
</form> <div>Plots per Row</div>
</Card></Col> {#if displayMessage && message.target == "ppr"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
<div class="mb-3">
<label for="value" class="form-label">Plots per Row</label>
<input
type="number"
class="form-control"
id="pprvalue"
name="value"
aria-describedby="plotsperrowHelp"
value={config.plot_view_plotsPerRow}
min="1"
/>
<div id="plotsperrowHelp" class="form-text">
How many plots to show next to each other on pages such as
/monitoring/job/, /monitoring/system/...
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col
>
<!-- BACKGROUND --> <!-- 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">
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <form
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> id="backgrounds-form"
<div>Colored Backgrounds</div> method="post"
{#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} action="/api/configuration/"
</CardTitle> class="card-body"
<input type="hidden" name="key" value="plot_general_colorBackground"/> on:submit|preventDefault={() =>
<div class="mb-3"> handleSettingSubmit("#backgrounds-form", "bg")}
<div> >
{#if config.plot_general_colorBackground} <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<input type="radio" id="true" name="value" value="true" checked/> <CardTitle
{:else} style="margin-bottom: 1em; display: flex; align-items: center;"
<input type="radio" id="true" name="value" value="true" /> >
{/if} <div>Colored Backgrounds</div>
<label for="true">Yes</label> {#if displayMessage && message.target == "bg"}<div
</div> style="margin-left: auto; font-size: 0.9em;"
<div> >
{#if config.plot_general_colorBackground} <code style="color: {message.color};" out:fade
<input type="radio" id="false" name="value" value="false" /> >Update: {message.msg}</code
{:else} >
<input type="radio" id="false" name="value" value="false" checked/> </div>{/if}
{/if} </CardTitle>
<label for="false">No</label> <input type="hidden" name="key" value="plot_general_colorBackground" />
</div> <div class="mb-3">
</div> <div>
<Button color="primary" type="submit">Submit</Button> {#if config.plot_general_colorBackground}
</form> <input type="radio" id="true" name="value" value="true" checked />
</Card></Col> {:else}
<input type="radio" id="true" name="value" value="true" />
{/if}
<label for="true">Yes</label>
</div>
<div>
{#if config.plot_general_colorBackground}
<input type="radio" id="false" name="value" value="false" />
{:else}
<input
type="radio"
id="false"
name="value"
value="false"
checked
/>
{/if}
<label for="false">No</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card></Col
>
</Row> </Row>
<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>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <form
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;"> id="colorscheme-form"
<div>Color Scheme for Timeseries Plots</div> method="post"
{#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} action="/api/configuration/"
</CardTitle> class="card-body"
<input type="hidden" name="key" value="plot_general_colorscheme"/> >
<Table hover> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<tbody> <CardTitle
{#each Object.entries(colorschemes) as [name, rgbrow]} style="margin-bottom: 1em; display: flex; align-items: center;"
<tr> >
<th scope="col">{name}</th> <div>Color Scheme for Timeseries Plots</div>
<td> {#if displayMessage && message.target == "cs"}<div
{#if rgbrow.join(',') == config.plot_general_colorscheme} style="margin-left: auto; font-size: 0.9em;"
<input type="radio" name="value" value={JSON.stringify(rgbrow)} checked on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/> >
{:else} <code style="color: {message.color};" out:fade
<input type="radio" name="value" value={JSON.stringify(rgbrow)} on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/> >Update: {message.msg}</code
{/if} >
</td> </div>{/if}
<td> </CardTitle>
{#each rgbrow as rgb} <input type="hidden" name="key" value="plot_general_colorscheme" />
<span class="color-dot" style="background-color: {rgb};"></span> <Table hover>
{/each} <tbody>
</td> {#each Object.entries(colorschemes) as [name, rgbrow]}
</tr> <tr>
{/each} <th scope="col">{name}</th>
</tbody> <td>
</Table> {#if rgbrow.join(",") == config.plot_general_colorscheme}
</form> <input
</Card></Col> type="radio"
name="value"
value={JSON.stringify(rgbrow)}
checked
on:click={() =>
handleSettingSubmit("#colorscheme-form", "cs")}
/>
{:else}
<input
type="radio"
name="value"
value={JSON.stringify(rgbrow)}
on:click={() =>
handleSettingSubmit("#colorscheme-form", "cs")}
/>
{/if}
</td>
<td>
{#each rgbrow as rgb}
<span class="color-dot" style="background-color: {rgb};"
></span>
{/each}
</td>
</tr>
{/each}
</tbody>
</Table>
</form>
</Card></Col
>
</Row> </Row>
<style> <style>
.color-dot { .color-dot {
height: 10px; height: 10px;
width: 10px; width: 10px;
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
} }
</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
<CardTitle class="mb-3">Create User</CardTitle> id="create-user-form"
<div class="mb-3"> method="post"
<label for="username" class="form-label">Username (ID)</label> action="/api/users/"
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/> class="card-body"
<div id="usernameHelp" class="form-text">Must be unique.</div> on:submit|preventDefault={handleUserSubmit}
</div> >
<div class="mb-3"> <CardTitle class="mb-3">Create User</CardTitle>
<label for="password" class="form-label">Password</label> <div class="mb-3">
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/> <label for="username" class="form-label">Username (ID)</label>
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div> <input
</div> type="text"
<div class="mb-3"> class="form-control"
<label for="name" class="form-label">Project</label> id="username"
<input type="text" class="form-control" id="project" name="project" aria-describedby="projectHelp"/> name="username"
<div id="projectHelp" class="form-text">Only Manager users can have a project. Allows to inspect jobs and users of given project.</div> aria-describedby="usernameHelp"
</div> />
<div class="mb-3"> <div id="usernameHelp" class="form-text">Must be unique.</div>
<label for="name" class="form-label">Name</label> </div>
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/> <div class="mb-3">
<div id="nameHelp" class="form-text">Optional, can be blank.</div> <label for="password" class="form-label">Password</label>
</div> <input
<div class="mb-3"> type="password"
<label for="email" class="form-label">Email address</label> class="form-control"
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/> id="password"
<div id="emailHelp" class="form-text">Optional, can be blank.</div> name="password"
</div> aria-describedby="passwordHelp"
/>
<div id="passwordHelp" class="form-text">
Only API users are allowed to have a blank password. Users with a blank
password can only authenticate via Tokens.
</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Project</label>
<input
type="text"
class="form-control"
id="project"
name="project"
aria-describedby="projectHelp"
/>
<div id="projectHelp" class="form-text">
Only Manager users can have a project. Allows to inspect jobs and users
of given project.
</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input
type="text"
class="form-control"
id="name"
name="name"
aria-describedby="nameHelp"
/>
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input
type="email"
class="form-control"
id="email"
name="email"
aria-describedby="emailHelp"
/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<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}
<label for={role}>{role.toUpperCase()} (Allowed to interact with REST API.)</label> >{role.toUpperCase()} (Allowed to interact with REST API.)</label
</div> >
{:else if i == 1} </div>
<div> {:else if i == 1}
<input type="radio" id={role} name="role" value={role} checked/> <div>
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created via LDAP sync.)</label> <input type="radio" id={role} name="role" value={role} checked />
</div> <label for={role}
{:else} >{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created
<div> via LDAP sync.)</label
<input type="radio" id={role} name="role" value={role}/> >
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label> </div>
</div> {:else}
{/if} <div>
{/each} <input type="radio" id={role} name="role" value={role} />
</div> <label for={role}
<p style="display: flex; align-items: center;"> >{role.charAt(0).toUpperCase() + role.slice(1)}</label
<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} </div>
</p> {/if}
</form> {/each}
</div>
<p style="display: flex; align-items: center;">
<Button type="submit" color="primary">Submit</Button>
{#if displayMessage}<div style="margin-left: 1.5em;">
<b
><code style="color: {message.color};" out:fade>{message.msg}</code
></b
>
</div>{/if}
</p>
</form>
</Card> </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()
formData.append('username', username)
formData.append('add-project', project)
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
} }
async function handleRemoveProject() { let formData = new FormData();
const username = document.querySelector('#project-username').value formData.append("username", username);
const project = document.querySelector('#project-id').value formData.append("add-project", project);
if (username == "" || project == "") { try {
alert('Please fill in a username and select a project.') const res = await fetch(`/api/user/${username}`, {
return method: "POST",
} body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
let formData = new FormData() async function handleRemoveProject() {
formData.append('username', username) const username = document.querySelector("#project-username").value;
formData.append('remove-project', project) const project = document.querySelector("#project-id").value;
try { if (username == "" || project == "") {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData }) alert("Please fill in a username and select a project.");
if (res.ok) { return;
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
} }
function popMessage(response, rescolor) { let formData = new FormData();
message = {msg: response, color: rescolor} formData.append("username", username);
displayMessage = true formData.append("remove-project", project);
setTimeout(function() {
displayMessage = false
}, 3500)
}
function reloadUserList() { try {
dispatch('reload') const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
} }
}
function popMessage(response, rescolor) {
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch("reload");
}
</script> </script>
<Card> <Card>
<CardBody> <CardBody>
<CardTitle class="mb-3">Edit Project Managed By User (Manager Only)</CardTitle> <CardTitle class="mb-3"
<div class="input-group mb-3"> >Edit Project Managed By User (Manager Only)</CardTitle
<input type="text" class="form-control" placeholder="username" id="project-username"/> >
<input type="text" class="form-control" placeholder="project-id" id="project-id"/> <div class="input-group mb-3">
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button --> <input
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components --> type="text"
<button class="btn btn-primary" type="button" id="add-project-button" on:click|preventDefault={handleAddProject}>Add</button> class="form-control"
<button class="btn btn-danger" type="button" id="remove-project-button" on:click|preventDefault={handleRemoveProject}>Remove</button> placeholder="username"
</div> id="project-username"
<p> />
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if} <input
</p> type="text"
</CardBody> class="form-control"
placeholder="project-id"
id="project-id"
/>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button
class="btn btn-primary"
type="button"
id="add-project-button"
on:click|preventDefault={handleAddProject}>Add</button
>
<button
class="btn btn-danger"
type="button"
id="remove-project-button"
on:click|preventDefault={handleRemoveProject}>Remove</button
>
</div>
<p>
{#if displayMessage}<b
><code style="color: {message.color};" out:fade
>Update: {message.msg}</code
></b
>{/if}
</p>
</CardBody>
</Card> </Card>

View File

@@ -1,104 +1,131 @@
<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()
formData.append('username', username)
formData.append('add-role', role)
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
} }
async function handleRemoveRole() { let formData = new FormData();
const username = document.querySelector('#role-username').value formData.append("username", username);
const role = document.querySelector('#role-select').value formData.append("add-role", role);
if (username == "" || role == "") { try {
alert('Please fill in a username and select a role.') const res = await fetch(`/api/user/${username}`, {
return method: "POST",
} body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
let formData = new FormData() async function handleRemoveRole() {
formData.append('username', username) const username = document.querySelector("#role-username").value;
formData.append('remove-role', role) const role = document.querySelector("#role-select").value;
try { if (username == "" || role == "") {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData }) alert("Please fill in a username and select a role.");
if (res.ok) { return;
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
} }
function popMessage(response, rescolor) { let formData = new FormData();
message = {msg: response, color: rescolor} formData.append("username", username);
displayMessage = true formData.append("remove-role", role);
setTimeout(function() {
displayMessage = false
}, 3500)
}
function reloadUserList() { try {
dispatch('reload') const res = await fetch(`/api/user/${username}`, {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
reloadUserList();
} else {
let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
} }
}
function popMessage(response, rescolor) {
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
function reloadUserList() {
dispatch("reload");
}
</script> </script>
<Card> <Card>
<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
<select class="form-select" id="role-select"> type="text"
<option selected value="">Role...</option> class="form-control"
{#each roles as role} placeholder="username"
<option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option> id="role-username"
{/each} />
</select> <select class="form-select" id="role-select">
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button --> <option selected value="">Role...</option>
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components --> {#each roles as role}
<button class="btn btn-primary" type="button" id="add-role-button" on:click|preventDefault={handleAddRole}>Add</button> <option value={role}
<button class="btn btn-danger" type="button" id="remove-role-button" on:click|preventDefault={handleRemoveRole}>Remove</button> >{role.charAt(0).toUpperCase() + role.slice(1)}</option
</div> >
<p> {/each}
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if} </select>
</p> <!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
</CardBody> <!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button
class="btn btn-primary"
type="button"
id="add-role-button"
on:click|preventDefault={handleAddRole}>Add</button
>
<button
class="btn btn-danger"
type="button"
id="remove-role-button"
on:click|preventDefault={handleRemoveRole}>Remove</button
>
</div>
<p>
{#if displayMessage}<b
><code style="color: {message.color};" out:fade
>Update: {message.msg}</code
></b
>{/if}
</p>
</CardBody>
</Card> </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
Active? type="checkbox"
</CardBody> id="scramble-names-checkbox"
style="margin-right: 1em;"
on:click={handleScramble}
bind:checked={scrambled}
/>
Active?
</CardBody>
</Card> </Card>

View File

@@ -1,68 +1,87 @@
<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
</p> >
<div style="width: 100%; max-height: 500px; overflow-y: scroll;"> <Button
<Table hover> color="secondary"
<thead> size="sm"
<tr> on:click={reloadUserList}
<th>Username</th> style="float: right;">Reload</Button
<th>Name</th> >
<th>Project(s)</th> </p>
<th>Email</th> <div style="width: 100%; max-height: 500px; overflow-y: scroll;">
<th>Roles</th> <Table hover>
<th>JWT</th> <thead>
<th>Delete</th> <tr>
</tr> <th>Username</th>
</thead> <th>Name</th>
<tbody id="users-list"> <th>Project(s)</th>
{#each userList as user} <th>Email</th>
<tr id="user-{user.username}"> <th>Roles</th>
<ShowUsersRow {user}/> <th>JWT</th>
<td><button class="btn btn-danger del-user" on:click={deleteUser(user.username)}>Delete</button></td> <th>Delete</th>
</tr> </tr>
{:else} </thead>
<tr> <tbody id="users-list">
<td colspan="4"> {#each userList as user}
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div> <tr id="user-{user.username}">
</td> <ShowUsersRow {user} />
</tr> <td
{/each} ><button
</tbody> class="btn btn-danger del-user"
</Table> on:click={deleteUser(user.username)}>Delete</button
</div> ></td
</CardBody> >
</tr>
{:else}
<tr>
<td colspan="4">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</td>
</tr>
{/each}
</tbody>
</Table>
</div>
</CardBody>
</Card> </Card>

View File

@@ -1,28 +1,32 @@
<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>
<td>{user.username}</td> <td>{user.username}</td>
<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)}
{:else} >Gen. JWT</Button
<textarea rows="3" cols="20">{jwt}</textarea> >
{/if} {:else}
<textarea rows="3" cols="20">{jwt}</textarea>
{/if}
</td> </td>

View File

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

View File

@@ -1,158 +1,244 @@
<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
$: moreDisabled = pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
$: betweenDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0
$: lessDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: moreDisabled =
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: betweenDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0;
</script> </script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader> <ModalHeader>Select Job Duration</ModalHeader>
Select Job Duration <ModalBody>
</ModalHeader> <h4>Duration more than</h4>
<ModalBody> <Row>
<h4>Duration more than</h4> <Col>
<Row> <div class="input-group mb-2 mr-sm-2">
<Col> <input
<div class="input-group mb-2 mr-sm-2"> type="number"
<input type="number" min="0" class="form-control" bind:value={pendingMoreThan.hours} disabled={moreDisabled}> min="0"
<div class="input-group-append"> class="form-control"
<div class="input-group-text">h</div> bind:value={pendingMoreThan.hours}
</div> disabled={moreDisabled}
</div> />
</Col> <div class="input-group-append">
<Col> <div class="input-group-text">h</div>
<div class="input-group mb-2 mr-sm-2"> </div>
<input type="number" min="0" max="59" class="form-control" bind:value={pendingMoreThan.mins} disabled={moreDisabled}> </div>
<div class="input-group-append"> </Col>
<div class="input-group-text">m</div> <Col>
</div> <div class="input-group mb-2 mr-sm-2">
</div> <input
</Col> type="number"
</Row> min="0"
<hr/> max="59"
class="form-control"
bind:value={pendingMoreThan.mins}
disabled={moreDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr />
<h4>Duration less than</h4> <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
<div class="input-group-append"> type="number"
<div class="input-group-text">h</div> min="0"
</div> class="form-control"
</div> bind:value={pendingLessThan.hours}
</Col> disabled={lessDisabled}
<Col> />
<div class="input-group mb-2 mr-sm-2"> <div class="input-group-append">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingLessThan.mins} disabled={lessDisabled}> <div class="input-group-text">h</div>
<div class="input-group-append"> </div>
<div class="input-group-text">m</div> </div>
</div> </Col>
</div> <Col>
</Col> <div class="input-group mb-2 mr-sm-2">
</Row> <input
<hr/> type="number"
min="0"
max="59"
class="form-control"
bind:value={pendingLessThan.mins}
disabled={lessDisabled}
/>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr />
<h4>Duration between</h4> <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
<div class="input-group-append"> type="number"
<div class="input-group-text">h</div> min="0"
</div> class="form-control"
</div> bind:value={pendingFrom.hours}
</Col> disabled={betweenDisabled}
<Col> />
<div class="input-group mb-2 mr-sm-2"> <div class="input-group-append">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingFrom.mins} disabled={betweenDisabled}> <div class="input-group-text">h</div>
<div class="input-group-append"> </div>
<div class="input-group-text">m</div> </div>
</div> </Col>
</div> <Col>
</Col> <div class="input-group mb-2 mr-sm-2">
</Row> <input
<h4>and</h4> type="number"
<Row> min="0"
<Col> max="59"
<div class="input-group mb-2 mr-sm-2"> class="form-control"
<input type="number" min="0" class="form-control" bind:value={pendingTo.hours} disabled={betweenDisabled}> bind:value={pendingFrom.mins}
<div class="input-group-append"> disabled={betweenDisabled}
<div class="input-group-text">h</div> />
</div> <div class="input-group-append">
</div> <div class="input-group-text">m</div>
</Col> </div>
<Col> </div>
<div class="input-group mb-2 mr-sm-2"> </Col>
<input type="number" min="0" max="59" class="form-control" bind:value={pendingTo.mins} disabled={betweenDisabled}> </Row>
<div class="input-group-append"> <h4>and</h4>
<div class="input-group-text">m</div> <Row>
</div> <Col>
</div> <div class="input-group mb-2 mr-sm-2">
</Col> <input
</Row> type="number"
</ModalBody> min="0"
<ModalFooter> class="form-control"
<Button color="primary" bind:value={pendingTo.hours}
on:click={() => { disabled={betweenDisabled}
isOpen = false />
lessThan = hoursAndMinsToSecs(pendingLessThan) <div class="input-group-append">
moreThan = hoursAndMinsToSecs(pendingMoreThan) <div class="input-group-text">h</div>
from = hoursAndMinsToSecs(pendingFrom) </div>
to = hoursAndMinsToSecs(pendingTo) </div>
dispatch('update', { lessThan, moreThan, from, to }) </Col>
}}> <Col>
Close & Apply <div class="input-group mb-2 mr-sm-2">
</Button> <input
<Button color="warning" on:click={() => { type="number"
lessThan = null min="0"
moreThan = null max="59"
from = null class="form-control"
to = null bind:value={pendingTo.mins}
reset() disabled={betweenDisabled}
}}>Reset Values</Button> />
<Button color="danger" on:click={() => { <div class="input-group-append">
isOpen = false <div class="input-group-text">m</div>
lessThan = null </div>
moreThan = null </div>
from = null </Col>
to = null </Row>
reset() </ModalBody>
dispatch('update', { lessThan, moreThan, from, to }) <ModalFooter>
}}>Reset Filter</Button> <Button
<Button on:click={() => (isOpen = false)}>Close</Button> color="primary"
</ModalFooter> on:click={() => {
isOpen = false;
lessThan = hoursAndMinsToSecs(pendingLessThan);
moreThan = hoursAndMinsToSecs(pendingMoreThan);
from = hoursAndMinsToSecs(pendingFrom);
to = hoursAndMinsToSecs(pendingTo);
dispatch("update", { lessThan, moreThan, from, to });
}}
>
Close & Apply
</Button>
<Button
color="warning"
on:click={() => {
lessThan = null;
moreThan = null;
from = null;
to = null;
reset();
}}>Reset Values</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
lessThan = null;
moreThan = null;
from = null;
to = null;
reset();
dispatch("update", { lessThan, moreThan, from, to });
}}>Reset Filter</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal> </Modal>

View File

@@ -10,373 +10,418 @@
- 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 TimeSelection from './TimeSelection.svelte' import JobStates, { allJobStates } from "./JobStates.svelte";
import StartTime from "./StartTime.svelte";
import Tags from "./Tags.svelte";
import Tag from "../Tag.svelte";
import Duration from "./Duration.svelte";
import Resources from "./Resources.svelte";
import Statistics from "./Stats.svelte";
// import TimeSelection from './TimeSelection.svelte'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher();
export let menuText = null export let 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:
startTime: filterPresets.startTime || { from: null, to: null }, filterPresets.states || filterPresets.state
tags: filterPresets.tags || [], ? [filterPresets.state].flat()
duration: filterPresets.duration || { lessThan: null, moreThan: null, from: null, to: null }, : allJobStates,
jobId: filterPresets.jobId || '', startTime: filterPresets.startTime || { from: null, to: null },
arrayJobId: filterPresets.arrayJobId || null, tags: filterPresets.tags || [],
user: filterPresets.user || '', duration: filterPresets.duration || {
project: filterPresets.project || '', lessThan: null,
jobName: filterPresets.jobName || '', moreThan: null,
from: null,
to: null,
},
jobId: filterPresets.jobId || "",
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || "",
project: filterPresets.project || "",
jobName: filterPresets.jobName || "",
node: filterPresets.node || null, node: filterPresets.node || null,
numNodes: filterPresets.numNodes || { from: null, to: null }, numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null }, numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
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,
isStartTimeOpen = false, isStartTimeOpen = false,
isTagsOpen = false, isTagsOpen = false,
isDurationOpen = false, isDurationOpen = false,
isResourcesOpen = false, isResourcesOpen = false,
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.states.length != allJobStates.length)
if (filters.partition) items.push({ state: filters.states });
items.push({ partition: { eq: filters.partition } }) if (filters.startTime.from || filters.startTime.to)
if (filters.states.length != allJobStates.length) items.push({
items.push({ state: filters.states }) startTime: { from: filters.startTime.from, to: filters.startTime.to },
if (filters.startTime.from || filters.startTime.to) });
items.push({ startTime: { from: filters.startTime.from, to: filters.startTime.to } }) if (filters.tags.length != 0) items.push({ tags: filters.tags });
if (filters.tags.length != 0) if (filters.duration.from || filters.duration.to)
items.push({ tags: filters.tags }) items.push({
if (filters.duration.from || filters.duration.to) 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({
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
items.push({ numHWThreads: { from: filters.numHWThreads.from, to: filters.numHWThreads.to } }) });
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
items.push({ numAccelerators: { from: filters.numAccelerators.from, to: filters.numAccelerators.to } }) items.push({
if (filters.user) numHWThreads: {
items.push({ user: { [filters.userMatch]: filters.user } }) from: filters.numHWThreads.from,
if (filters.project) to: filters.numHWThreads.to,
items.push({ project: { [filters.projectMatch]: filters.project } }) },
if (filters.jobName) });
items.push({ jobName: { contains: filters.jobName } }) if (
for (let stat of filters.stats) filters.numAccelerators.from != null ||
items.push({ [stat.field]: { from: stat.from, to: stat.to } }) filters.numAccelerators.to != null
)
items.push({
numAccelerators: {
from: filters.numAccelerators.from,
to: filters.numAccelerators.to,
},
});
if (filters.user)
items.push({ user: { [filters.userMatch]: filters.user } });
if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } });
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
for (let stat of filters.stats)
items.push({ [stat.field]: { from: stat.from, to: stat.to } });
dispatch('update', { filters: items }) 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.states.length != allJobStates.length)
if (filters.partition) for (let state of filters.states) opts.push(`state=${state}`);
opts.push(`partition=${filters.partition}`) if (filters.startTime.from && filters.startTime.to)
if (filters.states.length != allJobStates.length) // if (filters.startTime.url) {
for (let state of filters.states) // opts.push(`startTime=${filters.startTime.url}`)
opts.push(`state=${state}`) // } else {
if (filters.startTime.from && filters.startTime.to) opts.push(
// if (filters.startTime.url) { `startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
// opts.push(`startTime=${filters.startTime.url}`) );
// } else { // }
opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`) if (filters.jobId.length != 0)
// } if (filters.jobIdMatch != "in") {
if (filters.jobId.length != 0) opts.push(`jobId=${filters.jobId}`);
if (filters.jobIdMatch != 'in') { } else {
opts.push(`jobId=${filters.jobId}`) for (let singleJobId of filters.jobId)
} else { opts.push(`jobId=${singleJobId}`);
for (let singleJobId of filters.jobId) }
opts.push(`jobId=${singleJobId}`) if (filters.jobIdMatch != "eq")
} opts.push(`jobIdMatch=${filters.jobIdMatch}`);
if (filters.jobIdMatch != 'eq') for (let tag of filters.tags) opts.push(`tag=${tag}`);
opts.push(`jobIdMatch=${filters.jobIdMatch}`) if (filters.duration.from && filters.duration.to)
for (let tag of filters.tags) opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
opts.push(`tag=${tag}`) if (filters.duration.lessThan)
if (filters.duration.from && filters.duration.to) opts.push(`duration=0-${filters.duration.lessThan}`);
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`) if (filters.duration.moreThan)
if (filters.duration.lessThan) opts.push(`duration=${filters.duration.moreThan}-604800`);
opts.push(`duration=0-${filters.duration.lessThan}`) if (filters.numNodes.from && filters.numNodes.to)
if (filters.duration.moreThan) opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
opts.push(`duration=${filters.duration.moreThan}-604800`) if (filters.numAccelerators.from && filters.numAccelerators.to)
if (filters.numNodes.from && filters.numNodes.to) opts.push(
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`) `numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`,
if (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) opts.push(`user=${singleUser}`);
for (let singleUser of filters.user) }
opts.push(`user=${singleUser}`) if (filters.userMatch != "contains")
} opts.push(`userMatch=${filters.userMatch}`);
if (filters.userMatch != 'contains') if (filters.project) opts.push(`project=${filters.project}`);
opts.push(`userMatch=${filters.userMatch}`) if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
if (filters.project) if (filters.projectMatch != "contains")
opts.push(`project=${filters.project}`) opts.push(`projectMatch=${filters.projectMatch}`);
if (filters.jobName)
opts.push(`jobName=${filters.jobName}`)
if (filters.projectMatch != 'contains')
opts.push(`projectMatch=${filters.projectMatch}`)
if (opts.length == 0 && window.location.search.length <= 1) 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>
<Row> <Row>
<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 {#if menuText}
</DropdownItem> <DropdownItem disabled>{menuText}</DropdownItem>
{#if menuText} <DropdownItem divider />
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu"/> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill"/> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range"/> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch"/> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags"/> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack"/> Resources
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider/>
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
{#each [
{ text: 'Last 6hrs', url: 'last6h', seconds: 6*60*60 },
// { text: 'Last 12hrs', seconds: 12*60*60 },
{ text: 'Last 24hrs', url: 'last24h', seconds: 24*60*60 },
// { text: 'Last 48hrs', seconds: 48*60*60 },
{ text: 'Last 7 days', url: 'last7d', seconds: 7*24*60*60 },
{ text: 'Last 30 days', url: 'last30d', seconds: 30*24*60*60 }
] as {text, url, seconds}}
<DropdownItem on:click={() => {
filters.startTime.from = (new Date(Date.now() - seconds * 1000)).toISOString()
filters.startTime.to = (new Date(Date.now())).toISOString()
filters.startTime.text = text,
filters.startTime.url = url
update()
}}>
<Icon name="calendar-range"/> {text}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</ButtonDropdown>
</Col>
<Col xs="auto">
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
{/if} {/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range" /> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch" /> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags" /> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack" /> Resources
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider />
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
{#each [{ text: "Last 6hrs", url: "last6h", seconds: 6 * 60 * 60 }, { text: "Last 24hrs", url: "last24h", seconds: 24 * 60 * 60 }, { text: "Last 7 days", url: "last7d", seconds: 7 * 24 * 60 * 60 }, { text: "Last 30 days", url: "last30d", seconds: 30 * 24 * 60 * 60 }] as { text, url, seconds }}
<DropdownItem
on:click={() => {
filters.startTime.from = new Date(
Date.now() - seconds * 1000,
).toISOString();
filters.startTime.to = new Date(Date.now()).toISOString();
(filters.startTime.text = text), (filters.startTime.url = url);
update();
}}
>
<Icon name="calendar-range" />
{text}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</ButtonDropdown>
</Col>
<Col xs="auto">
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
{/if}
{#if filters.states.length != allJobStates.length} {#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}
{#if filters.startTime.from || filters.startTime.to} {#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{#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(
{/if} filters.startTime.to,
</Info> ).toLocaleString()}
{/if} {/if}
</Info>
{/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 -
</Info> {Math.floor(filters.duration.to / 3600)}h:{Math.floor(
{/if} (filters.duration.to % 3600) / 60,
)}m
</Info>
{/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(
</Info> filters.duration.lessThan / 3600,
{/if} )}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
</Info>
{/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(
</Info> filters.duration.moreThan / 3600,
{/if} )}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
</Info>
{/if}
{#if filters.tags.length != 0} {#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}> <Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId} {#each filters.tags as tagId}
<Tag id={tagId} clickable={false} /> <Tag id={tagId} clickable={false} />
{/each} {/each}
</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 || <Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
filters.numAccelerators.from != null || filters.numAccelerators.to != null } {#if isNodesModified}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{#if isNodesModified } Nodes: {filters.numNodes.from} - {filters.numNodes.to} {/if}
{#if isNodesModified && isHwthreadsModified }, {/if}
{#if isHwthreadsModified } HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} {/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified }, {/if}
{#if isAccsModified } Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} {/if}
</Info>
{/if} {/if}
{#if isNodesModified && isHwthreadsModified},
{/if}
{#if isHwthreadsModified}
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
{/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
{/if}
{#if isAccsModified}
Accelerators: {filters.numAccelerators.from} - {filters
.numAccelerators.to}
{/if}
</Info>
{/if}
{#if filters.node != null } {#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node} Node: {filters.node}
</Info> </Info>
{/if} {/if}
{#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
</Info> .map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
{/if} .join(", ")}
</Col> </Info>
{/if}
</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}
bind:lessThan={filters.duration.lessThan} bind:lessThan={filters.duration.lessThan}
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
bind:isOpen={isResourcesOpen} cluster={filters.cluster}
bind:numNodes={filters.numNodes} bind:isOpen={isResourcesOpen}
bind:numHWThreads={filters.numHWThreads} bind:numNodes={filters.numNodes}
bind:numAccelerators={filters.numAccelerators} bind:numHWThreads={filters.numHWThreads}
bind:namedNode={filters.node} bind:numAccelerators={filters.numAccelerators}
bind:isNodesModified={isNodesModified} bind:namedNode={filters.node}
bind:isHwthreadsModified={isHwthreadsModified} bind:isNodesModified
bind:isAccsModified={isAccsModified} bind:isHwthreadsModified
on:update={() => update()} /> bind:isAccsModified
on:update={() => update()}
/>
<Statistics cluster={filters.cluster} <Statistics
bind:isOpen={isStatsOpen} cluster={filters.cluster}
bind:stats={filters.stats} bind:isOpen={isStatsOpen}
on:update={() => update()} /> bind:stats={filters.stats}
on:update={() => update()}
/>
<style> <style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) { :global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block; display: block;
margin-top: 0px; margin-top: 0px;
padding-top: 0px; padding-top: 0px;
transform: none !important; transform: none !important;
} }
</style> </style>

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 <ModalBody>
</ModalHeader> <ListGroup>
<ModalBody> {#each allJobStates as state}
<ListGroup> <ListGroupItem>
{#each allJobStates as state} <input
<ListGroupItem> type="checkbox"
<input type=checkbox bind:group={pendingStates} name="flavours" value={state}> bind:group={pendingStates}
{state} name="flavours"
</ListGroupItem> value={state}
{/each} />
</ListGroup> {state}
</ModalBody> </ListGroupItem>
<ModalFooter> {/each}
<Button color="primary" disabled={pendingStates.length == 0} on:click={() => { </ListGroup>
isOpen = false </ModalBody>
states = [...pendingStates] <ModalFooter>
dispatch('update', { states }) <Button
}}>Close & Apply</Button> color="primary"
<Button color="danger" on:click={() => { disabled={pendingStates.length == 0}
isOpen = false on:click={() => {
states = [...allJobStates] isOpen = false;
pendingStates = [...allJobStates] states = [...pendingStates];
dispatch('update', { states }) dispatch("update", { states });
}}>Reset</Button> }}>Close & Apply</Button
<Button on:click={() => (isOpen = false)}>Close</Button> >
</ModalFooter> <Button
color="danger"
on:click={() => {
isOpen = false;
states = [...allJobStates];
pendingStates = [...allJobStates];
dispatch("update", { states });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal> </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,
if ($initialized) { minNumHWThreads = 1,
if (cluster != null) { maxNumHWThreads = 0,
const { subClusters } = clusters.find(c => c.name == cluster) minNumAccelerators = 0,
const { filterRanges } = header.clusters.find(c => c.name == cluster) maxNumAccelerators = 0;
minNumNodes = filterRanges.numNodes.from $: {
maxNumNodes = filterRanges.numNodes.to if ($initialized) {
maxNumAccelerators = findMaxNumAccels([{ subClusters }]) if (cluster != null) {
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]) const { subClusters } = clusters.find((c) => c.name == cluster);
} else if (clusters.length > 0) { const { filterRanges } = header.clusters.find((c) => c.name == cluster);
const { filterRanges } = header.clusters[0] minNumNodes = filterRanges.numNodes.from;
minNumNodes = filterRanges.numNodes.from maxNumNodes = filterRanges.numNodes.to;
maxNumNodes = filterRanges.numNodes.to maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
maxNumAccelerators = findMaxNumAccels(clusters) maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]);
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters) } else if (clusters.length > 0) {
for (let cluster of header.clusters) { const { filterRanges } = header.clusters[0];
const { filterRanges } = cluster minNumNodes = filterRanges.numNodes.from;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from) maxNumNodes = filterRanges.numNodes.to;
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to) maxNumAccelerators = findMaxNumAccels(clusters);
} maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters);
} for (let cluster of header.clusters) {
const { filterRanges } = cluster;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
} }
}
} }
}
$: { $: {
if (isOpen && $initialized && pendingNumNodes.from == null && pendingNumNodes.to == null) { 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 <ModalBody>
</ModalHeader> <h6>Named Node</h6>
<ModalBody> <input type="text" class="form-control" bind:value={pendingNamedNode} />
<h6>Named Node</h6> <h6 style="margin-top: 1rem;">Number of Nodes</h6>
<input type="text" class="form-control" bind:value={pendingNamedNode}> <DoubleRangeSlider
<h6 style="margin-top: 1rem;">Number of Nodes</h6> on:change={({ detail }) => {
<DoubleRangeSlider pendingNumNodes = { from: detail[0], to: detail[1] };
on:change={({ detail }) => { isNodesModified = true;
pendingNumNodes = { from: detail[0], to: detail[1] } }}
isNodesModified = true min={minNumNodes}
}} max={maxNumNodes}
min={minNumNodes} max={maxNumNodes} firstSlider={pendingNumNodes.from}
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to} secondSlider={pendingNumNodes.to}
inputFieldFrom={pendingNumNodes.from} inputFieldTo={pendingNumNodes.to}/> inputFieldFrom={pendingNumNodes.from}
<h6 style="margin-top: 1rem;">Number of HWThreads (Use for Single-Node Jobs)</h6> inputFieldTo={pendingNumNodes.to}
<DoubleRangeSlider />
on:change={({ detail }) => { <h6 style="margin-top: 1rem;">
pendingNumHWThreads = { from: detail[0], to: detail[1] } Number of HWThreads (Use for Single-Node Jobs)
isHwthreadsModified = true </h6>
}} <DoubleRangeSlider
min={minNumHWThreads} max={maxNumHWThreads} on:change={({ detail }) => {
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to} pendingNumHWThreads = { from: detail[0], to: detail[1] };
inputFieldFrom={pendingNumHWThreads.from} inputFieldTo={pendingNumHWThreads.to}/> isHwthreadsModified = true;
{#if maxNumAccelerators != null && maxNumAccelerators > 1} }}
<h6 style="margin-top: 1rem;">Number of Accelerators</h6> min={minNumHWThreads}
<DoubleRangeSlider max={maxNumHWThreads}
on:change={({ detail }) => { firstSlider={pendingNumHWThreads.from}
pendingNumAccelerators = { from: detail[0], to: detail[1] } secondSlider={pendingNumHWThreads.to}
isAccsModified = true inputFieldFrom={pendingNumHWThreads.from}
}} inputFieldTo={pendingNumHWThreads.to}
min={minNumAccelerators} max={maxNumAccelerators} />
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to} {#if maxNumAccelerators != null && maxNumAccelerators > 1}
inputFieldFrom={pendingNumAccelerators.from} inputFieldTo={pendingNumAccelerators.to}/> <h6 style="margin-top: 1rem;">Number of Accelerators</h6>
{/if} <DoubleRangeSlider
</ModalBody> on:change={({ detail }) => {
<ModalFooter> pendingNumAccelerators = { from: detail[0], to: detail[1] };
<Button color="primary" isAccsModified = true;
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null} }}
on:click={() => { min={minNumAccelerators}
isOpen = false max={maxNumAccelerators}
pendingNumNodes = isNodesModified ? pendingNumNodes : { from: null, to: null } firstSlider={pendingNumAccelerators.from}
pendingNumHWThreads = isHwthreadsModified ? pendingNumHWThreads : { from: null, to: null } secondSlider={pendingNumAccelerators.to}
pendingNumAccelerators = isAccsModified ? pendingNumAccelerators : { from: null, to: null } inputFieldFrom={pendingNumAccelerators.from}
numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to } inputFieldTo={pendingNumAccelerators.to}
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to } />
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to } {/if}
namedNode = pendingNamedNode </ModalBody>
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode }) <ModalFooter>
}}> <Button
Close & Apply color="primary"
</Button> disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
<Button color="danger" on:click={() => { on:click={() => {
isOpen = false isOpen = false;
pendingNumNodes = { from: null, to: null } pendingNumNodes = isNodesModified
pendingNumHWThreads = { from: null, to: null } ? pendingNumNodes
pendingNumAccelerators = { from: null, to: null } : { from: null, to: null };
pendingNamedNode = null pendingNumHWThreads = isHwthreadsModified
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to } ? pendingNumHWThreads
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to } : { from: null, to: null };
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to } pendingNumAccelerators = isAccsModified
isNodesModified = false ? pendingNumAccelerators
isHwthreadsModified = false : { from: null, to: null };
isAccsModified = false numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
namedNode = pendingNamedNode numHWThreads = {
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode}) from: pendingNumHWThreads.from,
}}>Reset</Button> to: pendingNumHWThreads.to,
<Button on:click={() => (isOpen = false)}>Close</Button> };
</ModalFooter> numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
namedNode = pendingNamedNode;
dispatch("update", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
});
}}
>
Close & Apply
</Button>
<Button
color="danger"
on:click={() => {
isOpen = false;
pendingNumNodes = { from: null, to: null };
pendingNumHWThreads = { from: null, to: null };
pendingNumAccelerators = { from: null, to: null };
pendingNamedNode = null;
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = {
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
isNodesModified = false;
isHwthreadsModified = false;
isAccsModified = false;
namedNode = pendingNamedNode;
dispatch("update", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
});
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal> </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 <ModalBody>
</ModalHeader> <h4>From</h4>
<ModalBody> <Row>
<h4>From</h4> <FormGroup class="col">
<Row> <Input type="date" bind:value={pendingFrom.date} />
<FormGroup class="col"> </FormGroup>
<Input type="date" bind:value={pendingFrom.date}/> <FormGroup class="col">
</FormGroup> <Input type="time" bind:value={pendingFrom.time} />
<FormGroup class="col"> </FormGroup>
<Input type="time" bind:value={pendingFrom.time}/> </Row>
</FormGroup> <h4>To</h4>
</Row> <Row>
<h4>To</h4> <FormGroup class="col">
<Row> <Input type="date" bind:value={pendingTo.date} />
<FormGroup class="col"> </FormGroup>
<Input type="date" bind:value={pendingTo.date}/> <FormGroup class="col">
</FormGroup> <Input type="time" bind:value={pendingTo.time} />
<FormGroup class="col"> </FormGroup>
<Input type="time" bind:value={pendingTo.time}/> </Row>
</FormGroup> </ModalBody>
</Row> <ModalFooter>
</ModalBody> <Button
<ModalFooter> color="primary"
<Button color="primary" disabled={pendingFrom.date == "0000-00-00" ||
disabled={pendingFrom.date == '0000-00-00' || pendingTo.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 >
</Button> Close & Apply
<Button color="danger" on:click={() => { </Button>
isOpen = false <Button
from = null color="danger"
to = null on:click={() => {
reset() isOpen = false;
dispatch('update', { from, to }) from = null;
}}>Reset</Button> to = null;
<Button on:click={() => (isOpen = false)}>Close</Button> reset();
</ModalFooter> dispatch("update", { from, to });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</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', },
text: 'Mem. Bw. (Avg.)', {
metric: 'mem_bw', field: "memBwAvg",
from: 0, to: 0, peak: 0, text: "Mem. Bw. (Avg.)",
enabled: false metric: "mem_bw",
}, from: 0,
{ to: 0,
field: 'loadAvg', peak: 0,
text: 'Load (Avg.)', enabled: false,
metric: 'cpu_load', },
from: 0, to: 0, peak: 0, {
enabled: false field: "loadAvg",
}, text: "Load (Avg.)",
{ metric: "cpu_load",
field: 'memUsedMax', from: 0,
text: 'Mem. used (Max.)', to: 0,
metric: 'mem_used', peak: 0,
from: 0, to: 0, peak: 0, enabled: false,
enabled: false },
{
field: "memUsedMax",
text: "Mem. used (Max.)",
metric: "mem_used",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
];
$: isModified = !statistics.every((a) => {
let b = stats.find((s) => s.field == a.field);
if (b == null) return !a.enabled;
return a.from == b.from && a.to == b.to;
});
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find((mc) => mc.name == metric);
return mc ? mc.peak : 0;
}
function resetRange(isInitialized, cluster) {
if (!isInitialized) return;
if (cluster != null) {
let c = clusters.find((c) => c.name == cluster);
for (let stat of statistics) {
stat.peak = getPeak(c, stat.metric);
stat.from = 0;
stat.to = stat.peak;
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric));
} }
] stat.from = 0;
stat.to = stat.peak;
$: isModified = !statistics.every(a => { }
let b = stats.find(s => s.field == a.field)
if (b == null)
return !a.enabled
return a.from == b.from && a.to == b.to
})
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find(mc => mc.name == metric)
return mc ? mc.peak : 0
} }
function resetRange(isInitialized, cluster) { statistics = [...statistics];
if (!isInitialized) }
return
if (cluster != null) { $: resetRange($initialized, cluster);
let c = clusters.find(c => c.name == cluster)
for (let stat of statistics) {
stat.peak = getPeak(c, stat.metric)
stat.from = 0
stat.to = stat.peak
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric))
}
stat.from = 0
stat.to = stat.peak
}
}
statistics = [...statistics]
}
$: resetRange($initialized, cluster)
</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) <ModalBody>
</ModalHeader> {#each statistics as stat}
<ModalBody> <h4>{stat.text}</h4>
{#each statistics as stat} <DoubleRangeSlider
<h4>{stat.text}</h4> on:change={({ detail }) => (
<DoubleRangeSlider (stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
on:change={({ detail }) => (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)} )}
min={0} max={stat.peak} min={0}
firstSlider={stat.from} secondSlider={stat.to} max={stat.peak}
inputFieldFrom={stat.from} inputFieldTo={stat.to}/> firstSlider={stat.from}
{/each} secondSlider={stat.to}
</ModalBody> inputFieldFrom={stat.from}
<ModalFooter> inputFieldTo={stat.to}
<Button color="primary" on:click={() => { />
isOpen = false {/each}
stats = statistics.filter(stat => stat.enabled) </ModalBody>
dispatch('update', { stats }) <ModalFooter>
}}>Close & Apply</Button> <Button
<Button color="danger" on:click={() => { color="primary"
isOpen = false on:click={() => {
resetRange($initialized, cluster) isOpen = false;
statistics.forEach(stat => (stat.enabled = false)) stats = statistics.filter((stat) => stat.enabled);
stats = [] dispatch("update", { stats });
dispatch('update', { stats }) }}>Close & Apply</Button
}}>Reset</Button> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button
</ModalFooter> color="danger"
on:click={() => {
isOpen = false;
resetRange($initialized, cluster);
statistics.forEach((stat) => (stat.enabled = false));
stats = [];
dispatch("update", { stats });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal> </Modal>

View File

@@ -1,67 +1,89 @@
<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 <ModalBody>
</ModalHeader> <Input type="text" placeholder="Search" bind:value={searchTerm} />
<ModalBody> <br />
<Input type="text" placeholder="Search" bind:value={searchTerm} /> <ListGroup>
<br/> {#if $initialized}
<ListGroup> {#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
{#if $initialized} <ListGroupItem>
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)} {#if pendingTags.includes(tag.id)}
<ListGroupItem> <Button
{#if pendingTags.includes(tag.id)} outline
<Button outline color="danger" color="danger"
on:click={() => (pendingTags = pendingTags.filter(id => id != tag.id))}> on:click={() =>
<Icon name="dash-circle" /> (pendingTags = pendingTags.filter((id) => id != tag.id))}
</Button> >
{:else} <Icon name="dash-circle" />
<Button outline color="success" </Button>
on:click={() => (pendingTags = [...pendingTags, tag.id])}> {:else}
<Icon name="plus-circle" /> <Button
</Button> outline
{/if} color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])}
<Tag tag={tag} /> >
</ListGroupItem> <Icon name="plus-circle" />
{:else} </Button>
<ListGroupItem disabled>No Tags</ListGroupItem>
{/each}
{/if} {/if}
</ListGroup>
</ModalBody> <Tag {tag} />
<ModalFooter> </ListGroupItem>
<Button color="primary" on:click={() => { {:else}
isOpen = false <ListGroupItem disabled>No Tags</ListGroupItem>
tags = [...pendingTags] {/each}
dispatch('update', { tags }) {/if}
}}>Close & Apply</Button> </ListGroup>
<Button color="danger" on:click={() => { </ModalBody>
isOpen = false <ModalFooter>
tags = [] <Button
pendingTags = [] color="primary"
dispatch('update', { tags }) on:click={() => {
}}>Reset</Button> isOpen = false;
<Button on:click={() => (isOpen = false)}>Close</Button> tags = [...pendingTags];
</ModalFooter> dispatch("update", { tags });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
tags = [];
pendingTags = [];
dispatch("update", { tags });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal> </Modal>

View File

@@ -1,81 +1,96 @@
<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;
$: pendingTo = to;
const dispatch = createEventDispatcher();
let timeRange =
to && from ? (to.getTime() - from.getTime()) / 1000 : anyEnabled ? -2 : -1;
function updateTimeRange(event) {
if (timeRange == -1) {
pendingFrom = null;
pendingTo = null;
return;
}
if (timeRange == -2) {
from = pendingFrom = null;
to = pendingTo = null;
dispatch("change", { from, to });
return;
} }
$: pendingFrom = from let now = Date.now(),
$: pendingTo = to t = timeRange * 1000;
from = pendingFrom = new Date(now - t);
to = pendingTo = new Date(now);
dispatch("change", { from, to });
}
const dispatch = createEventDispatcher() function updateExplicitTimeRange(type, event) {
let timeRange = to && from let d = new Date(Date.parse(event.target.value));
? (to.getTime() - from.getTime()) / 1000 if (type == "from") pendingFrom = d;
: (anyEnabled ? -2 : -1) else pendingTo = d;
function updateTimeRange(event) { if (pendingFrom != null && pendingTo != null) {
if (timeRange == -1) { from = pendingFrom;
pendingFrom = null to = pendingTo;
pendingTo = null dispatch("change", { from, to });
return
}
if (timeRange == -2) {
from = pendingFrom = null
to = pendingTo = null
dispatch('change', { from, to })
return
}
let now = Date.now(), t = timeRange * 1000
from = pendingFrom = new Date(now - t)
to = pendingTo = new Date(now)
dispatch('change', { from, to })
}
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == 'from') pendingFrom = d
else pendingTo = d
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom
to = pendingTo
dispatch('change', { from, to })
}
} }
}
</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
{#if customEnabled} class="form-select"
<option value={-1}>Custom</option> bind:value={timeRange}
{/if} on:change={updateTimeRange}
{#if anyEnabled} >
<option value={-2}>Any</option> {#if customEnabled}
{/if} <option value={-1}>Custom</option>
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</select>
{#if timeRange == -1}
<InputGroupText>from</InputGroupText>
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('from', event)}></Input>
<InputGroupText>to</InputGroupText>
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('to', event)}></Input>
{/if} {/if}
{#if anyEnabled}
<option value={-2}>Any</option>
{/if}
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</select>
{#if timeRange == -1}
<InputGroupText>from</InputGroupText>
<Input
type="datetime-local"
on:change={(event) => updateExplicitTimeRange("from", event)}
></Input>
<InputGroupText>to</InputGroupText>
<Input
type="datetime-local"
on:change={(event) => updateExplicitTimeRange("to", event)}
></Input>
{/if}
</InputGroup> </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}
</select> on:change={modeChanged}
<Input >
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} <option value={"user"}>Search User</option>
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} /> <option value={"project"}>Search Project</option>
</InputGroup> </select>
<Input
type="text"
bind:value={term}
on:change={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder={mode == "user" ? "filter username..." : "filter project..."}
/>
</InputGroup>
{:else} {: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}
</InputGroup> on:change={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder="filter project..."
/>
</InputGroup>
{/if} {/if}

View File

@@ -6,115 +6,138 @@
- 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;
function formatDuration(duration) { function formatDuration(duration) {
const hours = Math.floor(duration / 3600); const hours = Math.floor(duration / 3600);
duration -= hours * 3600; duration -= hours * 3600;
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) {
switch (state) {
case "running":
return "success";
case "completed":
return "primary";
default:
return "danger";
} }
}
function getStateColor(state) {
switch (state) {
case 'running':
return 'success'
case 'completed':
return 'primary'
default:
return 'danger'
}
}
</script> </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"
{#if job.metaData?.jobName} ><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
<br/> ({job.cluster})</span
{#if job.metaData?.jobName.length <= 25} >
<div>{job.metaData.jobName}</div> {#if job.metaData?.jobName}
{:else} <br />
<div class="truncate" style="cursor:help; width:230px;" title={job.metaData.jobName}>{job.metaData.jobName}</div> {#if job.metaData?.jobName.length <= 25}
{/if} <div>{job.metaData.jobName}</div>
{/if} {:else}
{#if job.arrayJobId} <div
Array Job: <a href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" target="_blank">#{job.arrayJobId}</a> class="truncate"
{/if} style="cursor:help; width:230px;"
</p> title={job.metaData.jobName}
>
{job.metaData.jobName}
</div>
{/if}
{/if}
{#if job.arrayJobId}
Array Job: <a
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
target="_blank">#{job.arrayJobId}</a
>
{/if}
</p>
<p> <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
{scrambleNames ? scramble(job.project) : job.project} class="fst-italic"
</a> href="/monitoring/jobs/?project={job.project}&projectMatch=eq"
{/if} target="_blank"
</p> >
{scrambleNames ? scramble(job.project) : job.project}
</a>
{/if}
</p>
<p> <p>
{#if job.numNodes == 1} {#if job.numNodes == 1}
{job.resources[0].hostname} {job.resources[0].hostname}
{: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> >
{#if job.walltime} <br />
<br/> Duration: <span class="fw-bold">{formatDuration(job.duration)}</span>
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span> <Badge color={getStateColor(job.state)}>{job.state}</Badge>
{/if} {#if job.walltime}
</p> <br />
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
{/if}
</p>
<p> <p>
{#each jobTags as tag} {#each jobTags as tag}
<Tag tag={tag}/> <Tag {tag} />
{/each} {/each}
</p> </p>
</div> </div>
<style> <style>
.truncate { .truncate {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
</style> </style>

View File

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

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
<Icon name="arrow-clockwise" /> Reload outline
</Button> on:click={() => dispatch("reload")}
<select class="form-select" bind:value={refreshInterval} on:change={refreshIntervalChanged}> disabled={refreshInterval != null}
<option value={null}>No periodic reload</option> >
<option value={ 30 * 1000}>Update every 30 seconds</option> <Icon name="arrow-clockwise" /> Reload
<option value={ 60 * 1000}>Update every minute</option> </Button>
<option value={2 * 60 * 1000}>Update every two minutes</option> <select
<option value={5 * 60 * 1000}>Update every 5 minutes</option> class="form-select"
</select> bind:value={refreshInterval}
</InputGroup> on:change={refreshIntervalChanged}
>
<option value={null}>No periodic reload</option>
<option value={30 * 1000}>Update every 30 seconds</option>
<option value={60 * 1000}>Update every minute</option>
<option value={2 * 60 * 1000}>Update every two minutes</option>
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
</select>
</InputGroup>

View File

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

View File

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

View File

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

View File

@@ -1,254 +1,339 @@
<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
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value) * data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value) * data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional) * data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
*/ */
// 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
// Dot Renderers
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
const size = 5 * devicePixelRatio;
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
let d = u.data[seriesIdx];
let deg360 = 2 * Math.PI;
for (let i = 0; i < d[0].length; i++) {
let p = new Path2D();
let xVal = d[0][i];
let yVal = d[1][i];
u.ctx.strokeStyle = getRGB(u.data[2][i])
u.ctx.fillStyle = getRGB(u.data[2][i])
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size/2, cy);
arc(p, cx, cy, size/2, 0, deg360);
}
u.ctx.fill(p);
}
});
return null;
}; };
}
// End Helpers
const drawPoints = (u, seriesIdx, idx0, idx1) => { // Dot Renderers
const size = 5 * devicePixelRatio; const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => { const size = 5 * devicePixelRatio;
let d = u.data[seriesIdx]; uPlot.orient(
u.ctx.strokeStyle = getRGB(0); u,
u.ctx.fillStyle = getRGB(0); seriesIdx,
let deg360 = 2 * Math.PI; (
let p = new Path2D(); series,
for (let i = 0; i < d[0].length; i++) { dataX,
let xVal = d[0][i]; dataY,
let yVal = d[1][i]; scaleX,
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) { scaleY,
let cx = valToPosX(xVal, scaleX, xDim, xOff); valToPosX,
let cy = valToPosY(yVal, scaleY, yDim, yOff); valToPosY,
p.moveTo(cx + size/2, cy); xOff,
arc(p, cx, cy, size/2, 0, deg360); yOff,
} xDim,
} yDim,
u.ctx.fill(p); moveTo,
}); lineTo,
return null; rect,
}; arc,
) => {
let d = u.data[seriesIdx];
let deg360 = 2 * Math.PI;
for (let i = 0; i < d[0].length; i++) {
let p = new Path2D();
let xVal = d[0][i];
let yVal = d[1][i];
u.ctx.strokeStyle = getRGB(u.data[2][i]);
u.ctx.fillStyle = getRGB(u.data[2][i]);
if (
xVal >= scaleX.min &&
xVal <= scaleX.max &&
yVal >= scaleY.min &&
yVal <= scaleY.max
) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
// Main Function p.moveTo(cx + size / 2, cy);
function render(plotData) { arc(p, cx, cy, size / 2, 0, deg360);
if (plotData) { }
const opts = { u.ctx.fill(p);
title: "",
mode: 2,
width: width,
height: height,
legend: {
show: false
},
cursor: { drag: { x: false, y: false } },
axes: [
{
label: 'Intensity [FLOPS/Byte]',
values: (u, vals) => vals.map(v => formatNumber(v))
},
{
label: 'Performace [GFLOPS]',
values: (u, vals) => vals.map(v => formatNumber(v))
}
],
scales: {
x: {
time: false,
range: [0.01, 1000],
distr: 3, // Render as log
log: 10, // log exp
},
y: {
range: [1.0, cluster?.flopRateSimd?.value ? nearestThousand(cluster.flopRateSimd.value) : 10000],
distr: 3, // Render as log
log: 10, // log exp
},
},
series: [
{},
{ paths: renderTime ? drawColorPoints : drawPoints }
],
hooks: {
drawClear: [
u => {
u.series.forEach((s, i) => {
if (i > 0)
s._paths = null;
});
},
],
draw: [
u => { // draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding // [top, right, bottom, left]
u.ctx.strokeStyle = 'black'
u.ctx.lineWidth = lineWidth
u.ctx.beginPath()
const ycut = 0.01 * cluster.memoryBandwidth.value
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, 'x', true),
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
// Debug get zoomLevel from browser
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
if (scalarKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Lower horizontal roofline
u.ctx.moveTo(scalarKneeX, flopRateScalarY)
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateScalarY)
}
if (simdKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Top horitontal roofline
u.ctx.moveTo(simdKneeX, flopRateSimdY)
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateSimdY)
}
let x1 = u.valToPos(0.01, 'x', true),
y1 = u.valToPos(ycut, 'y', true)
let x2 = u.valToPos(simdKnee, 'x', true),
y2 = flopRateSimdY
let xAxisIntersect = lineIntersect(
x1, y1, x2, y2,
u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
)
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x
y1 = xAxisIntersect.y
}
// Diagonal
u.ctx.moveTo(x1, y1)
u.ctx.lineTo(x2, y2)
u.ctx.stroke()
// Reset grid lineWidth
u.ctx.lineWidth = 0.15
}
}
]
},
// cursor: { drag: { x: true, y: true } } // Activate zoom
};
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
console.log('No data for roofline!')
} }
},
);
return null;
};
const drawPoints = (u, seriesIdx, idx0, idx1) => {
const size = 5 * devicePixelRatio;
uPlot.orient(
u,
seriesIdx,
(
series,
dataX,
dataY,
scaleX,
scaleY,
valToPosX,
valToPosY,
xOff,
yOff,
xDim,
yDim,
moveTo,
lineTo,
rect,
arc,
) => {
let d = u.data[seriesIdx];
u.ctx.strokeStyle = getRGB(0);
u.ctx.fillStyle = getRGB(0);
let deg360 = 2 * Math.PI;
let p = new Path2D();
for (let i = 0; i < d[0].length; i++) {
let xVal = d[0][i];
let yVal = d[1][i];
if (
xVal >= scaleX.min &&
xVal <= scaleX.max &&
yVal >= scaleY.min &&
yVal <= scaleY.max
) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size / 2, cy);
arc(p, cx, cy, size / 2, 0, deg360);
}
}
u.ctx.fill(p);
},
);
return null;
};
// Main Function
function render(plotData) {
if (plotData) {
const opts = {
title: "",
mode: 2,
width: width,
height: height,
legend: {
show: false,
},
cursor: { drag: { x: false, y: false } },
axes: [
{
label: "Intensity [FLOPS/Byte]",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
label: "Performace [GFLOPS]",
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
scales: {
x: {
time: false,
range: [0.01, 1000],
distr: 3, // Render as log
log: 10, // log exp
},
y: {
range: [
1.0,
cluster?.flopRateSimd?.value
? nearestThousand(cluster.flopRateSimd.value)
: 10000,
],
distr: 3, // Render as log
log: 10, // log exp
},
},
series: [{}, { paths: renderTime ? drawColorPoints : drawPoints }],
hooks: {
drawClear: [
(u) => {
u.series.forEach((s, i) => {
if (i > 0) s._paths = null;
});
},
],
draw: [
(u) => {
// draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding; // [top, right, bottom, left]
u.ctx.strokeStyle = "black";
u.ctx.lineWidth = lineWidth;
u.ctx.beginPath();
const ycut = 0.01 * cluster.memoryBandwidth.value;
const scalarKnee =
(cluster.flopRateScalar.value - ycut) /
cluster.memoryBandwidth.value;
const simdKnee =
(cluster.flopRateSimd.value - ycut) /
cluster.memoryBandwidth.value;
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, "x", true),
flopRateScalarY = u.valToPos(
cluster.flopRateScalar.value,
"y",
true,
),
flopRateSimdY = u.valToPos(
cluster.flopRateSimd.value,
"y",
true,
);
// Debug get zoomLevel from browser
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
if (
scalarKneeX <
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio
) {
// Lower horizontal roofline
u.ctx.moveTo(scalarKneeX, flopRateScalarY);
u.ctx.lineTo(
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio,
flopRateScalarY,
);
}
if (
simdKneeX <
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio
) {
// Top horitontal roofline
u.ctx.moveTo(simdKneeX, flopRateSimdY);
u.ctx.lineTo(
width * window.devicePixelRatio -
padding[1] * window.devicePixelRatio,
flopRateSimdY,
);
}
let x1 = u.valToPos(0.01, "x", true),
y1 = u.valToPos(ycut, "y", true);
let x2 = u.valToPos(simdKnee, "x", true),
y2 = flopRateSimdY;
let xAxisIntersect = lineIntersect(
x1,
y1,
x2,
y2,
u.valToPos(0.01, "x", true),
u.valToPos(1.0, "y", true), // X-Axis Start Coords
u.valToPos(1000, "x", true),
u.valToPos(1.0, "y", true), // X-Axis End Coords
);
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x;
y1 = xAxisIntersect.y;
}
// Diagonal
u.ctx.moveTo(x1, y1);
u.ctx.lineTo(x2, y2);
u.ctx.stroke();
// Reset grid lineWidth
u.ctx.lineWidth = 0.15;
}
},
],
},
// cursor: { drag: { x: true, y: true } } // Activate zoom
};
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
console.log("No data for roofline!");
} }
}
// Svelte and Sizechange // 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) clearTimeout(timeoutId);
if (timeoutId != null)
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}