mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-04-23 20:01:42 +02:00
Merge branch 'hotfix' of https://github.com/ClusterCockpit/cc-backend into hotfix
This commit is contained in:
commit
7940317857
@ -38,7 +38,6 @@ var metricDataRepos map[string]MetricDataRepository = map[string]MetricDataRepos
|
||||
var useArchive bool
|
||||
|
||||
func Init(disableArchive bool) error {
|
||||
|
||||
useArchive = !disableArchive
|
||||
for _, cluster := range config.Keys.Clusters {
|
||||
if cluster.MetricDataRepository != nil {
|
||||
@ -80,7 +79,8 @@ var cache *lrucache.Cache = lrucache.New(128 * 1024 * 1024)
|
||||
func LoadData(job *schema.Job,
|
||||
metrics []string,
|
||||
scopes []schema.MetricScope,
|
||||
ctx context.Context) (schema.JobData, error) {
|
||||
ctx context.Context,
|
||||
) (schema.JobData, error) {
|
||||
data := cache.Get(cacheKey(job, metrics, scopes), func() (_ interface{}, ttl time.Duration, size int) {
|
||||
var jd schema.JobData
|
||||
var err error
|
||||
@ -109,7 +109,8 @@ func LoadData(job *schema.Job,
|
||||
jd, err = repo.LoadData(job, metrics, scopes, ctx)
|
||||
if err != nil {
|
||||
if len(jd) != 0 {
|
||||
log.Warnf("partial error: %s", err.Error())
|
||||
log.Errorf("partial error: %s", err.Error())
|
||||
return err, 0, 0
|
||||
} else {
|
||||
log.Error("Error while loading job data from metric repository")
|
||||
return err, 0, 0
|
||||
@ -179,8 +180,8 @@ func LoadAverages(
|
||||
job *schema.Job,
|
||||
metrics []string,
|
||||
data [][]schema.Float,
|
||||
ctx context.Context) error {
|
||||
|
||||
ctx context.Context,
|
||||
) error {
|
||||
if job.State != schema.JobStateRunning && useArchive {
|
||||
return archive.LoadAveragesFromArchive(job, metrics, data) // #166 change also here?
|
||||
}
|
||||
@ -219,8 +220,8 @@ func LoadNodeData(
|
||||
metrics, nodes []string,
|
||||
scopes []schema.MetricScope,
|
||||
from, to time.Time,
|
||||
ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) {
|
||||
|
||||
ctx context.Context,
|
||||
) (map[string]map[string][]*schema.JobMetric, error) {
|
||||
repo, ok := metricDataRepos[cluster]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", cluster)
|
||||
@ -252,8 +253,8 @@ func LoadNodeData(
|
||||
func cacheKey(
|
||||
job *schema.Job,
|
||||
metrics []string,
|
||||
scopes []schema.MetricScope) string {
|
||||
|
||||
scopes []schema.MetricScope,
|
||||
) string {
|
||||
// Duration and StartTime do not need to be in the cache key as StartTime is less unique than
|
||||
// job.ID and the TTL of the cache entry makes sure it does not stay there forever.
|
||||
return fmt.Sprintf("%d(%s):[%v],[%v]",
|
||||
@ -267,8 +268,8 @@ func cacheKey(
|
||||
func prepareJobData(
|
||||
job *schema.Job,
|
||||
jobData schema.JobData,
|
||||
scopes []schema.MetricScope) {
|
||||
|
||||
scopes []schema.MetricScope,
|
||||
) {
|
||||
const maxSeriesSize int = 15
|
||||
for _, scopes := range jobData {
|
||||
for _, jm := range scopes {
|
||||
@ -295,7 +296,6 @@ func prepareJobData(
|
||||
|
||||
// Writes a running job to the job-archive
|
||||
func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.JobMeta, error) {
|
||||
|
||||
allMetrics := make([]string, 0)
|
||||
metricConfigs := archive.GetCluster(job.Cluster).MetricConfig
|
||||
for _, mc := range metricConfigs {
|
||||
|
618
web/frontend/package-lock.json
generated
618
web/frontend/package-lock.json
generated
@ -9,27 +9,26 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@urql/svelte": "^4.0.1",
|
||||
"chart.js": "^4.3.3",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@sveltestrap/sveltestrap": "^6.2.6",
|
||||
"@urql/svelte": "^4.1.0",
|
||||
"chart.js": "^4.4.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"graphql": "^16.6.0",
|
||||
"mathjs": "^12.0.0",
|
||||
"svelte-chartjs": "^3.1.2",
|
||||
"sveltestrap": "^5.11.1",
|
||||
"uplot": "^1.6.24",
|
||||
"wonka": "^6.3.2"
|
||||
"graphql": "^16.8.1",
|
||||
"mathjs": "^12.4.0",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"uplot": "^1.6.30",
|
||||
"wonka": "^6.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@rollup/plugin-terser": "^0.4.1",
|
||||
"@timohausmann/quadtree-js": "^1.2.5",
|
||||
"rollup": "^3.21.0",
|
||||
"rollup-plugin-css-only": "^4.3.0",
|
||||
"rollup-plugin-svelte": "^7.1.4",
|
||||
"svelte": "^3.58.0"
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@timohausmann/quadtree-js": "^1.2.6",
|
||||
"rollup": "^4.12.1",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"rollup-plugin-svelte": "^7.1.6",
|
||||
"svelte": "^4.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@0no-co/graphql.web": {
|
||||
@ -45,10 +44,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
|
||||
"integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
|
||||
"integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@ -57,33 +68,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
|
||||
"dev": true,
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true,
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"dev": true,
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
@ -104,13 +112,9 @@
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||
"dev": true,
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
@ -131,9 +135,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz",
|
||||
"integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==",
|
||||
"version": "25.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
|
||||
"integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
@ -141,13 +145,13 @@
|
||||
"estree-walker": "^2.0.2",
|
||||
"glob": "^8.0.3",
|
||||
"is-reference": "1.2.1",
|
||||
"magic-string": "^0.27.0"
|
||||
"magic-string": "^0.30.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.68.0||^3.0.0"
|
||||
"rollup": "^2.68.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
@ -156,9 +160,6 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
@ -176,7 +177,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
@ -185,23 +185,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-replace": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
|
||||
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
|
||||
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"magic-string": "^0.30.3"
|
||||
"magic-string": "^0.30.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
@ -209,32 +204,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-terser": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
|
||||
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
|
||||
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
|
||||
@ -249,7 +219,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
||||
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
@ -271,7 +240,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
@ -279,6 +247,186 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz",
|
||||
"integrity": "sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.1.tgz",
|
||||
"integrity": "sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.1.tgz",
|
||||
"integrity": "sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.1.tgz",
|
||||
"integrity": "sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.1.tgz",
|
||||
"integrity": "sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.1.tgz",
|
||||
"integrity": "sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.1.tgz",
|
||||
"integrity": "sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.1.tgz",
|
||||
"integrity": "sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.1.tgz",
|
||||
"integrity": "sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.1.tgz",
|
||||
"integrity": "sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.1.tgz",
|
||||
"integrity": "sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.1.tgz",
|
||||
"integrity": "sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.1.tgz",
|
||||
"integrity": "sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sveltestrap/sveltestrap": {
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-6.2.6.tgz",
|
||||
"integrity": "sha512-iB50tbVzsFXp0M10pe3XywRkNxjKPIHXJzV44mb1FhajWNWwxme8MkBis9m2QNivM2hyw5zDHjgGuzwTOB76JQ==",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0 || ^5.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@timohausmann/quadtree-js": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz",
|
||||
@ -289,9 +437,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
@ -300,20 +445,20 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@urql/core": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.2.0.tgz",
|
||||
"integrity": "sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.3.0.tgz",
|
||||
"integrity": "sha512-wT+FeL8DG4x5o6RfHEnONNFVDM3616ouzATMYUClB6CB+iIu2mwfBKd7xSUxYOZmwtxna5/hDRQdMl3nbQZlnw==",
|
||||
"dependencies": {
|
||||
"@0no-co/graphql.web": "^1.0.1",
|
||||
"wonka": "^6.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@urql/svelte": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.0.4.tgz",
|
||||
"integrity": "sha512-HYz9dHdqEcs9d82WWczQ3XG+zuup3TS01H+txaij/QfQ+KHjrlrn0EkOHQQd1S+H8+nFjFU2x9+HE3+3fuwL1A==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.1.0.tgz",
|
||||
"integrity": "sha512-Ov3EclCjaXPPTjKNTcIDlAG3qY/jhLjl/J9yyz9FeLUQ9S2jEgsvlzNXibrY27f4ihD4gH36CNGuj1XOi5hEEQ==",
|
||||
"dependencies": {
|
||||
"@urql/core": "^4.1.0",
|
||||
"@urql/core": "^4.3.0",
|
||||
"wonka": "^6.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -321,13 +466,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
||||
"dev": true,
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -335,6 +476,22 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
|
||||
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -369,14 +526,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
|
||||
"integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
|
||||
"integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=7"
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/code-red": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
|
||||
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@types/estree": "^1.0.1",
|
||||
"acorn": "^8.10.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"periscopic": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/code-red/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
@ -403,6 +580,18 @@
|
||||
"url": "https://www.patreon.com/infusion"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
@ -432,6 +621,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-latex": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
|
||||
@ -482,13 +679,6 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "8.1.0",
|
||||
@ -510,9 +700,6 @@
|
||||
}
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.8.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||
"version": "16.8.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||
@ -521,21 +708,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
||||
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
||||
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
|
||||
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
@ -570,16 +751,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.0"
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -605,25 +782,28 @@
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
|
||||
},
|
||||
"node_modules/locate-character": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
||||
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
|
||||
"dev": true,
|
||||
"dev": true,
|
||||
"version": "0.30.8",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
||||
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mathjs": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.0.0.tgz",
|
||||
"integrity": "sha512-Oz3swPplNPe7taoP6WrkKhQzhDE2SwvOgLzu8H3EN+hEadw2GjEJUm6Xl+hrioHoB8g2BYb3gfw1glSzhdBKYw==",
|
||||
"version": "12.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.0.tgz",
|
||||
"integrity": "sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"complex.js": "^2.1.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"escape-latex": "^1.2.0",
|
||||
@ -640,6 +820,11 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
@ -667,6 +852,32 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/periscopic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"is-reference": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/periscopic/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/periscopic/node_modules/is-reference": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
@ -688,19 +899,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
@ -727,28 +930,38 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.29.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
||||
"version": "3.29.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz",
|
||||
"integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.12.1",
|
||||
"@rollup/rollup-android-arm64": "4.12.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.12.1",
|
||||
"@rollup/rollup-darwin-x64": "4.12.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.12.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.12.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.12.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.12.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.12.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.12.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.12.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.12.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.12.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-css-only": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
|
||||
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
|
||||
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
|
||||
@ -761,7 +974,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "<5"
|
||||
"rollup": "<5"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-svelte": {
|
||||
@ -820,18 +1032,15 @@
|
||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smob": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
|
||||
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
|
||||
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
|
||||
@ -846,6 +1055,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
@ -869,40 +1086,58 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "3.59.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
|
||||
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
|
||||
"version": "4.2.12",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
|
||||
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@jridgewell/trace-mapping": "^0.3.18",
|
||||
"@types/estree": "^1.0.1",
|
||||
"acorn": "^8.9.0",
|
||||
"aria-query": "^5.3.0",
|
||||
"axobject-query": "^4.0.0",
|
||||
"code-red": "^1.0.3",
|
||||
"css-tree": "^2.3.1",
|
||||
"estree-walker": "^3.0.3",
|
||||
"is-reference": "^3.0.1",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.4",
|
||||
"periscopic": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-chartjs": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz",
|
||||
"integrity": "sha512-3+6gY2IJ9Ua8R9pk3iS1ypa7Z9OoXCJb9oPwIfTp7caJM+X+RrWnH2CTkGAq7FeSxc2nnmW08tYN88Q8Y+5M+w==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz",
|
||||
"integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.5.0 || ^4.0.0",
|
||||
"svelte": "^3.45.0"
|
||||
"svelte": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sveltestrap": {
|
||||
"version": "5.11.2",
|
||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
|
||||
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
|
||||
"version": "5.11.2",
|
||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
|
||||
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
|
||||
"node_modules/svelte/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.53.1"
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte/node_modules/is-reference": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz",
|
||||
"integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==",
|
||||
"version": "5.29.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
|
||||
"integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
@ -931,12 +1166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uplot": {
|
||||
"version": "1.6.27",
|
||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
|
||||
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
|
||||
"version": "1.6.27",
|
||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
|
||||
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
|
||||
"version": "1.6.30",
|
||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.30.tgz",
|
||||
"integrity": "sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q=="
|
||||
},
|
||||
"node_modules/wonka": {
|
||||
"version": "6.3.4",
|
||||
|
@ -7,25 +7,25 @@
|
||||
"dev": "rollup -c -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@rollup/plugin-terser": "^0.4.1",
|
||||
"@timohausmann/quadtree-js": "^1.2.5",
|
||||
"rollup": "^3.21.0",
|
||||
"rollup-plugin-css-only": "^4.3.0",
|
||||
"rollup-plugin-svelte": "^7.1.4",
|
||||
"svelte": "^3.58.0"
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@timohausmann/quadtree-js": "^1.2.6",
|
||||
"rollup": "^4.12.1",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"rollup-plugin-svelte": "^7.1.6",
|
||||
"svelte": "^4.2.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@urql/svelte": "^4.0.1",
|
||||
"chart.js": "^4.3.3",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@sveltestrap/sveltestrap": "^6.2.6",
|
||||
"@urql/svelte": "^4.1.0",
|
||||
"chart.js": "^4.4.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"graphql": "^16.6.0",
|
||||
"mathjs": "^12.0.0",
|
||||
"svelte-chartjs": "^3.1.2",
|
||||
"sveltestrap": "^5.11.1",
|
||||
"uplot": "^1.6.24",
|
||||
"wonka": "^6.3.2"
|
||||
"graphql": "^16.8.1",
|
||||
"mathjs": "^12.4.0",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"uplot": "^1.6.30",
|
||||
"wonka": "^6.3.4"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,31 +1,30 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import { init } from './utils.js'
|
||||
import { Card, CardHeader, CardTitle } from 'sveltestrap'
|
||||
import { getContext } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import PlotSettings from './config/PlotSettings.svelte'
|
||||
import AdminSettings from './config/AdminSettings.svelte'
|
||||
import PlotSettings from "./config/PlotSettings.svelte";
|
||||
import AdminSettings from "./config/AdminSettings.svelte";
|
||||
|
||||
const { query: initq } = init()
|
||||
const { query: initq } = init();
|
||||
|
||||
const ccconfig = getContext('cc-config')
|
||||
|
||||
export let isAdmin
|
||||
const ccconfig = getContext("cc-config");
|
||||
|
||||
export let isAdmin;
|
||||
</script>
|
||||
|
||||
{#if isAdmin == true}
|
||||
<Card style="margin-bottom: 1.5em;">
|
||||
<Card style="margin-bottom: 1.5em;">
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||
</CardHeader>
|
||||
<AdminSettings/>
|
||||
</Card>
|
||||
<AdminSettings />
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
||||
</CardHeader>
|
||||
<PlotSettings config={ccconfig}/>
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
||||
</CardHeader>
|
||||
<PlotSettings config={ccconfig} />
|
||||
</Card>
|
||||
|
@ -1,178 +1,169 @@
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
Collapse,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
Nav,
|
||||
NavbarToggler,
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
} from "sveltestrap";
|
||||
import NavbarLinks from "./NavbarLinks.svelte";
|
||||
import NavbarTools from "./NavbarTools.svelte";
|
||||
import {
|
||||
Icon,
|
||||
Collapse,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
Nav,
|
||||
NavbarToggler,
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import NavbarLinks from "./NavbarLinks.svelte";
|
||||
import NavbarTools from "./NavbarTools.svelte";
|
||||
|
||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||
export let authlevel; // Integer
|
||||
export let clusters; // array of names
|
||||
export let roles; // Role Enum-Like
|
||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||
export let authlevel; // Integer
|
||||
export let clusters; // array of names
|
||||
export let roles; // Role Enum-Like
|
||||
|
||||
let isOpen = false;
|
||||
let screenSize;
|
||||
let isOpen = false;
|
||||
let screenSize;
|
||||
|
||||
const jobsTitle = new Map();
|
||||
jobsTitle.set(2, "Job Search");
|
||||
jobsTitle.set(3, "Managed Jobs");
|
||||
jobsTitle.set(4, "Jobs");
|
||||
jobsTitle.set(5, "Jobs");
|
||||
const usersTitle = new Map();
|
||||
usersTitle.set(3, "Managed Users");
|
||||
usersTitle.set(4, "Users");
|
||||
usersTitle.set(5, "Users");
|
||||
const jobsTitle = new Map();
|
||||
jobsTitle.set(2, "Job Search");
|
||||
jobsTitle.set(3, "Managed Jobs");
|
||||
jobsTitle.set(4, "Jobs");
|
||||
jobsTitle.set(5, "Jobs");
|
||||
const usersTitle = new Map();
|
||||
usersTitle.set(3, "Managed Users");
|
||||
usersTitle.set(4, "Users");
|
||||
usersTitle.set(5, "Users");
|
||||
|
||||
const views = [
|
||||
{
|
||||
title: "My Jobs",
|
||||
requiredRole: roles.user,
|
||||
href: `/monitoring/user/${username}`,
|
||||
icon: "bar-chart-line-fill",
|
||||
perCluster: false,
|
||||
menu: "none",
|
||||
},
|
||||
{
|
||||
title: jobsTitle.get(authlevel),
|
||||
requiredRole: roles.user,
|
||||
href: `/monitoring/jobs/`,
|
||||
icon: "card-list",
|
||||
perCluster: false,
|
||||
menu: "none",
|
||||
},
|
||||
{
|
||||
title: usersTitle.get(authlevel),
|
||||
requiredRole: roles.manager,
|
||||
href: "/monitoring/users/",
|
||||
icon: "people-fill",
|
||||
perCluster: false,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
requiredRole: roles.support,
|
||||
href: "/monitoring/projects/",
|
||||
icon: "folder",
|
||||
perCluster: false,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Tags",
|
||||
requiredRole: roles.user,
|
||||
href: "/monitoring/tags/",
|
||||
icon: "tags",
|
||||
perCluster: false,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Analysis",
|
||||
requiredRole: roles.support,
|
||||
href: "/monitoring/analysis/",
|
||||
icon: "graph-up",
|
||||
perCluster: true,
|
||||
menu: "Stats",
|
||||
},
|
||||
{
|
||||
title: "Nodes",
|
||||
requiredRole: roles.admin,
|
||||
href: "/monitoring/systems/",
|
||||
icon: "cpu",
|
||||
perCluster: true,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
requiredRole: roles.admin,
|
||||
href: "/monitoring/status/",
|
||||
icon: "cpu",
|
||||
perCluster: true,
|
||||
menu: "Stats",
|
||||
},
|
||||
];
|
||||
const views = [
|
||||
{
|
||||
title: "My Jobs",
|
||||
requiredRole: roles.user,
|
||||
href: `/monitoring/user/${username}`,
|
||||
icon: "bar-chart-line-fill",
|
||||
perCluster: false,
|
||||
menu: "none",
|
||||
},
|
||||
{
|
||||
title: jobsTitle.get(authlevel),
|
||||
requiredRole: roles.user,
|
||||
href: `/monitoring/jobs/`,
|
||||
icon: "card-list",
|
||||
perCluster: false,
|
||||
menu: "none",
|
||||
},
|
||||
{
|
||||
title: usersTitle.get(authlevel),
|
||||
requiredRole: roles.manager,
|
||||
href: "/monitoring/users/",
|
||||
icon: "people-fill",
|
||||
perCluster: false,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
requiredRole: roles.support,
|
||||
href: "/monitoring/projects/",
|
||||
icon: "folder",
|
||||
perCluster: false,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Tags",
|
||||
requiredRole: roles.user,
|
||||
href: "/monitoring/tags/",
|
||||
icon: "tags",
|
||||
perCluster: false,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Analysis",
|
||||
requiredRole: roles.support,
|
||||
href: "/monitoring/analysis/",
|
||||
icon: "graph-up",
|
||||
perCluster: true,
|
||||
menu: "Stats",
|
||||
},
|
||||
{
|
||||
title: "Nodes",
|
||||
requiredRole: roles.admin,
|
||||
href: "/monitoring/systems/",
|
||||
icon: "cpu",
|
||||
perCluster: true,
|
||||
menu: "Groups",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
requiredRole: roles.admin,
|
||||
href: "/monitoring/status/",
|
||||
icon: "cpu",
|
||||
perCluster: true,
|
||||
menu: "Stats",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={screenSize} />
|
||||
<Navbar color="light" light expand="md" fixed="top">
|
||||
<NavbarBrand href="/">
|
||||
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
|
||||
</NavbarBrand>
|
||||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||
<Collapse
|
||||
style="justify-content: space-between"
|
||||
{isOpen}
|
||||
navbar
|
||||
expand="md"
|
||||
on:update={({ detail }) => (isOpen = detail.isOpen)}
|
||||
>
|
||||
<Nav navbar>
|
||||
{#if screenSize > 1500 || screenSize < 768}
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) => item.requiredRole <= authlevel
|
||||
)}
|
||||
/>
|
||||
{:else if screenSize > 1300}
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) =>
|
||||
item.requiredRole <= authlevel &&
|
||||
item.menu != "Stats"
|
||||
)}
|
||||
/>
|
||||
<Dropdown nav>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name="graph-up" />
|
||||
Stats
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="dropdown-menu-lg-end">
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) =>
|
||||
item.requiredRole <= authlevel &&
|
||||
item.menu == "Stats"
|
||||
)}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{:else}
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) =>
|
||||
item.requiredRole <= authlevel &&
|
||||
item.menu == "none"
|
||||
)}
|
||||
/>
|
||||
{#each Array("Groups", "Stats") as menu}
|
||||
<Dropdown nav>
|
||||
<DropdownToggle nav caret>
|
||||
{menu}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="dropdown-menu-lg-end">
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) =>
|
||||
item.requiredRole <= authlevel &&
|
||||
item.menu == menu
|
||||
)}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/each}
|
||||
{/if}
|
||||
</Nav>
|
||||
<NavbarTools {username} {authlevel} {roles} {screenSize} />
|
||||
</Collapse>
|
||||
<NavbarBrand href="/">
|
||||
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
|
||||
</NavbarBrand>
|
||||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||
<Collapse
|
||||
style="justify-content: space-between"
|
||||
{isOpen}
|
||||
navbar
|
||||
expand="md"
|
||||
on:update={({ detail }) => (isOpen = detail.isOpen)}
|
||||
>
|
||||
<Nav navbar>
|
||||
{#if screenSize > 1500 || screenSize < 768}
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter((item) => item.requiredRole <= authlevel)}
|
||||
/>
|
||||
{:else if screenSize > 1300}
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) => item.requiredRole <= authlevel && item.menu != "Stats",
|
||||
)}
|
||||
/>
|
||||
<Dropdown nav>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name="graph-up" />
|
||||
Stats
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="dropdown-menu-lg-end">
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) =>
|
||||
item.requiredRole <= authlevel && item.menu == "Stats",
|
||||
)}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{:else}
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) => item.requiredRole <= authlevel && item.menu == "none",
|
||||
)}
|
||||
/>
|
||||
{#each Array("Groups", "Stats") as menu}
|
||||
<Dropdown nav>
|
||||
<DropdownToggle nav caret>
|
||||
{menu}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="dropdown-menu-lg-end">
|
||||
<NavbarLinks
|
||||
{clusters}
|
||||
links={views.filter(
|
||||
(item) => item.requiredRole <= authlevel && item.menu == menu,
|
||||
)}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/each}
|
||||
{/if}
|
||||
</Nav>
|
||||
<NavbarTools {username} {authlevel} {roles} {screenSize} />
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
|
@ -1,65 +1,73 @@
|
||||
<script>
|
||||
import { Modal, ModalBody, ModalHeader, ModalFooter,
|
||||
Button, ListGroup, ListGroupItem } from 'sveltestrap'
|
||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||
|
||||
export let cluster
|
||||
export let metricsInHistograms
|
||||
export let isOpen
|
||||
export let cluster;
|
||||
export let metricsInHistograms;
|
||||
export let isOpen;
|
||||
|
||||
let availableMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // 'net_bw', 'file_bw'
|
||||
let pendingMetrics = [...metricsInHistograms] // Copy
|
||||
const client = getContextClient()
|
||||
let availableMetrics = ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // 'net_bw', 'file_bw'
|
||||
let pendingMetrics = [...metricsInHistograms]; // Copy
|
||||
const client = getContextClient();
|
||||
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`mutation($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}`,
|
||||
variables: { name, value }
|
||||
})
|
||||
}
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}
|
||||
`,
|
||||
variables: { name, value },
|
||||
});
|
||||
};
|
||||
|
||||
function updateConfiguration(data) {
|
||||
updateConfigurationMutation({
|
||||
name: data.name,
|
||||
value: JSON.stringify(data.value)
|
||||
}).subscribe(res => {
|
||||
if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
function updateConfiguration(data) {
|
||||
updateConfigurationMutation({
|
||||
name: data.name,
|
||||
value: JSON.stringify(data.value),
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeAndApply() {
|
||||
metricsInHistograms = [...pendingMetrics] // Set for parent
|
||||
isOpen = !isOpen
|
||||
updateConfiguration({
|
||||
name: cluster ? `user_view_histogramMetrics:${cluster}` : 'user_view_histogramMetrics',
|
||||
value: metricsInHistograms
|
||||
})
|
||||
}
|
||||
function closeAndApply() {
|
||||
metricsInHistograms = [...pendingMetrics]; // Set for parent
|
||||
isOpen = !isOpen;
|
||||
updateConfiguration({
|
||||
name: cluster
|
||||
? `user_view_histogramMetrics:${cluster}`
|
||||
: "user_view_histogramMetrics",
|
||||
value: metricsInHistograms,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {isOpen}
|
||||
toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select metrics presented in histograms
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each availableMetrics as metric (metric)}
|
||||
<ListGroupItem>
|
||||
<input type="checkbox" bind:group={pendingMetrics} value={metric}>
|
||||
{metric}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={closeAndApply}> Close & Apply </Button>
|
||||
<Button color="secondary" on:click={() => (isOpen = !isOpen)}> Close </Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select metrics presented in histograms</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each availableMetrics as metric (metric)}
|
||||
<ListGroupItem>
|
||||
<input type="checkbox" bind:group={pendingMetrics} value={metric} />
|
||||
{metric}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
||||
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,43 +1,50 @@
|
||||
<script>
|
||||
import {
|
||||
init,
|
||||
groupByScope,
|
||||
fetchMetricsStore,
|
||||
checkMetricDisabled,
|
||||
transformDataForRoofline
|
||||
} from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Spinner,
|
||||
TabContent,
|
||||
TabPane,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Button,
|
||||
Icon,
|
||||
} from "sveltestrap";
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import Metric from "./Metric.svelte";
|
||||
import Polar from "./plots/Polar.svelte";
|
||||
import Roofline from "./plots/Roofline.svelte";
|
||||
import JobInfo from "./joblist/JobInfo.svelte";
|
||||
import TagManagement from "./TagManagement.svelte";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import StatsTable from "./StatsTable.svelte";
|
||||
import JobFootprint from "./JobFootprint.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
init,
|
||||
groupByScope,
|
||||
fetchMetricsStore,
|
||||
checkMetricDisabled,
|
||||
transformDataForRoofline,
|
||||
} from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Spinner,
|
||||
TabContent,
|
||||
TabPane,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Button,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import Metric from "./Metric.svelte";
|
||||
import Polar from "./plots/Polar.svelte";
|
||||
import Roofline from "./plots/Roofline.svelte";
|
||||
import JobInfo from "./joblist/JobInfo.svelte";
|
||||
import TagManagement from "./TagManagement.svelte";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import StatsTable from "./StatsTable.svelte";
|
||||
import JobFootprint from "./JobFootprint.svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export let dbid;
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
export let dbid;
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
|
||||
const accMetrics = ['acc_utilization', 'acc_mem_used', 'acc_power', 'nv_mem_util', 'nv_sm_clock', 'nv_temp'];
|
||||
let accNodeOnly
|
||||
const accMetrics = [
|
||||
"acc_utilization",
|
||||
"acc_mem_used",
|
||||
"acc_power",
|
||||
"nv_mem_util",
|
||||
"nv_sm_clock",
|
||||
"nv_temp",
|
||||
];
|
||||
let accNodeOnly;
|
||||
|
||||
const { query: initq } = init(`
|
||||
const { query: initq } = init(`
|
||||
job(id: "${dbid}") {
|
||||
id, jobId, user, project, cluster, startTime,
|
||||
duration, numNodes, numHWThreads, numAcc,
|
||||
@ -52,417 +59,390 @@
|
||||
}
|
||||
`);
|
||||
|
||||
const ccconfig = getContext("cc-config"),
|
||||
clusters = getContext("clusters"),
|
||||
metrics = getContext("metrics")
|
||||
const ccconfig = getContext("cc-config"),
|
||||
clusters = getContext("clusters"),
|
||||
metrics = getContext("metrics");
|
||||
|
||||
let isMetricsSelectionOpen = false,
|
||||
selectedMetrics = [],
|
||||
isFetched = new Set();
|
||||
const [jobMetrics, startFetching] = fetchMetricsStore();
|
||||
getContext("on-init")(() => {
|
||||
let job = $initq.data.job;
|
||||
if (!job) return;
|
||||
let isMetricsSelectionOpen = false,
|
||||
selectedMetrics = [],
|
||||
isFetched = new Set();
|
||||
const [jobMetrics, startFetching] = fetchMetricsStore();
|
||||
getContext("on-init")(() => {
|
||||
let job = $initq.data.job;
|
||||
if (!job) return;
|
||||
|
||||
selectedMetrics =
|
||||
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
|
||||
clusters
|
||||
.find((c) => c.name == job.cluster)
|
||||
.metricConfig.map((mc) => mc.name);
|
||||
selectedMetrics =
|
||||
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
|
||||
clusters
|
||||
.find((c) => c.name == job.cluster)
|
||||
.metricConfig.map((mc) => mc.name);
|
||||
|
||||
let toFetch = new Set([
|
||||
"flops_any",
|
||||
"mem_bw",
|
||||
...selectedMetrics,
|
||||
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
|
||||
ccconfig[`job_view_polarPlotMetrics`]),
|
||||
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
||||
ccconfig[`job_view_nodestats_selectedMetrics`]),
|
||||
]);
|
||||
let toFetch = new Set([
|
||||
"flops_any",
|
||||
"mem_bw",
|
||||
...selectedMetrics,
|
||||
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
|
||||
ccconfig[`job_view_polarPlotMetrics`]),
|
||||
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
||||
ccconfig[`job_view_nodestats_selectedMetrics`]),
|
||||
]);
|
||||
|
||||
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
|
||||
accNodeOnly = [...toFetch].some(function(m) {
|
||||
if (accMetrics.includes(m)) {
|
||||
const mc = metrics(job.cluster, m)
|
||||
return mc.scope !== 'accelerator'
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (job.numAcc === 0 || accNodeOnly === true) {
|
||||
// No Accels or Accels on Node Scope
|
||||
startFetching(
|
||||
job,
|
||||
[...toFetch],
|
||||
job.numNodes > 2
|
||||
? ["node"]
|
||||
: ["node", "socket", "core"]
|
||||
);
|
||||
} else {
|
||||
// Accels and not on node scope
|
||||
startFetching(
|
||||
job,
|
||||
[...toFetch],
|
||||
job.numNodes > 2
|
||||
? ["node", "accelerator"]
|
||||
: ["node", "accelerator", "socket", "core"]
|
||||
);
|
||||
}
|
||||
|
||||
isFetched = toFetch;
|
||||
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
|
||||
accNodeOnly = [...toFetch].some(function (m) {
|
||||
if (accMetrics.includes(m)) {
|
||||
const mc = metrics(job.cluster, m);
|
||||
return mc.scope !== "accelerator";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const lazyFetchMoreMetrics = () => {
|
||||
let notYetFetched = new Set();
|
||||
for (let m of selectedMetrics) {
|
||||
if (!isFetched.has(m)) {
|
||||
notYetFetched.add(m);
|
||||
isFetched.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
if (notYetFetched.size > 0)
|
||||
startFetching(
|
||||
$initq.data.job,
|
||||
[...notYetFetched],
|
||||
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"]
|
||||
);
|
||||
};
|
||||
|
||||
// Fetch more data once required:
|
||||
$: if ($initq.data && $jobMetrics.data && selectedMetrics)
|
||||
lazyFetchMoreMetrics();
|
||||
|
||||
let plots = {},
|
||||
jobTags,
|
||||
statsTable,
|
||||
jobFootprint;
|
||||
|
||||
$: document.title = $initq.fetching
|
||||
? "Loading..."
|
||||
: $initq.error
|
||||
? "Error"
|
||||
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
|
||||
|
||||
// Find out what metrics or hosts are missing:
|
||||
let missingMetrics = [],
|
||||
missingHosts = [],
|
||||
somethingMissing = false;
|
||||
$: if ($initq.data && $jobMetrics.data) {
|
||||
let job = $initq.data.job,
|
||||
metrics = $jobMetrics.data.jobMetrics,
|
||||
metricNames = clusters
|
||||
.find((c) => c.name == job.cluster)
|
||||
.metricConfig.map((mc) => mc.name);
|
||||
|
||||
// Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
|
||||
missingMetrics = metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some((jm) => jm.name == metric) &&
|
||||
!checkMetricDisabled(
|
||||
metric,
|
||||
$initq.data.job.cluster,
|
||||
$initq.data.job.subCluster
|
||||
)
|
||||
);
|
||||
missingHosts = job.resources
|
||||
.map(({ hostname }) => ({
|
||||
hostname: hostname,
|
||||
metrics: metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some(
|
||||
(jm) =>
|
||||
jm.scope == "node" &&
|
||||
jm.metric.series.some(
|
||||
(series) => series.hostname == hostname
|
||||
)
|
||||
)
|
||||
),
|
||||
}))
|
||||
.filter(({ metrics }) => metrics.length > 0);
|
||||
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
|
||||
if (job.numAcc === 0 || accNodeOnly === true) {
|
||||
// No Accels or Accels on Node Scope
|
||||
startFetching(
|
||||
job,
|
||||
[...toFetch],
|
||||
job.numNodes > 2 ? ["node"] : ["node", "socket", "core"],
|
||||
);
|
||||
} else {
|
||||
// Accels and not on node scope
|
||||
startFetching(
|
||||
job,
|
||||
[...toFetch],
|
||||
job.numNodes > 2
|
||||
? ["node", "accelerator"]
|
||||
: ["node", "accelerator", "socket", "core"],
|
||||
);
|
||||
}
|
||||
|
||||
const orderAndMap = (grouped, selectedMetrics) =>
|
||||
selectedMetrics.map((metric) => ({
|
||||
metric: metric,
|
||||
data: grouped.find((group) => group[0].name == metric),
|
||||
disabled: checkMetricDisabled(
|
||||
metric,
|
||||
$initq.data.job.cluster,
|
||||
$initq.data.job.subCluster
|
||||
isFetched = toFetch;
|
||||
});
|
||||
|
||||
const lazyFetchMoreMetrics = () => {
|
||||
let notYetFetched = new Set();
|
||||
for (let m of selectedMetrics) {
|
||||
if (!isFetched.has(m)) {
|
||||
notYetFetched.add(m);
|
||||
isFetched.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
if (notYetFetched.size > 0)
|
||||
startFetching(
|
||||
$initq.data.job,
|
||||
[...notYetFetched],
|
||||
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"],
|
||||
);
|
||||
};
|
||||
|
||||
// Fetch more data once required:
|
||||
$: if ($initq.data && $jobMetrics.data && selectedMetrics)
|
||||
lazyFetchMoreMetrics();
|
||||
|
||||
let plots = {},
|
||||
jobTags,
|
||||
statsTable,
|
||||
jobFootprint;
|
||||
|
||||
$: document.title = $initq.fetching
|
||||
? "Loading..."
|
||||
: $initq.error
|
||||
? "Error"
|
||||
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
|
||||
|
||||
// Find out what metrics or hosts are missing:
|
||||
let missingMetrics = [],
|
||||
missingHosts = [],
|
||||
somethingMissing = false;
|
||||
$: if ($initq.data && $jobMetrics.data) {
|
||||
let job = $initq.data.job,
|
||||
metrics = $jobMetrics.data.jobMetrics,
|
||||
metricNames = clusters
|
||||
.find((c) => c.name == job.cluster)
|
||||
.metricConfig.map((mc) => mc.name);
|
||||
|
||||
// Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
|
||||
missingMetrics = metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some((jm) => jm.name == metric) &&
|
||||
!checkMetricDisabled(
|
||||
metric,
|
||||
$initq.data.job.cluster,
|
||||
$initq.data.job.subCluster,
|
||||
),
|
||||
);
|
||||
missingHosts = job.resources
|
||||
.map(({ hostname }) => ({
|
||||
hostname: hostname,
|
||||
metrics: metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some(
|
||||
(jm) =>
|
||||
jm.scope == "node" &&
|
||||
jm.metric.series.some((series) => series.hostname == hostname),
|
||||
),
|
||||
}));
|
||||
),
|
||||
}))
|
||||
.filter(({ metrics }) => metrics.length > 0);
|
||||
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
|
||||
}
|
||||
|
||||
const orderAndMap = (grouped, selectedMetrics) =>
|
||||
selectedMetrics.map((metric) => ({
|
||||
metric: metric,
|
||||
data: grouped.find((group) => group[0].name == metric),
|
||||
disabled: checkMetricDisabled(
|
||||
metric,
|
||||
$initq.data.job.cluster,
|
||||
$initq.data.job.subCluster,
|
||||
),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq.data}
|
||||
<JobInfo job={$initq.data.job} {jobTags} />
|
||||
{:else}
|
||||
<Spinner secondary />
|
||||
{/if}
|
||||
</Col>
|
||||
{#if $jobMetrics.data}
|
||||
{#key $jobMetrics.data}
|
||||
<Col>
|
||||
<JobFootprint
|
||||
bind:this={jobFootprint}
|
||||
job={$initq.data.job}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
/>
|
||||
</Col>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if $jobMetrics.data && $initq.data}
|
||||
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
|
||||
{#if authlevel > roles.manager}
|
||||
<Col>
|
||||
<h5>
|
||||
Concurrent Jobs <Icon
|
||||
name="info-circle"
|
||||
style="cursor:help;"
|
||||
title="Shared jobs running on the same node with overlapping runtimes"
|
||||
/>
|
||||
</h5>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/monitoring/jobs/?{$initq.data.job
|
||||
.concurrentJobs.listQuery}"
|
||||
target="_blank">See All</a
|
||||
>
|
||||
</li>
|
||||
{#each $initq.data.job.concurrentJobs.items as pjob, index}
|
||||
<li>
|
||||
<a
|
||||
href="/monitoring/job/{pjob.id}"
|
||||
target="_blank">{pjob.jobId}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Col>
|
||||
{:else}
|
||||
<Col>
|
||||
<h5>
|
||||
{$initq.data.job.concurrentJobs.items.length} Concurrent
|
||||
Jobs
|
||||
</h5>
|
||||
<p>
|
||||
Number of shared jobs on the same node with overlapping
|
||||
runtimes.
|
||||
</p>
|
||||
</Col>
|
||||
{/if}
|
||||
{/if}
|
||||
<Col>
|
||||
<Polar
|
||||
metrics={ccconfig[
|
||||
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
||||
] || ccconfig[`job_view_polarPlotMetrics`]}
|
||||
cluster={$initq.data.job.cluster}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Roofline
|
||||
renderTime={true}
|
||||
cluster={clusters
|
||||
.find((c) => c.name == $initq.data.job.cluster)
|
||||
.subClusters.find(
|
||||
(sc) => sc.name == $initq.data.job.subCluster
|
||||
)}
|
||||
data={
|
||||
transformDataForRoofline (
|
||||
$jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric,
|
||||
$jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq.data}
|
||||
<JobInfo job={$initq.data.job} {jobTags} />
|
||||
{:else}
|
||||
<Col />
|
||||
<Col />
|
||||
<Spinner secondary />
|
||||
{/if}
|
||||
</Col>
|
||||
{#if $jobMetrics.data}
|
||||
{#key $jobMetrics.data}
|
||||
<Col>
|
||||
<JobFootprint
|
||||
bind:this={jobFootprint}
|
||||
job={$initq.data.job}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
/>
|
||||
</Col>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if $jobMetrics.data && $initq.data}
|
||||
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
|
||||
{#if authlevel > roles.manager}
|
||||
<Col>
|
||||
<h5>
|
||||
Concurrent Jobs <Icon
|
||||
name="info-circle"
|
||||
style="cursor:help;"
|
||||
title="Shared jobs running on the same node with overlapping runtimes"
|
||||
/>
|
||||
</h5>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/monitoring/jobs/?{$initq.data.job.concurrentJobs
|
||||
.listQuery}"
|
||||
target="_blank">See All</a
|
||||
>
|
||||
</li>
|
||||
{#each $initq.data.job.concurrentJobs.items as pjob, index}
|
||||
<li>
|
||||
<a href="/monitoring/job/{pjob.id}" target="_blank"
|
||||
>{pjob.jobId}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Col>
|
||||
{:else}
|
||||
<Col>
|
||||
<h5>
|
||||
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
|
||||
</h5>
|
||||
<p>
|
||||
Number of shared jobs on the same node with overlapping runtimes.
|
||||
</p>
|
||||
</Col>
|
||||
{/if}
|
||||
{/if}
|
||||
<Col>
|
||||
<Polar
|
||||
metrics={ccconfig[
|
||||
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
||||
] || ccconfig[`job_view_polarPlotMetrics`]}
|
||||
cluster={$initq.data.job.cluster}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Roofline
|
||||
renderTime={true}
|
||||
cluster={clusters
|
||||
.find((c) => c.name == $initq.data.job.cluster)
|
||||
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
|
||||
data={transformDataForRoofline(
|
||||
$jobMetrics.data.jobMetrics.find(
|
||||
(m) => m.name == "flops_any" && m.scope == "node",
|
||||
).metric,
|
||||
$jobMetrics.data.jobMetrics.find(
|
||||
(m) => m.name == "mem_bw" && m.scope == "node",
|
||||
).metric,
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
{:else}
|
||||
<Col />
|
||||
<Col />
|
||||
{/if}
|
||||
</Row>
|
||||
<Row class="mb-3">
|
||||
<Col xs="auto">
|
||||
{#if $initq.data}
|
||||
<TagManagement job={$initq.data.job} bind:jobTags />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
{#if $initq.data}
|
||||
<Button outline on:click={() => (isMetricsSelectionOpen = true)}>
|
||||
<Icon name="graph-up" /> Metrics
|
||||
</Button>
|
||||
{/if}
|
||||
</Col>
|
||||
<!-- <Col xs="auto">
|
||||
<Col xs="auto">
|
||||
{#if $initq.data}
|
||||
<TagManagement job={$initq.data.job} bind:jobTags />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
{#if $initq.data}
|
||||
<Button outline on:click={() => (isMetricsSelectionOpen = true)}>
|
||||
<Icon name="graph-up" /> Metrics
|
||||
</Button>
|
||||
{/if}
|
||||
</Col>
|
||||
<!-- <Col xs="auto">
|
||||
<Zoom timeseriesPlots={plots} />
|
||||
</Col> -->
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{#if $jobMetrics.error}
|
||||
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
|
||||
<Card body color="warning"
|
||||
>Not monitored or archiving failed</Card
|
||||
>
|
||||
<br />
|
||||
{/if}
|
||||
<Card body color="danger">{$jobMetrics.error.message}</Card>
|
||||
{:else if $jobMetrics.fetching}
|
||||
<Spinner secondary />
|
||||
{:else if $jobMetrics.data && $initq.data}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="job"
|
||||
items={orderAndMap(
|
||||
groupByScope($jobMetrics.data.jobMetrics),
|
||||
selectedMetrics
|
||||
)}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
>
|
||||
{#if item.data}
|
||||
<Metric
|
||||
bind:this={plots[item.metric]}
|
||||
on:more-loaded={({ detail }) =>
|
||||
statsTable.moreLoaded(detail)}
|
||||
job={$initq.data.job}
|
||||
metricName={item.metric}
|
||||
rawData={item.data.map((x) => x.metric)}
|
||||
scopes={item.data.map((x) => x.scope)}
|
||||
{width}
|
||||
isShared={$initq.data.job.exclusive != 1}
|
||||
resources={$initq.data.job.resources}
|
||||
/>
|
||||
{:else}
|
||||
<Card body color="warning"
|
||||
>No dataset returned for <code>{item.metric}</code
|
||||
></Card
|
||||
>
|
||||
{/if}
|
||||
</PlotTable>
|
||||
<Col>
|
||||
{#if $jobMetrics.error}
|
||||
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
|
||||
<Card body color="warning">Not monitored or archiving failed</Card>
|
||||
<br />
|
||||
{/if}
|
||||
<Card body color="danger">{$jobMetrics.error.message}</Card>
|
||||
{:else if $jobMetrics.fetching}
|
||||
<Spinner secondary />
|
||||
{:else if $jobMetrics.data && $initq.data}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="job"
|
||||
items={orderAndMap(
|
||||
groupByScope($jobMetrics.data.jobMetrics),
|
||||
selectedMetrics,
|
||||
)}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
>
|
||||
{#if item.data}
|
||||
<Metric
|
||||
bind:this={plots[item.metric]}
|
||||
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
|
||||
job={$initq.data.job}
|
||||
metricName={item.metric}
|
||||
rawData={item.data.map((x) => x.metric)}
|
||||
scopes={item.data.map((x) => x.scope)}
|
||||
{width}
|
||||
isShared={$initq.data.job.exclusive != 1}
|
||||
resources={$initq.data.job.resources}
|
||||
/>
|
||||
{:else}
|
||||
<Card body color="warning"
|
||||
>No dataset returned for <code>{item.metric}</code></Card
|
||||
>
|
||||
{/if}
|
||||
</Col>
|
||||
</PlotTable>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row class="mt-2">
|
||||
<Col>
|
||||
{#if $initq.data}
|
||||
<TabContent>
|
||||
{#if somethingMissing}
|
||||
<TabPane
|
||||
tabId="resources"
|
||||
tab="Resources"
|
||||
active={somethingMissing}
|
||||
>
|
||||
<div style="margin: 10px;">
|
||||
<Card color="warning">
|
||||
<CardHeader>
|
||||
<CardTitle
|
||||
>Missing Metrics/Reseources</CardTitle
|
||||
>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if missingMetrics.length > 0}
|
||||
<p>
|
||||
No data at all is available for the
|
||||
metrics: {missingMetrics.join(", ")}
|
||||
</p>
|
||||
{/if}
|
||||
{#if missingHosts.length > 0}
|
||||
<p>
|
||||
Some metrics are missing for the
|
||||
following hosts:
|
||||
</p>
|
||||
<ul>
|
||||
{#each missingHosts as missing}
|
||||
<li>
|
||||
{missing.hostname}: {missing.metrics.join(
|
||||
", "
|
||||
)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</TabPane>
|
||||
{/if}
|
||||
<TabPane
|
||||
tabId="stats"
|
||||
tab="Statistics Table"
|
||||
active={!somethingMissing}
|
||||
>
|
||||
{#if $jobMetrics.data}
|
||||
{#key $jobMetrics.data}
|
||||
<StatsTable
|
||||
bind:this={statsTable}
|
||||
job={$initq.data.job}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</TabPane>
|
||||
<TabPane tabId="job-script" tab="Job Script">
|
||||
<div class="pre-wrapper">
|
||||
{#if $initq.data.job.metaData?.jobScript}
|
||||
<pre><code
|
||||
>{$initq.data.job.metaData?.jobScript}</code
|
||||
></pre>
|
||||
{:else}
|
||||
<Card body color="warning"
|
||||
>No job script available</Card
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tabId="slurm-info" tab="Slurm Info">
|
||||
<div class="pre-wrapper">
|
||||
{#if $initq.data.job.metaData?.slurmInfo}
|
||||
<pre><code
|
||||
>{$initq.data.job.metaData?.slurmInfo}</code
|
||||
></pre>
|
||||
{:else}
|
||||
<Card body color="warning"
|
||||
>No additional slurm information available</Card
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
<Col>
|
||||
{#if $initq.data}
|
||||
<TabContent>
|
||||
{#if somethingMissing}
|
||||
<TabPane tabId="resources" tab="Resources" active={somethingMissing}>
|
||||
<div style="margin: 10px;">
|
||||
<Card color="warning">
|
||||
<CardHeader>
|
||||
<CardTitle>Missing Metrics/Reseources</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if missingMetrics.length > 0}
|
||||
<p>
|
||||
No data at all is available for the metrics: {missingMetrics.join(
|
||||
", ",
|
||||
)}
|
||||
</p>
|
||||
{/if}
|
||||
{#if missingHosts.length > 0}
|
||||
<p>Some metrics are missing for the following hosts:</p>
|
||||
<ul>
|
||||
{#each missingHosts as missing}
|
||||
<li>
|
||||
{missing.hostname}: {missing.metrics.join(", ")}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</TabPane>
|
||||
{/if}
|
||||
</Col>
|
||||
<TabPane
|
||||
tabId="stats"
|
||||
tab="Statistics Table"
|
||||
active={!somethingMissing}
|
||||
>
|
||||
{#if $jobMetrics.data}
|
||||
{#key $jobMetrics.data}
|
||||
<StatsTable
|
||||
bind:this={statsTable}
|
||||
job={$initq.data.job}
|
||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</TabPane>
|
||||
<TabPane tabId="job-script" tab="Job Script">
|
||||
<div class="pre-wrapper">
|
||||
{#if $initq.data.job.metaData?.jobScript}
|
||||
<pre><code>{$initq.data.job.metaData?.jobScript}</code></pre>
|
||||
{:else}
|
||||
<Card body color="warning">No job script available</Card>
|
||||
{/if}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tabId="slurm-info" tab="Slurm Info">
|
||||
<div class="pre-wrapper">
|
||||
{#if $initq.data.job.metaData?.slurmInfo}
|
||||
<pre><code>{$initq.data.job.metaData?.slurmInfo}</code></pre>
|
||||
{:else}
|
||||
<Card body color="warning"
|
||||
>No additional slurm information available</Card
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{#if $initq.data}
|
||||
<MetricSelection
|
||||
cluster={$initq.data.job.cluster}
|
||||
configName="job_view_selectedMetrics"
|
||||
bind:metrics={selectedMetrics}
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
/>
|
||||
<MetricSelection
|
||||
cluster={$initq.data.job.cluster}
|
||||
configName="job_view_selectedMetrics"
|
||||
bind:metrics={selectedMetrics}
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.pre-wrapper {
|
||||
font-size: 1.1rem;
|
||||
margin: 10px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
.pre-wrapper {
|
||||
font-size: 1.1rem;
|
||||
margin: 10px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
columns: 2;
|
||||
-webkit-columns: 2;
|
||||
-moz-columns: 2;
|
||||
}
|
||||
ul {
|
||||
columns: 2;
|
||||
-webkit-columns: 2;
|
||||
-moz-columns: 2;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,129 +1,199 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
Progress,
|
||||
Icon,
|
||||
Tooltip
|
||||
} from "sveltestrap";
|
||||
import { mean, round } from 'mathjs'
|
||||
|
||||
export let job
|
||||
export let jobMetrics
|
||||
export let view = 'job'
|
||||
export let width = 'auto'
|
||||
|
||||
const clusters = getContext('clusters')
|
||||
const subclusterConfig = clusters.find((c) => c.name == job.cluster).subClusters.find((sc) => sc.name == job.subCluster)
|
||||
|
||||
const footprintMetrics = (job.numAcc !== 0)
|
||||
? (job.exclusive !== 1) // GPU
|
||||
? ['acc_utilization', 'acc_mem_used', 'nv_sm_clock', 'nv_mem_util'] // Shared
|
||||
: ['acc_utilization', 'acc_mem_used', 'nv_sm_clock', 'nv_mem_util'] // Exclusive
|
||||
: (job.exclusive !== 1) // CPU only
|
||||
? ['flops_any', 'mem_used'] // Shared
|
||||
: ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // Exclusive
|
||||
|
||||
const footprintData = footprintMetrics.map((fm) => {
|
||||
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
|
||||
let mv = null
|
||||
if (fm === 'cpu_load' && job.loadAvg !== 0) {
|
||||
mv = round(job.loadAvg, 2)
|
||||
} else if (fm === 'flops_any' && job.flopsAnyAvg !== 0) {
|
||||
mv = round(job.flopsAnyAvg, 2)
|
||||
} else if (fm === 'mem_bw' && job.memBwAvg !== 0) {
|
||||
mv = round(job.memBwAvg, 2)
|
||||
} else { // Calculate from jobMetrics
|
||||
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === 'node')
|
||||
if (jm?.metric?.statisticsSeries) {
|
||||
mv = round(mean(jm.metric.statisticsSeries.mean), 2)
|
||||
} else if (jm?.metric?.series?.length > 1) {
|
||||
const avgs = jm.metric.series.map(jms => jms.statistics.avg)
|
||||
mv = round(mean(avgs), 2)
|
||||
} else if (jm?.metric?.series) {
|
||||
mv = round(jm.metric.series[0].statistics.avg, 2)
|
||||
} else {
|
||||
mv = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
// Unit
|
||||
const fmc = getContext('metrics')(job.cluster, fm)
|
||||
let unit = ''
|
||||
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base
|
||||
|
||||
// Threshold / -Differences
|
||||
const fmt = findJobThresholds(job, fmc, subclusterConfig)
|
||||
if (fm === 'flops_any') fmt.peak = round((fmt.peak * 0.85), 0)
|
||||
|
||||
// Define basic data
|
||||
const fmBase = {
|
||||
name: fm,
|
||||
unit: unit,
|
||||
avg: mv,
|
||||
max: fmt.peak
|
||||
}
|
||||
|
||||
if (evalFootprint(fm, mv, fmt, 'alert')) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: 'danger',
|
||||
message:`Metric average way ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
|
||||
impact: 3
|
||||
}
|
||||
} else if (evalFootprint(fm, mv, fmt, 'caution')) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: 'warning',
|
||||
message: `Metric average ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
|
||||
impact: 2
|
||||
}
|
||||
} else if (evalFootprint(fm, mv, fmt, 'normal')) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: 'success',
|
||||
message: 'Metric average within expected thresholds.',
|
||||
impact: 1
|
||||
}
|
||||
} else if (evalFootprint(fm, mv, fmt, 'peak')) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: 'info',
|
||||
message: 'Metric average above expected normal thresholds: Check for artifacts recommended.',
|
||||
impact: 0
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...fmBase,
|
||||
color: 'secondary',
|
||||
message: 'Metric average above expected peak threshold: Check for artifacts!',
|
||||
impact: -1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function evalFootprint(metric, mean, thresholds, level) {
|
||||
// mem_used has inverse logic regarding threshold levels, notify levels triggered if mean > threshold
|
||||
switch (level) {
|
||||
case 'peak':
|
||||
if (metric === 'mem_used') return false // mem_used over peak -> return false to trigger impact -1
|
||||
else return (mean <= thresholds.peak && mean > thresholds.normal)
|
||||
case 'alert':
|
||||
if (metric === 'mem_used') return (mean <= thresholds.peak && mean >= thresholds.alert)
|
||||
else return (mean <= thresholds.alert && mean >= 0)
|
||||
case 'caution':
|
||||
if (metric === 'mem_used') return (mean < thresholds.alert && mean >= thresholds.caution)
|
||||
else return (mean <= thresholds.caution && mean > thresholds.alert)
|
||||
case 'normal':
|
||||
if (metric === 'mem_used') return (mean < thresholds.caution && mean >= 0)
|
||||
else return (mean <= thresholds.normal && mean > thresholds.caution)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
<script context="module">
|
||||
export function findJobThresholds(job, metricConfig, subClusterConfig) {
|
||||
if (!job || !metricConfig || !subClusterConfig) {
|
||||
console.warn("Argument missing for findJobThresholds!");
|
||||
return null;
|
||||
}
|
||||
|
||||
const subclusterThresholds = metricConfig.subClusters.find(
|
||||
(sc) => sc.name == subClusterConfig.name,
|
||||
);
|
||||
const defaultThresholds = {
|
||||
peak: subclusterThresholds
|
||||
? subclusterThresholds.peak
|
||||
: metricConfig.peak,
|
||||
normal: subclusterThresholds
|
||||
? subclusterThresholds.normal
|
||||
: metricConfig.normal,
|
||||
caution: subclusterThresholds
|
||||
? subclusterThresholds.caution
|
||||
: metricConfig.caution,
|
||||
alert: subclusterThresholds
|
||||
? subclusterThresholds.alert
|
||||
: metricConfig.alert,
|
||||
};
|
||||
|
||||
if (job.exclusive === 1) {
|
||||
// Exclusive: Use as defined
|
||||
return defaultThresholds;
|
||||
} else {
|
||||
// Shared: Handle specifically
|
||||
if (metricConfig.name === "cpu_load") {
|
||||
// Special: Avg Aggregation BUT scaled based on #hwthreads
|
||||
return {
|
||||
peak: job.numHWThreads,
|
||||
normal: job.numHWThreads,
|
||||
caution: defaultThresholds.caution,
|
||||
alert: defaultThresholds.alert,
|
||||
};
|
||||
} else if (metricConfig.aggregation === "avg") {
|
||||
return defaultThresholds;
|
||||
} else if (metricConfig.aggregation === "sum") {
|
||||
const jobFraction =
|
||||
job.numHWThreads / subClusterConfig.topology.node.length;
|
||||
return {
|
||||
peak: round(defaultThresholds.peak * jobFraction, 0),
|
||||
normal: round(defaultThresholds.normal * jobFraction, 0),
|
||||
caution: round(defaultThresholds.caution * jobFraction, 0),
|
||||
alert: round(defaultThresholds.alert * jobFraction, 0),
|
||||
};
|
||||
} else {
|
||||
console.warn(
|
||||
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||
metricConfig,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} // Other job.exclusive cases?
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
Progress,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { mean, round } from "mathjs";
|
||||
|
||||
export let job;
|
||||
export let jobMetrics;
|
||||
export let view = "job";
|
||||
export let width = "auto";
|
||||
|
||||
const clusters = getContext("clusters");
|
||||
const subclusterConfig = clusters
|
||||
.find((c) => c.name == job.cluster)
|
||||
.subClusters.find((sc) => sc.name == job.subCluster);
|
||||
|
||||
const footprintMetrics =
|
||||
job.numAcc !== 0
|
||||
? job.exclusive !== 1 // GPU
|
||||
? ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Shared
|
||||
: ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Exclusive
|
||||
: (job.exclusive !== 1) // CPU Only
|
||||
? ["flops_any", "mem_used"] // Shared
|
||||
: ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // Exclusive
|
||||
|
||||
const footprintData = footprintMetrics.map((fm) => {
|
||||
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
|
||||
let mv = null;
|
||||
if (fm === "cpu_load" && job.loadAvg !== 0) {
|
||||
mv = round(job.loadAvg, 2);
|
||||
} else if (fm === "flops_any" && job.flopsAnyAvg !== 0) {
|
||||
mv = round(job.flopsAnyAvg, 2);
|
||||
} else if (fm === "mem_bw" && job.memBwAvg !== 0) {
|
||||
mv = round(job.memBwAvg, 2);
|
||||
} else {
|
||||
// Calculate from jobMetrics
|
||||
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
|
||||
if (jm?.metric?.statisticsSeries) {
|
||||
mv = round(mean(jm.metric.statisticsSeries.mean), 2);
|
||||
} else if (jm?.metric?.series?.length > 1) {
|
||||
const avgs = jm.metric.series.map((jms) => jms.statistics.avg);
|
||||
mv = round(mean(avgs), 2);
|
||||
} else if (jm?.metric?.series) {
|
||||
mv = round(jm.metric.series[0].statistics.avg, 2);
|
||||
} else {
|
||||
mv = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Unit
|
||||
const fmc = getContext("metrics")(job.cluster, fm);
|
||||
let unit = "";
|
||||
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base;
|
||||
|
||||
// Threshold / -Differences
|
||||
const fmt = findJobThresholds(job, fmc, subclusterConfig);
|
||||
if (fm === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0);
|
||||
|
||||
// Define basic data
|
||||
const fmBase = {
|
||||
name: fm,
|
||||
unit: unit,
|
||||
avg: mv,
|
||||
max: fmt.peak,
|
||||
};
|
||||
|
||||
if (evalFootprint(fm, mv, fmt, "alert")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "danger",
|
||||
message: `Metric average way ${fm === "mem_used" ? "above" : "below"} expected normal thresholds.`,
|
||||
impact: 3,
|
||||
};
|
||||
} else if (evalFootprint(fm, mv, fmt, "caution")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "warning",
|
||||
message: `Metric average ${fm === "mem_used" ? "above" : "below"} expected normal thresholds.`,
|
||||
impact: 2,
|
||||
};
|
||||
} else if (evalFootprint(fm, mv, fmt, "normal")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "success",
|
||||
message: "Metric average within expected thresholds.",
|
||||
impact: 1,
|
||||
};
|
||||
} else if (evalFootprint(fm, mv, fmt, "peak")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "info",
|
||||
message:
|
||||
"Metric average above expected normal thresholds: Check for artifacts recommended.",
|
||||
impact: 0,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "secondary",
|
||||
message:
|
||||
"Metric average above expected peak threshold: Check for artifacts!",
|
||||
impact: -1,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function evalFootprint(metric, mean, thresholds, level) {
|
||||
// mem_used has inverse logic regarding threshold levels, notify levels triggered if mean > threshold
|
||||
switch (level) {
|
||||
case "peak":
|
||||
if (metric === "mem_used")
|
||||
return false; // mem_used over peak -> return false to trigger impact -1
|
||||
else return mean <= thresholds.peak && mean > thresholds.normal;
|
||||
case "alert":
|
||||
if (metric === "mem_used")
|
||||
return mean <= thresholds.peak && mean >= thresholds.alert;
|
||||
else return mean <= thresholds.alert && mean >= 0;
|
||||
case "caution":
|
||||
if (metric === "mem_used")
|
||||
return mean < thresholds.alert && mean >= thresholds.caution;
|
||||
else return mean <= thresholds.caution && mean > thresholds.alert;
|
||||
case "normal":
|
||||
if (metric === "mem_used")
|
||||
return mean < thresholds.caution && mean >= 0;
|
||||
else return mean <= thresholds.normal && mean > thresholds.caution;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script context="module">
|
||||
@ -155,13 +225,7 @@
|
||||
} else if (metricConfig.aggregation === 'avg' ){
|
||||
return defaultThresholds
|
||||
} else if (metricConfig.aggregation === 'sum' ){
|
||||
let jobFraction = 0.0
|
||||
if (job.numAcc > 0) {
|
||||
jobFraction = job.numAcc / subClusterConfig.topology.accelerators.length
|
||||
} else if (job.numHWThreads > 0) {
|
||||
jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
|
||||
}
|
||||
|
||||
const jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
|
||||
return {
|
||||
peak: round((defaultThresholds.peak * jobFraction), 0),
|
||||
normal: round((defaultThresholds.normal * jobFraction), 0),
|
||||
@ -177,62 +241,67 @@
|
||||
</script>
|
||||
|
||||
<Card class="h-auto mt-1" style="width: {width}px;">
|
||||
{#if view === 'job'}
|
||||
{#if view === "job"}
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-0 d-flex justify-content-center">
|
||||
Core Metrics Footprint
|
||||
</CardTitle>
|
||||
<CardTitle class="mb-0 d-flex justify-content-center">
|
||||
Core Metrics Footprint
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{/if}
|
||||
<CardBody>
|
||||
{#each footprintData as fpd, index}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div> <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} <!-- To increase margin to tooltip: No other way manageable ... -->
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
target={`footprint-${job.jobId}-${index}`}
|
||||
placement="right"
|
||||
offset={[0, 20]}>{fpd.message}</Tooltip
|
||||
>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Progress value={fpd.avg} max={fpd.max} color={fpd.color} />
|
||||
</div>
|
||||
{/each}
|
||||
{#if job?.metaData?.message}
|
||||
<hr class="mt-1 mb-2" />
|
||||
{@html job.metaData.message}
|
||||
{/if}
|
||||
<CardBody>
|
||||
{#each footprintData as fpd, index}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div> <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} <!-- To increase margin to tooltip: No other way manageable ... -->
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip target={`footprint-${job.jobId}-${index}`} placement="right" offset={[0, 20]}>{fpd.message}</Tooltip>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Progress
|
||||
value={fpd.avg}
|
||||
max={fpd.max}
|
||||
color={fpd.color}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if job?.metaData?.message}
|
||||
<hr class="mt-1 mb-2"/>
|
||||
{@html job.metaData.message}
|
||||
{/if}
|
||||
</CardBody>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,102 +1,121 @@
|
||||
<script>
|
||||
import { onMount, getContext } from 'svelte'
|
||||
import { init } from './utils.js'
|
||||
import { Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
|
||||
import Filters from './filters/Filters.svelte'
|
||||
import JobList from './joblist/JobList.svelte'
|
||||
import Refresher from './joblist/Refresher.svelte'
|
||||
import Sorting from './joblist/SortSelection.svelte'
|
||||
import MetricSelection from './MetricSelection.svelte'
|
||||
import UserOrProject from './filters/UserOrProject.svelte'
|
||||
import { onMount, getContext } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Icon,
|
||||
Card,
|
||||
Spinner,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import Filters from "./filters/Filters.svelte";
|
||||
import JobList from "./joblist/JobList.svelte";
|
||||
import Refresher from "./joblist/Refresher.svelte";
|
||||
import Sorting from "./joblist/SortSelection.svelte";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import UserOrProject from "./filters/UserOrProject.svelte";
|
||||
|
||||
const { query: initq } = init()
|
||||
const { query: initq } = init();
|
||||
|
||||
const ccconfig = getContext('cc-config')
|
||||
const ccconfig = getContext("cc-config");
|
||||
|
||||
export let filterPresets = {}
|
||||
export let authlevel
|
||||
export let roles
|
||||
export let filterPresets = {};
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobList, matchedJobs = null
|
||||
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
|
||||
let metrics = filterPresets.cluster
|
||||
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics
|
||||
: ccconfig.plot_list_selectedMetrics
|
||||
let showFootprint = filterPresets.cluster
|
||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||
: !!ccconfig.plot_list_showFootprint
|
||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
|
||||
|
||||
// The filterPresets are handled by the Filters component,
|
||||
// so we need to wait for it to be ready before we can start a query.
|
||||
// This is also why JobList component starts out with a paused query.
|
||||
onMount(() => filterComponent.update())
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobList,
|
||||
matchedJobs = null;
|
||||
let sorting = { field: "startTime", order: "DESC" },
|
||||
isSortingOpen = false,
|
||||
isMetricsSelectionOpen = false;
|
||||
let metrics = filterPresets.cluster
|
||||
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
|
||||
ccconfig.plot_list_selectedMetrics
|
||||
: ccconfig.plot_list_selectedMetrics;
|
||||
let showFootprint = filterPresets.cluster
|
||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||
: !!ccconfig.plot_list_showFootprint;
|
||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
||||
|
||||
// The filterPresets are handled by the Filters component,
|
||||
// so we need to wait for it to be ready before we can start a query.
|
||||
// This is also why JobList component starts out with a paused query.
|
||||
onMount(() => filterComponent.update());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
{#if $initq.fetching}
|
||||
<Col xs="auto">
|
||||
<Spinner/>
|
||||
</Col>
|
||||
{:else if $initq.error}
|
||||
<Col xs="auto">
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
{#if $initq.fetching}
|
||||
<Col xs="auto">
|
||||
<Spinner />
|
||||
</Col>
|
||||
{:else if $initq.error}
|
||||
<Col xs="auto">
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="auto">
|
||||
<Button
|
||||
outline color="primary"
|
||||
on:click={() => (isSortingOpen = true)}>
|
||||
<Icon name="sort-up"/> Sorting
|
||||
</Button>
|
||||
<Button
|
||||
outline color="primary"
|
||||
on:click={() => (isMetricsSelectionOpen = true)}>
|
||||
<Icon name="graph-up"/> Metrics
|
||||
</Button>
|
||||
<Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Filters
|
||||
filterPresets={filterPresets}
|
||||
bind:this={filterComponent}
|
||||
on:update={({ detail }) => {
|
||||
selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null
|
||||
jobList.update(detail.filters)
|
||||
}
|
||||
} />
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
|
||||
<Icon name="sort-up" /> Sorting
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
color="primary"
|
||||
on:click={() => (isMetricsSelectionOpen = true)}
|
||||
>
|
||||
<Icon name="graph-up" /> Metrics
|
||||
</Button>
|
||||
<Button disabled outline
|
||||
>{matchedJobs == null ? "Loading..." : `${matchedJobs} jobs`}</Button
|
||||
>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Filters
|
||||
{filterPresets}
|
||||
bind:this={filterComponent}
|
||||
on:update={({ detail }) => {
|
||||
selectedCluster = detail.filters[0]?.cluster
|
||||
? detail.filters[0].cluster.eq
|
||||
: null;
|
||||
jobList.update(detail.filters);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs="3" style="margin-left: auto;">
|
||||
<UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filterComponent.update(detail)}/>
|
||||
</Col>
|
||||
<Col xs="2">
|
||||
<Refresher on:reload={() => jobList.refresh()} />
|
||||
</Col>
|
||||
<Col xs="3" style="margin-left: auto;">
|
||||
<UserOrProject
|
||||
bind:authlevel
|
||||
bind:roles
|
||||
on:update={({ detail }) => filterComponent.update(detail)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="2">
|
||||
<Refresher on:reload={() => jobList.refresh()} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br/>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<JobList
|
||||
bind:metrics={metrics}
|
||||
bind:sorting={sorting}
|
||||
bind:matchedJobs={matchedJobs}
|
||||
bind:this={jobList}
|
||||
bind:showFootprint={showFootprint} />
|
||||
</Col>
|
||||
<Col>
|
||||
<JobList
|
||||
bind:metrics
|
||||
bind:sorting
|
||||
bind:matchedJobs
|
||||
bind:this={jobList}
|
||||
bind:showFootprint
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Sorting
|
||||
bind:sorting={sorting}
|
||||
bind:isOpen={isSortingOpen} />
|
||||
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
|
||||
|
||||
<MetricSelection
|
||||
bind:cluster={selectedCluster}
|
||||
configName="plot_list_selectedMetrics"
|
||||
bind:metrics={metrics}
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint={showFootprint}
|
||||
view='list'/>
|
||||
bind:cluster={selectedCluster}
|
||||
configName="plot_list_selectedMetrics"
|
||||
bind:metrics
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint
|
||||
view="list"
|
||||
/>
|
||||
|
@ -2,52 +2,58 @@
|
||||
@component List of users or projects
|
||||
-->
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Icon,
|
||||
Table,
|
||||
Card,
|
||||
Spinner,
|
||||
InputGroup,
|
||||
Input,
|
||||
} from "sveltestrap";
|
||||
import Filters from "./filters/Filters.svelte";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Icon,
|
||||
Table,
|
||||
Card,
|
||||
Spinner,
|
||||
InputGroup,
|
||||
Input,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import Filters from "./filters/Filters.svelte";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
||||
|
||||
const {} = init();
|
||||
const {} = init();
|
||||
|
||||
export let type;
|
||||
export let filterPresets;
|
||||
export let type;
|
||||
export let filterPresets;
|
||||
|
||||
// By default, look at the jobs of the last 30 days:
|
||||
if (filterPresets?.startTime == null) {
|
||||
if (filterPresets == null)
|
||||
filterPresets = {}
|
||||
// By default, look at the jobs of the last 30 days:
|
||||
if (filterPresets?.startTime == null) {
|
||||
if (filterPresets == null) filterPresets = {};
|
||||
|
||||
const lastMonth = (new Date(Date.now() - (30*24*60*60*1000))).toISOString()
|
||||
const now = (new Date(Date.now())).toISOString()
|
||||
filterPresets.startTime = { from: lastMonth, to: now, text: 'Last 30 Days', url: 'last30d' }
|
||||
}
|
||||
const lastMonth = new Date(
|
||||
Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const now = new Date(Date.now()).toISOString();
|
||||
filterPresets.startTime = {
|
||||
from: lastMonth,
|
||||
to: now,
|
||||
text: "Last 30 Days",
|
||||
url: "last30d",
|
||||
};
|
||||
}
|
||||
|
||||
console.assert(
|
||||
type == "USER" || type == "PROJECT",
|
||||
"Invalid list type provided!"
|
||||
);
|
||||
console.assert(
|
||||
type == "USER" || type == "PROJECT",
|
||||
"Invalid list type provided!",
|
||||
);
|
||||
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobFilters = [];
|
||||
let nameFilter = "";
|
||||
let sorting = { field: "totalJobs", direction: "down" };
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobFilters = [];
|
||||
let nameFilter = "";
|
||||
let sorting = { field: "totalJobs", direction: "down" };
|
||||
|
||||
const client = getContextClient();
|
||||
$: stats = queryStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
const client = getContextClient();
|
||||
$: stats = queryStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
query($jobFilters: [JobFilter!]!) {
|
||||
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
|
||||
id
|
||||
@ -58,185 +64,178 @@
|
||||
totalAccHours
|
||||
}
|
||||
}`,
|
||||
variables: { jobFilters }
|
||||
});
|
||||
variables: { jobFilters },
|
||||
});
|
||||
|
||||
function changeSorting(event, field) {
|
||||
let target = event.target;
|
||||
while (target.tagName != "BUTTON") target = target.parentElement;
|
||||
function changeSorting(event, field) {
|
||||
let target = event.target;
|
||||
while (target.tagName != "BUTTON") target = target.parentElement;
|
||||
|
||||
let direction = target.children[0].className.includes("up")
|
||||
? "down"
|
||||
: "up";
|
||||
target.children[0].className = `bi-sort-numeric-${direction}`;
|
||||
sorting = { field, direction };
|
||||
}
|
||||
let direction = target.children[0].className.includes("up") ? "down" : "up";
|
||||
target.children[0].className = `bi-sort-numeric-${direction}`;
|
||||
sorting = { field, direction };
|
||||
}
|
||||
|
||||
function sort(stats, sorting, nameFilter) {
|
||||
const cmp =
|
||||
sorting.field == "id"
|
||||
? sorting.direction == "up"
|
||||
? (a, b) => a.id < b.id
|
||||
: (a, b) => a.id > b.id
|
||||
: sorting.direction == "up"
|
||||
? (a, b) => a[sorting.field] - b[sorting.field]
|
||||
: (a, b) => b[sorting.field] - a[sorting.field];
|
||||
function sort(stats, sorting, nameFilter) {
|
||||
const cmp =
|
||||
sorting.field == "id"
|
||||
? sorting.direction == "up"
|
||||
? (a, b) => a.id < b.id
|
||||
: (a, b) => a.id > b.id
|
||||
: sorting.direction == "up"
|
||||
? (a, b) => a[sorting.field] - b[sorting.field]
|
||||
: (a, b) => b[sorting.field] - a[sorting.field];
|
||||
|
||||
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
|
||||
}
|
||||
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
|
||||
}
|
||||
|
||||
onMount(() => filterComponent.update());
|
||||
onMount(() => filterComponent.update());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
<Col xs="auto">
|
||||
<InputGroup>
|
||||
<Button disabled outline>
|
||||
Search {type.toLowerCase()}s
|
||||
</Button>
|
||||
<Input
|
||||
bind:value={nameFilter}
|
||||
placeholder="Filter by {{
|
||||
USER: 'username',
|
||||
PROJECT: 'project',
|
||||
}[type]}"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Filters
|
||||
bind:this={filterComponent}
|
||||
{filterPresets}
|
||||
startTimeQuickSelect={true}
|
||||
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
|
||||
on:update={({ detail }) => {
|
||||
jobFilters = detail.filters;
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<InputGroup>
|
||||
<Button disabled outline>
|
||||
Search {type.toLowerCase()}s
|
||||
</Button>
|
||||
<Input
|
||||
bind:value={nameFilter}
|
||||
placeholder="Filter by {{
|
||||
USER: 'username',
|
||||
PROJECT: 'project',
|
||||
}[type]}"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Filters
|
||||
bind:this={filterComponent}
|
||||
{filterPresets}
|
||||
startTimeQuickSelect={true}
|
||||
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
|
||||
on:update={({ detail }) => {
|
||||
jobFilters = detail.filters;
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Table>
|
||||
<thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{{
|
||||
USER: "Username",
|
||||
PROJECT: "Project Name",
|
||||
}[type]}
|
||||
<Button
|
||||
color={sorting.field == "id" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "id")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
{#if type == "USER"}
|
||||
<th scope="col">
|
||||
Name
|
||||
<Button
|
||||
color={sorting.field == "name" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "name")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
{/if}
|
||||
<th scope="col">
|
||||
Total Jobs
|
||||
<Button
|
||||
color={sorting.field == "totalJobs" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalJobs")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
Total Walltime
|
||||
<Button
|
||||
color={sorting.field == "totalWalltime" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalWalltime")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
Total Core Hours
|
||||
<Button
|
||||
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalCoreHours")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
Total Accelerator Hours
|
||||
<Button
|
||||
color={sorting.field == "totalAccHours" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalAccHours")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if $stats.fetching}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center;"><Spinner secondary /></td>
|
||||
</tr>
|
||||
{:else if $stats.error}
|
||||
<tr>
|
||||
<td colspan="4"
|
||||
><Card body color="danger" class="mb-3">{$stats.error.message}</Card
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{:else if $stats.data}
|
||||
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{({
|
||||
USER: "Username",
|
||||
PROJECT: "Project Name",
|
||||
})[type]}
|
||||
<Button
|
||||
color={sorting.field == "id" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "id")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<td>
|
||||
{#if type == "USER"}
|
||||
<th scope="col">
|
||||
Name
|
||||
<Button
|
||||
color={sorting.field == "name" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "name")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
{/if}
|
||||
<th scope="col">
|
||||
Total Jobs
|
||||
<Button
|
||||
color={sorting.field == "totalJobs" ? "primary" : "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalJobs")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
Total Walltime
|
||||
<Button
|
||||
color={sorting.field == "totalWalltime"
|
||||
? "primary"
|
||||
: "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalWalltime")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
Total Core Hours
|
||||
<Button
|
||||
color={sorting.field == "totalCoreHours"
|
||||
? "primary"
|
||||
: "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalCoreHours")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
Total Accelerator Hours
|
||||
<Button
|
||||
color={sorting.field == "totalAccHours"
|
||||
? "primary"
|
||||
: "light"}
|
||||
size="sm"
|
||||
on:click={(e) => changeSorting(e, "totalAccHours")}
|
||||
>
|
||||
<Icon name="sort-numeric-down" />
|
||||
</Button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if $stats.fetching}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center;"
|
||||
><Spinner secondary /></td
|
||||
>
|
||||
</tr>
|
||||
{:else if $stats.error}
|
||||
<tr>
|
||||
<td colspan="4"
|
||||
><Card body color="danger" class="mb-3"
|
||||
>{$stats.error.message}</Card
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{:else if $stats.data}
|
||||
{#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
|
||||
<tr>
|
||||
<td>
|
||||
{#if type == "USER"}
|
||||
<a href="/monitoring/user/{row.id}"
|
||||
>{scrambleNames ? scramble(row.id) : row.id}</a
|
||||
>
|
||||
{:else if type == "PROJECT"}
|
||||
<a href="/monitoring/jobs/?project={row.id}"
|
||||
>{scrambleNames ? scramble(row.id) : row.id}</a
|
||||
>
|
||||
{:else}
|
||||
{row.id}
|
||||
{/if}
|
||||
</td>
|
||||
{#if type == "USER"}
|
||||
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
|
||||
{/if}
|
||||
<td>{row.totalJobs}</td>
|
||||
<td>{row.totalWalltime}</td>
|
||||
<td>{row.totalCoreHours}</td>
|
||||
<td>{row.totalAccHours}</td>
|
||||
</tr>
|
||||
<a href="/monitoring/user/{row.id}"
|
||||
>{scrambleNames ? scramble(row.id) : row.id}</a
|
||||
>
|
||||
{:else if type == "PROJECT"}
|
||||
<a href="/monitoring/jobs/?project={row.id}"
|
||||
>{scrambleNames ? scramble(row.id) : row.id}</a
|
||||
>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4"
|
||||
><i>No {type.toLowerCase()}s/jobs found</i></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
{row.id}
|
||||
{/if}
|
||||
</td>
|
||||
{#if type == "USER"}
|
||||
<td
|
||||
>{scrambleNames
|
||||
? scramble(row?.name ? row.name : "-")
|
||||
: row?.name
|
||||
? row.name
|
||||
: "-"}</td
|
||||
>
|
||||
{/if}
|
||||
<td>{row.totalJobs}</td>
|
||||
<td>{row.totalWalltime}</td>
|
||||
<td>{row.totalCoreHours}</td>
|
||||
<td>{row.totalAccHours}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4"><i>No {type.toLowerCase()}s/jobs found</i></td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
@ -1,95 +1,118 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher } from 'svelte'
|
||||
import Timeseries from './plots/MetricPlot.svelte'
|
||||
import { InputGroup, InputGroupText, Spinner, Card } from 'sveltestrap'
|
||||
import { fetchMetrics, minScope } from './utils'
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import Timeseries from "./plots/MetricPlot.svelte";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Spinner,
|
||||
Card,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fetchMetrics, minScope } from "./utils";
|
||||
|
||||
export let job
|
||||
export let metricName
|
||||
export let scopes
|
||||
export let width
|
||||
export let rawData
|
||||
export let isShared = false
|
||||
export let job;
|
||||
export let metricName;
|
||||
export let scopes;
|
||||
export let width;
|
||||
export let rawData;
|
||||
export let isShared = false;
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const cluster = getContext('clusters').find(cluster => cluster.name == job.cluster)
|
||||
const subCluster = cluster.subClusters.find(subCluster => subCluster.name == job.subCluster)
|
||||
const metricConfig = cluster.metricConfig.find(metricConfig => metricConfig.name == metricName)
|
||||
|
||||
let selectedHost = null, plot, fetching = false, error = null
|
||||
let selectedScope = minScope(scopes)
|
||||
const dispatch = createEventDispatcher();
|
||||
const cluster = getContext("clusters").find(
|
||||
(cluster) => cluster.name == job.cluster,
|
||||
);
|
||||
const subCluster = cluster.subClusters.find(
|
||||
(subCluster) => subCluster.name == job.subCluster,
|
||||
);
|
||||
const metricConfig = cluster.metricConfig.find(
|
||||
(metricConfig) => metricConfig.name == metricName,
|
||||
);
|
||||
|
||||
$: availableScopes = scopes
|
||||
$: selectedScopeIndex = scopes.findIndex(s => s == selectedScope)
|
||||
$: data = rawData[selectedScopeIndex]
|
||||
$: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost)
|
||||
let selectedHost = null,
|
||||
plot,
|
||||
fetching = false,
|
||||
error = null;
|
||||
let selectedScope = minScope(scopes);
|
||||
|
||||
let from = null, to = null
|
||||
export function setTimeRange(f, t) {
|
||||
from = f, to = t
|
||||
$: availableScopes = scopes;
|
||||
$: selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
|
||||
$: data = rawData[selectedScopeIndex];
|
||||
$: series = data?.series.filter(
|
||||
(series) => selectedHost == null || series.hostname == selectedHost,
|
||||
);
|
||||
|
||||
let from = null,
|
||||
to = null;
|
||||
export function setTimeRange(f, t) {
|
||||
(from = f), (to = t);
|
||||
}
|
||||
|
||||
$: if (plot != null) plot.setTimeRange(from, to);
|
||||
|
||||
export async function loadMore() {
|
||||
fetching = true;
|
||||
let response = await fetchMetrics(job, [metricName], ["core"]);
|
||||
fetching = false;
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
$: if (plot != null) plot.setTimeRange(from, to)
|
||||
|
||||
export async function loadMore() {
|
||||
fetching = true
|
||||
let response = await fetchMetrics(job, [metricName], ["core"])
|
||||
fetching = false
|
||||
|
||||
if (response.error) {
|
||||
error = response.error
|
||||
return
|
||||
}
|
||||
|
||||
for (let jm of response.data.jobMetrics) {
|
||||
if (jm.scope != "node") {
|
||||
scopes = [...scopes, jm.scope]
|
||||
rawData.push(jm.metric)
|
||||
selectedScope = jm.scope
|
||||
selectedScopeIndex = scopes.findIndex(s => s == jm.scope)
|
||||
dispatch('more-loaded', jm)
|
||||
}
|
||||
}
|
||||
for (let jm of response.data.jobMetrics) {
|
||||
if (jm.scope != "node") {
|
||||
scopes = [...scopes, jm.scope];
|
||||
rawData.push(jm.metric);
|
||||
selectedScope = jm.scope;
|
||||
selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
|
||||
dispatch("more-loaded", jm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if (selectedScope == "load-more") loadMore()
|
||||
$: if (selectedScope == "load-more") loadMore();
|
||||
</script>
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupText style="min-width: 150px;">
|
||||
{metricName} ({(metricConfig?.unit?.prefix ? metricConfig.unit.prefix : '') +
|
||||
(metricConfig?.unit?.base ? metricConfig.unit.base : '')})
|
||||
</InputGroupText>
|
||||
<select class="form-select" bind:value={selectedScope}>
|
||||
{#each availableScopes as scope}
|
||||
<option value={scope}>{scope}</option>
|
||||
{/each}
|
||||
{#if availableScopes.length == 1 && metricConfig?.scope != "node"}
|
||||
<option value={"load-more"}>Load more...</option>
|
||||
{/if}
|
||||
</select>
|
||||
{#if job.resources.length > 1}
|
||||
<select class="form-select" bind:value={selectedHost}>
|
||||
<option value={null}>All Hosts</option>
|
||||
{#each job.resources as { hostname }}
|
||||
<option value={hostname}>{hostname}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<InputGroupText style="min-width: 150px;">
|
||||
{metricName} ({(metricConfig?.unit?.prefix
|
||||
? metricConfig.unit.prefix
|
||||
: "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
|
||||
</InputGroupText>
|
||||
<select class="form-select" bind:value={selectedScope}>
|
||||
{#each availableScopes as scope}
|
||||
<option value={scope}>{scope}</option>
|
||||
{/each}
|
||||
{#if availableScopes.length == 1 && metricConfig?.scope != "node"}
|
||||
<option value={"load-more"}>Load more...</option>
|
||||
{/if}
|
||||
</select>
|
||||
{#if job.resources.length > 1}
|
||||
<select class="form-select" bind:value={selectedHost}>
|
||||
<option value={null}>All Hosts</option>
|
||||
{#each job.resources as { hostname }}
|
||||
<option value={hostname}>{hostname}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</InputGroup>
|
||||
{#key series}
|
||||
{#if fetching == true}
|
||||
<Spinner/>
|
||||
{:else if error != null}
|
||||
<Card body color="danger">{error.message}</Card>
|
||||
{:else if series != null}
|
||||
<Timeseries
|
||||
bind:this={plot}
|
||||
width={width} height={300}
|
||||
cluster={cluster} subCluster={subCluster}
|
||||
timestep={data.timestep}
|
||||
scope={selectedScope} metric={metricName}
|
||||
series={series}
|
||||
isShared={isShared}
|
||||
resources={job.resources} />
|
||||
{/if}
|
||||
{#if fetching == true}
|
||||
<Spinner />
|
||||
{:else if error != null}
|
||||
<Card body color="danger">{error.message}</Card>
|
||||
{:else if series != null}
|
||||
<Timeseries
|
||||
bind:this={plot}
|
||||
{width}
|
||||
height={300}
|
||||
{cluster}
|
||||
{subCluster}
|
||||
timestep={data.timestep}
|
||||
scope={selectedScope}
|
||||
metric={metricName}
|
||||
{series}
|
||||
{isShared}
|
||||
resources={job.resources}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
|
@ -8,181 +8,206 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Modal, ModalBody, ModalHeader, ModalFooter, Button, ListGroup } from 'sveltestrap'
|
||||
import { getContext } from 'svelte'
|
||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
Button,
|
||||
ListGroup,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { getContext } from "svelte";
|
||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||
|
||||
export let metrics
|
||||
export let isOpen
|
||||
export let configName
|
||||
export let allMetrics = null
|
||||
export let cluster = null
|
||||
export let showFootprint
|
||||
export let view = 'job'
|
||||
export let metrics;
|
||||
export let isOpen;
|
||||
export let configName;
|
||||
export let allMetrics = null;
|
||||
export let cluster = null;
|
||||
export let showFootprint;
|
||||
export let view = "job";
|
||||
|
||||
const clusters = getContext('clusters'),
|
||||
onInit = getContext('on-init')
|
||||
const clusters = getContext("clusters"),
|
||||
onInit = getContext("on-init");
|
||||
|
||||
let newMetricsOrder = []
|
||||
let unorderedMetrics = [...metrics]
|
||||
let pendingShowFootprint = !!showFootprint
|
||||
let newMetricsOrder = [];
|
||||
let unorderedMetrics = [...metrics];
|
||||
let pendingShowFootprint = !!showFootprint;
|
||||
|
||||
onInit(() => {
|
||||
if (allMetrics == null) allMetrics = new Set()
|
||||
for (let c of clusters)
|
||||
for (let metric of c.metricConfig)
|
||||
allMetrics.add(metric.name)
|
||||
})
|
||||
onInit(() => {
|
||||
if (allMetrics == null) allMetrics = new Set();
|
||||
for (let c of clusters)
|
||||
for (let metric of c.metricConfig) allMetrics.add(metric.name);
|
||||
});
|
||||
|
||||
$: {
|
||||
if (allMetrics != null) {
|
||||
if (cluster == null) {
|
||||
// console.log('Reset to full metric list')
|
||||
for (let c of clusters)
|
||||
for (let metric of c.metricConfig)
|
||||
allMetrics.add(metric.name)
|
||||
} else {
|
||||
// console.log('Recalculate available metrics for ' + cluster)
|
||||
allMetrics.clear()
|
||||
for (let c of clusters)
|
||||
if (c.name == cluster)
|
||||
for (let metric of c.metricConfig)
|
||||
allMetrics.add(metric.name)
|
||||
}
|
||||
$: {
|
||||
if (allMetrics != null) {
|
||||
if (cluster == null) {
|
||||
// console.log('Reset to full metric list')
|
||||
for (let c of clusters)
|
||||
for (let metric of c.metricConfig) allMetrics.add(metric.name);
|
||||
} else {
|
||||
// console.log('Recalculate available metrics for ' + cluster)
|
||||
allMetrics.clear();
|
||||
for (let c of clusters)
|
||||
if (c.name == cluster)
|
||||
for (let metric of c.metricConfig) allMetrics.add(metric.name);
|
||||
}
|
||||
|
||||
newMetricsOrder = [...allMetrics].filter(m => !metrics.includes(m))
|
||||
newMetricsOrder.unshift(...metrics.filter(m => allMetrics.has(m)))
|
||||
unorderedMetrics = unorderedMetrics.filter(m => allMetrics.has(m))
|
||||
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
|
||||
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
|
||||
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
|
||||
}
|
||||
}
|
||||
|
||||
const client = getContextClient();
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}
|
||||
`,
|
||||
variables: { name, value },
|
||||
});
|
||||
};
|
||||
|
||||
let columnHovering = null;
|
||||
|
||||
function columnsDragStart(event, i) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setData("text/plain", i);
|
||||
}
|
||||
|
||||
function columnsDrag(event, target) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
const start = Number.parseInt(event.dataTransfer.getData("text/plain"));
|
||||
if (start < target) {
|
||||
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start]);
|
||||
newMetricsOrder.splice(start, 1);
|
||||
} else {
|
||||
newMetricsOrder.splice(target, 0, newMetricsOrder[start]);
|
||||
newMetricsOrder.splice(start + 1, 1);
|
||||
}
|
||||
columnHovering = null;
|
||||
}
|
||||
|
||||
const client = getContextClient();
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}
|
||||
`,
|
||||
variables: { name, value }
|
||||
})}
|
||||
function closeAndApply() {
|
||||
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
|
||||
isOpen = false;
|
||||
|
||||
let columnHovering = null
|
||||
showFootprint = !!pendingShowFootprint;
|
||||
|
||||
function columnsDragStart(event, i) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
event.dataTransfer.setData('text/plain', i)
|
||||
}
|
||||
updateConfigurationMutation({
|
||||
name: cluster == null ? configName : `${configName}:${cluster}`,
|
||||
value: JSON.stringify(metrics),
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
});
|
||||
|
||||
function columnsDrag(event, target) {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
const start = Number.parseInt(event.dataTransfer.getData("text/plain"))
|
||||
if (start < target) {
|
||||
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start])
|
||||
newMetricsOrder.splice(start, 1)
|
||||
} else {
|
||||
newMetricsOrder.splice(target, 0, newMetricsOrder[start])
|
||||
newMetricsOrder.splice(start + 1, 1)
|
||||
}
|
||||
columnHovering = null
|
||||
}
|
||||
|
||||
function closeAndApply() {
|
||||
metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m))
|
||||
isOpen = false
|
||||
|
||||
showFootprint = !!pendingShowFootprint
|
||||
|
||||
updateConfigurationMutation({
|
||||
name: cluster == null ? configName : `${configName}:${cluster}`,
|
||||
value: JSON.stringify(metrics)
|
||||
}).subscribe(res => {
|
||||
if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
|
||||
updateConfigurationMutation({
|
||||
name: cluster == null ? 'plot_list_showFootprint' : `plot_list_showFootprint:${cluster}`,
|
||||
value: JSON.stringify(showFootprint)
|
||||
}).subscribe(res => {
|
||||
if (res.fetching === false && res.error) {
|
||||
console.log('Error on footprint subscription: ' + res.error)
|
||||
throw res.error
|
||||
}
|
||||
})
|
||||
}
|
||||
updateConfigurationMutation({
|
||||
name:
|
||||
cluster == null
|
||||
? "plot_list_showFootprint"
|
||||
: `plot_list_showFootprint:${cluster}`,
|
||||
value: JSON.stringify(showFootprint),
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && res.error) {
|
||||
console.log("Error on footprint subscription: " + res.error);
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
li.cc-config-column {
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
li.cc-config-column.is-active {
|
||||
background-color: #3273dc;
|
||||
color: #fff;
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Configure columns (Metric availability shown)
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#if view === 'list'}
|
||||
<li class="list-group-item">
|
||||
<input type="checkbox" bind:checked={pendingShowFootprint}> Show Footprint
|
||||
</li>
|
||||
<hr/>
|
||||
{/if}
|
||||
{#each newMetricsOrder as metric, index (metric)}
|
||||
<li class="cc-config-column list-group-item"
|
||||
draggable={true} ondragover="return false"
|
||||
on:dragstart={event => columnsDragStart(event, index)}
|
||||
on:drop|preventDefault={event => columnsDrag(event, index)}
|
||||
on:dragenter={() => columnHovering = index}
|
||||
class:is-active={columnHovering === index}>
|
||||
{#if unorderedMetrics.includes(metric)}
|
||||
<input type="checkbox" bind:group={unorderedMetrics} value={metric} checked>
|
||||
{:else}
|
||||
<input type="checkbox" bind:group={unorderedMetrics} value={metric}>
|
||||
{/if}
|
||||
{metric}
|
||||
<span style="float: right;">
|
||||
{cluster == null ?
|
||||
clusters // No single cluster specified: List Clusters with Metric
|
||||
.filter(c => c.metricConfig.find(m => m.name == metric) != null)
|
||||
.map(c => c.name).join(', ') :
|
||||
clusters // Single cluster requested: List Subclusters with do not have metric remove flag
|
||||
.filter(c => c.name == cluster)
|
||||
.filter(c => c.metricConfig.find(m => m.name == metric) != null)
|
||||
.map(function(c) {
|
||||
let scNames = c.subClusters.map(sc => sc.name)
|
||||
scNames.forEach(function(scName){
|
||||
let met = c.metricConfig.find(m => m.name == metric)
|
||||
let msc = met.subClusters.find(msc => msc.name == scName)
|
||||
if (msc != null) {
|
||||
if (msc.remove == true) {
|
||||
scNames = scNames.filter(scn => scn != msc.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
return scNames
|
||||
})
|
||||
.join(', ')}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#if view === "list"}
|
||||
<li class="list-group-item">
|
||||
<input type="checkbox" bind:checked={pendingShowFootprint} /> Show Footprint
|
||||
</li>
|
||||
<hr />
|
||||
{/if}
|
||||
{#each newMetricsOrder as metric, index (metric)}
|
||||
<li
|
||||
class="cc-config-column list-group-item"
|
||||
draggable={true}
|
||||
ondragover="return false"
|
||||
on:dragstart={(event) => columnsDragStart(event, index)}
|
||||
on:drop|preventDefault={(event) => columnsDrag(event, index)}
|
||||
on:dragenter={() => (columnHovering = index)}
|
||||
class:is-active={columnHovering === index}
|
||||
>
|
||||
{#if unorderedMetrics.includes(metric)}
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:group={unorderedMetrics}
|
||||
value={metric}
|
||||
checked
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:group={unorderedMetrics}
|
||||
value={metric}
|
||||
/>
|
||||
{/if}
|
||||
{metric}
|
||||
<span style="float: right;">
|
||||
{cluster == null
|
||||
? clusters // No single cluster specified: List Clusters with Metric
|
||||
.filter(
|
||||
(c) => c.metricConfig.find((m) => m.name == metric) != null,
|
||||
)
|
||||
.map((c) => c.name)
|
||||
.join(", ")
|
||||
: clusters // Single cluster requested: List Subclusters with do not have metric remove flag
|
||||
.filter((c) => c.name == cluster)
|
||||
.filter(
|
||||
(c) => c.metricConfig.find((m) => m.name == metric) != null,
|
||||
)
|
||||
.map(function (c) {
|
||||
let scNames = c.subClusters.map((sc) => sc.name);
|
||||
scNames.forEach(function (scName) {
|
||||
let met = c.metricConfig.find((m) => m.name == metric);
|
||||
let msc = met.subClusters.find(
|
||||
(msc) => msc.name == scName,
|
||||
);
|
||||
if (msc != null) {
|
||||
if (msc.remove == true) {
|
||||
scNames = scNames.filter((scn) => scn != msc.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
return scNames;
|
||||
})
|
||||
.join(", ")}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
li.cc-config-column {
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
li.cc-config-column.is-active {
|
||||
background-color: #3273dc;
|
||||
color: #fff;
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,39 +1,38 @@
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
NavLink,
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "sveltestrap";
|
||||
import {
|
||||
Icon,
|
||||
NavLink,
|
||||
Dropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let clusters; // array of names
|
||||
export let links; // array of nav links
|
||||
export let clusters; // array of names
|
||||
export let links; // array of nav links
|
||||
</script>
|
||||
|
||||
{#each links as item}
|
||||
{#if !item.perCluster}
|
||||
<NavLink href={item.href} active={window.location.pathname == item.href}
|
||||
><Icon name={item.icon} /> {item.title}</NavLink
|
||||
>
|
||||
{:else}
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name={item.icon} />
|
||||
{item.title}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="dropdown-menu-lg-end">
|
||||
{#each clusters as cluster}
|
||||
<DropdownItem
|
||||
href={item.href + cluster.name}
|
||||
active={window.location.pathname ==
|
||||
item.href + cluster.name}
|
||||
>
|
||||
{cluster.name}
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
{#if !item.perCluster}
|
||||
<NavLink href={item.href} active={window.location.pathname == item.href}
|
||||
><Icon name={item.icon} /> {item.title}</NavLink
|
||||
>
|
||||
{:else}
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name={item.icon} />
|
||||
{item.title}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="dropdown-menu-lg-end">
|
||||
{#each clusters as cluster}
|
||||
<DropdownItem
|
||||
href={item.href + cluster.name}
|
||||
active={window.location.pathname == item.href + cluster.name}
|
||||
>
|
||||
{cluster.name}
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
{/each}
|
||||
|
@ -1,143 +1,153 @@
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
Nav,
|
||||
NavItem,
|
||||
InputGroup,
|
||||
Input,
|
||||
Button,
|
||||
InputGroupText,
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
} from "sveltestrap";
|
||||
import {
|
||||
Icon,
|
||||
Nav,
|
||||
NavItem,
|
||||
InputGroup,
|
||||
Input,
|
||||
Button,
|
||||
InputGroupText,
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||
export let authlevel; // Integer
|
||||
export let roles; // Role Enum-Like
|
||||
export let screenSize; // screensize
|
||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||
export let authlevel; // Integer
|
||||
export let roles; // Role Enum-Like
|
||||
export let screenSize; // screensize
|
||||
</script>
|
||||
|
||||
<Nav navbar>
|
||||
{#if screenSize >= 768}
|
||||
<NavItem>
|
||||
<form method="GET" action="/search">
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search 'type:<query>' ..."
|
||||
name="searchId"
|
||||
style="margin-left: 10px;"
|
||||
/>
|
||||
<!-- bootstrap classes w/o effect -->
|
||||
<Button outline type="submit" title="Search"><Icon name="search" /></Button
|
||||
>
|
||||
<InputGroupText
|
||||
style="cursor:help;"
|
||||
title={authlevel >= roles.support
|
||||
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
|
||||
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
|
||||
><Icon name="info-circle" /></InputGroupText
|
||||
>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
|
||||
<Button outline style="margin-left: 10px;">
|
||||
<Icon name="book" />
|
||||
</Button>
|
||||
</a>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<Button
|
||||
outline
|
||||
on:click={() => (window.location.href = "/config")}
|
||||
style="margin-left: 10px;"
|
||||
title="Settings"
|
||||
>
|
||||
<Icon name="gear" />
|
||||
</Button>
|
||||
</NavItem>
|
||||
{#if username}
|
||||
<NavItem>
|
||||
<form method="POST" action="/logout">
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
type="submit"
|
||||
style="margin-left: 10px;"
|
||||
title="Logout {username}"
|
||||
>
|
||||
{#if screenSize > 1630}
|
||||
<Icon name="box-arrow-right"/> Logout {username}
|
||||
{:else}
|
||||
<Icon name="box-arrow-right"/>
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</NavItem>
|
||||
{/if}
|
||||
{:else}
|
||||
<NavItem>
|
||||
<Container>
|
||||
<Row cols={3}>
|
||||
<Col xs="4">
|
||||
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
|
||||
<Button outline size="sm" class="my-2 w-100">
|
||||
<Icon name="box-arrow-up-right" /> Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Col>
|
||||
<Col xs="4">
|
||||
<Button
|
||||
outline
|
||||
on:click={() => (window.location.href = "/config")}
|
||||
size="sm"
|
||||
class="my-2 w-100"
|
||||
>
|
||||
{#if authlevel >= roles.admin}
|
||||
<Icon name="gear" /> Admin Settings
|
||||
{:else}
|
||||
<Icon name="gear" /> Plotting Options
|
||||
{/if}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col xs="4">
|
||||
<form method="POST" action="/logout">
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
type="submit"
|
||||
size="sm"
|
||||
class="my-2 w-100"
|
||||
>
|
||||
<Icon name="box-arrow-right" /> Logout {username}
|
||||
</Button>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</NavItem>
|
||||
<NavItem style="margin-left: 10px; margin-right:10px;">
|
||||
<form method="GET" action="/search">
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search 'type:<query>' ..."
|
||||
name="searchId"
|
||||
/>
|
||||
<Button outline type="submit"><Icon name="search" /></Button
|
||||
>
|
||||
<InputGroupText
|
||||
style="cursor:help;"
|
||||
title={authlevel >= roles.support
|
||||
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
|
||||
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
|
||||
><Icon name="info-circle" /></InputGroupText
|
||||
>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</NavItem>
|
||||
{#if screenSize >= 768}
|
||||
<NavItem>
|
||||
<form method="GET" action="/search">
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search 'type:<query>' ..."
|
||||
name="searchId"
|
||||
style="margin-left: 10px;"
|
||||
/>
|
||||
<!-- bootstrap classes w/o effect -->
|
||||
<Button outline type="submit" title="Search"
|
||||
><Icon name="search" /></Button
|
||||
>
|
||||
<InputGroupText
|
||||
style="cursor:help;"
|
||||
title={authlevel >= roles.support
|
||||
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
|
||||
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
|
||||
><Icon name="info-circle" /></InputGroupText
|
||||
>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<a
|
||||
href="https://www.clustercockpit.org/docs/webinterface/"
|
||||
title="Documentation"
|
||||
rel="nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<Button outline style="margin-left: 10px;">
|
||||
<Icon name="book" />
|
||||
</Button>
|
||||
</a>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<Button
|
||||
outline
|
||||
on:click={() => (window.location.href = "/config")}
|
||||
style="margin-left: 10px;"
|
||||
title="Settings"
|
||||
>
|
||||
<Icon name="gear" />
|
||||
</Button>
|
||||
</NavItem>
|
||||
{#if username}
|
||||
<NavItem>
|
||||
<form method="POST" action="/logout">
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
type="submit"
|
||||
style="margin-left: 10px;"
|
||||
title="Logout {username}"
|
||||
>
|
||||
{#if screenSize > 1630}
|
||||
<Icon name="box-arrow-right" /> Logout {username}
|
||||
{:else}
|
||||
<Icon name="box-arrow-right" />
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</NavItem>
|
||||
{/if}
|
||||
{:else}
|
||||
<NavItem>
|
||||
<Container>
|
||||
<Row cols={3}>
|
||||
<Col xs="4">
|
||||
<a
|
||||
href="https://www.clustercockpit.org/docs/webinterface/"
|
||||
title="Documentation"
|
||||
rel="nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<Button outline size="sm" class="my-2 w-100">
|
||||
<Icon name="box-arrow-up-right" /> Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Col>
|
||||
<Col xs="4">
|
||||
<Button
|
||||
outline
|
||||
on:click={() => (window.location.href = "/config")}
|
||||
size="sm"
|
||||
class="my-2 w-100"
|
||||
>
|
||||
{#if authlevel >= roles.admin}
|
||||
<Icon name="gear" /> Admin Settings
|
||||
{:else}
|
||||
<Icon name="gear" /> Plotting Options
|
||||
{/if}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col xs="4">
|
||||
<form method="POST" action="/logout">
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
type="submit"
|
||||
size="sm"
|
||||
class="my-2 w-100"
|
||||
>
|
||||
<Icon name="box-arrow-right" /> Logout {username}
|
||||
</Button>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</NavItem>
|
||||
<NavItem style="margin-left: 10px; margin-right:10px;">
|
||||
<form method="GET" action="/search">
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search 'type:<query>' ..."
|
||||
name="searchId"
|
||||
/>
|
||||
<Button outline type="submit"><Icon name="search" /></Button>
|
||||
<InputGroupText
|
||||
style="cursor:help;"
|
||||
title={authlevel >= roles.support
|
||||
? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name"
|
||||
: "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}
|
||||
><Icon name="info-circle" /></InputGroupText
|
||||
>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</NavItem>
|
||||
{/if}
|
||||
</Nav>
|
||||
|
@ -1,238 +1,230 @@
|
||||
<script>
|
||||
import { init, checkMetricDisabled } from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
Spinner,
|
||||
Card,
|
||||
} from "sveltestrap";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import TimeSelection from "./filters/TimeSelection.svelte";
|
||||
import Refresher from './joblist/Refresher.svelte';
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import MetricPlot from "./plots/MetricPlot.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { init, checkMetricDisabled } from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
Spinner,
|
||||
Card,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import TimeSelection from "./filters/TimeSelection.svelte";
|
||||
import Refresher from "./joblist/Refresher.svelte";
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import MetricPlot from "./plots/MetricPlot.svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export let cluster;
|
||||
export let hostname;
|
||||
export let from = null;
|
||||
export let to = null;
|
||||
export let cluster;
|
||||
export let hostname;
|
||||
export let from = null;
|
||||
export let to = null;
|
||||
|
||||
const { query: initq } = init();
|
||||
const { query: initq } = init();
|
||||
|
||||
if (from == null || to == null) {
|
||||
to = new Date(Date.now());
|
||||
from = new Date(to.getTime());
|
||||
from.setMinutes(from.getMinutes() - 30);
|
||||
if (from == null || to == null) {
|
||||
to = new Date(Date.now());
|
||||
from = new Date(to.getTime());
|
||||
from.setMinutes(from.getMinutes() - 30);
|
||||
}
|
||||
|
||||
const ccconfig = getContext("cc-config");
|
||||
const clusters = getContext("clusters");
|
||||
const client = getContextClient();
|
||||
const nodeMetricsQuery = gql`
|
||||
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
||||
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
|
||||
host
|
||||
subCluster
|
||||
metrics {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
timestep
|
||||
unit {
|
||||
base
|
||||
prefix
|
||||
}
|
||||
series {
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ccconfig = getContext("cc-config");
|
||||
const clusters = getContext("clusters");
|
||||
const client = getContextClient();
|
||||
const nodeMetricsQuery = gql`
|
||||
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
||||
nodeMetrics(
|
||||
cluster: $cluster
|
||||
nodes: $nodes
|
||||
from: $from
|
||||
to: $to
|
||||
) {
|
||||
host
|
||||
subCluster
|
||||
metrics {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
timestep
|
||||
unit {
|
||||
base
|
||||
prefix
|
||||
}
|
||||
series {
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
$: nodeMetricsData = queryStore({
|
||||
client: client,
|
||||
query: nodeMetricsQuery,
|
||||
variables: {
|
||||
cluster: cluster,
|
||||
nodes: [hostname],
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
$: nodeMetricsData = queryStore({
|
||||
client: client,
|
||||
query: nodeMetricsQuery,
|
||||
variables: {
|
||||
cluster: cluster,
|
||||
nodes: [hostname],
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
},
|
||||
});
|
||||
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let sorting = { field: "startTime", order: "DESC" };
|
||||
$: filter = [
|
||||
{ cluster: { eq: cluster } },
|
||||
{ node: { contains: hostname } },
|
||||
{ state: ["running"] },
|
||||
// {startTime: {
|
||||
// from: from.toISOString(),
|
||||
// to: to.toISOString()
|
||||
// }}
|
||||
];
|
||||
|
||||
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let sorting = { field: "startTime", order: "DESC" };
|
||||
$: filter = [
|
||||
{ cluster: { eq: cluster } },
|
||||
{ node: { contains: hostname } },
|
||||
{ state: ["running"] },
|
||||
// {startTime: {
|
||||
// from: from.toISOString(),
|
||||
// to: to.toISOString()
|
||||
// }}
|
||||
];
|
||||
|
||||
const nodeJobsQuery = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
# items {
|
||||
# id
|
||||
# jobId
|
||||
# }
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: nodeJobsData = queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
let metricUnits = {};
|
||||
$: if ($nodeMetricsData.data) {
|
||||
let thisCluster = clusters.find((c) => c.name == cluster);
|
||||
if (thisCluster) {
|
||||
for (let metric of thisCluster.metricConfig) {
|
||||
if (metric.unit.prefix || metric.unit.base) {
|
||||
metricUnits[metric.name] =
|
||||
"(" +
|
||||
(metric.unit.prefix ? metric.unit.prefix : "") +
|
||||
(metric.unit.base ? metric.unit.base : "") +
|
||||
")";
|
||||
} else {
|
||||
// If no unit defined: Omit Unit Display
|
||||
metricUnits[metric.name] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
const nodeJobsQuery = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
# items {
|
||||
# id
|
||||
# jobId
|
||||
# }
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||
$: nodeJobsData = queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
let metricUnits = {};
|
||||
$: if ($nodeMetricsData.data) {
|
||||
let thisCluster = clusters.find((c) => c.name == cluster);
|
||||
if (thisCluster) {
|
||||
for (let metric of thisCluster.metricConfig) {
|
||||
if (metric.unit.prefix || metric.unit.base) {
|
||||
metricUnits[metric.name] =
|
||||
"(" +
|
||||
(metric.unit.prefix ? metric.unit.prefix : "") +
|
||||
(metric.unit.base ? metric.unit.base : "") +
|
||||
")";
|
||||
} else {
|
||||
// If no unit defined: Omit Unit Display
|
||||
metricUnits[metric.name] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq.fetching}
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq.fetching}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
||||
<InputGroupText>{hostname} ({cluster})</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
{#if $nodeJobsData.fetching}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
||||
<InputGroupText>{hostname} ({cluster})</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
{#if $nodeJobsData.fetching}
|
||||
<Spinner />
|
||||
{:else if $nodeJobsData.data}
|
||||
Currently running jobs on this node: {$nodeJobsData.data.jobs
|
||||
.count}
|
||||
[
|
||||
<a
|
||||
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
|
||||
target="_blank">View in Job List</a
|
||||
> ]
|
||||
{:else}
|
||||
No currently running jobs.
|
||||
{/if}
|
||||
</Col>
|
||||
<Col>
|
||||
<Refresher on:reload={() => {
|
||||
const diff = Date.now() - to
|
||||
from = new Date(from.getTime() + diff)
|
||||
to = new Date(to.getTime() + diff)
|
||||
}} />
|
||||
</Col>
|
||||
<Col>
|
||||
<TimeSelection bind:from bind:to />
|
||||
</Col>
|
||||
{/if}
|
||||
{:else if $nodeJobsData.data}
|
||||
Currently running jobs on this node: {$nodeJobsData.data.jobs.count}
|
||||
[
|
||||
<a
|
||||
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
|
||||
target="_blank">View in Job List</a
|
||||
> ]
|
||||
{:else}
|
||||
No currently running jobs.
|
||||
{/if}
|
||||
</Col>
|
||||
<Col>
|
||||
<Refresher
|
||||
on:reload={() => {
|
||||
const diff = Date.now() - to;
|
||||
from = new Date(from.getTime() + diff);
|
||||
to = new Date(to.getTime() + diff);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<TimeSelection bind:from bind:to />
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
{#if $nodeMetricsData.error}
|
||||
<Card body color="danger">{$nodeMetricsData.error.message}</Card>
|
||||
{:else if $nodeMetricsData.fetching || $initq.fetching}
|
||||
<Spinner />
|
||||
<Col>
|
||||
{#if $nodeMetricsData.error}
|
||||
<Card body color="danger">{$nodeMetricsData.error.message}</Card>
|
||||
{:else if $nodeMetricsData.fetching || $initq.fetching}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="node"
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
items={$nodeMetricsData.data.nodeMetrics[0].metrics
|
||||
.map((m) => ({
|
||||
...m,
|
||||
disabled: checkMetricDisabled(
|
||||
m.name,
|
||||
cluster,
|
||||
$nodeMetricsData.data.nodeMetrics[0].subCluster,
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))}
|
||||
>
|
||||
<h4 style="text-align: center; padding-top:15px;">
|
||||
{item.name}
|
||||
{metricUnits[item.name]}
|
||||
</h4>
|
||||
{#if item.disabled === false && item.metric}
|
||||
<MetricPlot
|
||||
{width}
|
||||
height={300}
|
||||
metric={item.name}
|
||||
timestep={item.metric.timestep}
|
||||
cluster={clusters.find((c) => c.name == cluster)}
|
||||
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
|
||||
series={item.metric.series}
|
||||
resources={[{ hostname: hostname }]}
|
||||
forNode={true}
|
||||
/>
|
||||
{:else if item.disabled === true && item.metric}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
|
||||
.subCluster}</code
|
||||
></Card
|
||||
>
|
||||
{:else}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="node"
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
items={$nodeMetricsData.data.nodeMetrics[0].metrics
|
||||
.map((m) => ({
|
||||
...m,
|
||||
disabled: checkMetricDisabled(
|
||||
m.name,
|
||||
cluster,
|
||||
$nodeMetricsData.data.nodeMetrics[0].subCluster
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))}
|
||||
>
|
||||
<h4 style="text-align: center; padding-top:15px;">
|
||||
{item.name}
|
||||
{metricUnits[item.name]}
|
||||
</h4>
|
||||
{#if item.disabled === false && item.metric}
|
||||
<MetricPlot
|
||||
{width}
|
||||
height={300}
|
||||
metric={item.name}
|
||||
timestep={item.metric.timestep}
|
||||
cluster={clusters.find((c) => c.name == cluster)}
|
||||
subCluster={$nodeMetricsData.data.nodeMetrics[0]
|
||||
.subCluster}
|
||||
series={item.metric.series}
|
||||
resources={[{hostname: hostname}]}
|
||||
forNode={true}
|
||||
/>
|
||||
{:else if item.disabled === true && item.metric}
|
||||
<Card
|
||||
style="margin-left: 2rem;margin-right: 2rem;"
|
||||
body
|
||||
color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
|
||||
.subCluster}</code
|
||||
></Card
|
||||
>
|
||||
{:else}
|
||||
<Card
|
||||
style="margin-left: 2rem;margin-right: 2rem;"
|
||||
body
|
||||
color="warning"
|
||||
>No dataset returned for <code>{item.name}</code></Card
|
||||
>
|
||||
{/if}
|
||||
</PlotTable>
|
||||
<Card
|
||||
style="margin-left: 2rem;margin-right: 2rem;"
|
||||
body
|
||||
color="warning"
|
||||
>No dataset returned for <code>{item.name}</code></Card
|
||||
>
|
||||
{/if}
|
||||
</Col>
|
||||
</PlotTable>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -1,139 +1,163 @@
|
||||
<script>
|
||||
import { Modal, ModalBody, ModalHeader, ModalFooter, InputGroup,
|
||||
Button, ListGroup, ListGroupItem, Icon } from 'sveltestrap'
|
||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
InputGroup,
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||
|
||||
export let availableMetrics
|
||||
export let metricsInHistograms
|
||||
export let metricsInScatterplots
|
||||
export let availableMetrics;
|
||||
export let metricsInHistograms;
|
||||
export let metricsInScatterplots;
|
||||
|
||||
const client = getContextClient();
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`mutation($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}`,
|
||||
variables: { name, value }
|
||||
})
|
||||
}
|
||||
const client = getContextClient();
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}
|
||||
`,
|
||||
variables: { name, value },
|
||||
});
|
||||
};
|
||||
|
||||
let isHistogramConfigOpen = false, isScatterPlotConfigOpen = false
|
||||
let selectedMetric1 = null, selectedMetric2 = null
|
||||
let isHistogramConfigOpen = false,
|
||||
isScatterPlotConfigOpen = false;
|
||||
let selectedMetric1 = null,
|
||||
selectedMetric2 = null;
|
||||
|
||||
function updateConfiguration(data) {
|
||||
updateConfigurationMutation({
|
||||
name: data.name,
|
||||
value: JSON.stringify(data.value)
|
||||
}).subscribe(res => {
|
||||
if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
function updateConfiguration(data) {
|
||||
updateConfigurationMutation({
|
||||
name: data.name,
|
||||
value: JSON.stringify(data.value),
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button outline
|
||||
on:click={() => (isHistogramConfigOpen = true)}>
|
||||
<Icon name=""/>
|
||||
Select Plots for Histograms
|
||||
<Button outline on:click={() => (isHistogramConfigOpen = true)}>
|
||||
<Icon name="" />
|
||||
Select Plots for Histograms
|
||||
</Button>
|
||||
|
||||
<Button outline
|
||||
on:click={() => (isScatterPlotConfigOpen = true)}>
|
||||
<Icon name=""/>
|
||||
Select Plots in Scatter Plots
|
||||
<Button outline on:click={() => (isScatterPlotConfigOpen = true)}>
|
||||
<Icon name="" />
|
||||
Select Plots in Scatter Plots
|
||||
</Button>
|
||||
|
||||
<Modal isOpen={isHistogramConfigOpen}
|
||||
toggle={() => (isHistogramConfigOpen = !isHistogramConfigOpen)}>
|
||||
<ModalHeader>
|
||||
Select metrics presented in histograms
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each availableMetrics as metric (metric)}
|
||||
<ListGroupItem>
|
||||
<input type="checkbox" bind:group={metricsInHistograms}
|
||||
value={metric}
|
||||
on:change={() => updateConfiguration({
|
||||
name: 'analysis_view_histogramMetrics',
|
||||
value: metricsInHistograms
|
||||
})} />
|
||||
<Modal
|
||||
isOpen={isHistogramConfigOpen}
|
||||
toggle={() => (isHistogramConfigOpen = !isHistogramConfigOpen)}
|
||||
>
|
||||
<ModalHeader>Select metrics presented in histograms</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each availableMetrics as metric (metric)}
|
||||
<ListGroupItem>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:group={metricsInHistograms}
|
||||
value={metric}
|
||||
on:change={() =>
|
||||
updateConfiguration({
|
||||
name: "analysis_view_histogramMetrics",
|
||||
value: metricsInHistograms,
|
||||
})}
|
||||
/>
|
||||
|
||||
{metric}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary"
|
||||
on:click={() => (isHistogramConfigOpen = false)}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
{metric}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => (isHistogramConfigOpen = false)}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
<Modal isOpen={isScatterPlotConfigOpen}
|
||||
toggle={() => (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}>
|
||||
<ModalHeader>
|
||||
Select metric pairs presented in scatter plots
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each metricsInScatterplots as pair}
|
||||
<ListGroupItem>
|
||||
<b>{pair[0]}</b> / <b>{pair[1]}</b>
|
||||
<Modal
|
||||
isOpen={isScatterPlotConfigOpen}
|
||||
toggle={() => (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}
|
||||
>
|
||||
<ModalHeader>Select metric pairs presented in scatter plots</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each metricsInScatterplots as pair}
|
||||
<ListGroupItem>
|
||||
<b>{pair[0]}</b> / <b>{pair[1]}</b>
|
||||
|
||||
<Button style="float: right;" outline color="danger"
|
||||
on:click={() => {
|
||||
metricsInScatterplots = metricsInScatterplots.filter(p => pair != p)
|
||||
updateConfiguration({
|
||||
name: 'analysis_view_scatterPlotMetrics',
|
||||
value: metricsInScatterplots
|
||||
});
|
||||
}}>
|
||||
<Icon name="x" />
|
||||
</Button>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
<Button
|
||||
style="float: right;"
|
||||
outline
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
metricsInScatterplots = metricsInScatterplots.filter(
|
||||
(p) => pair != p,
|
||||
);
|
||||
updateConfiguration({
|
||||
name: "analysis_view_scatterPlotMetrics",
|
||||
value: metricsInScatterplots,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon name="x" />
|
||||
</Button>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
|
||||
<br/>
|
||||
<br />
|
||||
|
||||
<InputGroup>
|
||||
<select bind:value={selectedMetric1} class="form-group form-select">
|
||||
<option value={null}>Choose Metric for X Axis</option>
|
||||
{#each availableMetrics as metric}
|
||||
<option value={metric}>{metric}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select bind:value={selectedMetric2} class="form-group form-select">
|
||||
<option value={null}>Choose Metric for Y Axis</option>
|
||||
{#each availableMetrics as metric}
|
||||
<option value={metric}>{metric}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Button outline disabled={selectedMetric1 == null || selectedMetric2 == null}
|
||||
on:click={() => {
|
||||
metricsInScatterplots = [...metricsInScatterplots, [selectedMetric1, selectedMetric2]]
|
||||
selectedMetric1 = null
|
||||
selectedMetric2 = null
|
||||
updateConfiguration({
|
||||
name: 'analysis_view_scatterPlotMetrics',
|
||||
value: metricsInScatterplots
|
||||
})
|
||||
}}>
|
||||
Add Plot
|
||||
</Button>
|
||||
</InputGroup>
|
||||
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary"
|
||||
on:click={() => (isScatterPlotConfigOpen = false)}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<InputGroup>
|
||||
<select bind:value={selectedMetric1} class="form-group form-select">
|
||||
<option value={null}>Choose Metric for X Axis</option>
|
||||
{#each availableMetrics as metric}
|
||||
<option value={metric}>{metric}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select bind:value={selectedMetric2} class="form-group form-select">
|
||||
<option value={null}>Choose Metric for Y Axis</option>
|
||||
{#each availableMetrics as metric}
|
||||
<option value={metric}>{metric}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Button
|
||||
outline
|
||||
disabled={selectedMetric1 == null || selectedMetric2 == null}
|
||||
on:click={() => {
|
||||
metricsInScatterplots = [
|
||||
...metricsInScatterplots,
|
||||
[selectedMetric1, selectedMetric2],
|
||||
];
|
||||
selectedMetric1 = null;
|
||||
selectedMetric2 = null;
|
||||
updateConfiguration({
|
||||
name: "analysis_view_scatterPlotMetrics",
|
||||
value: metricsInScatterplots,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Plot
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,139 +1,154 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import { Button, Table, InputGroup, InputGroupText, Icon } from 'sveltestrap'
|
||||
import MetricSelection from './MetricSelection.svelte'
|
||||
import StatsTableEntry from './StatsTableEntry.svelte'
|
||||
import { maxScope } from './utils.js'
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import StatsTableEntry from "./StatsTableEntry.svelte";
|
||||
import { maxScope } from "./utils.js";
|
||||
|
||||
export let job
|
||||
export let jobMetrics
|
||||
export let job;
|
||||
export let jobMetrics;
|
||||
|
||||
const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(),
|
||||
scopesForMetric = (metric) => jobMetrics
|
||||
.filter(jm => jm.name == metric)
|
||||
.map(jm => jm.scope)
|
||||
const allMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort(),
|
||||
scopesForMetric = (metric) =>
|
||||
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
|
||||
|
||||
let hosts = job.resources.map(r => r.hostname).sort(),
|
||||
selectedScopes = {},
|
||||
sorting = {},
|
||||
isMetricSelectionOpen = false,
|
||||
selectedMetrics = getContext('cc-config')[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|
||||
|| getContext('cc-config')['job_view_nodestats_selectedMetrics']
|
||||
|
||||
for (let metric of allMetrics) {
|
||||
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
||||
// -> Else: Load smallest available granularity as default as per availability
|
||||
const availableScopes = scopesForMetric(metric)
|
||||
if (job.exclusive != 1 || job.numNodes == 1) {
|
||||
if (availableScopes.includes('accelerator')) {
|
||||
selectedScopes[metric] = 'accelerator'
|
||||
} else if (availableScopes.includes('core')) {
|
||||
selectedScopes[metric] = 'core'
|
||||
} else if (availableScopes.includes('socket')) {
|
||||
selectedScopes[metric] = 'socket'
|
||||
} else {
|
||||
selectedScopes[metric] = 'node'
|
||||
}
|
||||
} else {
|
||||
selectedScopes[metric] = maxScope(availableScopes)
|
||||
}
|
||||
let hosts = job.resources.map((r) => r.hostname).sort(),
|
||||
selectedScopes = {},
|
||||
sorting = {},
|
||||
isMetricSelectionOpen = false,
|
||||
selectedMetrics =
|
||||
getContext("cc-config")[
|
||||
`job_view_nodestats_selectedMetrics:${job.cluster}`
|
||||
] || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
||||
|
||||
sorting[metric] = {
|
||||
min: { dir: 'up', active: false },
|
||||
avg: { dir: 'up', active: false },
|
||||
max: { dir: 'up', active: false },
|
||||
}
|
||||
for (let metric of allMetrics) {
|
||||
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
||||
// -> Else: Load smallest available granularity as default as per availability
|
||||
const availableScopes = scopesForMetric(metric);
|
||||
if (job.exclusive != 1 || job.numNodes == 1) {
|
||||
if (availableScopes.includes("accelerator")) {
|
||||
selectedScopes[metric] = "accelerator";
|
||||
} else if (availableScopes.includes("core")) {
|
||||
selectedScopes[metric] = "core";
|
||||
} else if (availableScopes.includes("socket")) {
|
||||
selectedScopes[metric] = "socket";
|
||||
} else {
|
||||
selectedScopes[metric] = "node";
|
||||
}
|
||||
} else {
|
||||
selectedScopes[metric] = maxScope(availableScopes);
|
||||
}
|
||||
|
||||
export function sortBy(metric, stat) {
|
||||
let s = sorting[metric][stat]
|
||||
if (s.active) {
|
||||
s.dir = s.dir == 'up' ? 'down' : 'up'
|
||||
} else {
|
||||
for (let metric in sorting)
|
||||
for (let stat in sorting[metric])
|
||||
sorting[metric][stat].active = false
|
||||
s.active = true
|
||||
}
|
||||
sorting[metric] = {
|
||||
min: { dir: "up", active: false },
|
||||
avg: { dir: "up", active: false },
|
||||
max: { dir: "up", active: false },
|
||||
};
|
||||
}
|
||||
|
||||
let series = jobMetrics.find(jm => jm.name == metric && jm.scope == 'node')?.metric.series
|
||||
sorting = {...sorting}
|
||||
hosts = hosts.sort((h1, h2) => {
|
||||
let s1 = series.find(s => s.hostname == h1)?.statistics
|
||||
let s2 = series.find(s => s.hostname == h2)?.statistics
|
||||
if (s1 == null || s2 == null)
|
||||
return -1
|
||||
|
||||
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
|
||||
})
|
||||
export function sortBy(metric, stat) {
|
||||
let s = sorting[metric][stat];
|
||||
if (s.active) {
|
||||
s.dir = s.dir == "up" ? "down" : "up";
|
||||
} else {
|
||||
for (let metric in sorting)
|
||||
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
|
||||
s.active = true;
|
||||
}
|
||||
|
||||
export function moreLoaded(jobMetric) {
|
||||
jobMetrics = [...jobMetrics, jobMetric]
|
||||
}
|
||||
let series = jobMetrics.find(
|
||||
(jm) => jm.name == metric && jm.scope == "node",
|
||||
)?.metric.series;
|
||||
sorting = { ...sorting };
|
||||
hosts = hosts.sort((h1, h2) => {
|
||||
let s1 = series.find((s) => s.hostname == h1)?.statistics;
|
||||
let s2 = series.find((s) => s.hostname == h2)?.statistics;
|
||||
if (s1 == null || s2 == null) return -1;
|
||||
|
||||
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
|
||||
});
|
||||
}
|
||||
|
||||
export function moreLoaded(jobMetric) {
|
||||
jobMetrics = [...jobMetrics, jobMetric];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' -->
|
||||
Metrics
|
||||
</Button>
|
||||
</th>
|
||||
{#each selectedMetrics as metric}
|
||||
<th colspan={selectedScopes[metric] == 'node' ? 3 : 4}>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
{metric}
|
||||
</InputGroupText>
|
||||
<select class="form-select"
|
||||
bind:value={selectedScopes[metric]}>
|
||||
{#each scopesForMetric(metric, jobMetrics) as scope}
|
||||
<option value={scope}>{scope}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</InputGroup>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Node</th>
|
||||
{#each selectedMetrics as metric}
|
||||
{#if selectedScopes[metric] != 'node'}
|
||||
<th>Id</th>
|
||||
{/if}
|
||||
{#each ['min', 'avg', 'max'] as stat}
|
||||
<th on:click={() => sortBy(metric, stat)}>
|
||||
{stat}
|
||||
{#if selectedScopes[metric] == 'node'}
|
||||
<Icon name="caret-{sorting[metric][stat].dir}{sorting[metric][stat].active ? '-fill' : ''}" />
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each hosts as host (host)}
|
||||
<tr>
|
||||
<th scope="col">{host}</th>
|
||||
{#each selectedMetrics as metric (metric)}
|
||||
<StatsTableEntry
|
||||
host={host} metric={metric}
|
||||
scope={selectedScopes[metric]}
|
||||
jobMetrics={jobMetrics} />
|
||||
{/each}
|
||||
</tr>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Button outline on:click={() => (isMetricSelectionOpen = true)}>
|
||||
<!-- log to click ', console.log(isMetricSelectionOpen)' -->
|
||||
Metrics
|
||||
</Button>
|
||||
</th>
|
||||
{#each selectedMetrics as metric}
|
||||
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
{metric}
|
||||
</InputGroupText>
|
||||
<select class="form-select" bind:value={selectedScopes[metric]}>
|
||||
{#each scopesForMetric(metric, jobMetrics) as scope}
|
||||
<option value={scope}>{scope}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</InputGroup>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Node</th>
|
||||
{#each selectedMetrics as metric}
|
||||
{#if selectedScopes[metric] != "node"}
|
||||
<th>Id</th>
|
||||
{/if}
|
||||
{#each ["min", "avg", "max"] as stat}
|
||||
<th on:click={() => sortBy(metric, stat)}>
|
||||
{stat}
|
||||
{#if selectedScopes[metric] == "node"}
|
||||
<Icon
|
||||
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
|
||||
.active
|
||||
? '-fill'
|
||||
: ''}"
|
||||
/>
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each hosts as host (host)}
|
||||
<tr>
|
||||
<th scope="col">{host}</th>
|
||||
{#each selectedMetrics as metric (metric)}
|
||||
<StatsTableEntry
|
||||
{host}
|
||||
{metric}
|
||||
scope={selectedScopes[metric]}
|
||||
{jobMetrics}
|
||||
/>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<br/>
|
||||
<br />
|
||||
|
||||
<MetricSelection
|
||||
cluster={job.cluster}
|
||||
configName='job_view_nodestats_selectedMetrics'
|
||||
allMetrics={new Set(allMetrics)}
|
||||
bind:metrics={selectedMetrics}
|
||||
bind:isOpen={isMetricSelectionOpen} />
|
||||
cluster={job.cluster}
|
||||
configName="job_view_nodestats_selectedMetrics"
|
||||
allMetrics={new Set(allMetrics)}
|
||||
bind:metrics={selectedMetrics}
|
||||
bind:isOpen={isMetricSelectionOpen}
|
||||
/>
|
||||
|
@ -1,82 +1,86 @@
|
||||
<script>
|
||||
import { Icon } from 'sveltestrap'
|
||||
|
||||
export let host
|
||||
export let metric
|
||||
export let scope
|
||||
export let jobMetrics
|
||||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
function compareNumbers(a, b) {
|
||||
return a.id - b.id;
|
||||
export let host;
|
||||
export let metric;
|
||||
export let scope;
|
||||
export let jobMetrics;
|
||||
|
||||
function compareNumbers(a, b) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
|
||||
function sortByField(field) {
|
||||
let s = sorting[field];
|
||||
if (s.active) {
|
||||
s.dir = s.dir == "up" ? "down" : "up";
|
||||
} else {
|
||||
for (let field in sorting) sorting[field].active = false;
|
||||
s.active = true;
|
||||
}
|
||||
|
||||
function sortByField(field) {
|
||||
let s = sorting[field]
|
||||
if (s.active) {
|
||||
s.dir = s.dir == 'up' ? 'down' : 'up'
|
||||
} else {
|
||||
for (let field in sorting)
|
||||
sorting[field].active = false
|
||||
s.active = true
|
||||
}
|
||||
sorting = { ...sorting };
|
||||
series = series.sort((a, b) => {
|
||||
if (a == null || b == null) return -1;
|
||||
|
||||
sorting = {...sorting}
|
||||
series = series.sort((a, b) => {
|
||||
if (a == null || b == null)
|
||||
return -1
|
||||
if (field === "id") {
|
||||
return s.dir != "up" ? a[field] - b[field] : b[field] - a[field];
|
||||
} else {
|
||||
return s.dir != "up"
|
||||
? a.statistics[field] - b.statistics[field]
|
||||
: b.statistics[field] - a.statistics[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (field === 'id') {
|
||||
return s.dir != 'up' ? a[field] - b[field] : b[field] - a[field]
|
||||
} else {
|
||||
return s.dir != 'up' ? a.statistics[field] - b.statistics[field] : b.statistics[field] - a.statistics[field]
|
||||
}
|
||||
})
|
||||
}
|
||||
let sorting = {
|
||||
id: { dir: "down", active: true },
|
||||
min: { dir: "up", active: false },
|
||||
avg: { dir: "up", active: false },
|
||||
max: { dir: "up", active: false },
|
||||
};
|
||||
|
||||
let sorting = {
|
||||
id: { dir: 'down', active: true },
|
||||
min: { dir: 'up', active: false },
|
||||
avg: { dir: 'up', active: false },
|
||||
max: { dir: 'up', active: false },
|
||||
}
|
||||
|
||||
$: series = jobMetrics
|
||||
.find(jm => jm.name == metric && jm.scope == scope)
|
||||
?.metric.series.filter(s => s.hostname == host && s.statistics != null)
|
||||
?.sort(compareNumbers)
|
||||
$: series = jobMetrics
|
||||
.find((jm) => jm.name == metric && jm.scope == scope)
|
||||
?.metric.series.filter((s) => s.hostname == host && s.statistics != null)
|
||||
?.sort(compareNumbers);
|
||||
</script>
|
||||
|
||||
{#if series == null || series.length == 0}
|
||||
<td colspan={scope == 'node' ? 3 : 4}><i>No data</i></td>
|
||||
{:else if series.length == 1 && scope == 'node'}
|
||||
<td>
|
||||
{series[0].statistics.min}
|
||||
</td>
|
||||
<td>
|
||||
{series[0].statistics.avg}
|
||||
</td>
|
||||
<td>
|
||||
{series[0].statistics.max}
|
||||
</td>
|
||||
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
|
||||
{:else if series.length == 1 && scope == "node"}
|
||||
<td>
|
||||
{series[0].statistics.min}
|
||||
</td>
|
||||
<td>
|
||||
{series[0].statistics.avg}
|
||||
</td>
|
||||
<td>
|
||||
{series[0].statistics.max}
|
||||
</td>
|
||||
{:else}
|
||||
<td colspan="4">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
{#each ['id', 'min', 'avg', 'max'] as field}
|
||||
<th on:click={() => sortByField(field)}>
|
||||
Sort
|
||||
<Icon name="caret-{sorting[field].dir}{sorting[field].active ? '-fill' : ''}" />
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{#each series as s, i}
|
||||
<tr>
|
||||
<th>{s.id ?? i}</th>
|
||||
<td>{s.statistics.min}</td>
|
||||
<td>{s.statistics.avg}</td>
|
||||
<td>{s.statistics.max}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</td>
|
||||
<td colspan="4">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
{#each ["id", "min", "avg", "max"] as field}
|
||||
<th on:click={() => sortByField(field)}>
|
||||
Sort
|
||||
<Icon
|
||||
name="caret-{sorting[field].dir}{sorting[field].active
|
||||
? '-fill'
|
||||
: ''}"
|
||||
/>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{#each series as s, i}
|
||||
<tr>
|
||||
<th>{s.id ?? i}</th>
|
||||
<td>{s.statistics.min}</td>
|
||||
<td>{s.statistics.avg}</td>
|
||||
<td>{s.statistics.max}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</td>
|
||||
{/if}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,159 +1,218 @@
|
||||
<script>
|
||||
import { init, checkMetricDisabled } from './utils.js'
|
||||
import Refresher from './joblist/Refresher.svelte'
|
||||
import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import TimeSelection from './filters/TimeSelection.svelte'
|
||||
import PlotTable from './PlotTable.svelte'
|
||||
import MetricPlot from './plots/MetricPlot.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { init, checkMetricDisabled } from "./utils.js";
|
||||
import Refresher from "./joblist/Refresher.svelte";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
Spinner,
|
||||
Card,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import TimeSelection from "./filters/TimeSelection.svelte";
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import MetricPlot from "./plots/MetricPlot.svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export let cluster
|
||||
export let from = null
|
||||
export let to = null
|
||||
export let cluster;
|
||||
export let from = null;
|
||||
export let to = null;
|
||||
|
||||
const { query: initq } = init()
|
||||
const { query: initq } = init();
|
||||
|
||||
if (from == null || to == null) {
|
||||
to = new Date(Date.now())
|
||||
from = new Date(to.getTime())
|
||||
from.setMinutes(from.getMinutes() - 30)
|
||||
}
|
||||
if (from == null || to == null) {
|
||||
to = new Date(Date.now());
|
||||
from = new Date(to.getTime());
|
||||
from.setMinutes(from.getMinutes() - 30);
|
||||
}
|
||||
|
||||
const clusters = getContext('clusters')
|
||||
const ccconfig = getContext('cc-config')
|
||||
const metricConfig = getContext('metrics')
|
||||
const clusters = getContext("clusters");
|
||||
const ccconfig = getContext("cc-config");
|
||||
const metricConfig = getContext("metrics");
|
||||
|
||||
let plotHeight = 300
|
||||
let hostnameFilter = ''
|
||||
let selectedMetric = ccconfig.system_view_selectedMetric
|
||||
let plotHeight = 300;
|
||||
let hostnameFilter = "";
|
||||
let selectedMetric = ccconfig.system_view_selectedMetric;
|
||||
|
||||
const client = getContextClient();
|
||||
$: nodesQuery = queryStore({
|
||||
client: client,
|
||||
query: gql`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
||||
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
|
||||
host
|
||||
subCluster
|
||||
metrics {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
timestep
|
||||
unit { base, prefix }
|
||||
series {
|
||||
statistics { min, avg, max }
|
||||
data
|
||||
}
|
||||
}
|
||||
const client = getContextClient();
|
||||
$: nodesQuery = queryStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
||||
nodeMetrics(
|
||||
cluster: $cluster
|
||||
metrics: $metrics
|
||||
from: $from
|
||||
to: $to
|
||||
) {
|
||||
host
|
||||
subCluster
|
||||
metrics {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
timestep
|
||||
unit {
|
||||
base
|
||||
prefix
|
||||
}
|
||||
series {
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
cluster: cluster,
|
||||
metrics: [selectedMetric],
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
cluster: cluster,
|
||||
metrics: [selectedMetric],
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
let metricUnits = {}
|
||||
$: if ($nodesQuery.data) {
|
||||
let thisCluster = clusters.find(c => c.name == cluster)
|
||||
if (thisCluster) {
|
||||
for (let metric of thisCluster.metricConfig) {
|
||||
if (metric.unit.prefix || metric.unit.base) {
|
||||
metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
|
||||
} else { // If no unit defined: Omit Unit Display
|
||||
metricUnits[metric.name] = ''
|
||||
}
|
||||
}
|
||||
let metricUnits = {};
|
||||
$: if ($nodesQuery.data) {
|
||||
let thisCluster = clusters.find((c) => c.name == cluster);
|
||||
if (thisCluster) {
|
||||
for (let metric of thisCluster.metricConfig) {
|
||||
if (metric.unit.prefix || metric.unit.base) {
|
||||
metricUnits[metric.name] =
|
||||
"(" +
|
||||
(metric.unit.prefix ? metric.unit.prefix : "") +
|
||||
(metric.unit.base ? metric.unit.base : "") +
|
||||
")";
|
||||
} else {
|
||||
// If no unit defined: Omit Unit Display
|
||||
metricUnits[metric.name] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq.fetching}
|
||||
<Spinner/>
|
||||
{:else}
|
||||
<Col>
|
||||
<Refresher on:reload={() => {
|
||||
const diff = Date.now() - to
|
||||
from = new Date(from.getTime() + diff)
|
||||
to = new Date(to.getTime() + diff)
|
||||
}} />
|
||||
</Col>
|
||||
<Col>
|
||||
<TimeSelection
|
||||
bind:from={from}
|
||||
bind:to={to} />
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||
<InputGroupText>Metric</InputGroupText>
|
||||
<select class="form-select" bind:value={selectedMetric}>
|
||||
{#each clusters.find(c => c.name == cluster).metricConfig as metric}
|
||||
<option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
||||
<InputGroupText>Find Node</InputGroupText>
|
||||
<Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
<br/>
|
||||
<Row>
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq.fetching}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<Col>
|
||||
{#if $nodesQuery.error}
|
||||
<Card body color="danger">{$nodesQuery.error.message}</Card>
|
||||
{:else if $nodesQuery.fetching || $initq.fetching}
|
||||
<Spinner/>
|
||||
{:else}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="systems"
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
items={$nodesQuery.data.nodeMetrics
|
||||
.filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
|
||||
.map(h => ({
|
||||
host: h.host,
|
||||
subCluster: h.subCluster,
|
||||
data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node'),
|
||||
disabled: checkMetricDisabled(selectedMetric, cluster, h.subCluster)
|
||||
}))
|
||||
.sort((a, b) => a.host.localeCompare(b.host))
|
||||
}>
|
||||
|
||||
<h4 style="width: 100%; text-align: center;"><a style="display: block;padding-top: 15px;" href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>
|
||||
{#if item.disabled === false && item.data}
|
||||
<MetricPlot
|
||||
width={width}
|
||||
height={plotHeight}
|
||||
timestep={item.data.metric.timestep}
|
||||
series={item.data.metric.series}
|
||||
metric={item.data.name}
|
||||
cluster={clusters.find(c => c.name == cluster)}
|
||||
subCluster={item.subCluster}
|
||||
resources={[{hostname: item.host}]}
|
||||
forNode={true}/>
|
||||
{:else if item.disabled === true && item.data}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
|
||||
{:else}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
|
||||
{/if}
|
||||
</PlotTable>
|
||||
{/if}
|
||||
<Refresher
|
||||
on:reload={() => {
|
||||
const diff = Date.now() - to;
|
||||
from = new Date(from.getTime() + diff);
|
||||
to = new Date(to.getTime() + diff);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<TimeSelection bind:from bind:to />
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||
<InputGroupText>Metric</InputGroupText>
|
||||
<select class="form-select" bind:value={selectedMetric}>
|
||||
{#each clusters.find((c) => c.name == cluster).metricConfig as metric}
|
||||
<option value={metric.name}
|
||||
>{metric.name} {metricUnits[metric.name]}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
||||
<InputGroupText>Find Node</InputGroupText>
|
||||
<Input
|
||||
placeholder="hostname..."
|
||||
type="text"
|
||||
bind:value={hostnameFilter}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
{#if $nodesQuery.error}
|
||||
<Card body color="danger">{$nodesQuery.error.message}</Card>
|
||||
{:else if $nodesQuery.fetching || $initq.fetching}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="systems"
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
items={$nodesQuery.data.nodeMetrics
|
||||
.filter(
|
||||
(h) =>
|
||||
h.host.includes(hostnameFilter) &&
|
||||
h.metrics.some(
|
||||
(m) => m.name == selectedMetric && m.scope == "node",
|
||||
),
|
||||
)
|
||||
.map((h) => ({
|
||||
host: h.host,
|
||||
subCluster: h.subCluster,
|
||||
data: h.metrics.find(
|
||||
(m) => m.name == selectedMetric && m.scope == "node",
|
||||
),
|
||||
disabled: checkMetricDisabled(
|
||||
selectedMetric,
|
||||
cluster,
|
||||
h.subCluster,
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.host.localeCompare(b.host))}
|
||||
>
|
||||
<h4 style="width: 100%; text-align: center;">
|
||||
<a
|
||||
style="display: block;padding-top: 15px;"
|
||||
href="/monitoring/node/{cluster}/{item.host}"
|
||||
>{item.host} ({item.subCluster})</a
|
||||
>
|
||||
</h4>
|
||||
{#if item.disabled === false && item.data}
|
||||
<MetricPlot
|
||||
{width}
|
||||
height={plotHeight}
|
||||
timestep={item.data.metric.timestep}
|
||||
series={item.data.metric.series}
|
||||
metric={item.data.name}
|
||||
cluster={clusters.find((c) => c.name == cluster)}
|
||||
subCluster={item.subCluster}
|
||||
resources={[{ hostname: item.host }]}
|
||||
forNode={true}
|
||||
/>
|
||||
{:else if item.disabled === true && item.data}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
>{selectedMetric}:{item.subCluster}</code
|
||||
></Card
|
||||
>
|
||||
{:else}
|
||||
<Card
|
||||
style="margin-left: 2rem;margin-right: 2rem;"
|
||||
body
|
||||
color="warning"
|
||||
>No dataset returned for <code>{selectedMetric}</code></Card
|
||||
>
|
||||
{/if}
|
||||
</PlotTable>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
@ -1,190 +1,234 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
||||
import { Icon, Button, ListGroupItem, Spinner, Modal, Input,
|
||||
ModalBody, ModalHeader, ModalFooter, Alert } from 'sveltestrap'
|
||||
import { fuzzySearchTags } from './utils.js'
|
||||
import Tag from './Tag.svelte'
|
||||
import { getContext } from "svelte";
|
||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||
import {
|
||||
Icon,
|
||||
Button,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
Modal,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
Alert,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fuzzySearchTags } from "./utils.js";
|
||||
import Tag from "./Tag.svelte";
|
||||
|
||||
export let job
|
||||
export let jobTags = job.tags
|
||||
export let job;
|
||||
export let jobTags = job.tags;
|
||||
|
||||
let allTags = getContext('tags'), initialized = getContext('initialized')
|
||||
let newTagType = '', newTagName = ''
|
||||
let filterTerm = ''
|
||||
let pendingChange = false
|
||||
let isOpen = false
|
||||
let allTags = getContext("tags"),
|
||||
initialized = getContext("initialized");
|
||||
let newTagType = "",
|
||||
newTagName = "";
|
||||
let filterTerm = "";
|
||||
let pendingChange = false;
|
||||
let isOpen = false;
|
||||
|
||||
const client = getContextClient();
|
||||
const client = getContextClient();
|
||||
|
||||
const createTagMutation = ({ type, name }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`mutation($type: String!, $name: String!) {
|
||||
createTag(type: $type, name: $name) { id, type, name }
|
||||
}`,
|
||||
variables: { type, name}
|
||||
})
|
||||
}
|
||||
|
||||
const addTagsToJobMutation = ({ job, tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
|
||||
addTagsToJob(job: $job, tagIds: $tagIds) { id, type, name }
|
||||
}`,
|
||||
variables: {job, tagIds}
|
||||
})
|
||||
}
|
||||
|
||||
const removeTagsFromJobMutation = ({ job, tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
|
||||
removeTagsFromJob(job: $job, tagIds: $tagIds) { id, type, name }
|
||||
}`,
|
||||
variables: {job, tagIds}
|
||||
})
|
||||
}
|
||||
|
||||
let allTagsFiltered // $initialized is in there because when it becomes true, allTags is initailzed.
|
||||
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags))
|
||||
|
||||
$: {
|
||||
newTagType = '';
|
||||
newTagName = '';
|
||||
let parts = filterTerm.split(':').map(s => s.trim())
|
||||
if (parts.length == 2 && parts.every(s => s.length > 0)) {
|
||||
newTagType = parts[0]
|
||||
newTagName = parts[1]
|
||||
const createTagMutation = ({ type, name }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($type: String!, $name: String!) {
|
||||
createTag(type: $type, name: $name) {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { type, name },
|
||||
});
|
||||
};
|
||||
|
||||
function isNewTag(type, name) {
|
||||
for (let tag of allTagsFiltered)
|
||||
if (tag.type == type && tag.name == name)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
const addTagsToJobMutation = ({ job, tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($job: ID!, $tagIds: [ID!]!) {
|
||||
addTagsToJob(job: $job, tagIds: $tagIds) {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { job, tagIds },
|
||||
});
|
||||
};
|
||||
|
||||
function createTag(type, name) {
|
||||
pendingChange = true
|
||||
createTagMutation({ type: type, name: name })
|
||||
.subscribe(res => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
pendingChange = false
|
||||
allTags = [...allTags, res.data.createTag]
|
||||
newTagType = ''
|
||||
newTagName = ''
|
||||
addTagToJob(res.data.createTag)
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
const removeTagsFromJobMutation = ({ job, tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($job: ID!, $tagIds: [ID!]!) {
|
||||
removeTagsFromJob(job: $job, tagIds: $tagIds) {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { job, tagIds },
|
||||
});
|
||||
};
|
||||
|
||||
function addTagToJob(tag) {
|
||||
pendingChange = tag.id
|
||||
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] })
|
||||
.subscribe(res => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobTags = job.tags = res.data.addTagsToJob;
|
||||
pendingChange = false;
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
let allTagsFiltered; // $initialized is in there because when it becomes true, allTags is initailzed.
|
||||
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
|
||||
|
||||
function removeTagFromJob(tag) {
|
||||
pendingChange = tag.id
|
||||
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] })
|
||||
.subscribe(res => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobTags = job.tags = res.data.removeTagsFromJob
|
||||
pendingChange = false
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
$: {
|
||||
newTagType = "";
|
||||
newTagName = "";
|
||||
let parts = filterTerm.split(":").map((s) => s.trim());
|
||||
if (parts.length == 2 && parts.every((s) => s.length > 0)) {
|
||||
newTagType = parts[0];
|
||||
newTagName = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
function isNewTag(type, name) {
|
||||
for (let tag of allTagsFiltered)
|
||||
if (tag.type == type && tag.name == name) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function createTag(type, name) {
|
||||
pendingChange = true;
|
||||
createTagMutation({ type: type, name: name }).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
pendingChange = false;
|
||||
allTags = [...allTags, res.data.createTag];
|
||||
newTagType = "";
|
||||
newTagName = "";
|
||||
addTagToJob(res.data.createTag);
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addTagToJob(tag) {
|
||||
pendingChange = tag.id;
|
||||
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobTags = job.tags = res.data.addTagsToJob;
|
||||
pendingChange = false;
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagFromJob(tag) {
|
||||
pendingChange = tag.id;
|
||||
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe(
|
||||
(res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobTags = job.tags = res.data.removeTagsFromJob;
|
||||
pendingChange = false;
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
ul.list-group {
|
||||
max-height: 450px;
|
||||
margin-bottom: 10px;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Manage Tags
|
||||
{#if pendingChange !== false}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else}
|
||||
<Icon name="tags" />
|
||||
{/if}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input style="width: 100%;"
|
||||
type="text" placeholder="Search Tags"
|
||||
bind:value={filterTerm} />
|
||||
<ModalHeader>
|
||||
Manage Tags
|
||||
{#if pendingChange !== false}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else}
|
||||
<Icon name="tags" />
|
||||
{/if}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input
|
||||
style="width: 100%;"
|
||||
type="text"
|
||||
placeholder="Search Tags"
|
||||
bind:value={filterTerm}
|
||||
/>
|
||||
|
||||
<br/>
|
||||
<br />
|
||||
|
||||
<Alert color="info">
|
||||
Search using "<code>type: name</code>". If no tag matches your search,
|
||||
a button for creating a new one will appear.
|
||||
</Alert>
|
||||
<Alert color="info">
|
||||
Search using "<code>type: name</code>". If no tag matches your search, a
|
||||
button for creating a new one will appear.
|
||||
</Alert>
|
||||
|
||||
<ul class="list-group">
|
||||
{#each allTagsFiltered as tag}
|
||||
<ListGroupItem>
|
||||
<Tag tag={tag}/>
|
||||
<ul class="list-group">
|
||||
{#each allTagsFiltered as tag}
|
||||
<ListGroupItem>
|
||||
<Tag {tag} />
|
||||
|
||||
<span style="float: right;">
|
||||
{#if pendingChange === tag.id}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else if job.tags.find(t => t.id == tag.id)}
|
||||
<Button size="sm" outline color="danger"
|
||||
on:click={() => removeTagFromJob(tag)}>
|
||||
<Icon name="x" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button size="sm" outline color="success"
|
||||
on:click={() => addTagToJob(tag)}>
|
||||
<Icon name="plus" />
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
</ListGroupItem>
|
||||
<span style="float: right;">
|
||||
{#if pendingChange === tag.id}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else if job.tags.find((t) => t.id == tag.id)}
|
||||
<Button
|
||||
size="sm"
|
||||
outline
|
||||
color="danger"
|
||||
on:click={() => removeTagFromJob(tag)}
|
||||
>
|
||||
<Icon name="x" />
|
||||
</Button>
|
||||
{:else}
|
||||
<ListGroupItem disabled>
|
||||
<i>No tags matching</i>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ul>
|
||||
<br/>
|
||||
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
|
||||
<Button outline color="success"
|
||||
on:click={e => (e.preventDefault(), createTag(newTagType, newTagName))}>
|
||||
Create & Add Tag:
|
||||
<Tag tag={({ type: newTagType, name: newTagName })} clickable={false}/>
|
||||
</Button>
|
||||
{:else if allTagsFiltered.length == 0}
|
||||
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<Button
|
||||
size="sm"
|
||||
outline
|
||||
color="success"
|
||||
on:click={() => addTagToJob(tag)}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
</ListGroupItem>
|
||||
{:else}
|
||||
<ListGroupItem disabled>
|
||||
<i>No tags matching</i>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ul>
|
||||
<br />
|
||||
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
on:click={(e) => (
|
||||
e.preventDefault(), createTag(newTagType, newTagName)
|
||||
)}
|
||||
>
|
||||
Create & Add Tag:
|
||||
<Tag tag={{ type: newTagType, name: newTagName }} clickable={false} />
|
||||
</Button>
|
||||
{:else if allTagsFiltered.length == 0}
|
||||
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
<Button outline on:click={() => (isOpen = true)}>
|
||||
Manage Tags <Icon name="tags" />
|
||||
Manage Tags <Icon name="tags" />
|
||||
</Button>
|
||||
|
||||
<style>
|
||||
ul.list-group {
|
||||
max-height: 450px;
|
||||
margin-bottom: 10px;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,237 +1,276 @@
|
||||
<script>
|
||||
import { onMount, getContext } from 'svelte'
|
||||
import { init, convert2uplot } from './utils.js'
|
||||
import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
|
||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
||||
import Filters from './filters/Filters.svelte'
|
||||
import JobList from './joblist/JobList.svelte'
|
||||
import Sorting from './joblist/SortSelection.svelte'
|
||||
import Refresher from './joblist/Refresher.svelte'
|
||||
import Histogram from './plots/Histogram.svelte'
|
||||
import MetricSelection from './MetricSelection.svelte'
|
||||
import HistogramSelection from './HistogramSelection.svelte'
|
||||
import PlotTable from './PlotTable.svelte'
|
||||
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
|
||||
import { onMount, getContext } from "svelte";
|
||||
import { init, convert2uplot } from "./utils.js";
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Icon,
|
||||
Card,
|
||||
Spinner,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import Filters from "./filters/Filters.svelte";
|
||||
import JobList from "./joblist/JobList.svelte";
|
||||
import Sorting from "./joblist/SortSelection.svelte";
|
||||
import Refresher from "./joblist/Refresher.svelte";
|
||||
import Histogram from "./plots/Histogram.svelte";
|
||||
import MetricSelection from "./MetricSelection.svelte";
|
||||
import HistogramSelection from "./HistogramSelection.svelte";
|
||||
import PlotTable from "./PlotTable.svelte";
|
||||
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
||||
|
||||
const { query: initq } = init()
|
||||
const { query: initq } = init();
|
||||
|
||||
const ccconfig = getContext('cc-config')
|
||||
const ccconfig = getContext("cc-config");
|
||||
|
||||
export let user
|
||||
export let filterPresets
|
||||
export let user;
|
||||
export let filterPresets;
|
||||
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobList;
|
||||
let jobFilters = [];
|
||||
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false
|
||||
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
|
||||
let w1, w2, histogramHeight = 250, isHistogramSelectionOpen = false
|
||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
|
||||
let showFootprint = filterPresets.cluster
|
||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||
: !!ccconfig.plot_list_showFootprint
|
||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||
let jobList;
|
||||
let jobFilters = [];
|
||||
let sorting = { field: "startTime", order: "DESC" },
|
||||
isSortingOpen = false;
|
||||
let metrics = ccconfig.plot_list_selectedMetrics,
|
||||
isMetricsSelectionOpen = false;
|
||||
let w1,
|
||||
w2,
|
||||
histogramHeight = 250,
|
||||
isHistogramSelectionOpen = false;
|
||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
||||
let showFootprint = filterPresets.cluster
|
||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||
: !!ccconfig.plot_list_showFootprint;
|
||||
|
||||
$: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
|
||||
$: metricsInHistograms = selectedCluster
|
||||
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []
|
||||
: ccconfig.user_view_histogramMetrics || [];
|
||||
|
||||
const client = getContextClient();
|
||||
$: stats = queryStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
query($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
|
||||
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
|
||||
totalJobs
|
||||
shortJobs
|
||||
totalWalltime
|
||||
totalCoreHours
|
||||
histDuration { count, value }
|
||||
histNumNodes { count, value }
|
||||
histMetrics { metric, unit, data { min, max, count, bin } }
|
||||
}}`,
|
||||
variables: { jobFilters, metricsInHistograms }
|
||||
})
|
||||
const client = getContextClient();
|
||||
$: stats = queryStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
|
||||
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
|
||||
totalJobs
|
||||
shortJobs
|
||||
totalWalltime
|
||||
totalCoreHours
|
||||
histDuration {
|
||||
count
|
||||
value
|
||||
}
|
||||
histNumNodes {
|
||||
count
|
||||
value
|
||||
}
|
||||
histMetrics {
|
||||
metric
|
||||
unit
|
||||
data {
|
||||
min
|
||||
max
|
||||
count
|
||||
bin
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { jobFilters, metricsInHistograms },
|
||||
});
|
||||
|
||||
onMount(() => filterComponent.update())
|
||||
onMount(() => filterComponent.update());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
{#if $initq.fetching}
|
||||
<Col>
|
||||
<Spinner/>
|
||||
</Col>
|
||||
{:else if $initq.error}
|
||||
<Col xs="auto">
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
|
||||
{#if $initq.fetching}
|
||||
<Col>
|
||||
<Spinner />
|
||||
</Col>
|
||||
{:else if $initq.error}
|
||||
<Col xs="auto">
|
||||
<Button
|
||||
outline color="primary"
|
||||
on:click={() => (isSortingOpen = true)}>
|
||||
<Icon name="sort-up"/> Sorting
|
||||
</Button>
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
outline color="primary"
|
||||
on:click={() => (isMetricsSelectionOpen = true)}>
|
||||
<Icon name="graph-up"/> Metrics
|
||||
</Button>
|
||||
<Col xs="auto">
|
||||
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
|
||||
<Icon name="sort-up" /> Sorting
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
outline color="secondary"
|
||||
on:click={() => (isHistogramSelectionOpen = true)}>
|
||||
<Icon name="bar-chart-line"/> Select Histograms
|
||||
</Button>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Filters
|
||||
filterPresets={filterPresets}
|
||||
startTimeQuickSelect={true}
|
||||
bind:this={filterComponent}
|
||||
on:update={({ detail }) => {
|
||||
jobFilters = [...detail.filters, { user: { eq: user.username } }]
|
||||
selectedCluster = jobFilters[0]?.cluster ? jobFilters[0].cluster.eq : null
|
||||
jobList.update(jobFilters)
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs="auto" style="margin-left: auto;">
|
||||
<Refresher on:reload={() => jobList.refresh()} />
|
||||
</Col>
|
||||
<Button
|
||||
outline
|
||||
color="primary"
|
||||
on:click={() => (isMetricsSelectionOpen = true)}
|
||||
>
|
||||
<Icon name="graph-up" /> Metrics
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
outline
|
||||
color="secondary"
|
||||
on:click={() => (isHistogramSelectionOpen = true)}
|
||||
>
|
||||
<Icon name="bar-chart-line" /> Select Histograms
|
||||
</Button>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Filters
|
||||
{filterPresets}
|
||||
startTimeQuickSelect={true}
|
||||
bind:this={filterComponent}
|
||||
on:update={({ detail }) => {
|
||||
jobFilters = [...detail.filters, { user: { eq: user.username } }];
|
||||
selectedCluster = jobFilters[0]?.cluster
|
||||
? jobFilters[0].cluster.eq
|
||||
: null;
|
||||
jobList.update(jobFilters);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="auto" style="margin-left: auto;">
|
||||
<Refresher on:reload={() => jobList.refresh()} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br/>
|
||||
<br />
|
||||
<Row>
|
||||
{#if $stats.error}
|
||||
<Col>
|
||||
<Card body color="danger">{$stats.error.message}</Card>
|
||||
</Col>
|
||||
{:else if !$stats.data}
|
||||
<Col>
|
||||
<Spinner secondary />
|
||||
</Col>
|
||||
{:else}
|
||||
<Col xs="4">
|
||||
<Table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Username</th>
|
||||
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
|
||||
</tr>
|
||||
{#if user.name}
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if user.email}
|
||||
<tr>
|
||||
<th scope="row">Email</th>
|
||||
<td>{user.email}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<th scope="row">Total Jobs</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Short Jobs</th>
|
||||
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total Walltime</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total Core Hours</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
<div class="col-4 text-center" bind:clientWidth={w1}>
|
||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||
<Histogram
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||
width={w1 - 25} height={histogramHeight}
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-4 text-center" bind:clientWidth={w2}>
|
||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||
<Histogram
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||
width={w2 - 25} height={histogramHeight}
|
||||
title="Number of Nodes Distribution"
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $stats.error}
|
||||
<Col>
|
||||
<Card body color="danger">{$stats.error.message}</Card>
|
||||
</Col>
|
||||
{:else if !$stats.data}
|
||||
<Col>
|
||||
<Spinner secondary />
|
||||
</Col>
|
||||
{:else}
|
||||
<Col xs="4">
|
||||
<Table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Username</th>
|
||||
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
|
||||
</tr>
|
||||
{#if user.name}
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if user.email}
|
||||
<tr>
|
||||
<th scope="row">Email</th>
|
||||
<td>{user.email}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<th scope="row">Total Jobs</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Short Jobs</th>
|
||||
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total Walltime</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total Core Hours</th>
|
||||
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
<div class="col-4 text-center" bind:clientWidth={w1}>
|
||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||
<Histogram
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||
width={w1 - 25}
|
||||
height={histogramHeight}
|
||||
title="Duration Distribution"
|
||||
xlabel="Current Runtimes"
|
||||
xunit="Hours"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-4 text-center" bind:clientWidth={w2}>
|
||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||
<Histogram
|
||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||
width={w2 - 25}
|
||||
height={histogramHeight}
|
||||
title="Number of Nodes Distribution"
|
||||
xlabel="Allocated Nodes"
|
||||
xunit="Nodes"
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</Row>
|
||||
{#if metricsInHistograms}
|
||||
<Row>
|
||||
{#if $stats.error}
|
||||
<Col>
|
||||
<Card body color="danger">{$stats.error.message}</Card>
|
||||
</Col>
|
||||
{:else if !$stats.data}
|
||||
<Col>
|
||||
<Spinner secondary />
|
||||
</Col>
|
||||
{:else}
|
||||
<Col>
|
||||
{#key $stats.data.jobsStatistics[0].histMetrics}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="user"
|
||||
items={$stats.data.jobsStatistics[0].histMetrics}
|
||||
itemsPerRow={3}>
|
||||
|
||||
<Histogram
|
||||
data={convert2uplot(item.data)}
|
||||
usesBins={true}
|
||||
width={width} height={250}
|
||||
title="Distribution of '{item.metric}' averages"
|
||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||
xunit={item.unit}
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"/>
|
||||
</PlotTable>
|
||||
{/key}
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
<Row>
|
||||
{#if $stats.error}
|
||||
<Col>
|
||||
<Card body color="danger">{$stats.error.message}</Card>
|
||||
</Col>
|
||||
{:else if !$stats.data}
|
||||
<Col>
|
||||
<Spinner secondary />
|
||||
</Col>
|
||||
{:else}
|
||||
<Col>
|
||||
{#key $stats.data.jobsStatistics[0].histMetrics}
|
||||
<PlotTable
|
||||
let:item
|
||||
let:width
|
||||
renderFor="user"
|
||||
items={$stats.data.jobsStatistics[0].histMetrics}
|
||||
itemsPerRow={3}
|
||||
>
|
||||
<Histogram
|
||||
data={convert2uplot(item.data)}
|
||||
usesBins={true}
|
||||
{width}
|
||||
height={250}
|
||||
title="Distribution of '{item.metric}' averages"
|
||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||
xunit={item.unit}
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
/>
|
||||
</PlotTable>
|
||||
{/key}
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{/if}
|
||||
<br/>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<JobList
|
||||
bind:metrics={metrics}
|
||||
bind:sorting={sorting}
|
||||
bind:this={jobList}
|
||||
bind:showFootprint={showFootprint} />
|
||||
</Col>
|
||||
<Col>
|
||||
<JobList bind:metrics bind:sorting bind:this={jobList} bind:showFootprint />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Sorting
|
||||
bind:sorting={sorting}
|
||||
bind:isOpen={isSortingOpen} />
|
||||
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
|
||||
|
||||
<MetricSelection
|
||||
bind:cluster={selectedCluster}
|
||||
configName="plot_list_selectedMetrics"
|
||||
bind:metrics
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint
|
||||
view="list"
|
||||
/>
|
||||
|
||||
<MetricSelection
|
||||
bind:cluster={selectedCluster}
|
||||
configName="plot_list_selectedMetrics"
|
||||
bind:metrics={metrics}
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint={showFootprint}
|
||||
view='list'/>
|
||||
|
||||
<HistogramSelection
|
||||
bind:cluster={selectedCluster}
|
||||
bind:metricsInHistograms={metricsInHistograms}
|
||||
bind:isOpen={isHistogramSelectionOpen} />
|
||||
bind:cluster={selectedCluster}
|
||||
bind:metricsInHistograms
|
||||
bind:isOpen={isHistogramSelectionOpen}
|
||||
/>
|
||||
|
@ -1,60 +1,65 @@
|
||||
<script>
|
||||
import { Icon, InputGroup, InputGroupText } from 'sveltestrap';
|
||||
import { Icon, InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let timeseriesPlots;
|
||||
export let timeseriesPlots;
|
||||
|
||||
let windowSize = 100; // Goes from 0 to 100
|
||||
let windowPosition = 50; // Goes from 0 to 100
|
||||
let windowSize = 100; // Goes from 0 to 100
|
||||
let windowPosition = 50; // Goes from 0 to 100
|
||||
|
||||
function updatePlots() {
|
||||
let ws = windowSize / (100 * 2),
|
||||
wp = windowPosition / 100;
|
||||
let from = (wp - ws),
|
||||
to = (wp + ws);
|
||||
Object
|
||||
.values(timeseriesPlots)
|
||||
.forEach(plot => plot.setTimeRange(from, to));
|
||||
}
|
||||
function updatePlots() {
|
||||
let ws = windowSize / (100 * 2),
|
||||
wp = windowPosition / 100;
|
||||
let from = wp - ws,
|
||||
to = wp + ws;
|
||||
Object.values(timeseriesPlots).forEach((plot) =>
|
||||
plot.setTimeRange(from, to),
|
||||
);
|
||||
}
|
||||
|
||||
// Rendering a big job can take a long time, so we
|
||||
// throttle the rerenders to every 100ms here.
|
||||
let timeoutId = null;
|
||||
function requestUpdatePlots() {
|
||||
if (timeoutId != null)
|
||||
window.cancelAnimationFrame(timeoutId);
|
||||
// Rendering a big job can take a long time, so we
|
||||
// throttle the rerenders to every 100ms here.
|
||||
let timeoutId = null;
|
||||
function requestUpdatePlots() {
|
||||
if (timeoutId != null) window.cancelAnimationFrame(timeoutId);
|
||||
|
||||
timeoutId = window.requestAnimationFrame(() => {
|
||||
updatePlots();
|
||||
timeoutId = null;
|
||||
}, 100);
|
||||
}
|
||||
timeoutId = window.requestAnimationFrame(() => {
|
||||
updatePlots();
|
||||
timeoutId = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
$: requestUpdatePlots(windowSize, windowPosition);
|
||||
$: requestUpdatePlots(windowSize, windowPosition);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="zoom-in"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Window Size:
|
||||
<input
|
||||
style="margin: 0em 0em 0em 1em"
|
||||
type="range"
|
||||
bind:value={windowSize}
|
||||
min=1 max=100 step=1 />
|
||||
<span style="width: 5em;">
|
||||
({windowSize}%)
|
||||
</span>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Window Position:
|
||||
<input
|
||||
style="margin: 0em 0em 0em 1em"
|
||||
type="range"
|
||||
bind:value={windowPosition}
|
||||
min=0 max=100 step=1 />
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="zoom-in" />
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Window Size:
|
||||
<input
|
||||
style="margin: 0em 0em 0em 1em"
|
||||
type="range"
|
||||
bind:value={windowSize}
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
/>
|
||||
<span style="width: 5em;">
|
||||
({windowSize}%)
|
||||
</span>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Window Position:
|
||||
<input
|
||||
style="margin: 0em 0em 0em 1em"
|
||||
type="range"
|
||||
bind:value={windowPosition}
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
@ -1,54 +1,53 @@
|
||||
<script>
|
||||
import { Row, Col } from 'sveltestrap'
|
||||
import { onMount } from 'svelte'
|
||||
import EditRole from './admin/EditRole.svelte'
|
||||
import EditProject from './admin/EditProject.svelte'
|
||||
import AddUser from './admin/AddUser.svelte'
|
||||
import ShowUsers from './admin/ShowUsers.svelte'
|
||||
import Options from './admin/Options.svelte'
|
||||
import { Row, Col } from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from "svelte";
|
||||
import EditRole from "./admin/EditRole.svelte";
|
||||
import EditProject from "./admin/EditProject.svelte";
|
||||
import AddUser from "./admin/AddUser.svelte";
|
||||
import ShowUsers from "./admin/ShowUsers.svelte";
|
||||
import Options from "./admin/Options.svelte";
|
||||
|
||||
let users = []
|
||||
let roles = []
|
||||
let users = [];
|
||||
let roles = [];
|
||||
|
||||
function getUserList() {
|
||||
fetch('/api/users/?via-ldap=false¬-just-user=true')
|
||||
.then(res => res.json())
|
||||
.then(usersRaw => {
|
||||
users = usersRaw
|
||||
})
|
||||
}
|
||||
function getUserList() {
|
||||
fetch("/api/users/?via-ldap=false¬-just-user=true")
|
||||
.then((res) => res.json())
|
||||
.then((usersRaw) => {
|
||||
users = usersRaw;
|
||||
});
|
||||
}
|
||||
|
||||
function getValidRoles() {
|
||||
fetch('/api/roles/')
|
||||
.then(res => res.json())
|
||||
.then(rolesRaw => {
|
||||
roles = rolesRaw
|
||||
})
|
||||
}
|
||||
function getValidRoles() {
|
||||
fetch("/api/roles/")
|
||||
.then((res) => res.json())
|
||||
.then((rolesRaw) => {
|
||||
roles = rolesRaw;
|
||||
});
|
||||
}
|
||||
|
||||
function initAdmin() {
|
||||
getUserList()
|
||||
getValidRoles()
|
||||
}
|
||||
|
||||
onMount(() => initAdmin())
|
||||
function initAdmin() {
|
||||
getUserList();
|
||||
getValidRoles();
|
||||
}
|
||||
|
||||
onMount(() => initAdmin());
|
||||
</script>
|
||||
|
||||
<Row cols={2} class="p-2 g-2" >
|
||||
<Col class="mb-1">
|
||||
<AddUser roles={roles} on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col class="mb-1">
|
||||
<ShowUsers on:reload={getUserList} bind:users={users}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<EditRole roles={roles} on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<EditProject on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Options/>
|
||||
</Col>
|
||||
<Row cols={2} class="p-2 g-2">
|
||||
<Col class="mb-1">
|
||||
<AddUser {roles} on:reload={getUserList} />
|
||||
</Col>
|
||||
<Col class="mb-1">
|
||||
<ShowUsers on:reload={getUserList} bind:users />
|
||||
</Col>
|
||||
<Col>
|
||||
<EditRole {roles} on:reload={getUserList} />
|
||||
</Col>
|
||||
<Col>
|
||||
<EditProject on:reload={getUserList} />
|
||||
</Col>
|
||||
<Col>
|
||||
<Options />
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -1,171 +1,498 @@
|
||||
<script>
|
||||
import { Button, Table, Row, Col, Card, CardBody, CardTitle } from 'sveltestrap'
|
||||
import { fade } from 'svelte/transition'
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
CardTitle,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
export let config
|
||||
export let config;
|
||||
|
||||
let message = {msg: '', target: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
let message = { msg: "", target: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
const colorschemes = {
|
||||
'Default': ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"],
|
||||
'Autumn': ['rgb(255,0,0)','rgb(255,11,0)','rgb(255,20,0)','rgb(255,30,0)','rgb(255,41,0)','rgb(255,50,0)','rgb(255,60,0)','rgb(255,71,0)','rgb(255,80,0)','rgb(255,90,0)','rgb(255,101,0)','rgb(255,111,0)','rgb(255,120,0)','rgb(255,131,0)','rgb(255,141,0)','rgb(255,150,0)','rgb(255,161,0)','rgb(255,171,0)','rgb(255,180,0)','rgb(255,190,0)','rgb(255,201,0)','rgb(255,210,0)','rgb(255,220,0)','rgb(255,231,0)','rgb(255,240,0)','rgb(255,250,0)'],
|
||||
'Beach': ['rgb(0,252,0)','rgb(0,233,0)','rgb(0,212,0)','rgb(0,189,0)','rgb(0,169,0)','rgb(0,148,0)','rgb(0,129,4)','rgb(0,145,46)','rgb(0,162,90)','rgb(0,180,132)','rgb(29,143,136)','rgb(73,88,136)','rgb(115,32,136)','rgb(81,9,64)','rgb(124,51,23)','rgb(162,90,0)','rgb(194,132,0)','rgb(220,171,0)','rgb(231,213,0)','rgb(0,0,13)','rgb(0,0,55)','rgb(0,0,92)','rgb(0,0,127)','rgb(0,0,159)','rgb(0,0,196)','rgb(0,0,233)'],
|
||||
'BlueRed': ['rgb(0,0,131)','rgb(0,0,168)','rgb(0,0,208)','rgb(0,0,247)','rgb(0,27,255)','rgb(0,67,255)','rgb(0,108,255)','rgb(0,148,255)','rgb(0,187,255)','rgb(0,227,255)','rgb(8,255,247)','rgb(48,255,208)','rgb(87,255,168)','rgb(127,255,127)','rgb(168,255,87)','rgb(208,255,48)','rgb(247,255,8)','rgb(255,224,0)','rgb(255,183,0)','rgb(255,143,0)','rgb(255,104,0)','rgb(255,64,0)','rgb(255,23,0)','rgb(238,0,0)','rgb(194,0,0)','rgb(150,0,0)'],
|
||||
'Rainbow': ['rgb(125,0,255)','rgb(85,0,255)','rgb(39,0,255)','rgb(0,6,255)','rgb(0,51,255)','rgb(0,97,255)','rgb(0,141,255)','rgb(0,187,255)','rgb(0,231,255)','rgb(0,255,233)','rgb(0,255,189)','rgb(0,255,143)','rgb(0,255,99)','rgb(0,255,53)','rgb(0,255,9)','rgb(37,255,0)','rgb(83,255,0)','rgb(127,255,0)','rgb(173,255,0)','rgb(217,255,0)','rgb(255,248,0)','rgb(255,203,0)','rgb(255,159,0)','rgb(255,113,0)','rgb(255,69,0)','rgb(255,23,0)'],
|
||||
'Binary': ['rgb(215,215,215)','rgb(206,206,206)','rgb(196,196,196)','rgb(185,185,185)','rgb(176,176,176)','rgb(166,166,166)','rgb(155,155,155)','rgb(145,145,145)','rgb(136,136,136)','rgb(125,125,125)','rgb(115,115,115)','rgb(106,106,106)','rgb(95,95,95)','rgb(85,85,85)','rgb(76,76,76)','rgb(66,66,66)','rgb(55,55,55)','rgb(46,46,46)','rgb(36,36,36)','rgb(25,25,25)','rgb(16,16,16)','rgb(6,6,6)'],
|
||||
'GistEarth': ['rgb(0,0,0)','rgb(2,7,117)','rgb(9,30,118)','rgb(16,53,120)','rgb(23,73,122)','rgb(31,93,124)','rgb(39,110,125)','rgb(47,126,127)','rgb(51,133,119)','rgb(57,138,106)','rgb(62,145,94)','rgb(66,150,82)','rgb(74,157,71)','rgb(97,162,77)','rgb(121,168,83)','rgb(136,173,85)','rgb(153,176,88)','rgb(170,180,92)','rgb(185,182,94)','rgb(189,173,99)','rgb(192,164,101)','rgb(203,169,124)','rgb(215,178,149)','rgb(226,192,176)','rgb(238,212,204)','rgb(248,236,236)'],
|
||||
'BlueWaves': ['rgb(83,0,215)','rgb(43,6,108)','rgb(9,16,16)','rgb(8,32,25)','rgb(0,50,8)','rgb(27,64,66)','rgb(69,67,178)','rgb(115,62,210)','rgb(155,50,104)','rgb(178,43,41)','rgb(180,51,34)','rgb(161,78,87)','rgb(124,117,187)','rgb(78,155,203)','rgb(34,178,85)','rgb(4,176,2)','rgb(9,152,27)','rgb(4,118,2)','rgb(34,92,85)','rgb(78,92,203)','rgb(124,127,187)','rgb(161,187,87)','rgb(180,248,34)','rgb(178,220,41)','rgb(155,217,104)','rgb(115,254,210)'],
|
||||
'BlueGreenRedYellow': ['rgb(0,0,0)','rgb(0,0,20)','rgb(0,0,41)','rgb(0,0,62)','rgb(0,25,83)','rgb(0,57,101)','rgb(0,87,101)','rgb(0,118,101)','rgb(0,150,101)','rgb(0,150,69)','rgb(0,148,37)','rgb(0,141,6)','rgb(60,120,0)','rgb(131,87,0)','rgb(180,25,0)','rgb(203,13,0)','rgb(208,36,0)','rgb(213,60,0)','rgb(219,83,0)','rgb(224,106,0)','rgb(229,129,0)','rgb(233,152,0)','rgb(238,176,0)','rgb(243,199,0)','rgb(248,222,0)','rgb(254,245,0)']
|
||||
};
|
||||
const colorschemes = {
|
||||
Default: [
|
||||
"#00bfff",
|
||||
"#0000ff",
|
||||
"#ff00ff",
|
||||
"#ff0000",
|
||||
"#ff8000",
|
||||
"#ffff00",
|
||||
"#80ff00",
|
||||
],
|
||||
Autumn: [
|
||||
"rgb(255,0,0)",
|
||||
"rgb(255,11,0)",
|
||||
"rgb(255,20,0)",
|
||||
"rgb(255,30,0)",
|
||||
"rgb(255,41,0)",
|
||||
"rgb(255,50,0)",
|
||||
"rgb(255,60,0)",
|
||||
"rgb(255,71,0)",
|
||||
"rgb(255,80,0)",
|
||||
"rgb(255,90,0)",
|
||||
"rgb(255,101,0)",
|
||||
"rgb(255,111,0)",
|
||||
"rgb(255,120,0)",
|
||||
"rgb(255,131,0)",
|
||||
"rgb(255,141,0)",
|
||||
"rgb(255,150,0)",
|
||||
"rgb(255,161,0)",
|
||||
"rgb(255,171,0)",
|
||||
"rgb(255,180,0)",
|
||||
"rgb(255,190,0)",
|
||||
"rgb(255,201,0)",
|
||||
"rgb(255,210,0)",
|
||||
"rgb(255,220,0)",
|
||||
"rgb(255,231,0)",
|
||||
"rgb(255,240,0)",
|
||||
"rgb(255,250,0)",
|
||||
],
|
||||
Beach: [
|
||||
"rgb(0,252,0)",
|
||||
"rgb(0,233,0)",
|
||||
"rgb(0,212,0)",
|
||||
"rgb(0,189,0)",
|
||||
"rgb(0,169,0)",
|
||||
"rgb(0,148,0)",
|
||||
"rgb(0,129,4)",
|
||||
"rgb(0,145,46)",
|
||||
"rgb(0,162,90)",
|
||||
"rgb(0,180,132)",
|
||||
"rgb(29,143,136)",
|
||||
"rgb(73,88,136)",
|
||||
"rgb(115,32,136)",
|
||||
"rgb(81,9,64)",
|
||||
"rgb(124,51,23)",
|
||||
"rgb(162,90,0)",
|
||||
"rgb(194,132,0)",
|
||||
"rgb(220,171,0)",
|
||||
"rgb(231,213,0)",
|
||||
"rgb(0,0,13)",
|
||||
"rgb(0,0,55)",
|
||||
"rgb(0,0,92)",
|
||||
"rgb(0,0,127)",
|
||||
"rgb(0,0,159)",
|
||||
"rgb(0,0,196)",
|
||||
"rgb(0,0,233)",
|
||||
],
|
||||
BlueRed: [
|
||||
"rgb(0,0,131)",
|
||||
"rgb(0,0,168)",
|
||||
"rgb(0,0,208)",
|
||||
"rgb(0,0,247)",
|
||||
"rgb(0,27,255)",
|
||||
"rgb(0,67,255)",
|
||||
"rgb(0,108,255)",
|
||||
"rgb(0,148,255)",
|
||||
"rgb(0,187,255)",
|
||||
"rgb(0,227,255)",
|
||||
"rgb(8,255,247)",
|
||||
"rgb(48,255,208)",
|
||||
"rgb(87,255,168)",
|
||||
"rgb(127,255,127)",
|
||||
"rgb(168,255,87)",
|
||||
"rgb(208,255,48)",
|
||||
"rgb(247,255,8)",
|
||||
"rgb(255,224,0)",
|
||||
"rgb(255,183,0)",
|
||||
"rgb(255,143,0)",
|
||||
"rgb(255,104,0)",
|
||||
"rgb(255,64,0)",
|
||||
"rgb(255,23,0)",
|
||||
"rgb(238,0,0)",
|
||||
"rgb(194,0,0)",
|
||||
"rgb(150,0,0)",
|
||||
],
|
||||
Rainbow: [
|
||||
"rgb(125,0,255)",
|
||||
"rgb(85,0,255)",
|
||||
"rgb(39,0,255)",
|
||||
"rgb(0,6,255)",
|
||||
"rgb(0,51,255)",
|
||||
"rgb(0,97,255)",
|
||||
"rgb(0,141,255)",
|
||||
"rgb(0,187,255)",
|
||||
"rgb(0,231,255)",
|
||||
"rgb(0,255,233)",
|
||||
"rgb(0,255,189)",
|
||||
"rgb(0,255,143)",
|
||||
"rgb(0,255,99)",
|
||||
"rgb(0,255,53)",
|
||||
"rgb(0,255,9)",
|
||||
"rgb(37,255,0)",
|
||||
"rgb(83,255,0)",
|
||||
"rgb(127,255,0)",
|
||||
"rgb(173,255,0)",
|
||||
"rgb(217,255,0)",
|
||||
"rgb(255,248,0)",
|
||||
"rgb(255,203,0)",
|
||||
"rgb(255,159,0)",
|
||||
"rgb(255,113,0)",
|
||||
"rgb(255,69,0)",
|
||||
"rgb(255,23,0)",
|
||||
],
|
||||
Binary: [
|
||||
"rgb(215,215,215)",
|
||||
"rgb(206,206,206)",
|
||||
"rgb(196,196,196)",
|
||||
"rgb(185,185,185)",
|
||||
"rgb(176,176,176)",
|
||||
"rgb(166,166,166)",
|
||||
"rgb(155,155,155)",
|
||||
"rgb(145,145,145)",
|
||||
"rgb(136,136,136)",
|
||||
"rgb(125,125,125)",
|
||||
"rgb(115,115,115)",
|
||||
"rgb(106,106,106)",
|
||||
"rgb(95,95,95)",
|
||||
"rgb(85,85,85)",
|
||||
"rgb(76,76,76)",
|
||||
"rgb(66,66,66)",
|
||||
"rgb(55,55,55)",
|
||||
"rgb(46,46,46)",
|
||||
"rgb(36,36,36)",
|
||||
"rgb(25,25,25)",
|
||||
"rgb(16,16,16)",
|
||||
"rgb(6,6,6)",
|
||||
],
|
||||
GistEarth: [
|
||||
"rgb(0,0,0)",
|
||||
"rgb(2,7,117)",
|
||||
"rgb(9,30,118)",
|
||||
"rgb(16,53,120)",
|
||||
"rgb(23,73,122)",
|
||||
"rgb(31,93,124)",
|
||||
"rgb(39,110,125)",
|
||||
"rgb(47,126,127)",
|
||||
"rgb(51,133,119)",
|
||||
"rgb(57,138,106)",
|
||||
"rgb(62,145,94)",
|
||||
"rgb(66,150,82)",
|
||||
"rgb(74,157,71)",
|
||||
"rgb(97,162,77)",
|
||||
"rgb(121,168,83)",
|
||||
"rgb(136,173,85)",
|
||||
"rgb(153,176,88)",
|
||||
"rgb(170,180,92)",
|
||||
"rgb(185,182,94)",
|
||||
"rgb(189,173,99)",
|
||||
"rgb(192,164,101)",
|
||||
"rgb(203,169,124)",
|
||||
"rgb(215,178,149)",
|
||||
"rgb(226,192,176)",
|
||||
"rgb(238,212,204)",
|
||||
"rgb(248,236,236)",
|
||||
],
|
||||
BlueWaves: [
|
||||
"rgb(83,0,215)",
|
||||
"rgb(43,6,108)",
|
||||
"rgb(9,16,16)",
|
||||
"rgb(8,32,25)",
|
||||
"rgb(0,50,8)",
|
||||
"rgb(27,64,66)",
|
||||
"rgb(69,67,178)",
|
||||
"rgb(115,62,210)",
|
||||
"rgb(155,50,104)",
|
||||
"rgb(178,43,41)",
|
||||
"rgb(180,51,34)",
|
||||
"rgb(161,78,87)",
|
||||
"rgb(124,117,187)",
|
||||
"rgb(78,155,203)",
|
||||
"rgb(34,178,85)",
|
||||
"rgb(4,176,2)",
|
||||
"rgb(9,152,27)",
|
||||
"rgb(4,118,2)",
|
||||
"rgb(34,92,85)",
|
||||
"rgb(78,92,203)",
|
||||
"rgb(124,127,187)",
|
||||
"rgb(161,187,87)",
|
||||
"rgb(180,248,34)",
|
||||
"rgb(178,220,41)",
|
||||
"rgb(155,217,104)",
|
||||
"rgb(115,254,210)",
|
||||
],
|
||||
BlueGreenRedYellow: [
|
||||
"rgb(0,0,0)",
|
||||
"rgb(0,0,20)",
|
||||
"rgb(0,0,41)",
|
||||
"rgb(0,0,62)",
|
||||
"rgb(0,25,83)",
|
||||
"rgb(0,57,101)",
|
||||
"rgb(0,87,101)",
|
||||
"rgb(0,118,101)",
|
||||
"rgb(0,150,101)",
|
||||
"rgb(0,150,69)",
|
||||
"rgb(0,148,37)",
|
||||
"rgb(0,141,6)",
|
||||
"rgb(60,120,0)",
|
||||
"rgb(131,87,0)",
|
||||
"rgb(180,25,0)",
|
||||
"rgb(203,13,0)",
|
||||
"rgb(208,36,0)",
|
||||
"rgb(213,60,0)",
|
||||
"rgb(219,83,0)",
|
||||
"rgb(224,106,0)",
|
||||
"rgb(229,129,0)",
|
||||
"rgb(233,152,0)",
|
||||
"rgb(238,176,0)",
|
||||
"rgb(243,199,0)",
|
||||
"rgb(248,222,0)",
|
||||
"rgb(254,245,0)",
|
||||
],
|
||||
};
|
||||
|
||||
async function handleSettingSubmit(selector, target) {
|
||||
let form = document.querySelector(selector)
|
||||
let formData = new FormData(form)
|
||||
try {
|
||||
const res = await fetch(form.action, { method: 'POST', body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, target, '#048109')
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, target, '#d63384')
|
||||
async function handleSettingSubmit(selector, target) {
|
||||
let form = document.querySelector(selector);
|
||||
let formData = new FormData(form);
|
||||
try {
|
||||
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, target, "#048109");
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (err) {
|
||||
popMessage(err, target, "#d63384");
|
||||
}
|
||||
|
||||
function popMessage(response, restarget, rescolor) {
|
||||
message = {msg: response, target: restarget, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function popMessage(response, restarget, rescolor) {
|
||||
message = { msg: response, target: restarget, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row cols={3} class="p-2 g-2">
|
||||
<!-- LINE WIDTH -->
|
||||
<Col><Card class="h-100">
|
||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
||||
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#line-width-form', 'lw')}>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Line Width</div>
|
||||
<!-- Expand If-Clause for clarity once -->
|
||||
{#if displayMessage && message.target == 'lw'}
|
||||
<div style="margin-left: auto; font-size: 0.9em;">
|
||||
<code style="color: {message.color};" out:fade>
|
||||
Update: {message.msg}
|
||||
</code>
|
||||
</div>
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_lineWidth"/>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Line Width</label>
|
||||
<input type="number" class="form-control" id="lwvalue" name="value" aria-describedby="lineWidthHelp" value="{config.plot_general_lineWidth}" min="1"/>
|
||||
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div>
|
||||
<!-- LINE WIDTH -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
||||
<form
|
||||
id="line-width-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#line-width-form", "lw")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Line Width</div>
|
||||
<!-- Expand If-Clause for clarity once -->
|
||||
{#if displayMessage && message.target == "lw"}
|
||||
<div style="margin-left: auto; font-size: 0.9em;">
|
||||
<code style="color: {message.color};" out:fade>
|
||||
Update: {message.msg}
|
||||
</code>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col>
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_lineWidth" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Line Width</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="lwvalue"
|
||||
name="value"
|
||||
aria-describedby="lineWidthHelp"
|
||||
value={config.plot_general_lineWidth}
|
||||
min="1"
|
||||
/>
|
||||
<div id="lineWidthHelp" class="form-text">
|
||||
Width of the lines in the timeseries plots.
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- PLOTS PER ROW -->
|
||||
<Col><Card class="h-100">
|
||||
<form id="plots-per-row-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#plots-per-row-form', 'ppr')}>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Plots per Row</div>
|
||||
{#if displayMessage && message.target == 'ppr'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_view_plotsPerRow"/>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Plots per Row</label>
|
||||
<input type="number" class="form-control" id="pprvalue" name="value" aria-describedby="plotsperrowHelp" value="{config.plot_view_plotsPerRow }" min="1"/>
|
||||
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col>
|
||||
<!-- PLOTS PER ROW -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="plots-per-row-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#plots-per-row-form", "ppr")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Plots per Row</div>
|
||||
{#if displayMessage && message.target == "ppr"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Plots per Row</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="pprvalue"
|
||||
name="value"
|
||||
aria-describedby="plotsperrowHelp"
|
||||
value={config.plot_view_plotsPerRow}
|
||||
min="1"
|
||||
/>
|
||||
<div id="plotsperrowHelp" class="form-text">
|
||||
How many plots to show next to each other on pages such as
|
||||
/monitoring/job/, /monitoring/system/...
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- BACKGROUND -->
|
||||
<Col><Card class="h-100">
|
||||
<form id="backgrounds-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#backgrounds-form', 'bg')}>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Colored Backgrounds</div>
|
||||
{#if displayMessage && message.target == 'bg'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorBackground"/>
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="true" name="value" value="true" checked/>
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Yes</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input type="radio" id="false" name="value" value="false" checked/>
|
||||
{/if}
|
||||
<label for="false">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col>
|
||||
<!-- BACKGROUND -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="backgrounds-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#backgrounds-form", "bg")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Colored Backgrounds</div>
|
||||
{#if displayMessage && message.target == "bg"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorBackground" />
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="true" name="value" value="true" checked />
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Yes</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
id="false"
|
||||
name="value"
|
||||
value="false"
|
||||
checked
|
||||
/>
|
||||
{/if}
|
||||
<label for="false">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
</Row>
|
||||
|
||||
<Row cols={1} class="p-2 g-2">
|
||||
<!-- COLORSCHEME -->
|
||||
<Col><Card>
|
||||
<form id="colorscheme-form" method="post" action="/api/configuration/" class="card-body">
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Color Scheme for Timeseries Plots</div>
|
||||
{#if displayMessage && message.target == 'cs'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorscheme"/>
|
||||
<Table hover>
|
||||
<tbody>
|
||||
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
||||
<tr>
|
||||
<th scope="col">{name}</th>
|
||||
<td>
|
||||
{#if rgbrow.join(',') == config.plot_general_colorscheme}
|
||||
<input type="radio" name="value" value={JSON.stringify(rgbrow)} checked on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
|
||||
{:else}
|
||||
<input type="radio" name="value" value={JSON.stringify(rgbrow)} on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#each rgbrow as rgb}
|
||||
<span class="color-dot" style="background-color: {rgb};"></span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</form>
|
||||
</Card></Col>
|
||||
<!-- COLORSCHEME -->
|
||||
<Col
|
||||
><Card>
|
||||
<form
|
||||
id="colorscheme-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Color Scheme for Timeseries Plots</div>
|
||||
{#if displayMessage && message.target == "cs"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorscheme" />
|
||||
<Table hover>
|
||||
<tbody>
|
||||
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
||||
<tr>
|
||||
<th scope="col">{name}</th>
|
||||
<td>
|
||||
{#if rgbrow.join(",") == config.plot_general_colorscheme}
|
||||
<input
|
||||
type="radio"
|
||||
name="value"
|
||||
value={JSON.stringify(rgbrow)}
|
||||
checked
|
||||
on:click={() =>
|
||||
handleSettingSubmit("#colorscheme-form", "cs")}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
name="value"
|
||||
value={JSON.stringify(rgbrow)}
|
||||
on:click={() =>
|
||||
handleSettingSubmit("#colorscheme-form", "cs")}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#each rgbrow as rgb}
|
||||
<span class="color-dot" style="background-color: {rgb};"
|
||||
></span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
</Row>
|
||||
|
||||
<style>
|
||||
.color-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.color-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,103 +1,156 @@
|
||||
<script>
|
||||
import { Button, Card, CardTitle } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
let message = { msg: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
export let roles = []
|
||||
export let roles = [];
|
||||
|
||||
async function handleUserSubmit() {
|
||||
let form = document.querySelector('#create-user-form')
|
||||
let formData = new FormData(form)
|
||||
async function handleUserSubmit() {
|
||||
let form = document.querySelector("#create-user-form");
|
||||
let formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const res = await fetch(form.action, { method: 'POST', body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
form.reset()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
try {
|
||||
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
reloadUserList();
|
||||
form.reset();
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = {msg: response, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
function popMessage(response, rescolor) {
|
||||
message = { msg: response, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
}
|
||||
function reloadUserList() {
|
||||
dispatch("reload");
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}>
|
||||
<CardTitle class="mb-3">Create User</CardTitle>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username (ID)</label>
|
||||
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
|
||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
|
||||
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Project</label>
|
||||
<input type="text" class="form-control" id="project" name="project" aria-describedby="projectHelp"/>
|
||||
<div id="projectHelp" class="form-text">Only Manager users can have a project. Allows to inspect jobs and users of given project.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
|
||||
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
|
||||
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<form
|
||||
id="create-user-form"
|
||||
method="post"
|
||||
action="/api/users/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={handleUserSubmit}
|
||||
>
|
||||
<CardTitle class="mb-3">Create User</CardTitle>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username (ID)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="username"
|
||||
name="username"
|
||||
aria-describedby="usernameHelp"
|
||||
/>
|
||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
aria-describedby="passwordHelp"
|
||||
/>
|
||||
<div id="passwordHelp" class="form-text">
|
||||
Only API users are allowed to have a blank password. Users with a blank
|
||||
password can only authenticate via Tokens.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Project</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="project"
|
||||
name="project"
|
||||
aria-describedby="projectHelp"
|
||||
/>
|
||||
<div id="projectHelp" class="form-text">
|
||||
Only Manager users can have a project. Allows to inspect jobs and users
|
||||
of given project.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="name"
|
||||
name="name"
|
||||
aria-describedby="nameHelp"
|
||||
/>
|
||||
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
aria-describedby="emailHelp"
|
||||
/>
|
||||
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<p>Role:</p>
|
||||
{#each roles as role, i}
|
||||
{#if i == 0}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role} checked/>
|
||||
<label for={role}>{role.toUpperCase()} (Allowed to interact with REST API.)</label>
|
||||
</div>
|
||||
{:else if i == 1}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role} checked/>
|
||||
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created via LDAP sync.)</label>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role}/>
|
||||
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<p style="display: flex; align-items: center;">
|
||||
<Button type="submit" color="primary">Submit</Button>
|
||||
{#if displayMessage}<div style="margin-left: 1.5em;"><b><code style="color: {message.color};" out:fade>{message.msg}</code></b></div>{/if}
|
||||
</p>
|
||||
</form>
|
||||
<div class="mb-3">
|
||||
<p>Role:</p>
|
||||
{#each roles as role, i}
|
||||
{#if i == 0}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role} checked />
|
||||
<label for={role}
|
||||
>{role.toUpperCase()} (Allowed to interact with REST API.)</label
|
||||
>
|
||||
</div>
|
||||
{:else if i == 1}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role} checked />
|
||||
<label for={role}
|
||||
>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created
|
||||
via LDAP sync.)</label
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role} />
|
||||
<label for={role}
|
||||
>{role.charAt(0).toUpperCase() + role.slice(1)}</label
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<p style="display: flex; align-items: center;">
|
||||
<Button type="submit" color="primary">Submit</Button>
|
||||
{#if displayMessage}<div style="margin-left: 1.5em;">
|
||||
<b
|
||||
><code style="color: {message.color};" out:fade>{message.msg}</code
|
||||
></b
|
||||
>
|
||||
</div>{/if}
|
||||
</p>
|
||||
</form>
|
||||
</Card>
|
||||
|
@ -1,97 +1,129 @@
|
||||
<script>
|
||||
import { Card, CardTitle, CardBody } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
let message = { msg: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
async function handleAddProject() {
|
||||
const username = document.querySelector('#project-username').value
|
||||
const project = document.querySelector('#project-id').value
|
||||
async function handleAddProject() {
|
||||
const username = document.querySelector("#project-username").value;
|
||||
const project = document.querySelector("#project-id").value;
|
||||
|
||||
if (username == "" || project == "") {
|
||||
alert('Please fill in a username and select a project.')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('add-project', project)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
if (username == "" || project == "") {
|
||||
alert("Please fill in a username and select a project.");
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleRemoveProject() {
|
||||
const username = document.querySelector('#project-username').value
|
||||
const project = document.querySelector('#project-id').value
|
||||
let formData = new FormData();
|
||||
formData.append("username", username);
|
||||
formData.append("add-project", project);
|
||||
|
||||
if (username == "" || project == "") {
|
||||
alert('Please fill in a username and select a project.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
reloadUserList();
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('remove-project', project)
|
||||
async function handleRemoveProject() {
|
||||
const username = document.querySelector("#project-username").value;
|
||||
const project = document.querySelector("#project-id").value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
if (username == "" || project == "") {
|
||||
alert("Please fill in a username and select a project.");
|
||||
return;
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = {msg: response, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
let formData = new FormData();
|
||||
formData.append("username", username);
|
||||
formData.append("remove-project", project);
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
reloadUserList();
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = { msg: response, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch("reload");
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Edit Project Managed By User (Manager Only)</CardTitle>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="username" id="project-username"/>
|
||||
<input type="text" class="form-control" placeholder="project-id" id="project-id"/>
|
||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||
<button class="btn btn-primary" type="button" id="add-project-button" on:click|preventDefault={handleAddProject}>Add</button>
|
||||
<button class="btn btn-danger" type="button" id="remove-project-button" on:click|preventDefault={handleRemoveProject}>Remove</button>
|
||||
</div>
|
||||
<p>
|
||||
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3"
|
||||
>Edit Project Managed By User (Manager Only)</CardTitle
|
||||
>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="username"
|
||||
id="project-username"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="project-id"
|
||||
id="project-id"
|
||||
/>
|
||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
id="add-project-button"
|
||||
on:click|preventDefault={handleAddProject}>Add</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
type="button"
|
||||
id="remove-project-button"
|
||||
on:click|preventDefault={handleRemoveProject}>Remove</button
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
{#if displayMessage}<b
|
||||
><code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
></b
|
||||
>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@ -1,104 +1,131 @@
|
||||
<script>
|
||||
import { Card, CardTitle, CardBody } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
let message = { msg: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
export let roles = []
|
||||
export let roles = [];
|
||||
|
||||
async function handleAddRole() {
|
||||
const username = document.querySelector('#role-username').value
|
||||
const role = document.querySelector('#role-select').value
|
||||
async function handleAddRole() {
|
||||
const username = document.querySelector("#role-username").value;
|
||||
const role = document.querySelector("#role-select").value;
|
||||
|
||||
if (username == "" || role == "") {
|
||||
alert('Please fill in a username and select a role.')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('add-role', role)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
if (username == "" || role == "") {
|
||||
alert("Please fill in a username and select a role.");
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleRemoveRole() {
|
||||
const username = document.querySelector('#role-username').value
|
||||
const role = document.querySelector('#role-select').value
|
||||
let formData = new FormData();
|
||||
formData.append("username", username);
|
||||
formData.append("add-role", role);
|
||||
|
||||
if (username == "" || role == "") {
|
||||
alert('Please fill in a username and select a role.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
reloadUserList();
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('remove-role', role)
|
||||
async function handleRemoveRole() {
|
||||
const username = document.querySelector("#role-username").value;
|
||||
const role = document.querySelector("#role-select").value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
if (username == "" || role == "") {
|
||||
alert("Please fill in a username and select a role.");
|
||||
return;
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = {msg: response, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
let formData = new FormData();
|
||||
formData.append("username", username);
|
||||
formData.append("remove-role", role);
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
reloadUserList();
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = { msg: response, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch("reload");
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Edit User Roles</CardTitle>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="username" id="role-username"/>
|
||||
<select class="form-select" id="role-select">
|
||||
<option selected value="">Role...</option>
|
||||
{#each roles as role}
|
||||
<option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||
<button class="btn btn-primary" type="button" id="add-role-button" on:click|preventDefault={handleAddRole}>Add</button>
|
||||
<button class="btn btn-danger" type="button" id="remove-role-button" on:click|preventDefault={handleRemoveRole}>Remove</button>
|
||||
</div>
|
||||
<p>
|
||||
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Edit User Roles</CardTitle>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="username"
|
||||
id="role-username"
|
||||
/>
|
||||
<select class="form-select" id="role-select">
|
||||
<option selected value="">Role...</option>
|
||||
{#each roles as role}
|
||||
<option value={role}
|
||||
>{role.charAt(0).toUpperCase() + role.slice(1)}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
id="add-role-button"
|
||||
on:click|preventDefault={handleAddRole}>Add</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
type="button"
|
||||
id="remove-role-button"
|
||||
on:click|preventDefault={handleRemoveRole}>Remove</button
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
{#if displayMessage}<b
|
||||
><code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
></b
|
||||
>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@ -1,29 +1,34 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { Card, CardBody, CardTitle } from 'sveltestrap'
|
||||
import { onMount } from "svelte";
|
||||
import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
|
||||
let scrambled
|
||||
let scrambled;
|
||||
|
||||
onMount(() => {
|
||||
scrambled = window.localStorage.getItem("cc-scramble-names") != null
|
||||
})
|
||||
onMount(() => {
|
||||
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
|
||||
});
|
||||
|
||||
function handleScramble() {
|
||||
if (!scrambled) {
|
||||
scrambled = true
|
||||
window.localStorage.setItem("cc-scramble-names", "true")
|
||||
} else {
|
||||
scrambled = false
|
||||
window.localStorage.removeItem("cc-scramble-names")
|
||||
}
|
||||
function handleScramble() {
|
||||
if (!scrambled) {
|
||||
scrambled = true;
|
||||
window.localStorage.setItem("cc-scramble-names", "true");
|
||||
} else {
|
||||
scrambled = false;
|
||||
window.localStorage.removeItem("cc-scramble-names");
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
||||
<input type="checkbox" id="scramble-names-checkbox" style="margin-right: 1em;" on:click={handleScramble} bind:checked={scrambled}/>
|
||||
Active?
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="scramble-names-checkbox"
|
||||
style="margin-right: 1em;"
|
||||
on:click={handleScramble}
|
||||
bind:checked={scrambled}
|
||||
/>
|
||||
Active?
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@ -1,68 +1,87 @@
|
||||
<script>
|
||||
import { Button, Table, Card, CardTitle, CardBody } from 'sveltestrap'
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import ShowUsersRow from './ShowUsersRow.svelte'
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ShowUsersRow from "./ShowUsersRow.svelte";
|
||||
|
||||
export let users = []
|
||||
export let users = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
}
|
||||
const dispatch = createEventDispatcher();
|
||||
function reloadUserList() {
|
||||
dispatch("reload");
|
||||
}
|
||||
|
||||
function deleteUser(username) {
|
||||
if (confirm('Are you sure?')) {
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
fetch('/api/users/', { method: 'DELETE', body: formData }).then(res => {
|
||||
if (res.status == 200) {
|
||||
reloadUserList()
|
||||
} else {
|
||||
confirm(res.statusText)
|
||||
}
|
||||
})
|
||||
function deleteUser(username) {
|
||||
if (confirm("Are you sure?")) {
|
||||
let formData = new FormData();
|
||||
formData.append("username", username);
|
||||
fetch("/api/users/", { method: "DELETE", body: formData }).then((res) => {
|
||||
if (res.status == 200) {
|
||||
reloadUserList();
|
||||
} else {
|
||||
confirm(res.statusText);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: userList = users
|
||||
|
||||
$: userList = users;
|
||||
</script>
|
||||
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Special Users</CardTitle>
|
||||
<p>
|
||||
Not created by an LDAP sync and/or having a role other than <code>user</code>
|
||||
<Button color="secondary" size="sm" on:click={reloadUserList} style="float: right;">Reload</Button>
|
||||
</p>
|
||||
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
||||
<Table hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Project(s)</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>JWT</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-list">
|
||||
{#each userList as user}
|
||||
<tr id="user-{user.username}">
|
||||
<ShowUsersRow {user}/>
|
||||
<td><button class="btn btn-danger del-user" on:click={deleteUser(user.username)}>Delete</button></td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Special Users</CardTitle>
|
||||
<p>
|
||||
Not created by an LDAP sync and/or having a role other than <code
|
||||
>user</code
|
||||
>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
on:click={reloadUserList}
|
||||
style="float: right;">Reload</Button
|
||||
>
|
||||
</p>
|
||||
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
||||
<Table hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Project(s)</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>JWT</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-list">
|
||||
{#each userList as user}
|
||||
<tr id="user-{user.username}">
|
||||
<ShowUsersRow {user} />
|
||||
<td
|
||||
><button
|
||||
class="btn btn-danger del-user"
|
||||
on:click={deleteUser(user.username)}>Delete</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@ -1,28 +1,32 @@
|
||||
<script>
|
||||
import { Button } from 'sveltestrap'
|
||||
import { Button } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let user
|
||||
let jwt = ""
|
||||
export let user;
|
||||
let jwt = "";
|
||||
|
||||
function getUserJwt(username) {
|
||||
fetch(`/api/jwt/?username=${username}`)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
jwt = text
|
||||
navigator.clipboard.writeText(text).catch(reason => console.error(reason))
|
||||
})
|
||||
}
|
||||
function getUserJwt(username) {
|
||||
fetch(`/api/jwt/?username=${username}`)
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
jwt = text;
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.catch((reason) => console.error(reason));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>{user.username}</td>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.projects}</td>
|
||||
<td>{user.email}</td>
|
||||
<td><code>{user.roles.join(', ')}</code></td>
|
||||
<td><code>{user.roles.join(", ")}</code></td>
|
||||
<td>
|
||||
{#if ! jwt}
|
||||
<Button color="success" on:click={getUserJwt(user.username)}>Gen. JWT</Button>
|
||||
{:else}
|
||||
<textarea rows="3" cols="20">{jwt}</textarea>
|
||||
{/if}
|
||||
{#if !jwt}
|
||||
<Button color="success" on:click={getUserJwt(user.username)}
|
||||
>Gen. JWT</Button
|
||||
>
|
||||
{:else}
|
||||
<textarea rows="3" cols="20">{jwt}</textarea>
|
||||
{/if}
|
||||
</td>
|
||||
|
@ -1,77 +1,95 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Button, ListGroup, ListGroupItem,
|
||||
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
const clusters = getContext('clusters'),
|
||||
initialized = getContext('initialized'),
|
||||
dispatch = createEventDispatcher()
|
||||
const clusters = getContext("clusters"),
|
||||
initialized = getContext("initialized"),
|
||||
dispatch = createEventDispatcher();
|
||||
|
||||
export let disableClusterSelection = false
|
||||
export let isModified = false
|
||||
export let isOpen = false
|
||||
export let cluster = null
|
||||
export let partition = null
|
||||
let pendingCluster = cluster, pendingPartition = partition
|
||||
$: isModified = pendingCluster != cluster || pendingPartition != partition
|
||||
export let disableClusterSelection = false;
|
||||
export let isModified = false;
|
||||
export let isOpen = false;
|
||||
export let cluster = null;
|
||||
export let partition = null;
|
||||
let pendingCluster = cluster,
|
||||
pendingPartition = partition;
|
||||
$: isModified = pendingCluster != cluster || pendingPartition != partition;
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select Cluster & Slurm Partition
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{#if $initialized}
|
||||
<h4>Cluster</h4>
|
||||
<ListGroup>
|
||||
<ListGroupItem
|
||||
disabled={disableClusterSelection}
|
||||
active={pendingCluster == null}
|
||||
on:click={() => (pendingCluster = null, pendingPartition = null)}>
|
||||
Any Cluster
|
||||
</ListGroupItem>
|
||||
{#each clusters as cluster}
|
||||
<ListGroupItem
|
||||
disabled={disableClusterSelection}
|
||||
active={pendingCluster == cluster.name}
|
||||
on:click={() => (pendingCluster = cluster.name, pendingPartition = null)}>
|
||||
{cluster.name}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{/if}
|
||||
{#if $initialized && pendingCluster != null}
|
||||
<br/>
|
||||
<h4>Partiton</h4>
|
||||
<ListGroup>
|
||||
<ListGroupItem
|
||||
active={pendingPartition == null}
|
||||
on:click={() => (pendingPartition = null)}>
|
||||
Any Partition
|
||||
</ListGroupItem>
|
||||
{#each clusters.find(c => c.name == pendingCluster).partitions as partition}
|
||||
<ListGroupItem
|
||||
active={pendingPartition == partition}
|
||||
on:click={() => (pendingPartition = partition)}>
|
||||
{partition}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => {
|
||||
isOpen = false
|
||||
cluster = pendingCluster
|
||||
partition = pendingPartition
|
||||
dispatch('update', { cluster, partition })
|
||||
}}>Close & Apply</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
cluster = pendingCluster = null
|
||||
partition = pendingPartition = null
|
||||
dispatch('update', { cluster, partition })
|
||||
}}>Reset</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select Cluster & Slurm Partition</ModalHeader>
|
||||
<ModalBody>
|
||||
{#if $initialized}
|
||||
<h4>Cluster</h4>
|
||||
<ListGroup>
|
||||
<ListGroupItem
|
||||
disabled={disableClusterSelection}
|
||||
active={pendingCluster == null}
|
||||
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
|
||||
>
|
||||
Any Cluster
|
||||
</ListGroupItem>
|
||||
{#each clusters as cluster}
|
||||
<ListGroupItem
|
||||
disabled={disableClusterSelection}
|
||||
active={pendingCluster == cluster.name}
|
||||
on:click={() => (
|
||||
(pendingCluster = cluster.name), (pendingPartition = null)
|
||||
)}
|
||||
>
|
||||
{cluster.name}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{/if}
|
||||
{#if $initialized && pendingCluster != null}
|
||||
<br />
|
||||
<h4>Partiton</h4>
|
||||
<ListGroup>
|
||||
<ListGroupItem
|
||||
active={pendingPartition == null}
|
||||
on:click={() => (pendingPartition = null)}
|
||||
>
|
||||
Any Partition
|
||||
</ListGroupItem>
|
||||
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition}
|
||||
<ListGroupItem
|
||||
active={pendingPartition == partition}
|
||||
on:click={() => (pendingPartition = partition)}
|
||||
>
|
||||
{partition}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
cluster = pendingCluster;
|
||||
partition = pendingPartition;
|
||||
dispatch("update", { cluster, partition });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
cluster = pendingCluster = null;
|
||||
partition = pendingPartition = null;
|
||||
dispatch("update", { cluster, partition });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,158 +1,244 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Row, Col, Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let isOpen = false
|
||||
export let lessThan = null
|
||||
export let moreThan = null
|
||||
export let from = null
|
||||
export let to = null
|
||||
export let isOpen = false;
|
||||
export let lessThan = null;
|
||||
export let moreThan = null;
|
||||
export let from = null;
|
||||
export let to = null;
|
||||
|
||||
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo
|
||||
let lessDisabled = false, moreDisabled = false, betweenDisabled = false
|
||||
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo;
|
||||
let lessDisabled = false,
|
||||
moreDisabled = false,
|
||||
betweenDisabled = false;
|
||||
|
||||
function reset() {
|
||||
pendingLessThan = lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan)
|
||||
pendingMoreThan = moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan)
|
||||
pendingFrom = from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from)
|
||||
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to)
|
||||
}
|
||||
function reset() {
|
||||
pendingLessThan =
|
||||
lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan);
|
||||
pendingMoreThan =
|
||||
moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan);
|
||||
pendingFrom =
|
||||
from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from);
|
||||
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to);
|
||||
}
|
||||
|
||||
reset()
|
||||
reset();
|
||||
|
||||
function secsToHoursAndMins(duration) {
|
||||
const hours = Math.floor(duration / 3600)
|
||||
duration -= hours * 3600
|
||||
const mins = Math.floor(duration / 60)
|
||||
return { hours, mins }
|
||||
}
|
||||
function secsToHoursAndMins(duration) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
duration -= hours * 3600;
|
||||
const mins = Math.floor(duration / 60);
|
||||
return { hours, mins };
|
||||
}
|
||||
|
||||
function hoursAndMinsToSecs({ hours, mins }) {
|
||||
return hours * 3600 + mins * 60
|
||||
}
|
||||
|
||||
$: lessDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
|
||||
$: moreDisabled = pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
|
||||
$: betweenDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0
|
||||
function hoursAndMinsToSecs({ hours, mins }) {
|
||||
return hours * 3600 + mins * 60;
|
||||
}
|
||||
|
||||
$: lessDisabled =
|
||||
pendingMoreThan.hours !== 0 ||
|
||||
pendingMoreThan.mins !== 0 ||
|
||||
pendingFrom.hours !== 0 ||
|
||||
pendingFrom.mins !== 0 ||
|
||||
pendingTo.hours !== 0 ||
|
||||
pendingTo.mins !== 0;
|
||||
$: moreDisabled =
|
||||
pendingLessThan.hours !== 0 ||
|
||||
pendingLessThan.mins !== 0 ||
|
||||
pendingFrom.hours !== 0 ||
|
||||
pendingFrom.mins !== 0 ||
|
||||
pendingTo.hours !== 0 ||
|
||||
pendingTo.mins !== 0;
|
||||
$: betweenDisabled =
|
||||
pendingMoreThan.hours !== 0 ||
|
||||
pendingMoreThan.mins !== 0 ||
|
||||
pendingLessThan.hours !== 0 ||
|
||||
pendingLessThan.mins !== 0;
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select Job Duration
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<h4>Duration more than</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" class="form-control" bind:value={pendingMoreThan.hours} disabled={moreDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingMoreThan.mins} disabled={moreDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr/>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select Job Duration</ModalHeader>
|
||||
<ModalBody>
|
||||
<h4>Duration more than</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
class="form-control"
|
||||
bind:value={pendingMoreThan.hours}
|
||||
disabled={moreDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
class="form-control"
|
||||
bind:value={pendingMoreThan.mins}
|
||||
disabled={moreDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
|
||||
<h4>Duration less than</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" class="form-control" bind:value={pendingLessThan.hours} disabled={lessDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingLessThan.mins} disabled={lessDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr/>
|
||||
<h4>Duration less than</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
class="form-control"
|
||||
bind:value={pendingLessThan.hours}
|
||||
disabled={lessDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
class="form-control"
|
||||
bind:value={pendingLessThan.mins}
|
||||
disabled={lessDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
|
||||
<h4>Duration between</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" class="form-control" bind:value={pendingFrom.hours} disabled={betweenDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingFrom.mins} disabled={betweenDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4>and</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" class="form-control" bind:value={pendingTo.hours} disabled={betweenDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingTo.mins} disabled={betweenDisabled}>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary"
|
||||
on:click={() => {
|
||||
isOpen = false
|
||||
lessThan = hoursAndMinsToSecs(pendingLessThan)
|
||||
moreThan = hoursAndMinsToSecs(pendingMoreThan)
|
||||
from = hoursAndMinsToSecs(pendingFrom)
|
||||
to = hoursAndMinsToSecs(pendingTo)
|
||||
dispatch('update', { lessThan, moreThan, from, to })
|
||||
}}>
|
||||
Close & Apply
|
||||
</Button>
|
||||
<Button color="warning" on:click={() => {
|
||||
lessThan = null
|
||||
moreThan = null
|
||||
from = null
|
||||
to = null
|
||||
reset()
|
||||
}}>Reset Values</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
lessThan = null
|
||||
moreThan = null
|
||||
from = null
|
||||
to = null
|
||||
reset()
|
||||
dispatch('update', { lessThan, moreThan, from, to })
|
||||
}}>Reset Filter</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<h4>Duration between</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
class="form-control"
|
||||
bind:value={pendingFrom.hours}
|
||||
disabled={betweenDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
class="form-control"
|
||||
bind:value={pendingFrom.mins}
|
||||
disabled={betweenDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4>and</h4>
|
||||
<Row>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
class="form-control"
|
||||
bind:value={pendingTo.hours}
|
||||
disabled={betweenDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div class="input-group mb-2 mr-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
class="form-control"
|
||||
bind:value={pendingTo.mins}
|
||||
disabled={betweenDisabled}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">m</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
lessThan = hoursAndMinsToSecs(pendingLessThan);
|
||||
moreThan = hoursAndMinsToSecs(pendingMoreThan);
|
||||
from = hoursAndMinsToSecs(pendingFrom);
|
||||
to = hoursAndMinsToSecs(pendingTo);
|
||||
dispatch("update", { lessThan, moreThan, from, to });
|
||||
}}
|
||||
>
|
||||
Close & Apply
|
||||
</Button>
|
||||
<Button
|
||||
color="warning"
|
||||
on:click={() => {
|
||||
lessThan = null;
|
||||
moreThan = null;
|
||||
from = null;
|
||||
to = null;
|
||||
reset();
|
||||
}}>Reset Values</Button
|
||||
>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
lessThan = null;
|
||||
moreThan = null;
|
||||
from = null;
|
||||
to = null;
|
||||
reset();
|
||||
dispatch("update", { lessThan, moreThan, from, to });
|
||||
}}>Reset Filter</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -10,373 +10,418 @@
|
||||
- void update(additionalFilters: Object?): Triggers an update
|
||||
-->
|
||||
<script>
|
||||
import { Row, Col, DropdownItem, DropdownMenu,
|
||||
DropdownToggle, ButtonDropdown, Icon } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Info from './InfoBox.svelte'
|
||||
import Cluster from './Cluster.svelte'
|
||||
import JobStates, { allJobStates } from './JobStates.svelte'
|
||||
import StartTime from './StartTime.svelte'
|
||||
import Tags from './Tags.svelte'
|
||||
import Tag from '../Tag.svelte'
|
||||
import Duration from './Duration.svelte'
|
||||
import Resources from './Resources.svelte'
|
||||
import Statistics from './Stats.svelte'
|
||||
// import TimeSelection from './TimeSelection.svelte'
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
ButtonDropdown,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Info from "./InfoBox.svelte";
|
||||
import Cluster from "./Cluster.svelte";
|
||||
import JobStates, { allJobStates } from "./JobStates.svelte";
|
||||
import StartTime from "./StartTime.svelte";
|
||||
import Tags from "./Tags.svelte";
|
||||
import Tag from "../Tag.svelte";
|
||||
import Duration from "./Duration.svelte";
|
||||
import Resources from "./Resources.svelte";
|
||||
import Statistics from "./Stats.svelte";
|
||||
// import TimeSelection from './TimeSelection.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let menuText = null
|
||||
export let filterPresets = {}
|
||||
export let disableClusterSelection = false
|
||||
export let startTimeQuickSelect = false
|
||||
export let menuText = null;
|
||||
export let filterPresets = {};
|
||||
export let disableClusterSelection = false;
|
||||
export let startTimeQuickSelect = false;
|
||||
|
||||
let filters = {
|
||||
projectMatch: filterPresets.projectMatch || 'contains',
|
||||
userMatch: filterPresets.userMatch || 'contains',
|
||||
jobIdMatch: filterPresets.jobIdMatch || 'eq',
|
||||
let filters = {
|
||||
projectMatch: filterPresets.projectMatch || "contains",
|
||||
userMatch: filterPresets.userMatch || "contains",
|
||||
jobIdMatch: filterPresets.jobIdMatch || "eq",
|
||||
|
||||
cluster: filterPresets.cluster || null,
|
||||
partition: filterPresets.partition || null,
|
||||
states: filterPresets.states || filterPresets.state ? [filterPresets.state].flat() : allJobStates,
|
||||
startTime: filterPresets.startTime || { from: null, to: null },
|
||||
tags: filterPresets.tags || [],
|
||||
duration: filterPresets.duration || { lessThan: null, moreThan: null, from: null, to: null },
|
||||
jobId: filterPresets.jobId || '',
|
||||
arrayJobId: filterPresets.arrayJobId || null,
|
||||
user: filterPresets.user || '',
|
||||
project: filterPresets.project || '',
|
||||
jobName: filterPresets.jobName || '',
|
||||
cluster: filterPresets.cluster || null,
|
||||
partition: filterPresets.partition || null,
|
||||
states:
|
||||
filterPresets.states || filterPresets.state
|
||||
? [filterPresets.state].flat()
|
||||
: allJobStates,
|
||||
startTime: filterPresets.startTime || { from: null, to: null },
|
||||
tags: filterPresets.tags || [],
|
||||
duration: filterPresets.duration || {
|
||||
lessThan: null,
|
||||
moreThan: null,
|
||||
from: null,
|
||||
to: null,
|
||||
},
|
||||
jobId: filterPresets.jobId || "",
|
||||
arrayJobId: filterPresets.arrayJobId || null,
|
||||
user: filterPresets.user || "",
|
||||
project: filterPresets.project || "",
|
||||
jobName: filterPresets.jobName || "",
|
||||
|
||||
node: filterPresets.node || null,
|
||||
numNodes: filterPresets.numNodes || { from: null, to: null },
|
||||
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
|
||||
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
||||
node: filterPresets.node || null,
|
||||
numNodes: filterPresets.numNodes || { from: null, to: null },
|
||||
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
|
||||
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
||||
|
||||
stats: [],
|
||||
}
|
||||
stats: [],
|
||||
};
|
||||
|
||||
let isClusterOpen = false,
|
||||
isJobStatesOpen = false,
|
||||
isStartTimeOpen = false,
|
||||
isTagsOpen = false,
|
||||
isDurationOpen = false,
|
||||
isResourcesOpen = false,
|
||||
isStatsOpen = false,
|
||||
isNodesModified = false,
|
||||
isHwthreadsModified = false,
|
||||
isAccsModified = false
|
||||
let isClusterOpen = false,
|
||||
isJobStatesOpen = false,
|
||||
isStartTimeOpen = false,
|
||||
isTagsOpen = false,
|
||||
isDurationOpen = false,
|
||||
isResourcesOpen = false,
|
||||
isStatsOpen = false,
|
||||
isNodesModified = false,
|
||||
isHwthreadsModified = false,
|
||||
isAccsModified = false;
|
||||
|
||||
// Can be called from the outside to trigger a 'update' event from this component.
|
||||
export function update(additionalFilters = null) {
|
||||
if (additionalFilters != null)
|
||||
for (let key in additionalFilters)
|
||||
filters[key] = additionalFilters[key]
|
||||
// Can be called from the outside to trigger a 'update' event from this component.
|
||||
export function update(additionalFilters = null) {
|
||||
if (additionalFilters != null)
|
||||
for (let key in additionalFilters) filters[key] = additionalFilters[key];
|
||||
|
||||
let items = []
|
||||
if (filters.cluster)
|
||||
items.push({ cluster: { eq: filters.cluster } })
|
||||
if (filters.node)
|
||||
items.push({ node: { contains: filters.node } })
|
||||
if (filters.partition)
|
||||
items.push({ partition: { eq: filters.partition } })
|
||||
if (filters.states.length != allJobStates.length)
|
||||
items.push({ state: filters.states })
|
||||
if (filters.startTime.from || filters.startTime.to)
|
||||
items.push({ startTime: { from: filters.startTime.from, to: filters.startTime.to } })
|
||||
if (filters.tags.length != 0)
|
||||
items.push({ tags: filters.tags })
|
||||
if (filters.duration.from || filters.duration.to)
|
||||
items.push({ duration: { from: filters.duration.from, to: filters.duration.to } })
|
||||
if (filters.duration.lessThan)
|
||||
items.push({ duration: { from: 0, to: filters.duration.lessThan } })
|
||||
if (filters.duration.moreThan)
|
||||
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }) // 7 days to include special jobs with long runtimes
|
||||
if (filters.jobId)
|
||||
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } })
|
||||
if (filters.arrayJobId != null)
|
||||
items.push({ arrayJobId: filters.arrayJobId })
|
||||
if (filters.numNodes.from != null || filters.numNodes.to != null)
|
||||
items.push({ numNodes: { from: filters.numNodes.from, to: filters.numNodes.to } })
|
||||
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
|
||||
items.push({ numHWThreads: { from: filters.numHWThreads.from, to: filters.numHWThreads.to } })
|
||||
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null)
|
||||
items.push({ numAccelerators: { from: filters.numAccelerators.from, to: filters.numAccelerators.to } })
|
||||
if (filters.user)
|
||||
items.push({ user: { [filters.userMatch]: filters.user } })
|
||||
if (filters.project)
|
||||
items.push({ project: { [filters.projectMatch]: filters.project } })
|
||||
if (filters.jobName)
|
||||
items.push({ jobName: { contains: filters.jobName } })
|
||||
for (let stat of filters.stats)
|
||||
items.push({ [stat.field]: { from: stat.from, to: stat.to } })
|
||||
let items = [];
|
||||
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
||||
if (filters.node) items.push({ node: { contains: filters.node } });
|
||||
if (filters.partition) items.push({ partition: { eq: filters.partition } });
|
||||
if (filters.states.length != allJobStates.length)
|
||||
items.push({ state: filters.states });
|
||||
if (filters.startTime.from || filters.startTime.to)
|
||||
items.push({
|
||||
startTime: { from: filters.startTime.from, to: filters.startTime.to },
|
||||
});
|
||||
if (filters.tags.length != 0) items.push({ tags: filters.tags });
|
||||
if (filters.duration.from || filters.duration.to)
|
||||
items.push({
|
||||
duration: { from: filters.duration.from, to: filters.duration.to },
|
||||
});
|
||||
if (filters.duration.lessThan)
|
||||
items.push({ duration: { from: 0, to: filters.duration.lessThan } });
|
||||
if (filters.duration.moreThan)
|
||||
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
|
||||
if (filters.jobId)
|
||||
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
||||
if (filters.arrayJobId != null)
|
||||
items.push({ arrayJobId: filters.arrayJobId });
|
||||
if (filters.numNodes.from != null || filters.numNodes.to != null)
|
||||
items.push({
|
||||
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
|
||||
});
|
||||
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
|
||||
items.push({
|
||||
numHWThreads: {
|
||||
from: filters.numHWThreads.from,
|
||||
to: filters.numHWThreads.to,
|
||||
},
|
||||
});
|
||||
if (
|
||||
filters.numAccelerators.from != null ||
|
||||
filters.numAccelerators.to != null
|
||||
)
|
||||
items.push({
|
||||
numAccelerators: {
|
||||
from: filters.numAccelerators.from,
|
||||
to: filters.numAccelerators.to,
|
||||
},
|
||||
});
|
||||
if (filters.user)
|
||||
items.push({ user: { [filters.userMatch]: filters.user } });
|
||||
if (filters.project)
|
||||
items.push({ project: { [filters.projectMatch]: filters.project } });
|
||||
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
|
||||
for (let stat of filters.stats)
|
||||
items.push({ [stat.field]: { from: stat.from, to: stat.to } });
|
||||
|
||||
dispatch('update', { filters: items })
|
||||
changeURL()
|
||||
return items
|
||||
}
|
||||
dispatch("update", { filters: items });
|
||||
changeURL();
|
||||
return items;
|
||||
}
|
||||
|
||||
function changeURL() {
|
||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000)
|
||||
function changeURL() {
|
||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||
|
||||
let opts = []
|
||||
if (filters.cluster)
|
||||
opts.push(`cluster=${filters.cluster}`)
|
||||
if (filters.node)
|
||||
opts.push(`node=${filters.node}`)
|
||||
if (filters.partition)
|
||||
opts.push(`partition=${filters.partition}`)
|
||||
if (filters.states.length != allJobStates.length)
|
||||
for (let state of filters.states)
|
||||
opts.push(`state=${state}`)
|
||||
if (filters.startTime.from && filters.startTime.to)
|
||||
// if (filters.startTime.url) {
|
||||
// opts.push(`startTime=${filters.startTime.url}`)
|
||||
// } else {
|
||||
opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`)
|
||||
// }
|
||||
if (filters.jobId.length != 0)
|
||||
if (filters.jobIdMatch != 'in') {
|
||||
opts.push(`jobId=${filters.jobId}`)
|
||||
} else {
|
||||
for (let singleJobId of filters.jobId)
|
||||
opts.push(`jobId=${singleJobId}`)
|
||||
}
|
||||
if (filters.jobIdMatch != 'eq')
|
||||
opts.push(`jobIdMatch=${filters.jobIdMatch}`)
|
||||
for (let tag of filters.tags)
|
||||
opts.push(`tag=${tag}`)
|
||||
if (filters.duration.from && filters.duration.to)
|
||||
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`)
|
||||
if (filters.duration.lessThan)
|
||||
opts.push(`duration=0-${filters.duration.lessThan}`)
|
||||
if (filters.duration.moreThan)
|
||||
opts.push(`duration=${filters.duration.moreThan}-604800`)
|
||||
if (filters.numNodes.from && filters.numNodes.to)
|
||||
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`)
|
||||
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
||||
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`)
|
||||
if (filters.user.length != 0)
|
||||
if (filters.userMatch != 'in') {
|
||||
opts.push(`user=${filters.user}`)
|
||||
} else {
|
||||
for (let singleUser of filters.user)
|
||||
opts.push(`user=${singleUser}`)
|
||||
}
|
||||
if (filters.userMatch != 'contains')
|
||||
opts.push(`userMatch=${filters.userMatch}`)
|
||||
if (filters.project)
|
||||
opts.push(`project=${filters.project}`)
|
||||
if (filters.jobName)
|
||||
opts.push(`jobName=${filters.jobName}`)
|
||||
if (filters.projectMatch != 'contains')
|
||||
opts.push(`projectMatch=${filters.projectMatch}`)
|
||||
let opts = [];
|
||||
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
||||
if (filters.node) opts.push(`node=${filters.node}`);
|
||||
if (filters.partition) opts.push(`partition=${filters.partition}`);
|
||||
if (filters.states.length != allJobStates.length)
|
||||
for (let state of filters.states) opts.push(`state=${state}`);
|
||||
if (filters.startTime.from && filters.startTime.to)
|
||||
// if (filters.startTime.url) {
|
||||
// opts.push(`startTime=${filters.startTime.url}`)
|
||||
// } else {
|
||||
opts.push(
|
||||
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
|
||||
);
|
||||
// }
|
||||
if (filters.jobId.length != 0)
|
||||
if (filters.jobIdMatch != "in") {
|
||||
opts.push(`jobId=${filters.jobId}`);
|
||||
} else {
|
||||
for (let singleJobId of filters.jobId)
|
||||
opts.push(`jobId=${singleJobId}`);
|
||||
}
|
||||
if (filters.jobIdMatch != "eq")
|
||||
opts.push(`jobIdMatch=${filters.jobIdMatch}`);
|
||||
for (let tag of filters.tags) opts.push(`tag=${tag}`);
|
||||
if (filters.duration.from && filters.duration.to)
|
||||
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
|
||||
if (filters.duration.lessThan)
|
||||
opts.push(`duration=0-${filters.duration.lessThan}`);
|
||||
if (filters.duration.moreThan)
|
||||
opts.push(`duration=${filters.duration.moreThan}-604800`);
|
||||
if (filters.numNodes.from && filters.numNodes.to)
|
||||
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
|
||||
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
||||
opts.push(
|
||||
`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`,
|
||||
);
|
||||
if (filters.user.length != 0)
|
||||
if (filters.userMatch != "in") {
|
||||
opts.push(`user=${filters.user}`);
|
||||
} else {
|
||||
for (let singleUser of filters.user) opts.push(`user=${singleUser}`);
|
||||
}
|
||||
if (filters.userMatch != "contains")
|
||||
opts.push(`userMatch=${filters.userMatch}`);
|
||||
if (filters.project) opts.push(`project=${filters.project}`);
|
||||
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
|
||||
if (filters.projectMatch != "contains")
|
||||
opts.push(`projectMatch=${filters.projectMatch}`);
|
||||
|
||||
if (opts.length == 0 && window.location.search.length <= 1)
|
||||
return
|
||||
if (opts.length == 0 && window.location.search.length <= 1) return;
|
||||
|
||||
let newurl = `${window.location.pathname}?${opts.join('&')}`
|
||||
window.history.replaceState(null, '', newurl)
|
||||
}
|
||||
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
||||
window.history.replaceState(null, "", newurl);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
<Col xs="auto">
|
||||
<ButtonDropdown class="cc-dropdown-on-hover">
|
||||
<DropdownToggle outline caret color="success">
|
||||
<Icon name="sliders"/>
|
||||
Filters
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem header>
|
||||
Manage Filters
|
||||
</DropdownItem>
|
||||
{#if menuText}
|
||||
<DropdownItem disabled>{menuText}</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
{/if}
|
||||
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
||||
<Icon name="cpu"/> Cluster/Partition
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
||||
<Icon name="gear-fill"/> Job States
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
||||
<Icon name="calendar-range"/> Start Time
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
||||
<Icon name="stopwatch"/> Duration
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
||||
<Icon name="tags"/> Tags
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
||||
<Icon name="hdd-stack"/> Resources
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
||||
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
|
||||
</DropdownItem>
|
||||
{#if startTimeQuickSelect}
|
||||
<DropdownItem divider/>
|
||||
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
|
||||
{#each [
|
||||
{ text: 'Last 6hrs', url: 'last6h', seconds: 6*60*60 },
|
||||
// { text: 'Last 12hrs', seconds: 12*60*60 },
|
||||
{ text: 'Last 24hrs', url: 'last24h', seconds: 24*60*60 },
|
||||
// { text: 'Last 48hrs', seconds: 48*60*60 },
|
||||
{ text: 'Last 7 days', url: 'last7d', seconds: 7*24*60*60 },
|
||||
{ text: 'Last 30 days', url: 'last30d', seconds: 30*24*60*60 }
|
||||
] as {text, url, seconds}}
|
||||
<DropdownItem on:click={() => {
|
||||
filters.startTime.from = (new Date(Date.now() - seconds * 1000)).toISOString()
|
||||
filters.startTime.to = (new Date(Date.now())).toISOString()
|
||||
filters.startTime.text = text,
|
||||
filters.startTime.url = url
|
||||
update()
|
||||
}}>
|
||||
<Icon name="calendar-range"/> {text}
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
{/if}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
{#if filters.cluster}
|
||||
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
|
||||
{filters.cluster}
|
||||
{#if filters.partition}
|
||||
({filters.partition})
|
||||
{/if}
|
||||
</Info>
|
||||
<Col xs="auto">
|
||||
<ButtonDropdown class="cc-dropdown-on-hover">
|
||||
<DropdownToggle outline caret color="success">
|
||||
<Icon name="sliders" />
|
||||
Filters
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem header>Manage Filters</DropdownItem>
|
||||
{#if menuText}
|
||||
<DropdownItem disabled>{menuText}</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
{/if}
|
||||
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
||||
<Icon name="cpu" /> Cluster/Partition
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
||||
<Icon name="gear-fill" /> Job States
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
||||
<Icon name="calendar-range" /> Start Time
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
||||
<Icon name="stopwatch" /> Duration
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
||||
<Icon name="tags" /> Tags
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
||||
<Icon name="hdd-stack" /> Resources
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
||||
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
|
||||
</DropdownItem>
|
||||
{#if startTimeQuickSelect}
|
||||
<DropdownItem divider />
|
||||
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
|
||||
{#each [{ text: "Last 6hrs", url: "last6h", seconds: 6 * 60 * 60 }, { text: "Last 24hrs", url: "last24h", seconds: 24 * 60 * 60 }, { text: "Last 7 days", url: "last7d", seconds: 7 * 24 * 60 * 60 }, { text: "Last 30 days", url: "last30d", seconds: 30 * 24 * 60 * 60 }] as { text, url, seconds }}
|
||||
<DropdownItem
|
||||
on:click={() => {
|
||||
filters.startTime.from = new Date(
|
||||
Date.now() - seconds * 1000,
|
||||
).toISOString();
|
||||
filters.startTime.to = new Date(Date.now()).toISOString();
|
||||
(filters.startTime.text = text), (filters.startTime.url = url);
|
||||
update();
|
||||
}}
|
||||
>
|
||||
<Icon name="calendar-range" />
|
||||
{text}
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
{/if}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
{#if filters.cluster}
|
||||
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
|
||||
{filters.cluster}
|
||||
{#if filters.partition}
|
||||
({filters.partition})
|
||||
{/if}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.states.length != allJobStates.length}
|
||||
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
||||
{filters.states.join(', ')}
|
||||
</Info>
|
||||
{/if}
|
||||
{#if filters.states.length != allJobStates.length}
|
||||
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
||||
{filters.states.join(", ")}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.startTime.from || filters.startTime.to}
|
||||
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
||||
{#if filters.startTime.text}
|
||||
{filters.startTime.text}
|
||||
{:else}
|
||||
{new Date(filters.startTime.from).toLocaleString()} - {new Date(filters.startTime.to).toLocaleString()}
|
||||
{/if}
|
||||
</Info>
|
||||
{#if filters.startTime.from || filters.startTime.to}
|
||||
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
||||
{#if filters.startTime.text}
|
||||
{filters.startTime.text}
|
||||
{:else}
|
||||
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
|
||||
filters.startTime.to,
|
||||
).toLocaleString()}
|
||||
{/if}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.duration.from || filters.duration.to}
|
||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(filters.duration.from % 3600 / 60)}m
|
||||
-
|
||||
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(filters.duration.to % 3600 / 60)}m
|
||||
</Info>
|
||||
{/if}
|
||||
{#if filters.duration.from || filters.duration.to}
|
||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
|
||||
(filters.duration.from % 3600) / 60,
|
||||
)}m -
|
||||
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
|
||||
(filters.duration.to % 3600) / 60,
|
||||
)}m
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.duration.lessThan}
|
||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||
Duration less than {Math.floor(filters.duration.lessThan / 3600)}h:{Math.floor(filters.duration.lessThan % 3600 / 60)}m
|
||||
</Info>
|
||||
{/if}
|
||||
{#if filters.duration.lessThan}
|
||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||
Duration less than {Math.floor(
|
||||
filters.duration.lessThan / 3600,
|
||||
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.duration.moreThan}
|
||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||
Duration more than {Math.floor(filters.duration.moreThan / 3600)}h:{Math.floor(filters.duration.moreThan % 3600 / 60)}m
|
||||
</Info>
|
||||
{/if}
|
||||
{#if filters.duration.moreThan}
|
||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||
Duration more than {Math.floor(
|
||||
filters.duration.moreThan / 3600,
|
||||
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.tags.length != 0}
|
||||
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
|
||||
{#each filters.tags as tagId}
|
||||
<Tag id={tagId} clickable={false} />
|
||||
{/each}
|
||||
</Info>
|
||||
{/if}
|
||||
{#if filters.tags.length != 0}
|
||||
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
|
||||
{#each filters.tags as tagId}
|
||||
<Tag id={tagId} clickable={false} />
|
||||
{/each}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.numNodes.from != null || filters.numNodes.to != null ||
|
||||
filters.numHWThreads.from != null || filters.numHWThreads.to != null ||
|
||||
filters.numAccelerators.from != null || filters.numAccelerators.to != null }
|
||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||
{#if isNodesModified } Nodes: {filters.numNodes.from} - {filters.numNodes.to} {/if}
|
||||
{#if isNodesModified && isHwthreadsModified }, {/if}
|
||||
{#if isHwthreadsModified } HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} {/if}
|
||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified }, {/if}
|
||||
{#if isAccsModified } Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} {/if}
|
||||
</Info>
|
||||
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
|
||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||
{#if isNodesModified}
|
||||
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
|
||||
{/if}
|
||||
{#if isNodesModified && isHwthreadsModified},
|
||||
{/if}
|
||||
{#if isHwthreadsModified}
|
||||
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
||||
{/if}
|
||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
||||
{/if}
|
||||
{#if isAccsModified}
|
||||
Accelerators: {filters.numAccelerators.from} - {filters
|
||||
.numAccelerators.to}
|
||||
{/if}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.node != null }
|
||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||
Node: {filters.node}
|
||||
</Info>
|
||||
{/if}
|
||||
{#if filters.node != null}
|
||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||
Node: {filters.node}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.stats.length > 0}
|
||||
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
||||
{filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')}
|
||||
</Info>
|
||||
{/if}
|
||||
</Col>
|
||||
{#if filters.stats.length > 0}
|
||||
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
||||
{filters.stats
|
||||
.map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
|
||||
.join(", ")}
|
||||
</Info>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Cluster
|
||||
disableClusterSelection={disableClusterSelection}
|
||||
bind:isOpen={isClusterOpen}
|
||||
bind:cluster={filters.cluster}
|
||||
bind:partition={filters.partition}
|
||||
on:update={() => update()} />
|
||||
{disableClusterSelection}
|
||||
bind:isOpen={isClusterOpen}
|
||||
bind:cluster={filters.cluster}
|
||||
bind:partition={filters.partition}
|
||||
on:update={() => update()}
|
||||
/>
|
||||
|
||||
<JobStates
|
||||
bind:isOpen={isJobStatesOpen}
|
||||
bind:states={filters.states}
|
||||
on:update={() => update()} />
|
||||
bind:isOpen={isJobStatesOpen}
|
||||
bind:states={filters.states}
|
||||
on:update={() => update()}
|
||||
/>
|
||||
|
||||
<StartTime
|
||||
bind:isOpen={isStartTimeOpen}
|
||||
bind:from={filters.startTime.from}
|
||||
bind:to={filters.startTime.to}
|
||||
on:update={() => {
|
||||
delete filters.startTime['text']
|
||||
delete filters.startTime['url']
|
||||
update()
|
||||
}} />
|
||||
bind:isOpen={isStartTimeOpen}
|
||||
bind:from={filters.startTime.from}
|
||||
bind:to={filters.startTime.to}
|
||||
on:update={() => {
|
||||
delete filters.startTime["text"];
|
||||
delete filters.startTime["url"];
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Duration
|
||||
bind:isOpen={isDurationOpen}
|
||||
bind:lessThan={filters.duration.lessThan}
|
||||
bind:moreThan={filters.duration.moreThan}
|
||||
bind:from={filters.duration.from}
|
||||
bind:to={filters.duration.to}
|
||||
on:update={() => update()} />
|
||||
bind:isOpen={isDurationOpen}
|
||||
bind:lessThan={filters.duration.lessThan}
|
||||
bind:moreThan={filters.duration.moreThan}
|
||||
bind:from={filters.duration.from}
|
||||
bind:to={filters.duration.to}
|
||||
on:update={() => update()}
|
||||
/>
|
||||
|
||||
<Tags
|
||||
bind:isOpen={isTagsOpen}
|
||||
bind:tags={filters.tags}
|
||||
on:update={() => update()} />
|
||||
bind:isOpen={isTagsOpen}
|
||||
bind:tags={filters.tags}
|
||||
on:update={() => update()}
|
||||
/>
|
||||
|
||||
<Resources cluster={filters.cluster}
|
||||
bind:isOpen={isResourcesOpen}
|
||||
bind:numNodes={filters.numNodes}
|
||||
bind:numHWThreads={filters.numHWThreads}
|
||||
bind:numAccelerators={filters.numAccelerators}
|
||||
bind:namedNode={filters.node}
|
||||
bind:isNodesModified={isNodesModified}
|
||||
bind:isHwthreadsModified={isHwthreadsModified}
|
||||
bind:isAccsModified={isAccsModified}
|
||||
on:update={() => update()} />
|
||||
<Resources
|
||||
cluster={filters.cluster}
|
||||
bind:isOpen={isResourcesOpen}
|
||||
bind:numNodes={filters.numNodes}
|
||||
bind:numHWThreads={filters.numHWThreads}
|
||||
bind:numAccelerators={filters.numAccelerators}
|
||||
bind:namedNode={filters.node}
|
||||
bind:isNodesModified
|
||||
bind:isHwthreadsModified
|
||||
bind:isAccsModified
|
||||
on:update={() => update()}
|
||||
/>
|
||||
|
||||
<Statistics cluster={filters.cluster}
|
||||
bind:isOpen={isStatsOpen}
|
||||
bind:stats={filters.stats}
|
||||
on:update={() => update()} />
|
||||
<Statistics
|
||||
cluster={filters.cluster}
|
||||
bind:isOpen={isStatsOpen}
|
||||
bind:stats={filters.stats}
|
||||
on:update={() => update()}
|
||||
/>
|
||||
|
||||
<style>
|
||||
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
|
||||
display: block;
|
||||
margin-top: 0px;
|
||||
padding-top: 0px;
|
||||
transform: none !important;
|
||||
}
|
||||
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
|
||||
display: block;
|
||||
margin-top: 0px;
|
||||
padding-top: 0px;
|
||||
transform: none !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import { Button, Icon } from 'sveltestrap'
|
||||
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let icon
|
||||
export let modified = false
|
||||
export let icon;
|
||||
export let modified = false;
|
||||
</script>
|
||||
|
||||
<Button outline color={modified ? 'warning' : 'primary'} on:click>
|
||||
<Icon name={icon}/>
|
||||
<slot />
|
||||
<Button outline color={modified ? "warning" : "primary"} on:click>
|
||||
<Icon name={icon} />
|
||||
<slot />
|
||||
</Button>
|
||||
|
@ -1,47 +1,76 @@
|
||||
<script context="module">
|
||||
export const allJobStates = [ 'running', 'completed', 'failed', 'cancelled', 'stopped', 'timeout', 'preempted', 'out_of_memory' ]
|
||||
export const allJobStates = [
|
||||
"running",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"stopped",
|
||||
"timeout",
|
||||
"preempted",
|
||||
"out_of_memory",
|
||||
];
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button, ListGroup, ListGroupItem,
|
||||
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let isModified = false
|
||||
export let isOpen = false
|
||||
export let states = [...allJobStates]
|
||||
export let isModified = false;
|
||||
export let isOpen = false;
|
||||
export let states = [...allJobStates];
|
||||
|
||||
let pendingStates = [...states]
|
||||
$: isModified = states.length != pendingStates.length || !states.every(state => pendingStates.includes(state))
|
||||
let pendingStates = [...states];
|
||||
$: isModified =
|
||||
states.length != pendingStates.length ||
|
||||
!states.every((state) => pendingStates.includes(state));
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select Job States
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each allJobStates as state}
|
||||
<ListGroupItem>
|
||||
<input type=checkbox bind:group={pendingStates} name="flavours" value={state}>
|
||||
{state}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" disabled={pendingStates.length == 0} on:click={() => {
|
||||
isOpen = false
|
||||
states = [...pendingStates]
|
||||
dispatch('update', { states })
|
||||
}}>Close & Apply</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
states = [...allJobStates]
|
||||
pendingStates = [...allJobStates]
|
||||
dispatch('update', { states })
|
||||
}}>Reset</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select Job States</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each allJobStates as state}
|
||||
<ListGroupItem>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:group={pendingStates}
|
||||
name="flavours"
|
||||
value={state}
|
||||
/>
|
||||
{state}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={pendingStates.length == 0}
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
states = [...pendingStates];
|
||||
dispatch("update", { states });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
states = [...allJobStates];
|
||||
pendingStates = [...allJobStates];
|
||||
dispatch("update", { states });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,145 +1,242 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import Header from '../Header.svelte';
|
||||
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
|
||||
|
||||
const clusters = getContext('clusters'),
|
||||
initialized = getContext('initialized'),
|
||||
dispatch = createEventDispatcher()
|
||||
const clusters = getContext("clusters"),
|
||||
initialized = getContext("initialized"),
|
||||
dispatch = createEventDispatcher();
|
||||
|
||||
export let cluster = null
|
||||
export let isOpen = false
|
||||
export let numNodes = { from: null, to: null }
|
||||
export let numHWThreads = { from: null, to: null }
|
||||
export let numAccelerators = { from: null, to: null }
|
||||
export let isNodesModified = false
|
||||
export let isHwthreadsModified = false
|
||||
export let isAccsModified = false
|
||||
export let namedNode = null
|
||||
export let cluster = null;
|
||||
export let isOpen = false;
|
||||
export let numNodes = { from: null, to: null };
|
||||
export let numHWThreads = { from: null, to: null };
|
||||
export let numAccelerators = { from: null, to: null };
|
||||
export let isNodesModified = false;
|
||||
export let isHwthreadsModified = false;
|
||||
export let isAccsModified = false;
|
||||
export let namedNode = null;
|
||||
|
||||
let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators, pendingNamedNode = namedNode
|
||||
let pendingNumNodes = numNodes,
|
||||
pendingNumHWThreads = numHWThreads,
|
||||
pendingNumAccelerators = numAccelerators,
|
||||
pendingNamedNode = namedNode;
|
||||
|
||||
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
|
||||
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
|
||||
const findMaxNumAccels = (clusters) =>
|
||||
clusters.reduce(
|
||||
(max, cluster) =>
|
||||
Math.max(
|
||||
max,
|
||||
cluster.subClusters.reduce(
|
||||
(max, sc) => Math.max(max, sc.topology.accelerators?.length || 0),
|
||||
0,
|
||||
),
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
// Limited to Single-Node Thread Count
|
||||
const findMaxNumHWTreadsPerNode = clusters => clusters.reduce((max, cluster) => Math.max(max,
|
||||
cluster.subClusters.reduce((max, sc) => Math.max(max, (sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode) || 0), 0)), 0)
|
||||
// Limited to Single-Node Thread Count
|
||||
const findMaxNumHWTreadsPerNode = (clusters) =>
|
||||
clusters.reduce(
|
||||
(max, cluster) =>
|
||||
Math.max(
|
||||
max,
|
||||
cluster.subClusters.reduce(
|
||||
(max, sc) =>
|
||||
Math.max(
|
||||
max,
|
||||
sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode || 0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
// console.log(header)
|
||||
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
|
||||
$: {
|
||||
if ($initialized) {
|
||||
if (cluster != null) {
|
||||
const { subClusters } = clusters.find(c => c.name == cluster)
|
||||
const { filterRanges } = header.clusters.find(c => c.name == cluster)
|
||||
minNumNodes = filterRanges.numNodes.from
|
||||
maxNumNodes = filterRanges.numNodes.to
|
||||
maxNumAccelerators = findMaxNumAccels([{ subClusters }])
|
||||
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }])
|
||||
} else if (clusters.length > 0) {
|
||||
const { filterRanges } = header.clusters[0]
|
||||
minNumNodes = filterRanges.numNodes.from
|
||||
maxNumNodes = filterRanges.numNodes.to
|
||||
maxNumAccelerators = findMaxNumAccels(clusters)
|
||||
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters)
|
||||
for (let cluster of header.clusters) {
|
||||
const { filterRanges } = cluster
|
||||
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from)
|
||||
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to)
|
||||
}
|
||||
}
|
||||
// console.log(header)
|
||||
let minNumNodes = 1,
|
||||
maxNumNodes = 0,
|
||||
minNumHWThreads = 1,
|
||||
maxNumHWThreads = 0,
|
||||
minNumAccelerators = 0,
|
||||
maxNumAccelerators = 0;
|
||||
$: {
|
||||
if ($initialized) {
|
||||
if (cluster != null) {
|
||||
const { subClusters } = clusters.find((c) => c.name == cluster);
|
||||
const { filterRanges } = header.clusters.find((c) => c.name == cluster);
|
||||
minNumNodes = filterRanges.numNodes.from;
|
||||
maxNumNodes = filterRanges.numNodes.to;
|
||||
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
||||
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]);
|
||||
} else if (clusters.length > 0) {
|
||||
const { filterRanges } = header.clusters[0];
|
||||
minNumNodes = filterRanges.numNodes.from;
|
||||
maxNumNodes = filterRanges.numNodes.to;
|
||||
maxNumAccelerators = findMaxNumAccels(clusters);
|
||||
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters);
|
||||
for (let cluster of header.clusters) {
|
||||
const { filterRanges } = cluster;
|
||||
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
|
||||
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (isOpen && $initialized && pendingNumNodes.from == null && pendingNumNodes.to == null) {
|
||||
pendingNumNodes = { from: 0, to: maxNumNodes }
|
||||
}
|
||||
$: {
|
||||
if (
|
||||
isOpen &&
|
||||
$initialized &&
|
||||
pendingNumNodes.from == null &&
|
||||
pendingNumNodes.to == null
|
||||
) {
|
||||
pendingNumNodes = { from: 0, to: maxNumNodes };
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (isOpen && $initialized && ((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) || (isHwthreadsModified == false))) {
|
||||
pendingNumHWThreads = { from: 0, to: maxNumHWThreads }
|
||||
}
|
||||
$: {
|
||||
if (
|
||||
isOpen &&
|
||||
$initialized &&
|
||||
((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) ||
|
||||
isHwthreadsModified == false)
|
||||
) {
|
||||
pendingNumHWThreads = { from: 0, to: maxNumHWThreads };
|
||||
}
|
||||
}
|
||||
|
||||
$: if ( maxNumAccelerators != null && maxNumAccelerators > 1 ) {
|
||||
if (isOpen && $initialized && pendingNumAccelerators.from == null && pendingNumAccelerators.to == null) {
|
||||
pendingNumAccelerators = { from: 0, to: maxNumAccelerators }
|
||||
}
|
||||
$: if (maxNumAccelerators != null && maxNumAccelerators > 1) {
|
||||
if (
|
||||
isOpen &&
|
||||
$initialized &&
|
||||
pendingNumAccelerators.from == null &&
|
||||
pendingNumAccelerators.to == null
|
||||
) {
|
||||
pendingNumAccelerators = { from: 0, to: maxNumAccelerators };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select number of utilized Resources
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<h6>Named Node</h6>
|
||||
<input type="text" class="form-control" bind:value={pendingNamedNode}>
|
||||
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumNodes = { from: detail[0], to: detail[1] }
|
||||
isNodesModified = true
|
||||
}}
|
||||
min={minNumNodes} max={maxNumNodes}
|
||||
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to}
|
||||
inputFieldFrom={pendingNumNodes.from} inputFieldTo={pendingNumNodes.to}/>
|
||||
<h6 style="margin-top: 1rem;">Number of HWThreads (Use for Single-Node Jobs)</h6>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumHWThreads = { from: detail[0], to: detail[1] }
|
||||
isHwthreadsModified = true
|
||||
}}
|
||||
min={minNumHWThreads} max={maxNumHWThreads}
|
||||
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to}
|
||||
inputFieldFrom={pendingNumHWThreads.from} inputFieldTo={pendingNumHWThreads.to}/>
|
||||
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
|
||||
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumAccelerators = { from: detail[0], to: detail[1] }
|
||||
isAccsModified = true
|
||||
}}
|
||||
min={minNumAccelerators} max={maxNumAccelerators}
|
||||
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to}
|
||||
inputFieldFrom={pendingNumAccelerators.from} inputFieldTo={pendingNumAccelerators.to}/>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary"
|
||||
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
|
||||
on:click={() => {
|
||||
isOpen = false
|
||||
pendingNumNodes = isNodesModified ? pendingNumNodes : { from: null, to: null }
|
||||
pendingNumHWThreads = isHwthreadsModified ? pendingNumHWThreads : { from: null, to: null }
|
||||
pendingNumAccelerators = isAccsModified ? pendingNumAccelerators : { from: null, to: null }
|
||||
numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to }
|
||||
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
|
||||
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
|
||||
namedNode = pendingNamedNode
|
||||
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode })
|
||||
}}>
|
||||
Close & Apply
|
||||
</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
pendingNumNodes = { from: null, to: null }
|
||||
pendingNumHWThreads = { from: null, to: null }
|
||||
pendingNumAccelerators = { from: null, to: null }
|
||||
pendingNamedNode = null
|
||||
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
|
||||
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
|
||||
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
|
||||
isNodesModified = false
|
||||
isHwthreadsModified = false
|
||||
isAccsModified = false
|
||||
namedNode = pendingNamedNode
|
||||
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode})
|
||||
}}>Reset</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select number of utilized Resources</ModalHeader>
|
||||
<ModalBody>
|
||||
<h6>Named Node</h6>
|
||||
<input type="text" class="form-control" bind:value={pendingNamedNode} />
|
||||
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumNodes = { from: detail[0], to: detail[1] };
|
||||
isNodesModified = true;
|
||||
}}
|
||||
min={minNumNodes}
|
||||
max={maxNumNodes}
|
||||
firstSlider={pendingNumNodes.from}
|
||||
secondSlider={pendingNumNodes.to}
|
||||
inputFieldFrom={pendingNumNodes.from}
|
||||
inputFieldTo={pendingNumNodes.to}
|
||||
/>
|
||||
<h6 style="margin-top: 1rem;">
|
||||
Number of HWThreads (Use for Single-Node Jobs)
|
||||
</h6>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumHWThreads = { from: detail[0], to: detail[1] };
|
||||
isHwthreadsModified = true;
|
||||
}}
|
||||
min={minNumHWThreads}
|
||||
max={maxNumHWThreads}
|
||||
firstSlider={pendingNumHWThreads.from}
|
||||
secondSlider={pendingNumHWThreads.to}
|
||||
inputFieldFrom={pendingNumHWThreads.from}
|
||||
inputFieldTo={pendingNumHWThreads.to}
|
||||
/>
|
||||
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
|
||||
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumAccelerators = { from: detail[0], to: detail[1] };
|
||||
isAccsModified = true;
|
||||
}}
|
||||
min={minNumAccelerators}
|
||||
max={maxNumAccelerators}
|
||||
firstSlider={pendingNumAccelerators.from}
|
||||
secondSlider={pendingNumAccelerators.to}
|
||||
inputFieldFrom={pendingNumAccelerators.from}
|
||||
inputFieldTo={pendingNumAccelerators.to}
|
||||
/>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
pendingNumNodes = isNodesModified
|
||||
? pendingNumNodes
|
||||
: { from: null, to: null };
|
||||
pendingNumHWThreads = isHwthreadsModified
|
||||
? pendingNumHWThreads
|
||||
: { from: null, to: null };
|
||||
pendingNumAccelerators = isAccsModified
|
||||
? pendingNumAccelerators
|
||||
: { from: null, to: null };
|
||||
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
|
||||
numHWThreads = {
|
||||
from: pendingNumHWThreads.from,
|
||||
to: pendingNumHWThreads.to,
|
||||
};
|
||||
numAccelerators = {
|
||||
from: pendingNumAccelerators.from,
|
||||
to: pendingNumAccelerators.to,
|
||||
};
|
||||
namedNode = pendingNamedNode;
|
||||
dispatch("update", {
|
||||
numNodes,
|
||||
numHWThreads,
|
||||
numAccelerators,
|
||||
namedNode,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Close & Apply
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
pendingNumNodes = { from: null, to: null };
|
||||
pendingNumHWThreads = { from: null, to: null };
|
||||
pendingNumAccelerators = { from: null, to: null };
|
||||
pendingNamedNode = null;
|
||||
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
|
||||
numHWThreads = {
|
||||
from: pendingNumHWThreads.from,
|
||||
to: pendingNumHWThreads.to,
|
||||
};
|
||||
numAccelerators = {
|
||||
from: pendingNumAccelerators.from,
|
||||
to: pendingNumAccelerators.to,
|
||||
};
|
||||
isNodesModified = false;
|
||||
isHwthreadsModified = false;
|
||||
isAccsModified = false;
|
||||
namedNode = pendingNamedNode;
|
||||
dispatch("update", {
|
||||
numNodes,
|
||||
numHWThreads,
|
||||
numAccelerators,
|
||||
namedNode,
|
||||
});
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,86 +1,121 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { parse, format, sub } from 'date-fns'
|
||||
import { Row, Button, Input, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap'
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { parse, format, sub } from "date-fns";
|
||||
import {
|
||||
Row,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
FormGroup,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let isModified = false
|
||||
export let isOpen = false
|
||||
export let from = null
|
||||
export let to = null
|
||||
export let isModified = false;
|
||||
export let isOpen = false;
|
||||
export let from = null;
|
||||
export let to = null;
|
||||
|
||||
let pendingFrom, pendingTo
|
||||
let pendingFrom, pendingTo;
|
||||
|
||||
const now = new Date(Date.now())
|
||||
const ago = sub(now, {months: 1})
|
||||
const defaultFrom = {date: format(ago, 'yyyy-MM-dd'), time: format(ago, 'HH:mm')}
|
||||
const defaultTo = {date: format(now, 'yyyy-MM-dd'), time: format(now, 'HH:mm')}
|
||||
const now = new Date(Date.now());
|
||||
const ago = sub(now, { months: 1 });
|
||||
const defaultFrom = {
|
||||
date: format(ago, "yyyy-MM-dd"),
|
||||
time: format(ago, "HH:mm"),
|
||||
};
|
||||
const defaultTo = {
|
||||
date: format(now, "yyyy-MM-dd"),
|
||||
time: format(now, "HH:mm"),
|
||||
};
|
||||
|
||||
function reset() {
|
||||
pendingFrom = from == null ? defaultFrom : fromRFC3339(from)
|
||||
pendingTo = to == null ? defaultTo : fromRFC3339(to)
|
||||
}
|
||||
function reset() {
|
||||
pendingFrom = from == null ? defaultFrom : fromRFC3339(from);
|
||||
pendingTo = to == null ? defaultTo : fromRFC3339(to);
|
||||
}
|
||||
|
||||
reset()
|
||||
reset();
|
||||
|
||||
function toRFC3339({ date, time }, secs = '00') {
|
||||
const parsedDate = parse(date+' '+time+':'+secs, 'yyyy-MM-dd HH:mm:ss', new Date())
|
||||
return parsedDate.toISOString()
|
||||
}
|
||||
function toRFC3339({ date, time }, secs = "00") {
|
||||
const parsedDate = parse(
|
||||
date + " " + time + ":" + secs,
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
new Date(),
|
||||
);
|
||||
return parsedDate.toISOString();
|
||||
}
|
||||
|
||||
function fromRFC3339(rfc3339) {
|
||||
const parsedDate = new Date(rfc3339)
|
||||
return { date: format(parsedDate, 'yyyy-MM-dd'), time: format(parsedDate, 'HH:mm') }
|
||||
}
|
||||
function fromRFC3339(rfc3339) {
|
||||
const parsedDate = new Date(rfc3339);
|
||||
return {
|
||||
date: format(parsedDate, "yyyy-MM-dd"),
|
||||
time: format(parsedDate, "HH:mm"),
|
||||
};
|
||||
}
|
||||
|
||||
$: isModified = (from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, '59'))
|
||||
&& !(from == null && pendingFrom.date == '0000-00-00' && pendingFrom.time == '00:00')
|
||||
&& !(to == null && pendingTo.date == '0000-00-00' && pendingTo.time == '00:00')
|
||||
$: isModified =
|
||||
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
|
||||
!(
|
||||
from == null &&
|
||||
pendingFrom.date == "0000-00-00" &&
|
||||
pendingFrom.time == "00:00"
|
||||
) &&
|
||||
!(
|
||||
to == null &&
|
||||
pendingTo.date == "0000-00-00" &&
|
||||
pendingTo.time == "00:00"
|
||||
);
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select Start Time
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<h4>From</h4>
|
||||
<Row>
|
||||
<FormGroup class="col">
|
||||
<Input type="date" bind:value={pendingFrom.date}/>
|
||||
</FormGroup>
|
||||
<FormGroup class="col">
|
||||
<Input type="time" bind:value={pendingFrom.time}/>
|
||||
</FormGroup>
|
||||
</Row>
|
||||
<h4>To</h4>
|
||||
<Row>
|
||||
<FormGroup class="col">
|
||||
<Input type="date" bind:value={pendingTo.date}/>
|
||||
</FormGroup>
|
||||
<FormGroup class="col">
|
||||
<Input type="time" bind:value={pendingTo.time}/>
|
||||
</FormGroup>
|
||||
</Row>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary"
|
||||
disabled={pendingFrom.date == '0000-00-00' || pendingTo.date == '0000-00-00'}
|
||||
on:click={() => {
|
||||
isOpen = false
|
||||
from = toRFC3339(pendingFrom)
|
||||
to = toRFC3339(pendingTo, '59')
|
||||
dispatch('update', { from, to })
|
||||
}}>
|
||||
Close & Apply
|
||||
</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
from = null
|
||||
to = null
|
||||
reset()
|
||||
dispatch('update', { from, to })
|
||||
}}>Reset</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select Start Time</ModalHeader>
|
||||
<ModalBody>
|
||||
<h4>From</h4>
|
||||
<Row>
|
||||
<FormGroup class="col">
|
||||
<Input type="date" bind:value={pendingFrom.date} />
|
||||
</FormGroup>
|
||||
<FormGroup class="col">
|
||||
<Input type="time" bind:value={pendingFrom.time} />
|
||||
</FormGroup>
|
||||
</Row>
|
||||
<h4>To</h4>
|
||||
<Row>
|
||||
<FormGroup class="col">
|
||||
<Input type="date" bind:value={pendingTo.date} />
|
||||
</FormGroup>
|
||||
<FormGroup class="col">
|
||||
<Input type="time" bind:value={pendingTo.time} />
|
||||
</FormGroup>
|
||||
</Row>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={pendingFrom.date == "0000-00-00" ||
|
||||
pendingTo.date == "0000-00-00"}
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
from = toRFC3339(pendingFrom);
|
||||
to = toRFC3339(pendingTo, "59");
|
||||
dispatch("update", { from, to });
|
||||
}}
|
||||
>
|
||||
Close & Apply
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
from = null;
|
||||
to = null;
|
||||
reset();
|
||||
dispatch("update", { from, to });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,115 +1,137 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
|
||||
|
||||
const clusters = getContext('clusters'),
|
||||
initialized = getContext('initialized'),
|
||||
dispatch = createEventDispatcher()
|
||||
const clusters = getContext("clusters"),
|
||||
initialized = getContext("initialized"),
|
||||
dispatch = createEventDispatcher();
|
||||
|
||||
export let cluster = null
|
||||
export let isModified = false
|
||||
export let isOpen = false
|
||||
export let stats = []
|
||||
export let cluster = null;
|
||||
export let isModified = false;
|
||||
export let isOpen = false;
|
||||
export let stats = [];
|
||||
|
||||
let statistics = [
|
||||
{
|
||||
field: 'flopsAnyAvg',
|
||||
text: 'FLOPs (Avg.)',
|
||||
metric: 'flops_any',
|
||||
from: 0, to: 0, peak: 0,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
field: 'memBwAvg',
|
||||
text: 'Mem. Bw. (Avg.)',
|
||||
metric: 'mem_bw',
|
||||
from: 0, to: 0, peak: 0,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
field: 'loadAvg',
|
||||
text: 'Load (Avg.)',
|
||||
metric: 'cpu_load',
|
||||
from: 0, to: 0, peak: 0,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
field: 'memUsedMax',
|
||||
text: 'Mem. used (Max.)',
|
||||
metric: 'mem_used',
|
||||
from: 0, to: 0, peak: 0,
|
||||
enabled: false
|
||||
let statistics = [
|
||||
{
|
||||
field: "flopsAnyAvg",
|
||||
text: "FLOPs (Avg.)",
|
||||
metric: "flops_any",
|
||||
from: 0,
|
||||
to: 0,
|
||||
peak: 0,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
field: "memBwAvg",
|
||||
text: "Mem. Bw. (Avg.)",
|
||||
metric: "mem_bw",
|
||||
from: 0,
|
||||
to: 0,
|
||||
peak: 0,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
field: "loadAvg",
|
||||
text: "Load (Avg.)",
|
||||
metric: "cpu_load",
|
||||
from: 0,
|
||||
to: 0,
|
||||
peak: 0,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
field: "memUsedMax",
|
||||
text: "Mem. used (Max.)",
|
||||
metric: "mem_used",
|
||||
from: 0,
|
||||
to: 0,
|
||||
peak: 0,
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
$: isModified = !statistics.every((a) => {
|
||||
let b = stats.find((s) => s.field == a.field);
|
||||
if (b == null) return !a.enabled;
|
||||
|
||||
return a.from == b.from && a.to == b.to;
|
||||
});
|
||||
|
||||
function getPeak(cluster, metric) {
|
||||
const mc = cluster.metricConfig.find((mc) => mc.name == metric);
|
||||
return mc ? mc.peak : 0;
|
||||
}
|
||||
|
||||
function resetRange(isInitialized, cluster) {
|
||||
if (!isInitialized) return;
|
||||
|
||||
if (cluster != null) {
|
||||
let c = clusters.find((c) => c.name == cluster);
|
||||
for (let stat of statistics) {
|
||||
stat.peak = getPeak(c, stat.metric);
|
||||
stat.from = 0;
|
||||
stat.to = stat.peak;
|
||||
}
|
||||
} else {
|
||||
for (let stat of statistics) {
|
||||
for (let c of clusters) {
|
||||
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric));
|
||||
}
|
||||
]
|
||||
|
||||
$: isModified = !statistics.every(a => {
|
||||
let b = stats.find(s => s.field == a.field)
|
||||
if (b == null)
|
||||
return !a.enabled
|
||||
|
||||
return a.from == b.from && a.to == b.to
|
||||
})
|
||||
|
||||
function getPeak(cluster, metric) {
|
||||
const mc = cluster.metricConfig.find(mc => mc.name == metric)
|
||||
return mc ? mc.peak : 0
|
||||
stat.from = 0;
|
||||
stat.to = stat.peak;
|
||||
}
|
||||
}
|
||||
|
||||
function resetRange(isInitialized, cluster) {
|
||||
if (!isInitialized)
|
||||
return
|
||||
statistics = [...statistics];
|
||||
}
|
||||
|
||||
if (cluster != null) {
|
||||
let c = clusters.find(c => c.name == cluster)
|
||||
for (let stat of statistics) {
|
||||
stat.peak = getPeak(c, stat.metric)
|
||||
stat.from = 0
|
||||
stat.to = stat.peak
|
||||
}
|
||||
} else {
|
||||
for (let stat of statistics) {
|
||||
for (let c of clusters) {
|
||||
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric))
|
||||
}
|
||||
stat.from = 0
|
||||
stat.to = stat.peak
|
||||
}
|
||||
}
|
||||
|
||||
statistics = [...statistics]
|
||||
}
|
||||
|
||||
$: resetRange($initialized, cluster)
|
||||
$: resetRange($initialized, cluster);
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Filter based on statistics (of non-running jobs)
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{#each statistics as stat}
|
||||
<h4>{stat.text}</h4>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)}
|
||||
min={0} max={stat.peak}
|
||||
firstSlider={stat.from} secondSlider={stat.to}
|
||||
inputFieldFrom={stat.from} inputFieldTo={stat.to}/>
|
||||
{/each}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => {
|
||||
isOpen = false
|
||||
stats = statistics.filter(stat => stat.enabled)
|
||||
dispatch('update', { stats })
|
||||
}}>Close & Apply</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
resetRange($initialized, cluster)
|
||||
statistics.forEach(stat => (stat.enabled = false))
|
||||
stats = []
|
||||
dispatch('update', { stats })
|
||||
}}>Reset</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Filter based on statistics (of non-running jobs)</ModalHeader>
|
||||
<ModalBody>
|
||||
{#each statistics as stat}
|
||||
<h4>{stat.text}</h4>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => (
|
||||
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
|
||||
)}
|
||||
min={0}
|
||||
max={stat.peak}
|
||||
firstSlider={stat.from}
|
||||
secondSlider={stat.to}
|
||||
inputFieldFrom={stat.from}
|
||||
inputFieldTo={stat.to}
|
||||
/>
|
||||
{/each}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
stats = statistics.filter((stat) => stat.enabled);
|
||||
dispatch("update", { stats });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
resetRange($initialized, cluster);
|
||||
statistics.forEach((stat) => (stat.enabled = false));
|
||||
stats = [];
|
||||
dispatch("update", { stats });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,67 +1,89 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Button, ListGroup, ListGroupItem, Input,
|
||||
Modal, ModalBody, ModalHeader, ModalFooter, Icon } from 'sveltestrap'
|
||||
import { fuzzySearchTags } from '../utils.js'
|
||||
import Tag from '../Tag.svelte'
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fuzzySearchTags } from "../utils.js";
|
||||
import Tag from "../Tag.svelte";
|
||||
|
||||
const allTags = getContext('tags'),
|
||||
initialized = getContext('initialized'),
|
||||
dispatch = createEventDispatcher()
|
||||
const allTags = getContext("tags"),
|
||||
initialized = getContext("initialized"),
|
||||
dispatch = createEventDispatcher();
|
||||
|
||||
export let isModified = false
|
||||
export let isOpen = false
|
||||
export let tags = []
|
||||
export let isModified = false;
|
||||
export let isOpen = false;
|
||||
export let tags = [];
|
||||
|
||||
let pendingTags = [...tags]
|
||||
$: isModified = tags.length != pendingTags.length || !tags.every(tagId => pendingTags.includes(tagId))
|
||||
let pendingTags = [...tags];
|
||||
$: isModified =
|
||||
tags.length != pendingTags.length ||
|
||||
!tags.every((tagId) => pendingTags.includes(tagId));
|
||||
|
||||
let searchTerm = ''
|
||||
let searchTerm = "";
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Select Tags
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input type="text" placeholder="Search" bind:value={searchTerm} />
|
||||
<br/>
|
||||
<ListGroup>
|
||||
{#if $initialized}
|
||||
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
|
||||
<ListGroupItem>
|
||||
{#if pendingTags.includes(tag.id)}
|
||||
<Button outline color="danger"
|
||||
on:click={() => (pendingTags = pendingTags.filter(id => id != tag.id))}>
|
||||
<Icon name="dash-circle" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button outline color="success"
|
||||
on:click={() => (pendingTags = [...pendingTags, tag.id])}>
|
||||
<Icon name="plus-circle" />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Tag tag={tag} />
|
||||
</ListGroupItem>
|
||||
{:else}
|
||||
<ListGroupItem disabled>No Tags</ListGroupItem>
|
||||
{/each}
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>Select Tags</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input type="text" placeholder="Search" bind:value={searchTerm} />
|
||||
<br />
|
||||
<ListGroup>
|
||||
{#if $initialized}
|
||||
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
|
||||
<ListGroupItem>
|
||||
{#if pendingTags.includes(tag.id)}
|
||||
<Button
|
||||
outline
|
||||
color="danger"
|
||||
on:click={() =>
|
||||
(pendingTags = pendingTags.filter((id) => id != tag.id))}
|
||||
>
|
||||
<Icon name="dash-circle" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
on:click={() => (pendingTags = [...pendingTags, tag.id])}
|
||||
>
|
||||
<Icon name="plus-circle" />
|
||||
</Button>
|
||||
{/if}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => {
|
||||
isOpen = false
|
||||
tags = [...pendingTags]
|
||||
dispatch('update', { tags })
|
||||
}}>Close & Apply</Button>
|
||||
<Button color="danger" on:click={() => {
|
||||
isOpen = false
|
||||
tags = []
|
||||
pendingTags = []
|
||||
dispatch('update', { tags })
|
||||
}}>Reset</Button>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<Tag {tag} />
|
||||
</ListGroupItem>
|
||||
{:else}
|
||||
<ListGroupItem disabled>No Tags</ListGroupItem>
|
||||
{/each}
|
||||
{/if}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
tags = [...pendingTags];
|
||||
dispatch("update", { tags });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
tags = [];
|
||||
pendingTags = [];
|
||||
dispatch("update", { tags });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,81 +1,96 @@
|
||||
<script>
|
||||
import { Icon, Input, InputGroup, InputGroupText } from 'sveltestrap'
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import {
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let from
|
||||
export let to
|
||||
export let customEnabled = true
|
||||
export let anyEnabled = false
|
||||
export let options = {
|
||||
'Last quarter hour': 15*60,
|
||||
'Last half hour': 30*60,
|
||||
'Last hour': 60*60,
|
||||
'Last 2hrs': 2*60*60,
|
||||
'Last 4hrs': 4*60*60,
|
||||
'Last 12hrs': 12*60*60,
|
||||
'Last 24hrs': 24*60*60
|
||||
export let from;
|
||||
export let to;
|
||||
export let customEnabled = true;
|
||||
export let anyEnabled = false;
|
||||
export let options = {
|
||||
"Last quarter hour": 15 * 60,
|
||||
"Last half hour": 30 * 60,
|
||||
"Last hour": 60 * 60,
|
||||
"Last 2hrs": 2 * 60 * 60,
|
||||
"Last 4hrs": 4 * 60 * 60,
|
||||
"Last 12hrs": 12 * 60 * 60,
|
||||
"Last 24hrs": 24 * 60 * 60,
|
||||
};
|
||||
|
||||
$: pendingFrom = from;
|
||||
$: pendingTo = to;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let timeRange =
|
||||
to && from ? (to.getTime() - from.getTime()) / 1000 : anyEnabled ? -2 : -1;
|
||||
|
||||
function updateTimeRange(event) {
|
||||
if (timeRange == -1) {
|
||||
pendingFrom = null;
|
||||
pendingTo = null;
|
||||
return;
|
||||
}
|
||||
if (timeRange == -2) {
|
||||
from = pendingFrom = null;
|
||||
to = pendingTo = null;
|
||||
dispatch("change", { from, to });
|
||||
return;
|
||||
}
|
||||
|
||||
$: pendingFrom = from
|
||||
$: pendingTo = to
|
||||
let now = Date.now(),
|
||||
t = timeRange * 1000;
|
||||
from = pendingFrom = new Date(now - t);
|
||||
to = pendingTo = new Date(now);
|
||||
dispatch("change", { from, to });
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let timeRange = to && from
|
||||
? (to.getTime() - from.getTime()) / 1000
|
||||
: (anyEnabled ? -2 : -1)
|
||||
function updateExplicitTimeRange(type, event) {
|
||||
let d = new Date(Date.parse(event.target.value));
|
||||
if (type == "from") pendingFrom = d;
|
||||
else pendingTo = d;
|
||||
|
||||
function updateTimeRange(event) {
|
||||
if (timeRange == -1) {
|
||||
pendingFrom = null
|
||||
pendingTo = null
|
||||
return
|
||||
}
|
||||
if (timeRange == -2) {
|
||||
from = pendingFrom = null
|
||||
to = pendingTo = null
|
||||
dispatch('change', { from, to })
|
||||
return
|
||||
}
|
||||
|
||||
let now = Date.now(), t = timeRange * 1000
|
||||
from = pendingFrom = new Date(now - t)
|
||||
to = pendingTo = new Date(now)
|
||||
dispatch('change', { from, to })
|
||||
}
|
||||
|
||||
function updateExplicitTimeRange(type, event) {
|
||||
let d = new Date(Date.parse(event.target.value));
|
||||
if (type == 'from') pendingFrom = d
|
||||
else pendingTo = d
|
||||
|
||||
if (pendingFrom != null && pendingTo != null) {
|
||||
from = pendingFrom
|
||||
to = pendingTo
|
||||
dispatch('change', { from, to })
|
||||
}
|
||||
if (pendingFrom != null && pendingTo != null) {
|
||||
from = pendingFrom;
|
||||
to = pendingTo;
|
||||
dispatch("change", { from, to });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputGroup class="inline-from">
|
||||
<InputGroupText><Icon name="clock-history"/></InputGroupText>
|
||||
<!-- <InputGroupText>
|
||||
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
||||
<!-- <InputGroupText>
|
||||
Time
|
||||
</InputGroupText> -->
|
||||
<select class="form-select" bind:value={timeRange} on:change={updateTimeRange}>
|
||||
{#if customEnabled}
|
||||
<option value={-1}>Custom</option>
|
||||
{/if}
|
||||
{#if anyEnabled}
|
||||
<option value={-2}>Any</option>
|
||||
{/if}
|
||||
{#each Object.entries(options) as [name, seconds]}
|
||||
<option value={seconds}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if timeRange == -1}
|
||||
<InputGroupText>from</InputGroupText>
|
||||
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('from', event)}></Input>
|
||||
<InputGroupText>to</InputGroupText>
|
||||
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('to', event)}></Input>
|
||||
<select
|
||||
class="form-select"
|
||||
bind:value={timeRange}
|
||||
on:change={updateTimeRange}
|
||||
>
|
||||
{#if customEnabled}
|
||||
<option value={-1}>Custom</option>
|
||||
{/if}
|
||||
{#if anyEnabled}
|
||||
<option value={-2}>Any</option>
|
||||
{/if}
|
||||
{#each Object.entries(options) as [name, seconds]}
|
||||
<option value={seconds}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if timeRange == -1}
|
||||
<InputGroupText>from</InputGroupText>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
on:change={(event) => updateExplicitTimeRange("from", event)}
|
||||
></Input>
|
||||
<InputGroupText>to</InputGroupText>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
on:change={(event) => updateExplicitTimeRange("to", event)}
|
||||
></Input>
|
||||
{/if}
|
||||
</InputGroup>
|
||||
|
@ -1,75 +1,84 @@
|
||||
<script>
|
||||
import { InputGroup, Input } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { InputGroup, Input } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let user = ''
|
||||
export let project = ''
|
||||
export let authlevel
|
||||
export let roles
|
||||
let mode = 'user', term = ''
|
||||
const throttle = 500
|
||||
export let user = "";
|
||||
export let project = "";
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
let mode = "user",
|
||||
term = "";
|
||||
const throttle = 500;
|
||||
|
||||
function modeChanged() {
|
||||
if (mode == 'user') {
|
||||
project = term
|
||||
term = user
|
||||
} else {
|
||||
user = term
|
||||
term = project
|
||||
}
|
||||
termChanged(0)
|
||||
function modeChanged() {
|
||||
if (mode == "user") {
|
||||
project = term;
|
||||
term = user;
|
||||
} else {
|
||||
user = term;
|
||||
term = project;
|
||||
}
|
||||
termChanged(0);
|
||||
}
|
||||
|
||||
let timeoutId = null
|
||||
// Compatibility: Handle "user role" and "no role" identically
|
||||
function termChanged(sleep = throttle) {
|
||||
if (authlevel >= roles.manager) {
|
||||
if (mode == 'user')
|
||||
user = term
|
||||
else
|
||||
project = term
|
||||
let timeoutId = null;
|
||||
// Compatibility: Handle "user role" and "no role" identically
|
||||
function termChanged(sleep = throttle) {
|
||||
if (authlevel >= roles.manager) {
|
||||
if (mode == "user") user = term;
|
||||
else project = term;
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch('update', {
|
||||
user,
|
||||
project
|
||||
})
|
||||
}, sleep)
|
||||
} else {
|
||||
project = term
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch("update", {
|
||||
user,
|
||||
project,
|
||||
});
|
||||
}, sleep);
|
||||
} else {
|
||||
project = term;
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch('update', {
|
||||
project
|
||||
})
|
||||
}, sleep)
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch("update", {
|
||||
project,
|
||||
});
|
||||
}, sleep);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if authlevel >= roles.manager}
|
||||
<InputGroup>
|
||||
<select style="max-width: 175px;" class="form-select"
|
||||
bind:value={mode} on:change={modeChanged}>
|
||||
<option value={'user'}>Search User</option>
|
||||
<option value={'project'}>Search Project</option>
|
||||
</select>
|
||||
<Input
|
||||
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
|
||||
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<select
|
||||
style="max-width: 175px;"
|
||||
class="form-select"
|
||||
bind:value={mode}
|
||||
on:change={modeChanged}
|
||||
>
|
||||
<option value={"user"}>Search User</option>
|
||||
<option value={"project"}>Search Project</option>
|
||||
</select>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={term}
|
||||
on:change={() => termChanged()}
|
||||
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
||||
placeholder={mode == "user" ? "filter username..." : "filter project..."}
|
||||
/>
|
||||
</InputGroup>
|
||||
{:else}
|
||||
<!-- Compatibility: Handle "user role" and "no role" identically-->
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...'
|
||||
/>
|
||||
</InputGroup>
|
||||
<!-- Compatibility: Handle "user role" and "no role" identically-->
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={term}
|
||||
on:change={() => termChanged()}
|
||||
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
||||
placeholder="filter project..."
|
||||
/>
|
||||
</InputGroup>
|
||||
{/if}
|
||||
|
@ -6,115 +6,138 @@
|
||||
- jobTags: Defaults to job.tags, usefull for dynamically updating the tags.
|
||||
-->
|
||||
<script context="module">
|
||||
export const scrambleNames = window.localStorage.getItem("cc-scramble-names")
|
||||
export const scramble = function(str) {
|
||||
if (str === '-') return str
|
||||
else return [...str].reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5).toString(32).substr(0, 6)
|
||||
}
|
||||
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
|
||||
export const scramble = function (str) {
|
||||
if (str === "-") return str;
|
||||
else
|
||||
return [...str]
|
||||
.reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5)
|
||||
.toString(32)
|
||||
.substr(0, 6);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Tag from '../Tag.svelte';
|
||||
import { Badge, Icon } from 'sveltestrap';
|
||||
import Tag from "../Tag.svelte";
|
||||
import { Badge, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let job;
|
||||
export let jobTags = job.tags;
|
||||
export let job;
|
||||
export let jobTags = job.tags;
|
||||
|
||||
function formatDuration(duration) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
duration -= hours * 3600;
|
||||
const minutes = Math.floor(duration / 60);
|
||||
duration -= minutes * 60;
|
||||
const seconds = duration;
|
||||
return `${hours}:${('0' + minutes).slice(-2)}:${('0' + seconds).slice(-2)}`;
|
||||
function formatDuration(duration) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
duration -= hours * 3600;
|
||||
const minutes = Math.floor(duration / 60);
|
||||
duration -= minutes * 60;
|
||||
const seconds = duration;
|
||||
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`;
|
||||
}
|
||||
|
||||
function getStateColor(state) {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "success";
|
||||
case "completed":
|
||||
return "primary";
|
||||
default:
|
||||
return "danger";
|
||||
}
|
||||
|
||||
function getStateColor(state) {
|
||||
switch (state) {
|
||||
case 'running':
|
||||
return 'success'
|
||||
case 'completed':
|
||||
return 'primary'
|
||||
default:
|
||||
return 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<span class="fw-bold"><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> ({job.cluster})</span>
|
||||
{#if job.metaData?.jobName}
|
||||
<br/>
|
||||
{#if job.metaData?.jobName.length <= 25}
|
||||
<div>{job.metaData.jobName}</div>
|
||||
{:else}
|
||||
<div class="truncate" style="cursor:help; width:230px;" title={job.metaData.jobName}>{job.metaData.jobName}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if job.arrayJobId}
|
||||
Array Job: <a href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" target="_blank">#{job.arrayJobId}</a>
|
||||
{/if}
|
||||
</p>
|
||||
<p>
|
||||
<span class="fw-bold"
|
||||
><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
||||
({job.cluster})</span
|
||||
>
|
||||
{#if job.metaData?.jobName}
|
||||
<br />
|
||||
{#if job.metaData?.jobName.length <= 25}
|
||||
<div>{job.metaData.jobName}</div>
|
||||
{:else}
|
||||
<div
|
||||
class="truncate"
|
||||
style="cursor:help; width:230px;"
|
||||
title={job.metaData.jobName}
|
||||
>
|
||||
{job.metaData.jobName}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if job.arrayJobId}
|
||||
Array Job: <a
|
||||
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
|
||||
target="_blank">#{job.arrayJobId}</a
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Icon name="person-fill"/>
|
||||
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
|
||||
{scrambleNames ? scramble(job.user) : job.user}
|
||||
</a>
|
||||
{#if job.userData && job.userData.name}
|
||||
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
|
||||
{/if}
|
||||
{#if job.project && job.project != 'no project'}
|
||||
<br/>
|
||||
<Icon name="people-fill"/>
|
||||
<a class="fst-italic" href="/monitoring/jobs/?project={job.project}&projectMatch=eq" target="_blank">
|
||||
{scrambleNames ? scramble(job.project) : job.project}
|
||||
</a>
|
||||
{/if}
|
||||
</p>
|
||||
<p>
|
||||
<Icon name="person-fill" />
|
||||
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
|
||||
{scrambleNames ? scramble(job.user) : job.user}
|
||||
</a>
|
||||
{#if job.userData && job.userData.name}
|
||||
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
|
||||
{/if}
|
||||
{#if job.project && job.project != "no project"}
|
||||
<br />
|
||||
<Icon name="people-fill" />
|
||||
<a
|
||||
class="fst-italic"
|
||||
href="/monitoring/jobs/?project={job.project}&projectMatch=eq"
|
||||
target="_blank"
|
||||
>
|
||||
{scrambleNames ? scramble(job.project) : job.project}
|
||||
</a>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{#if job.numNodes == 1}
|
||||
{job.resources[0].hostname}
|
||||
{:else}
|
||||
{job.numNodes}
|
||||
{/if}
|
||||
<Icon name="pc-horizontal"/>
|
||||
{#if job.exclusive != 1}
|
||||
(shared)
|
||||
{/if}
|
||||
{#if job.numAcc > 0}
|
||||
, {job.numAcc} <Icon name="gpu-card"/>
|
||||
{/if}
|
||||
{#if job.numHWThreads > 0}
|
||||
, {job.numHWThreads} <Icon name="cpu"/>
|
||||
{/if}
|
||||
<br/>
|
||||
{job.subCluster}
|
||||
</p>
|
||||
<p>
|
||||
{#if job.numNodes == 1}
|
||||
{job.resources[0].hostname}
|
||||
{:else}
|
||||
{job.numNodes}
|
||||
{/if}
|
||||
<Icon name="pc-horizontal" />
|
||||
{#if job.exclusive != 1}
|
||||
(shared)
|
||||
{/if}
|
||||
{#if job.numAcc > 0}
|
||||
, {job.numAcc} <Icon name="gpu-card" />
|
||||
{/if}
|
||||
{#if job.numHWThreads > 0}
|
||||
, {job.numHWThreads} <Icon name="cpu" />
|
||||
{/if}
|
||||
<br />
|
||||
{job.subCluster}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Start: <span class="fw-bold">{(new Date(job.startTime)).toLocaleString()}</span>
|
||||
<br/>
|
||||
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span> <Badge color="{getStateColor(job.state)}">{job.state}</Badge>
|
||||
{#if job.walltime}
|
||||
<br/>
|
||||
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
|
||||
{/if}
|
||||
</p>
|
||||
<p>
|
||||
Start: <span class="fw-bold"
|
||||
>{new Date(job.startTime).toLocaleString()}</span
|
||||
>
|
||||
<br />
|
||||
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span>
|
||||
<Badge color={getStateColor(job.state)}>{job.state}</Badge>
|
||||
{#if job.walltime}
|
||||
<br />
|
||||
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{#each jobTags as tag}
|
||||
<Tag tag={tag}/>
|
||||
{/each}
|
||||
</p>
|
||||
<p>
|
||||
{#each jobTags as tag}
|
||||
<Tag {tag} />
|
||||
{/each}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
@ -9,284 +9,275 @@
|
||||
- update(filters?: [JobFilter])
|
||||
-->
|
||||
<script>
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient,
|
||||
mutationStore,
|
||||
} from "@urql/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { Row, Table, Card, Spinner } from "sveltestrap";
|
||||
import Pagination from "./Pagination.svelte";
|
||||
import JobListRow from "./Row.svelte";
|
||||
import { stickyHeader } from "../utils.js";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient,
|
||||
mutationStore,
|
||||
} from "@urql/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import Pagination from "./Pagination.svelte";
|
||||
import JobListRow from "./Row.svelte";
|
||||
import { stickyHeader } from "../utils.js";
|
||||
|
||||
const ccconfig = getContext("cc-config"),
|
||||
clusters = getContext("clusters"),
|
||||
initialized = getContext("initialized");
|
||||
const ccconfig = getContext("cc-config"),
|
||||
clusters = getContext("clusters"),
|
||||
initialized = getContext("initialized");
|
||||
|
||||
export let sorting = { field: "startTime", order: "DESC" };
|
||||
export let matchedJobs = 0;
|
||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||
export let showFootprint;
|
||||
export let sorting = { field: "startTime", order: "DESC" };
|
||||
export let matchedJobs = 0;
|
||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||
export let showFootprint;
|
||||
|
||||
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let filter = [];
|
||||
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let filter = [];
|
||||
|
||||
const client = getContextClient();
|
||||
const query = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
items {
|
||||
id
|
||||
jobId
|
||||
user
|
||||
project
|
||||
cluster
|
||||
subCluster
|
||||
startTime
|
||||
duration
|
||||
numNodes
|
||||
numHWThreads
|
||||
numAcc
|
||||
walltime
|
||||
resources {
|
||||
hostname
|
||||
}
|
||||
SMT
|
||||
exclusive
|
||||
partition
|
||||
arrayJobId
|
||||
monitoringStatus
|
||||
state
|
||||
tags {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
userData {
|
||||
name
|
||||
}
|
||||
metaData
|
||||
flopsAnyAvg
|
||||
memBwAvg
|
||||
loadAvg
|
||||
}
|
||||
count
|
||||
}
|
||||
const client = getContextClient();
|
||||
const query = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
items {
|
||||
id
|
||||
jobId
|
||||
user
|
||||
project
|
||||
cluster
|
||||
subCluster
|
||||
startTime
|
||||
duration
|
||||
numNodes
|
||||
numHWThreads
|
||||
numAcc
|
||||
walltime
|
||||
resources {
|
||||
hostname
|
||||
}
|
||||
SMT
|
||||
exclusive
|
||||
partition
|
||||
arrayJobId
|
||||
monitoringStatus
|
||||
state
|
||||
tags {
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
userData {
|
||||
name
|
||||
}
|
||||
metaData
|
||||
flopsAnyAvg
|
||||
memBwAvg
|
||||
loadAvg
|
||||
}
|
||||
`;
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: jobs = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { paging, sorting, filter }
|
||||
$: jobs = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
|
||||
|
||||
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
||||
export function refresh() {
|
||||
jobs = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { paging, sorting, filter },
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
}
|
||||
|
||||
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
|
||||
|
||||
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
||||
export function refresh() {
|
||||
jobs = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { paging, sorting, filter },
|
||||
requestPolicy: 'network-only'
|
||||
});
|
||||
// (Re-)query and optionally set new filters.
|
||||
export function update(filters) {
|
||||
if (filters != null) {
|
||||
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
|
||||
if (minRunningFor && minRunningFor > 0) {
|
||||
filters.push({ minRunningFor });
|
||||
}
|
||||
filter = filters;
|
||||
}
|
||||
page = 1;
|
||||
paging = paging = { page, itemsPerPage };
|
||||
}
|
||||
|
||||
// (Re-)query and optionally set new filters.
|
||||
export function update(filters) {
|
||||
if (filters != null) {
|
||||
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
|
||||
if (minRunningFor && minRunningFor > 0) {
|
||||
filters.push({ minRunningFor });
|
||||
}
|
||||
filter = filters;
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}
|
||||
page = 1;
|
||||
paging = paging = { page, itemsPerPage };
|
||||
}
|
||||
`,
|
||||
variables: { name, value },
|
||||
});
|
||||
};
|
||||
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($name: String!, $value: String!) {
|
||||
updateConfiguration(name: $name, value: $value)
|
||||
}
|
||||
`,
|
||||
variables: { name, value }
|
||||
});
|
||||
}
|
||||
function updateConfiguration(value, page) {
|
||||
updateConfigurationMutation({
|
||||
name: "plot_list_jobsPerPage",
|
||||
value: value,
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateConfiguration(value, page) {
|
||||
updateConfigurationMutation({ name: 'plot_list_jobsPerPage', value: value })
|
||||
.subscribe(res => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error
|
||||
// console.log('Error on subscription: ' + res.error)
|
||||
}
|
||||
})
|
||||
};
|
||||
let plotWidth = null;
|
||||
let tableWidth = null;
|
||||
let jobInfoColumnWidth = 250;
|
||||
|
||||
let plotWidth = null;
|
||||
let tableWidth = null;
|
||||
let jobInfoColumnWidth = 250;
|
||||
|
||||
$: if (showFootprint) {
|
||||
plotWidth = Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10
|
||||
)
|
||||
} else {
|
||||
plotWidth = Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / metrics.length - 10
|
||||
)
|
||||
}
|
||||
|
||||
let headerPaddingTop = 0;
|
||||
stickyHeader(
|
||||
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
||||
(x) => (headerPaddingTop = x)
|
||||
$: if (showFootprint) {
|
||||
plotWidth = Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
|
||||
);
|
||||
} else {
|
||||
plotWidth = Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / metrics.length - 10,
|
||||
);
|
||||
}
|
||||
|
||||
let headerPaddingTop = 0;
|
||||
stickyHeader(
|
||||
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
||||
(x) => (headerPaddingTop = x),
|
||||
);
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
<div class="col cc-table-wrapper" bind:clientWidth={tableWidth}>
|
||||
<Table cellspacing="0px" cellpadding="0px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="position-sticky top-0"
|
||||
scope="col"
|
||||
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
Job Info
|
||||
</th>
|
||||
{#if showFootprint}
|
||||
<th
|
||||
class="position-sticky top-0"
|
||||
scope="col"
|
||||
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
Job Footprint
|
||||
</th>
|
||||
{/if}
|
||||
{#each metrics as metric (metric)}
|
||||
<th
|
||||
class="position-sticky top-0 text-center"
|
||||
scope="col"
|
||||
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
{metric}
|
||||
{#if $initialized}
|
||||
({clusters
|
||||
.map((cluster) =>
|
||||
cluster.metricConfig.find(
|
||||
(m) => m.name == metric
|
||||
)
|
||||
)
|
||||
.filter((m) => m != null)
|
||||
.map(
|
||||
(m) =>
|
||||
(m.unit?.prefix
|
||||
? m.unit?.prefix
|
||||
: "") +
|
||||
(m.unit?.base ? m.unit?.base : "")
|
||||
) // Build unitStr
|
||||
.reduce(
|
||||
(arr, unitStr) =>
|
||||
arr.includes(unitStr)
|
||||
? arr
|
||||
: [...arr, unitStr],
|
||||
[]
|
||||
) // w/o this, output would be [unitStr, unitStr]
|
||||
.join(", ")})
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if $jobs.error}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}>
|
||||
<Card body color="danger" class="mb-3"
|
||||
><h2>{$jobs.error.message}</h2></Card
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{:else if $jobs.fetching || !$jobs.data}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}>
|
||||
<Spinner secondary />
|
||||
</td>
|
||||
</tr>
|
||||
{:else if $jobs.data && $initialized}
|
||||
{#each $jobs.data.jobs.items as job (job)}
|
||||
<JobListRow {job} {metrics} {plotWidth} {showFootprint}/>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}>
|
||||
No jobs found
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="col cc-table-wrapper" bind:clientWidth={tableWidth}>
|
||||
<Table cellspacing="0px" cellpadding="0px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="position-sticky top-0"
|
||||
scope="col"
|
||||
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
Job Info
|
||||
</th>
|
||||
{#if showFootprint}
|
||||
<th
|
||||
class="position-sticky top-0"
|
||||
scope="col"
|
||||
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
Job Footprint
|
||||
</th>
|
||||
{/if}
|
||||
{#each metrics as metric (metric)}
|
||||
<th
|
||||
class="position-sticky top-0 text-center"
|
||||
scope="col"
|
||||
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
{metric}
|
||||
{#if $initialized}
|
||||
({clusters
|
||||
.map((cluster) =>
|
||||
cluster.metricConfig.find((m) => m.name == metric),
|
||||
)
|
||||
.filter((m) => m != null)
|
||||
.map(
|
||||
(m) =>
|
||||
(m.unit?.prefix ? m.unit?.prefix : "") +
|
||||
(m.unit?.base ? m.unit?.base : ""),
|
||||
) // Build unitStr
|
||||
.reduce(
|
||||
(arr, unitStr) =>
|
||||
arr.includes(unitStr) ? arr : [...arr, unitStr],
|
||||
[],
|
||||
) // w/o this, output would be [unitStr, unitStr]
|
||||
.join(", ")})
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if $jobs.error}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}>
|
||||
<Card body color="danger" class="mb-3"
|
||||
><h2>{$jobs.error.message}</h2></Card
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{:else if $jobs.fetching || !$jobs.data}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}>
|
||||
<Spinner secondary />
|
||||
</td>
|
||||
</tr>
|
||||
{:else if $jobs.data && $initialized}
|
||||
{#each $jobs.data.jobs.items as job (job)}
|
||||
<JobListRow {job} {metrics} {plotWidth} {showFootprint} />
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}> No jobs found </td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Pagination
|
||||
bind:page
|
||||
{itemsPerPage}
|
||||
itemText="Jobs"
|
||||
totalItems={matchedJobs}
|
||||
on:update={({ detail }) => {
|
||||
if (detail.itemsPerPage != itemsPerPage) {
|
||||
updateConfiguration(
|
||||
detail.itemsPerPage.toString(),
|
||||
detail.page
|
||||
)
|
||||
} else {
|
||||
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
|
||||
}
|
||||
}}
|
||||
bind:page
|
||||
{itemsPerPage}
|
||||
itemText="Jobs"
|
||||
totalItems={matchedJobs}
|
||||
on:update={({ detail }) => {
|
||||
if (detail.itemsPerPage != itemsPerPage) {
|
||||
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
||||
} else {
|
||||
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.cc-table-wrapper {
|
||||
overflow: initial;
|
||||
}
|
||||
.cc-table-wrapper {
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.cc-table-wrapper > :global(table) {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.cc-table-wrapper > :global(table) {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.cc-table-wrapper :global(button) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.cc-table-wrapper :global(button) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.cc-table-wrapper > :global(table > tbody > tr > td) {
|
||||
margin: 0px;
|
||||
padding-left: 5px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.cc-table-wrapper > :global(table > tbody > tr > td) {
|
||||
margin: 0px;
|
||||
padding-left: 5px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
th.position-sticky.top-0 {
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
th.position-sticky.top-0 {
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
|
@ -5,39 +5,46 @@
|
||||
- 'reload': When fired, the parent component shoud refresh its contents
|
||||
-->
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button, Icon, InputGroup } from 'sveltestrap'
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { Button, Icon, InputGroup } from "@sveltestrap/sveltestrap";
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let refreshInterval = null;
|
||||
let refreshIntervalId = null;
|
||||
function refreshIntervalChanged() {
|
||||
if (refreshIntervalId != null)
|
||||
clearInterval(refreshIntervalId);
|
||||
let refreshInterval = null;
|
||||
let refreshIntervalId = null;
|
||||
function refreshIntervalChanged() {
|
||||
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
|
||||
|
||||
if (refreshInterval == null)
|
||||
return;
|
||||
if (refreshInterval == null) return;
|
||||
|
||||
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
|
||||
}
|
||||
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
|
||||
}
|
||||
|
||||
export let initially = null
|
||||
if (initially != null) {
|
||||
refreshInterval = initially * 1000
|
||||
refreshIntervalChanged()
|
||||
}
|
||||
export let initially = null;
|
||||
if (initially != null) {
|
||||
refreshInterval = initially * 1000;
|
||||
refreshIntervalChanged();
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputGroup>
|
||||
<Button outline on:click={() => dispatch("reload")} disabled={refreshInterval != null}>
|
||||
<Icon name="arrow-clockwise" /> Reload
|
||||
</Button>
|
||||
<select class="form-select" bind:value={refreshInterval} on:change={refreshIntervalChanged}>
|
||||
<option value={null}>No periodic reload</option>
|
||||
<option value={ 30 * 1000}>Update every 30 seconds</option>
|
||||
<option value={ 60 * 1000}>Update every minute</option>
|
||||
<option value={2 * 60 * 1000}>Update every two minutes</option>
|
||||
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<Button
|
||||
outline
|
||||
on:click={() => dispatch("reload")}
|
||||
disabled={refreshInterval != null}
|
||||
>
|
||||
<Icon name="arrow-clockwise" /> Reload
|
||||
</Button>
|
||||
<select
|
||||
class="form-select"
|
||||
bind:value={refreshInterval}
|
||||
on:change={refreshIntervalChanged}
|
||||
>
|
||||
<option value={null}>No periodic reload</option>
|
||||
<option value={30 * 1000}>Update every 30 seconds</option>
|
||||
<option value={60 * 1000}>Update every minute</option>
|
||||
<option value={2 * 60 * 1000}>Update every two minutes</option>
|
||||
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
|
||||
|
@ -9,169 +9,190 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { Card, Spinner } from "sveltestrap";
|
||||
import MetricPlot from "../plots/MetricPlot.svelte";
|
||||
import JobInfo from "./JobInfo.svelte";
|
||||
import JobFootprint from "../JobFootprint.svelte";
|
||||
import { maxScope, checkMetricDisabled } from "../utils.js";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import MetricPlot from "../plots/MetricPlot.svelte";
|
||||
import JobInfo from "./JobInfo.svelte";
|
||||
import JobFootprint from "../JobFootprint.svelte";
|
||||
import { maxScope, checkMetricDisabled } from "../utils.js";
|
||||
|
||||
export let job;
|
||||
export let metrics;
|
||||
export let plotWidth;
|
||||
export let plotHeight = 275;
|
||||
export let showFootprint;
|
||||
export let job;
|
||||
export let metrics;
|
||||
export let plotWidth;
|
||||
export let plotHeight = 275;
|
||||
export let showFootprint;
|
||||
|
||||
let { id } = job;
|
||||
let scopes = [job.numNodes == 1 ? "core" : "node"];
|
||||
let { id } = job;
|
||||
let scopes = [job.numNodes == 1 ? "core" : "node"];
|
||||
|
||||
function distinct(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
function distinct(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
||||
const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
|
||||
const client = getContextClient();
|
||||
const query = gql`
|
||||
query ($id: ID!, $queryMetrics: [String!]!, $scopes: [MetricScope!]!) {
|
||||
jobMetrics(id: $id, metrics: $queryMetrics, scopes: $scopes) {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
unit {
|
||||
prefix
|
||||
base
|
||||
}
|
||||
timestep
|
||||
statisticsSeries {
|
||||
min
|
||||
mean
|
||||
max
|
||||
}
|
||||
series {
|
||||
hostname
|
||||
id
|
||||
data
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
||||
const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
|
||||
const client = getContextClient();
|
||||
const query = gql`
|
||||
query ($id: ID!, $queryMetrics: [String!]!, $scopes: [MetricScope!]!) {
|
||||
jobMetrics(id: $id, metrics: $queryMetrics, scopes: $scopes) {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
unit {
|
||||
prefix
|
||||
base
|
||||
}
|
||||
timestep
|
||||
statisticsSeries {
|
||||
min
|
||||
mean
|
||||
max
|
||||
}
|
||||
series {
|
||||
hostname
|
||||
id
|
||||
data
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id, queryMetrics, scopes }
|
||||
$: metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id, queryMetrics, scopes },
|
||||
});
|
||||
|
||||
let queryMetrics = null;
|
||||
$: if (showFootprint) {
|
||||
queryMetrics = [
|
||||
"cpu_load",
|
||||
"flops_any",
|
||||
"mem_used",
|
||||
"mem_bw",
|
||||
"acc_utilization",
|
||||
...metrics,
|
||||
].filter(distinct);
|
||||
scopes = ["node"];
|
||||
} else {
|
||||
queryMetrics = [...metrics];
|
||||
scopes = [job.numNodes == 1 ? "core" : "node"];
|
||||
}
|
||||
|
||||
export function refresh() {
|
||||
metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id, queryMetrics, scopes },
|
||||
// requestPolicy: 'network-only' // use default cache-first for refresh
|
||||
});
|
||||
}
|
||||
|
||||
let queryMetrics = null
|
||||
$: if (showFootprint) {
|
||||
queryMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw', 'acc_utilization', ...metrics].filter(distinct)
|
||||
scopes = ["node"]
|
||||
} else {
|
||||
queryMetrics = [...metrics]
|
||||
scopes = [job.numNodes == 1 ? "core" : "node"]
|
||||
}
|
||||
const selectScope = (jobMetrics) =>
|
||||
jobMetrics.reduce(
|
||||
(a, b) =>
|
||||
maxScope([a.scope, b.scope]) == a.scope
|
||||
? job.numNodes > 1
|
||||
? a
|
||||
: b
|
||||
: job.numNodes > 1
|
||||
? b
|
||||
: a,
|
||||
jobMetrics[0],
|
||||
);
|
||||
|
||||
export function refresh() {
|
||||
metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id, queryMetrics, scopes },
|
||||
// requestPolicy: 'network-only' // use default cache-first for refresh
|
||||
});
|
||||
}
|
||||
const sortAndSelectScope = (jobMetrics) =>
|
||||
metrics
|
||||
.map((name) => jobMetrics.filter((jobMetric) => jobMetric.name == name))
|
||||
.map((jobMetrics) => ({
|
||||
disabled: false,
|
||||
data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null,
|
||||
}))
|
||||
.map((jobMetric) => {
|
||||
if (jobMetric.data) {
|
||||
return {
|
||||
disabled: checkMetricDisabled(
|
||||
jobMetric.data.name,
|
||||
job.cluster,
|
||||
job.subCluster,
|
||||
),
|
||||
data: jobMetric.data,
|
||||
};
|
||||
} else {
|
||||
return jobMetric;
|
||||
}
|
||||
});
|
||||
|
||||
const selectScope = (jobMetrics) =>
|
||||
jobMetrics.reduce(
|
||||
(a, b) =>
|
||||
maxScope([a.scope, b.scope]) == a.scope
|
||||
? job.numNodes > 1
|
||||
? a
|
||||
: b
|
||||
: job.numNodes > 1
|
||||
? b
|
||||
: a,
|
||||
jobMetrics[0]
|
||||
);
|
||||
|
||||
|
||||
const sortAndSelectScope = (jobMetrics) => metrics
|
||||
.map(name => jobMetrics.filter(jobMetric => jobMetric.name == name))
|
||||
.map(jobMetrics => ({ disabled: false, data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null }))
|
||||
.map(jobMetric => {
|
||||
if (jobMetric.data) {
|
||||
return { disabled: checkMetricDisabled(jobMetric.data.name, job.cluster, job.subCluster), data: jobMetric.data }
|
||||
} else {
|
||||
return jobMetric
|
||||
}
|
||||
})
|
||||
|
||||
if (job.monitoringStatus) refresh();
|
||||
if (job.monitoringStatus) refresh();
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<JobInfo {job} />
|
||||
<td>
|
||||
<JobInfo {job} />
|
||||
</td>
|
||||
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
|
||||
<td colspan={metrics.length}>
|
||||
<Card body color="warning">Not monitored or archiving failed</Card>
|
||||
</td>
|
||||
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
|
||||
<td colspan={metrics.length}>
|
||||
<Card body color="warning">Not monitored or archiving failed</Card>
|
||||
</td>
|
||||
{:else if $metricsQuery.fetching}
|
||||
<td colspan={metrics.length} style="text-align: center;">
|
||||
<Spinner secondary />
|
||||
</td>
|
||||
{:else if $metricsQuery.error}
|
||||
<td colspan={metrics.length}>
|
||||
<Card body color="danger" class="mb-3">
|
||||
{$metricsQuery.error.message.length > 500
|
||||
? $metricsQuery.error.message.substring(0, 499) + "..."
|
||||
: $metricsQuery.error.message}
|
||||
</Card>
|
||||
</td>
|
||||
{:else}
|
||||
{#if showFootprint}
|
||||
<td>
|
||||
<JobFootprint
|
||||
job={job}
|
||||
jobMetrics={$metricsQuery.data.jobMetrics}
|
||||
width={plotWidth}
|
||||
view="list"
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
|
||||
<td>
|
||||
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
|
||||
{#if metric.disabled == false && metric.data}
|
||||
<MetricPlot
|
||||
width={plotWidth}
|
||||
height={plotHeight}
|
||||
timestep={metric.data.metric.timestep}
|
||||
scope={metric.data.scope}
|
||||
series={metric.data.metric.series}
|
||||
statisticsSeries={metric.data.metric.statisticsSeries}
|
||||
metric={metric.data.name}
|
||||
{cluster}
|
||||
subCluster={job.subCluster}
|
||||
isShared={(job.exclusive != 1)}
|
||||
resources={job.resources}
|
||||
numhwthreads={job.numHWThreads}
|
||||
numaccs={job.numAcc}
|
||||
/>
|
||||
{:else if metric.disabled == true && metric.data}
|
||||
<Card body color="info">Metric disabled for subcluster <code>{metric.data.name}:{job.subCluster}</code></Card>
|
||||
{:else}
|
||||
<Card body color="warning">No dataset returned</Card>
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{:else if $metricsQuery.fetching}
|
||||
<td colspan={metrics.length} style="text-align: center;">
|
||||
<Spinner secondary />
|
||||
</td>
|
||||
{:else if $metricsQuery.error}
|
||||
<td colspan={metrics.length}>
|
||||
<Card body color="danger" class="mb-3">
|
||||
{$metricsQuery.error.message.length > 500
|
||||
? $metricsQuery.error.message.substring(0, 499) + "..."
|
||||
: $metricsQuery.error.message}
|
||||
</Card>
|
||||
</td>
|
||||
{:else}
|
||||
{#if showFootprint}
|
||||
<td>
|
||||
<JobFootprint
|
||||
{job}
|
||||
jobMetrics={$metricsQuery.data.jobMetrics}
|
||||
width={plotWidth}
|
||||
view="list"
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
|
||||
<td>
|
||||
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
|
||||
{#if metric.disabled == false && metric.data}
|
||||
<MetricPlot
|
||||
width={plotWidth}
|
||||
height={plotHeight}
|
||||
timestep={metric.data.metric.timestep}
|
||||
scope={metric.data.scope}
|
||||
series={metric.data.metric.series}
|
||||
statisticsSeries={metric.data.metric.statisticsSeries}
|
||||
metric={metric.data.name}
|
||||
{cluster}
|
||||
subCluster={job.subCluster}
|
||||
isShared={job.exclusive != 1}
|
||||
resources={job.resources}
|
||||
numhwthreads={job.numHWThreads}
|
||||
numaccs={job.numAcc}
|
||||
/>
|
||||
{:else if metric.disabled == true && metric.data}
|
||||
<Card body color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
>{metric.data.name}:{job.subCluster}</code
|
||||
></Card
|
||||
>
|
||||
{:else}
|
||||
<Card body color="warning">No dataset returned</Card>
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{/if}
|
||||
</tr>
|
||||
|
@ -7,65 +7,94 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Icon, Button, ListGroup, ListGroupItem,
|
||||
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import {
|
||||
Icon,
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let isOpen = false
|
||||
export let sorting = { field: 'startTime', order: 'DESC' }
|
||||
export let isOpen = false;
|
||||
export let sorting = { field: "startTime", order: "DESC" };
|
||||
|
||||
let sortableColumns = [
|
||||
{ field: 'startTime', text: 'Start Time', order: 'DESC' },
|
||||
{ field: 'duration', text: 'Duration', order: 'DESC' },
|
||||
{ field: 'numNodes', text: 'Number of Nodes', order: 'DESC' },
|
||||
{ field: 'memUsedMax', text: 'Max. Memory Used', order: 'DESC' },
|
||||
{ field: 'flopsAnyAvg', text: 'Avg. FLOPs', order: 'DESC' },
|
||||
{ field: 'memBwAvg', text: 'Avg. Memory Bandwidth', order: 'DESC' },
|
||||
{ field: 'netBwAvg', text: 'Avg. Network Bandwidth', order: 'DESC' }
|
||||
]
|
||||
let sortableColumns = [
|
||||
{ field: "startTime", text: "Start Time", order: "DESC" },
|
||||
{ field: "duration", text: "Duration", order: "DESC" },
|
||||
{ field: "numNodes", text: "Number of Nodes", order: "DESC" },
|
||||
{ field: "memUsedMax", text: "Max. Memory Used", order: "DESC" },
|
||||
{ field: "flopsAnyAvg", text: "Avg. FLOPs", order: "DESC" },
|
||||
{ field: "memBwAvg", text: "Avg. Memory Bandwidth", order: "DESC" },
|
||||
{ field: "netBwAvg", text: "Avg. Network Bandwidth", order: "DESC" },
|
||||
];
|
||||
|
||||
let activeColumnIdx = sortableColumns.findIndex(col => col.field == sorting.field)
|
||||
sortableColumns[activeColumnIdx].order = sorting.order
|
||||
let activeColumnIdx = sortableColumns.findIndex(
|
||||
(col) => col.field == sorting.field,
|
||||
);
|
||||
sortableColumns[activeColumnIdx].order = sorting.order;
|
||||
</script>
|
||||
|
||||
<Modal isOpen={isOpen} toggle={() => { isOpen = !isOpen }}>
|
||||
<ModalHeader>
|
||||
Sort rows
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each sortableColumns as col, i (col)}
|
||||
<ListGroupItem>
|
||||
<button class="sort" on:click={() => {
|
||||
if (activeColumnIdx == i) {
|
||||
col.order = col.order == 'DESC' ? 'ASC' : 'DESC'
|
||||
} else {
|
||||
sortableColumns[activeColumnIdx] = { ...sortableColumns[activeColumnIdx] }
|
||||
}
|
||||
<Modal
|
||||
{isOpen}
|
||||
toggle={() => {
|
||||
isOpen = !isOpen;
|
||||
}}
|
||||
>
|
||||
<ModalHeader>Sort rows</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#each sortableColumns as col, i (col)}
|
||||
<ListGroupItem>
|
||||
<button
|
||||
class="sort"
|
||||
on:click={() => {
|
||||
if (activeColumnIdx == i) {
|
||||
col.order = col.order == "DESC" ? "ASC" : "DESC";
|
||||
} else {
|
||||
sortableColumns[activeColumnIdx] = {
|
||||
...sortableColumns[activeColumnIdx],
|
||||
};
|
||||
}
|
||||
|
||||
sortableColumns[i] = { ...sortableColumns[i] }
|
||||
activeColumnIdx = i
|
||||
sortableColumns = [...sortableColumns]
|
||||
sorting = { field: col.field, order: col.order }
|
||||
}}>
|
||||
<Icon name="arrow-{col.order == 'DESC' ? 'down' : 'up'}-circle{i == activeColumnIdx ? '-fill' : ''}"/>
|
||||
</button>
|
||||
sortableColumns[i] = { ...sortableColumns[i] };
|
||||
activeColumnIdx = i;
|
||||
sortableColumns = [...sortableColumns];
|
||||
sorting = { field: col.field, order: col.order };
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-{col.order == 'DESC' ? 'down' : 'up'}-circle{i ==
|
||||
activeColumnIdx
|
||||
? '-fill'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{col.text}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" on:click={() => { isOpen = false }}>Close</Button>
|
||||
</ModalFooter>
|
||||
{col.text}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
}}>Close</Button
|
||||
>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.sort {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: 0 0;
|
||||
transition: all 70ms;
|
||||
}
|
||||
</style>
|
||||
.sort {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: 0 0;
|
||||
transition: all 70ms;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -5,221 +5,222 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import uPlot from 'uplot'
|
||||
import { formatNumber } from '../units.js'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Card } from 'sveltestrap'
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let data
|
||||
export let usesBins = false
|
||||
export let width = 500
|
||||
export let height = 300
|
||||
export let title = ''
|
||||
export let xlabel = ''
|
||||
export let xunit = 'X'
|
||||
export let ylabel = ''
|
||||
export let yunit = 'Y'
|
||||
export let data;
|
||||
export let usesBins = false;
|
||||
export let width = 500;
|
||||
export let height = 300;
|
||||
export let title = "";
|
||||
export let xlabel = "";
|
||||
export let xunit = "X";
|
||||
export let ylabel = "";
|
||||
export let yunit = "Y";
|
||||
|
||||
const { bars } = uPlot.paths
|
||||
const { bars } = uPlot.paths;
|
||||
|
||||
const drawStyles = {
|
||||
bars: 1,
|
||||
points: 2,
|
||||
const drawStyles = {
|
||||
bars: 1,
|
||||
points: 2,
|
||||
};
|
||||
|
||||
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
||||
let s = u.series[seriesIdx];
|
||||
let style = s.drawStyle;
|
||||
|
||||
let renderer = // If bars to wide, change here
|
||||
style == drawStyles.bars ? bars({ size: [0.75, 100] }) : () => null;
|
||||
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
}
|
||||
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({
|
||||
className,
|
||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||
} = {}) {
|
||||
let legendEl;
|
||||
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
|
||||
uPlot.assign(legendEl.style, {
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||
...style,
|
||||
});
|
||||
|
||||
// hide series color markers
|
||||
const idents = legendEl.querySelectorAll(".u-marker");
|
||||
|
||||
for (let i = 0; i < idents.length; i++) idents[i].style.display = "none";
|
||||
|
||||
const overEl = u.over;
|
||||
overEl.style.overflow = "visible";
|
||||
|
||||
// move legend into plot bounds
|
||||
overEl.appendChild(legendEl);
|
||||
|
||||
// show/hide tooltip on enter/exit
|
||||
overEl.addEventListener("mouseenter", () => {
|
||||
legendEl.style.display = null;
|
||||
});
|
||||
overEl.addEventListener("mouseleave", () => {
|
||||
legendEl.style.display = "none";
|
||||
});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
legendEl.style.transform =
|
||||
"translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let plotWrapper = null;
|
||||
let uplot = null;
|
||||
let timeoutId = null;
|
||||
|
||||
function render() {
|
||||
let opts = {
|
||||
width: width,
|
||||
height: height,
|
||||
title: title,
|
||||
plugins: [legendAsTooltipPlugin()],
|
||||
cursor: {
|
||||
points: {
|
||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
||||
width: (u, seriesIdx, size) => size / 4,
|
||||
stroke: (u, seriesIdx) =>
|
||||
u.series[seriesIdx].points.stroke(u, seriesIdx) + "90",
|
||||
fill: (u, seriesIdx) => "#fff",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
},
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'x',
|
||||
label: xlabel,
|
||||
labelGap: 10,
|
||||
size: 25,
|
||||
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map((v) => formatNumber(v)),
|
||||
},
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'y',
|
||||
label: ylabel,
|
||||
labelGap: 10,
|
||||
size: 35,
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map((v) => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
label: xunit !== "" ? xunit : null,
|
||||
value: (u, ts, sidx, didx) => {
|
||||
if (usesBins) {
|
||||
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0;
|
||||
const max = u.data[sidx][didx];
|
||||
ts = min + " - " + max; // narrow spaces
|
||||
}
|
||||
return ts;
|
||||
},
|
||||
},
|
||||
Object.assign(
|
||||
{
|
||||
label: yunit !== "" ? yunit : null,
|
||||
width: 1 / devicePixelRatio,
|
||||
drawStyle: drawStyles.points,
|
||||
lineInterpolation: null,
|
||||
paths,
|
||||
},
|
||||
{
|
||||
drawStyle: drawStyles.bars,
|
||||
lineInterpolation: null,
|
||||
stroke: "#85abce",
|
||||
fill: "#85abce", // + "1A", // Transparent Fill
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
||||
let s = u.series[seriesIdx];
|
||||
let style = s.drawStyle;
|
||||
uplot = new uPlot(opts, data, plotWrapper);
|
||||
}
|
||||
|
||||
let renderer = ( // If bars to wide, change here
|
||||
style == drawStyles.bars ? (
|
||||
bars({size: [0.75, 100]})
|
||||
) :
|
||||
() => null
|
||||
)
|
||||
onMount(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
}
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
||||
let legendEl;
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
uPlot.assign(legendEl.style, {
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||
...style
|
||||
});
|
||||
render();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// hide series color markers
|
||||
const idents = legendEl.querySelectorAll(".u-marker");
|
||||
|
||||
for (let i = 0; i < idents.length; i++)
|
||||
idents[i].style.display = "none";
|
||||
|
||||
const overEl = u.over;
|
||||
overEl.style.overflow = "visible";
|
||||
|
||||
// move legend into plot bounds
|
||||
overEl.appendChild(legendEl);
|
||||
|
||||
// show/hide tooltip on enter/exit
|
||||
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
||||
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let plotWrapper = null
|
||||
let uplot = null
|
||||
let timeoutId = null
|
||||
|
||||
function render() {
|
||||
let opts = {
|
||||
width: width,
|
||||
height: height,
|
||||
title: title,
|
||||
plugins: [
|
||||
legendAsTooltipPlugin()
|
||||
],
|
||||
cursor: {
|
||||
points: {
|
||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
||||
width: (u, seriesIdx, size) => size / 4,
|
||||
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90',
|
||||
fill: (u, seriesIdx) => "#fff",
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
time: false
|
||||
},
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'x',
|
||||
label: xlabel,
|
||||
labelGap: 10,
|
||||
size: 25,
|
||||
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map(v => formatNumber(v)),
|
||||
},
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'y',
|
||||
label: ylabel,
|
||||
labelGap: 10,
|
||||
size: 35,
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map(v => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
label: xunit !== '' ? xunit : null,
|
||||
value: (u, ts, sidx, didx) => {
|
||||
if (usesBins) {
|
||||
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0
|
||||
const max = u.data[sidx][didx]
|
||||
ts = min + ' - ' + max // narrow spaces
|
||||
}
|
||||
return ts
|
||||
}
|
||||
},
|
||||
Object.assign({
|
||||
label: yunit !== '' ? yunit : null,
|
||||
width: 1 / devicePixelRatio,
|
||||
drawStyle: drawStyles.points,
|
||||
lineInterpolation: null,
|
||||
paths,
|
||||
}, {
|
||||
drawStyle: drawStyles.bars,
|
||||
lineInterpolation: null,
|
||||
stroke: "#85abce",
|
||||
fill: "#85abce", // + "1A", // Transparent Fill
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
||||
uplot = new uPlot(opts, data, plotWrapper)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
render()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
|
||||
render()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
$: sizeChanged(width, height);
|
||||
</script>
|
||||
|
||||
{#if data.length > 0}
|
||||
<div bind:this={plotWrapper}/>
|
||||
<div bind:this={plotWrapper} />
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
|
||||
<Card class="mx-4" body color="warning"
|
||||
>Cannot render histogram: No data!</Card
|
||||
>
|
||||
{/if}
|
||||
|
||||
|
||||
|
@ -1,3 +1,121 @@
|
||||
<script context="module">
|
||||
export function formatTime(t, forNode = false) {
|
||||
if (t !== null) {
|
||||
if (isNaN(t)) {
|
||||
return t;
|
||||
} else {
|
||||
const tAbs = Math.abs(t);
|
||||
const h = Math.floor(tAbs / 3600);
|
||||
const m = Math.floor((tAbs % 3600) / 60);
|
||||
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
||||
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
|
||||
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
|
||||
else return `${forNode ? "-" : ""}${h}:${m}h`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function timeIncrs(timestep, maxX, forNode) {
|
||||
if (forNode === true) {
|
||||
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
||||
} else {
|
||||
let incrs = [];
|
||||
for (let t = timestep; t < maxX; t *= 10)
|
||||
incrs.push(t, t * 2, t * 3, t * 5);
|
||||
|
||||
return incrs;
|
||||
}
|
||||
}
|
||||
|
||||
export function findThresholds(
|
||||
metricConfig,
|
||||
scope,
|
||||
subCluster,
|
||||
isShared,
|
||||
hwthreads,
|
||||
) {
|
||||
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
|
||||
if (!metricConfig || !scope || !subCluster) {
|
||||
console.warn("Argument missing for findThresholds!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
(scope == "node" && isShared == false) ||
|
||||
metricConfig.aggregation == "avg"
|
||||
) {
|
||||
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
|
||||
// console.log('subClusterConfigs array empty, use metricConfig defaults')
|
||||
return {
|
||||
normal: metricConfig.normal,
|
||||
caution: metricConfig.caution,
|
||||
alert: metricConfig.alert,
|
||||
peak: metricConfig.peak,
|
||||
};
|
||||
} else if (
|
||||
metricConfig.subClusters &&
|
||||
metricConfig.subClusters.length > 0
|
||||
) {
|
||||
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
|
||||
let forSubCluster = metricConfig.subClusters.find(
|
||||
(sc) => sc.name == subCluster.name,
|
||||
);
|
||||
if (
|
||||
forSubCluster &&
|
||||
forSubCluster.normal &&
|
||||
forSubCluster.caution &&
|
||||
forSubCluster.alert &&
|
||||
forSubCluster.peak
|
||||
)
|
||||
return forSubCluster;
|
||||
else
|
||||
return {
|
||||
normal: metricConfig.normal,
|
||||
caution: metricConfig.caution,
|
||||
alert: metricConfig.alert,
|
||||
peak: metricConfig.peak,
|
||||
};
|
||||
} else {
|
||||
console.warn("metricConfig.subClusters not found!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (metricConfig.aggregation != "sum") {
|
||||
console.warn(
|
||||
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||
metricConfig,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let divisor = 1
|
||||
if (isShared == true) { // Shared
|
||||
if (numaccs > 0) divisor = subCluster.topology.accelerators.length / numaccs;
|
||||
else if (numhwthreads > 0) divisor = subCluster.topology.node.length / numhwthreads;
|
||||
}
|
||||
else if (scope == 'socket') divisor = subCluster.topology.socket.length;
|
||||
else if (scope == "core") divisor = subCluster.topology.core.length;
|
||||
else if (scope == "accelerator")
|
||||
divisor = subCluster.topology.accelerators.length;
|
||||
else if (scope == "hwthread") divisor = subCluster.topology.node.length;
|
||||
else {
|
||||
// console.log('TODO: how to calc thresholds for ', scope)
|
||||
return null;
|
||||
}
|
||||
|
||||
let mc =
|
||||
metricConfig?.subClusters?.find((sc) => sc.name == subCluster.name) ||
|
||||
metricConfig;
|
||||
return {
|
||||
peak: mc.peak / divisor,
|
||||
normal: mc.normal / divisor,
|
||||
caution: mc.caution / divisor,
|
||||
alert: mc.alert / divisor,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
|
||||
@ -21,433 +139,407 @@
|
||||
// TODO: Move helper functions to module context?
|
||||
-->
|
||||
<script>
|
||||
import uPlot from 'uplot'
|
||||
import { formatNumber } from '../units.js'
|
||||
import { getContext, onMount, onDestroy } from 'svelte'
|
||||
import { Card } from 'sveltestrap'
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let metric
|
||||
export let scope = 'node'
|
||||
export let resources = []
|
||||
export let width
|
||||
export let height
|
||||
export let timestep
|
||||
export let series
|
||||
export let useStatsSeries = null
|
||||
export let statisticsSeries = null
|
||||
export let cluster
|
||||
export let subCluster
|
||||
export let isShared = false
|
||||
export let forNode = false
|
||||
export let numhwthreads = 0
|
||||
export let numaccs = 0
|
||||
export let metric;
|
||||
export let scope = "node";
|
||||
export let resources = [];
|
||||
export let width;
|
||||
export let height;
|
||||
export let timestep;
|
||||
export let series;
|
||||
export let useStatsSeries = null;
|
||||
export let statisticsSeries = null;
|
||||
export let cluster;
|
||||
export let subCluster;
|
||||
export let isShared = false;
|
||||
export let forNode = false;
|
||||
export let hwthreads = 0;
|
||||
|
||||
if (useStatsSeries == null)
|
||||
useStatsSeries = statisticsSeries != null
|
||||
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
||||
|
||||
if (useStatsSeries == false && series == null)
|
||||
useStatsSeries = true
|
||||
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
||||
|
||||
const metricConfig = getContext('metrics')(cluster, metric)
|
||||
const clusterCockpitConfig = getContext('cc-config')
|
||||
const resizeSleepTime = 250
|
||||
const normalLineColor = '#000000'
|
||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio
|
||||
const lineColors = clusterCockpitConfig.plot_general_colorscheme
|
||||
const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' }
|
||||
const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster, isShared, numhwthreads, numaccs)
|
||||
const metricConfig = getContext("metrics")(cluster, metric);
|
||||
const clusterCockpitConfig = getContext("cc-config");
|
||||
const resizeSleepTime = 250;
|
||||
const normalLineColor = "#000000";
|
||||
const lineWidth =
|
||||
clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
||||
const lineColors = clusterCockpitConfig.plot_general_colorscheme;
|
||||
const backgroundColors = {
|
||||
normal: "rgba(255, 255, 255, 1.0)",
|
||||
caution: "rgba(255, 128, 0, 0.3)",
|
||||
alert: "rgba(255, 0, 0, 0.3)",
|
||||
};
|
||||
const thresholds = findThresholds(
|
||||
metricConfig,
|
||||
scope,
|
||||
typeof subCluster == "string"
|
||||
? cluster.subClusters.find((sc) => sc.name == subCluster)
|
||||
: subCluster,
|
||||
isShared,
|
||||
hwthreads,
|
||||
);
|
||||
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
||||
let legendEl;
|
||||
const dataSize = series.length
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({
|
||||
className,
|
||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||
} = {}) {
|
||||
let legendEl;
|
||||
const dataSize = series.length;
|
||||
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
|
||||
uPlot.assign(legendEl.style, {
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||
...style
|
||||
});
|
||||
uPlot.assign(legendEl.style, {
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||
...style,
|
||||
});
|
||||
|
||||
// conditional hide series color markers:
|
||||
if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory
|
||||
dataSize === 1 || // Only one Y-Dataseries
|
||||
dataSize > 6 ){ // More than 6 Y-Dataseries
|
||||
const idents = legendEl.querySelectorAll(".u-marker");
|
||||
for (let i = 0; i < idents.length; i++)
|
||||
idents[i].style.display = "none";
|
||||
}
|
||||
// conditional hide series color markers:
|
||||
if (
|
||||
useStatsSeries === true || // Min/Max/Avg Self-Explanatory
|
||||
dataSize === 1 || // Only one Y-Dataseries
|
||||
dataSize > 6
|
||||
) {
|
||||
// More than 6 Y-Dataseries
|
||||
const idents = legendEl.querySelectorAll(".u-marker");
|
||||
for (let i = 0; i < idents.length; i++)
|
||||
idents[i].style.display = "none";
|
||||
}
|
||||
|
||||
const overEl = u.over;
|
||||
overEl.style.overflow = "visible";
|
||||
const overEl = u.over;
|
||||
overEl.style.overflow = "visible";
|
||||
|
||||
// move legend into plot bounds
|
||||
overEl.appendChild(legendEl);
|
||||
// move legend into plot bounds
|
||||
overEl.appendChild(legendEl);
|
||||
|
||||
// show/hide tooltip on enter/exit
|
||||
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
||||
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
||||
// show/hide tooltip on enter/exit
|
||||
overEl.addEventListener("mouseenter", () => {
|
||||
legendEl.style.display = null;
|
||||
});
|
||||
overEl.addEventListener("mouseleave", () => {
|
||||
legendEl.style.display = "none";
|
||||
});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
const width = u.over.querySelector(".u-legend").offsetWidth;
|
||||
legendEl.style.transform = "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
if (dataSize <= 12 || useStatsSeries === true) {
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
}
|
||||
}
|
||||
} else { // Setting legend-opts show/live as object with false here will not work ...
|
||||
return {}
|
||||
}
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function backgroundColor() {
|
||||
if (clusterCockpitConfig.plot_general_colorBackground == false
|
||||
|| !thresholds
|
||||
|| !(series && series.every(s => s.statistics != null)))
|
||||
return backgroundColors.normal
|
||||
|
||||
let cond = thresholds.alert < thresholds.caution
|
||||
? (a, b) => a <= b
|
||||
: (a, b) => a >= b
|
||||
|
||||
let avg = series.reduce((sum, series) => sum + series.statistics.avg, 0) / series.length
|
||||
|
||||
if (Number.isNaN(avg))
|
||||
return backgroundColors.normal
|
||||
|
||||
if (cond(avg, thresholds.alert))
|
||||
return backgroundColors.alert
|
||||
|
||||
if (cond(avg, thresholds.caution))
|
||||
return backgroundColors.caution
|
||||
|
||||
return backgroundColors.normal
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
const width = u.over.querySelector(".u-legend").offsetWidth;
|
||||
legendEl.style.transform =
|
||||
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
function lineColor(i, n) {
|
||||
if (n >= lineColors.length)
|
||||
return lineColors[i % lineColors.length];
|
||||
else
|
||||
return lineColors[Math.floor((i / n) * lineColors.length)];
|
||||
if (dataSize <= 12 || useStatsSeries === true) {
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Setting legend-opts show/live as object with false here will not work ...
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const longestSeries = useStatsSeries
|
||||
? statisticsSeries.mean.length
|
||||
: series.reduce((n, series) => Math.max(n, series.data.length), 0)
|
||||
const maxX = longestSeries * timestep
|
||||
let maxY = null
|
||||
|
||||
if (thresholds !== null) {
|
||||
maxY = useStatsSeries
|
||||
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal)
|
||||
: (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal)
|
||||
function backgroundColor() {
|
||||
if (
|
||||
clusterCockpitConfig.plot_general_colorBackground == false ||
|
||||
!thresholds ||
|
||||
!(series && series.every((s) => s.statistics != null))
|
||||
)
|
||||
return backgroundColors.normal;
|
||||
|
||||
if (maxY >= (10 * thresholds.peak)) { // Hard y-range render limit if outliers in series data
|
||||
maxY = (10 * thresholds.peak)
|
||||
}
|
||||
let cond =
|
||||
thresholds.alert < thresholds.caution
|
||||
? (a, b) => a <= b
|
||||
: (a, b) => a >= b;
|
||||
|
||||
let avg =
|
||||
series.reduce((sum, series) => sum + series.statistics.avg, 0) /
|
||||
series.length;
|
||||
|
||||
if (Number.isNaN(avg)) return backgroundColors.normal;
|
||||
|
||||
if (cond(avg, thresholds.alert)) return backgroundColors.alert;
|
||||
|
||||
if (cond(avg, thresholds.caution)) return backgroundColors.caution;
|
||||
|
||||
return backgroundColors.normal;
|
||||
}
|
||||
|
||||
function lineColor(i, n) {
|
||||
if (n >= lineColors.length) return lineColors[i % lineColors.length];
|
||||
else return lineColors[Math.floor((i / n) * lineColors.length)];
|
||||
}
|
||||
|
||||
const longestSeries = useStatsSeries
|
||||
? statisticsSeries.mean.length
|
||||
: series.reduce((n, series) => Math.max(n, series.data.length), 0);
|
||||
const maxX = longestSeries * timestep;
|
||||
let maxY = null;
|
||||
|
||||
if (thresholds !== null) {
|
||||
maxY = useStatsSeries
|
||||
? statisticsSeries.max.reduce(
|
||||
(max, x) => Math.max(max, x),
|
||||
thresholds.normal,
|
||||
) || thresholds.normal
|
||||
: series.reduce(
|
||||
(max, series) => Math.max(max, series.statistics?.max),
|
||||
thresholds.normal,
|
||||
) || thresholds.normal;
|
||||
|
||||
if (maxY >= 10 * thresholds.peak) {
|
||||
// Hard y-range render limit if outliers in series data
|
||||
maxY = 10 * thresholds.peak;
|
||||
}
|
||||
}
|
||||
|
||||
const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts, forNode)}]
|
||||
const plotData = [new Array(longestSeries)]
|
||||
const plotSeries = [
|
||||
{
|
||||
label: "Runtime",
|
||||
value: (u, ts, sidx, didx) =>
|
||||
didx == null ? null : formatTime(ts, forNode),
|
||||
},
|
||||
];
|
||||
const plotData = [new Array(longestSeries)];
|
||||
|
||||
if (forNode === true) {
|
||||
// Negative Timestamp Buildup
|
||||
for (let i = 0; i <= longestSeries; i++) {
|
||||
plotData[0][i] = (longestSeries - i) * timestep * -1;
|
||||
}
|
||||
} else {
|
||||
// Positive Timestamp Buildup
|
||||
for (
|
||||
let j = 0;
|
||||
j < longestSeries;
|
||||
j++ // TODO: Cache/Reuse this array?
|
||||
)
|
||||
plotData[0][j] = j * timestep;
|
||||
}
|
||||
|
||||
let plotBands = undefined;
|
||||
if (useStatsSeries) {
|
||||
plotData.push(statisticsSeries.min);
|
||||
plotData.push(statisticsSeries.max);
|
||||
plotData.push(statisticsSeries.mean);
|
||||
|
||||
if (forNode === true) {
|
||||
// Negative Timestamp Buildup
|
||||
for (let i = 0; i <= longestSeries; i++) {
|
||||
plotData[0][i] = (longestSeries - i) * timestep * -1
|
||||
}
|
||||
// timestamp 0 with null value for reversed time axis
|
||||
if (plotData[1].length != 0) plotData[1].push(null);
|
||||
if (plotData[2].length != 0) plotData[2].push(null);
|
||||
if (plotData[3].length != 0) plotData[3].push(null);
|
||||
}
|
||||
|
||||
plotSeries.push({
|
||||
label: "min",
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: "red",
|
||||
});
|
||||
plotSeries.push({
|
||||
label: "max",
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: "green",
|
||||
});
|
||||
plotSeries.push({
|
||||
label: "mean",
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: "black",
|
||||
});
|
||||
|
||||
plotBands = [
|
||||
{ series: [2, 3], fill: "rgba(0,255,0,0.1)" },
|
||||
{ series: [3, 1], fill: "rgba(255,0,0,0.1)" },
|
||||
];
|
||||
} else {
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
plotData.push(series[i].data);
|
||||
if (forNode === true && plotData[1].length != 0) plotData[1].push(null); // timestamp 0 with null value for reversed time axis
|
||||
plotSeries.push({
|
||||
label:
|
||||
scope === "node"
|
||||
? resources[i].hostname
|
||||
: // scope === 'accelerator' ? resources[0].accelerators[i] :
|
||||
scope + " #" + (i + 1),
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: lineColor(i, series.length),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const opts = {
|
||||
width,
|
||||
height,
|
||||
plugins: [legendAsTooltipPlugin()],
|
||||
series: plotSeries,
|
||||
axes: [
|
||||
{
|
||||
scale: "x",
|
||||
space: 35,
|
||||
incrs: timeIncrs(timestep, maxX, forNode),
|
||||
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
|
||||
},
|
||||
{
|
||||
scale: "y",
|
||||
grid: { show: true },
|
||||
labelFont: "sans-serif",
|
||||
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
bands: plotBands,
|
||||
padding: [5, 10, -20, 0],
|
||||
hooks: {
|
||||
draw: [
|
||||
(u) => {
|
||||
// Draw plot type label:
|
||||
let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
|
||||
useStatsSeries
|
||||
? ": min/avg/max"
|
||||
: metricConfig != null && scope != metricConfig.scope
|
||||
? ` (${metricConfig.aggregation})`
|
||||
: ""
|
||||
}`;
|
||||
let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
|
||||
u.ctx.save();
|
||||
u.ctx.textAlign = "start"; // 'end'
|
||||
u.ctx.fillStyle = "black";
|
||||
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10);
|
||||
u.ctx.textAlign = "end";
|
||||
u.ctx.fillStyle = "black";
|
||||
u.ctx.fillText(
|
||||
textr,
|
||||
u.bbox.left + u.bbox.width - 10,
|
||||
u.bbox.top + 10,
|
||||
);
|
||||
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
||||
|
||||
if (!thresholds) {
|
||||
u.ctx.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
let y = u.valToPos(thresholds.normal, "y", true);
|
||||
u.ctx.save();
|
||||
u.ctx.lineWidth = lineWidth;
|
||||
u.ctx.strokeStyle = normalLineColor;
|
||||
u.ctx.setLineDash([5, 5]);
|
||||
u.ctx.beginPath();
|
||||
u.ctx.moveTo(u.bbox.left, y);
|
||||
u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
|
||||
u.ctx.stroke();
|
||||
u.ctx.restore();
|
||||
},
|
||||
],
|
||||
},
|
||||
scales: {
|
||||
x: { time: false },
|
||||
y: maxY ? { range: [0, maxY * 1.1] } : {},
|
||||
},
|
||||
legend: {
|
||||
// Display legend until max 12 Y-dataseries
|
||||
show: series.length <= 12 || useStatsSeries === true ? true : false,
|
||||
live: series.length <= 12 || useStatsSeries === true ? true : false,
|
||||
},
|
||||
cursor: { drag: { x: true, y: true } },
|
||||
};
|
||||
|
||||
// console.log(opts)
|
||||
|
||||
let plotWrapper = null;
|
||||
let uplot = null;
|
||||
let timeoutId = null;
|
||||
let prevWidth = null,
|
||||
prevHeight = null;
|
||||
|
||||
function render() {
|
||||
if (!width || Number.isNaN(width) || width < 0) return;
|
||||
|
||||
if (prevWidth != null && Math.abs(prevWidth - width) < 10) return;
|
||||
|
||||
prevWidth = width;
|
||||
prevHeight = height;
|
||||
|
||||
if (!uplot) {
|
||||
opts.width = width;
|
||||
opts.height = height;
|
||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||
} else {
|
||||
// Positive Timestamp Buildup
|
||||
for (let j = 0; j < longestSeries; j++) // TODO: Cache/Reuse this array?
|
||||
plotData[0][j] = j * timestep
|
||||
uplot.setSize({ width, height });
|
||||
}
|
||||
}
|
||||
|
||||
let plotBands = undefined
|
||||
if (useStatsSeries) {
|
||||
plotData.push(statisticsSeries.min)
|
||||
plotData.push(statisticsSeries.max)
|
||||
plotData.push(statisticsSeries.mean)
|
||||
function onSizeChange() {
|
||||
if (!uplot) return;
|
||||
|
||||
if (forNode === true) { // timestamp 0 with null value for reversed time axis
|
||||
if (plotData[1].length != 0) plotData[1].push(null)
|
||||
if (plotData[2].length != 0) plotData[2].push(null)
|
||||
if (plotData[3].length != 0) plotData[3].push(null)
|
||||
}
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
plotSeries.push({ label: 'min', scale: 'y', width: lineWidth, stroke: 'red' })
|
||||
plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' })
|
||||
plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' })
|
||||
|
||||
plotBands = [
|
||||
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' },
|
||||
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' }
|
||||
];
|
||||
} else {
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
plotData.push(series[i].data)
|
||||
if (forNode === true && plotData[1].length != 0) plotData[1].push(null) // timestamp 0 with null value for reversed time axis
|
||||
plotSeries.push({
|
||||
label: scope === 'node' ? resources[i].hostname :
|
||||
// scope === 'accelerator' ? resources[0].accelerators[i] :
|
||||
scope + ' #' + (i+1),
|
||||
scale: 'y',
|
||||
width: lineWidth,
|
||||
stroke: lineColor(i, series.length)
|
||||
})
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
render();
|
||||
}, resizeSleepTime);
|
||||
}
|
||||
|
||||
$: if (series[0].data.length > 0) {
|
||||
onSizeChange(width, height);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (series[0].data.length > 0) {
|
||||
plotWrapper.style.backgroundColor = backgroundColor();
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
const opts = {
|
||||
width,
|
||||
height,
|
||||
plugins: [
|
||||
legendAsTooltipPlugin()
|
||||
],
|
||||
series: plotSeries,
|
||||
axes: [
|
||||
{
|
||||
scale: 'x',
|
||||
space: 35,
|
||||
incrs: timeIncrs(timestep, maxX, forNode),
|
||||
values: (_, vals) => vals.map(v => formatTime(v, forNode))
|
||||
},
|
||||
{
|
||||
scale: 'y',
|
||||
grid: { show: true },
|
||||
labelFont: 'sans-serif',
|
||||
values: (u, vals) => vals.map(v => formatNumber(v))
|
||||
}
|
||||
],
|
||||
bands: plotBands,
|
||||
padding: [5, 10, -20, 0],
|
||||
hooks: {
|
||||
draw: [(u) => {
|
||||
// Draw plot type label:
|
||||
let textl = `${scope}${plotSeries.length > 2 ? 's' : ''}${
|
||||
useStatsSeries ? ': min/avg/max' : (metricConfig != null && scope != metricConfig.scope ? ` (${metricConfig.aggregation})` : '')}`
|
||||
let textr = `${(isShared && (scope != 'core' && scope != 'accelerator')) ? '[Shared]' : '' }`
|
||||
u.ctx.save()
|
||||
u.ctx.textAlign = 'start' // 'end'
|
||||
u.ctx.fillStyle = 'black'
|
||||
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10)
|
||||
u.ctx.textAlign = 'end'
|
||||
u.ctx.fillStyle = 'black'
|
||||
u.ctx.fillText(textr, u.bbox.left + u.bbox.width - 10, u.bbox.top + 10)
|
||||
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
if (!thresholds) {
|
||||
u.ctx.restore()
|
||||
return
|
||||
}
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
let y = u.valToPos(thresholds.normal, 'y', true)
|
||||
u.ctx.save()
|
||||
u.ctx.lineWidth = lineWidth
|
||||
u.ctx.strokeStyle = normalLineColor
|
||||
u.ctx.setLineDash([5, 5])
|
||||
u.ctx.beginPath()
|
||||
u.ctx.moveTo(u.bbox.left, y)
|
||||
u.ctx.lineTo(u.bbox.left + u.bbox.width, y)
|
||||
u.ctx.stroke()
|
||||
u.ctx.restore()
|
||||
}]
|
||||
},
|
||||
scales: {
|
||||
x: { time: false },
|
||||
y: maxY ? { range: [0., maxY * 1.1] } : {}
|
||||
},
|
||||
legend : { // Display legend until max 12 Y-dataseries
|
||||
show: (series.length <= 12 || useStatsSeries === true) ? true : false,
|
||||
live: (series.length <= 12 || useStatsSeries === true) ? true : false
|
||||
},
|
||||
cursor: { drag: { x: true, y: true } }
|
||||
}
|
||||
|
||||
// console.log(opts)
|
||||
|
||||
let plotWrapper = null
|
||||
let uplot = null
|
||||
let timeoutId = null
|
||||
let prevWidth = null, prevHeight = null
|
||||
|
||||
function render() {
|
||||
if (!width || Number.isNaN(width) || width < 0)
|
||||
return
|
||||
|
||||
if (prevWidth != null && Math.abs(prevWidth - width) < 10)
|
||||
return
|
||||
|
||||
prevWidth = width
|
||||
prevHeight = height
|
||||
|
||||
if (!uplot) {
|
||||
opts.width = width
|
||||
opts.height = height
|
||||
uplot = new uPlot(opts, plotData, plotWrapper)
|
||||
} else {
|
||||
uplot.setSize({ width, height })
|
||||
}
|
||||
}
|
||||
|
||||
function onSizeChange() {
|
||||
if (!uplot)
|
||||
return
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
render()
|
||||
}, resizeSleepTime)
|
||||
}
|
||||
|
||||
$: if (series[0].data.length > 0) {
|
||||
onSizeChange(width, height)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (series[0].data.length > 0) {
|
||||
plotWrapper.style.backgroundColor = backgroundColor()
|
||||
render()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
|
||||
// `from` and `to` must be numbers between 0 and 1.
|
||||
export function setTimeRange(from, to) {
|
||||
if (!uplot || from > to)
|
||||
return false
|
||||
|
||||
uplot.setScale('x', { min: from * maxX, max: to * maxX })
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
<script context="module">
|
||||
export function formatTime(t, forNode = false) {
|
||||
if (t !== null) {
|
||||
if (isNaN(t)) {
|
||||
return t
|
||||
} else {
|
||||
const tAbs = Math.abs(t)
|
||||
const h = Math.floor(tAbs / 3600)
|
||||
const m = Math.floor((tAbs % 3600) / 60)
|
||||
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
||||
if (h == 0)
|
||||
return `${forNode && m != 0 ? '-' : ''}${m}m`
|
||||
else if (m == 0)
|
||||
return `${forNode?'-':''}${h}h`
|
||||
else
|
||||
return `${forNode?'-':''}${h}:${m}h`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function timeIncrs(timestep, maxX, forNode) {
|
||||
if (forNode === true) {
|
||||
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600] // forNode fixed increments
|
||||
} else {
|
||||
let incrs = []
|
||||
for (let t = timestep; t < maxX; t *= 10)
|
||||
incrs.push(t, t * 2, t * 3, t * 5)
|
||||
|
||||
return incrs
|
||||
}
|
||||
}
|
||||
|
||||
export function findThresholds(metricConfig, scope, subCluster, isShared, numhwthreads, numaccs) {
|
||||
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
|
||||
if (!metricConfig || !scope || !subCluster) {
|
||||
console.warn('Argument missing for findThresholds!')
|
||||
return null
|
||||
}
|
||||
|
||||
if ((scope == 'node' && isShared == false) || metricConfig.aggregation == 'avg') {
|
||||
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
|
||||
// console.log('subClusterConfigs array empty, use metricConfig defaults')
|
||||
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak }
|
||||
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
|
||||
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
|
||||
let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
|
||||
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert && forSubCluster.peak) return forSubCluster
|
||||
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak}
|
||||
} else {
|
||||
console.warn('metricConfig.subClusters not found!')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (metricConfig.aggregation != 'sum') {
|
||||
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
|
||||
return null
|
||||
}
|
||||
|
||||
let divisor = 1
|
||||
if (isShared == true) { // Shared
|
||||
if (numaccs > 0) {
|
||||
divisor = subCluster.topology.accelerators.length / numaccs
|
||||
} else if (numhwthreads > 0) {
|
||||
divisor = subCluster.topology.node.length / numhwthreads
|
||||
}
|
||||
else if (scope == 'socket')
|
||||
divisor = subCluster.topology.socket.length
|
||||
else if (scope == 'core')
|
||||
divisor = subCluster.topology.core.length
|
||||
else if (scope == 'accelerator')
|
||||
divisor = subCluster.topology.accelerators.length
|
||||
else if (scope == 'hwthread')
|
||||
divisor = subCluster.topology.node.length
|
||||
else
|
||||
// console.log('TODO: how to calc thresholds for ', scope)
|
||||
return null
|
||||
}
|
||||
|
||||
let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig
|
||||
return {
|
||||
peak: mc.peak / divisor,
|
||||
normal: mc.normal / divisor,
|
||||
caution: mc.caution / divisor,
|
||||
alert: mc.alert / divisor
|
||||
}
|
||||
}
|
||||
// `from` and `to` must be numbers between 0 and 1.
|
||||
export function setTimeRange(from, to) {
|
||||
if (!uplot || from > to) return false;
|
||||
|
||||
uplot.setScale("x", { min: from * maxX, max: to * maxX });
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if series[0].data.length > 0}
|
||||
<div bind:this={plotWrapper} class="cc-plot"></div>
|
||||
<div bind:this={plotWrapper} class="cc-plot"></div>
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
|
||||
<Card class="mx-4" body color="warning"
|
||||
>Cannot render plot: No series data returned for <code>{metric}</code></Card
|
||||
>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.cc-plot {
|
||||
border-radius: 5px;
|
||||
}
|
||||
.cc-plot {
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,254 +1,339 @@
|
||||
<script>
|
||||
import uPlot from 'uplot'
|
||||
import { formatNumber } from '../units.js'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Card } from 'sveltestrap'
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let data = null
|
||||
export let renderTime = false
|
||||
export let allowSizeChange = false
|
||||
export let cluster = null
|
||||
export let width = 600
|
||||
export let height = 350
|
||||
export let data = null;
|
||||
export let renderTime = false;
|
||||
export let allowSizeChange = false;
|
||||
export let cluster = null;
|
||||
export let width = 600;
|
||||
export let height = 350;
|
||||
|
||||
let plotWrapper = null
|
||||
let uplot = null
|
||||
let timeoutId = null
|
||||
let plotWrapper = null;
|
||||
let uplot = null;
|
||||
let timeoutId = null;
|
||||
|
||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth
|
||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
|
||||
|
||||
/* Data Format
|
||||
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
|
||||
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
|
||||
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
|
||||
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
|
||||
*/
|
||||
/* Data Format
|
||||
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
|
||||
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
|
||||
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
|
||||
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
|
||||
*/
|
||||
|
||||
// Helpers
|
||||
function getGradientR(x) {
|
||||
if (x < 0.5) return 0
|
||||
if (x > 0.75) return 255
|
||||
x = (x - 0.5) * 4.0
|
||||
return Math.floor(x * 255.0)
|
||||
}
|
||||
function getGradientG(x) {
|
||||
if (x > 0.25 && x < 0.75) return 255
|
||||
if (x < 0.25) x = x * 4.0
|
||||
else x = 1.0 - (x - 0.75) * 4.0
|
||||
return Math.floor(x * 255.0)
|
||||
}
|
||||
function getGradientB(x) {
|
||||
if (x < 0.25) return 255
|
||||
if (x > 0.5) return 0
|
||||
x = 1.0 - (x - 0.25) * 4.0
|
||||
return Math.floor(x * 255.0)
|
||||
}
|
||||
function getRGB(c) {
|
||||
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
|
||||
}
|
||||
function nearestThousand (num) {
|
||||
return Math.ceil(num/1000) * 1000
|
||||
}
|
||||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
||||
return {
|
||||
x: x1 + a * (x2 - x1),
|
||||
y: y1 + a * (y2 - y1)
|
||||
}
|
||||
}
|
||||
// End Helpers
|
||||
|
||||
// Dot Renderers
|
||||
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||
let d = u.data[seriesIdx];
|
||||
let deg360 = 2 * Math.PI;
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let p = new Path2D();
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
u.ctx.strokeStyle = getRGB(u.data[2][i])
|
||||
u.ctx.fillStyle = getRGB(u.data[2][i])
|
||||
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
p.moveTo(cx + size/2, cy);
|
||||
arc(p, cx, cy, size/2, 0, deg360);
|
||||
}
|
||||
u.ctx.fill(p);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
// Helpers
|
||||
function getGradientR(x) {
|
||||
if (x < 0.5) return 0;
|
||||
if (x > 0.75) return 255;
|
||||
x = (x - 0.5) * 4.0;
|
||||
return Math.floor(x * 255.0);
|
||||
}
|
||||
function getGradientG(x) {
|
||||
if (x > 0.25 && x < 0.75) return 255;
|
||||
if (x < 0.25) x = x * 4.0;
|
||||
else x = 1.0 - (x - 0.75) * 4.0;
|
||||
return Math.floor(x * 255.0);
|
||||
}
|
||||
function getGradientB(x) {
|
||||
if (x < 0.25) return 255;
|
||||
if (x > 0.5) return 0;
|
||||
x = 1.0 - (x - 0.25) * 4.0;
|
||||
return Math.floor(x * 255.0);
|
||||
}
|
||||
function getRGB(c) {
|
||||
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`;
|
||||
}
|
||||
function nearestThousand(num) {
|
||||
return Math.ceil(num / 1000) * 1000;
|
||||
}
|
||||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
||||
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l;
|
||||
return {
|
||||
x: x1 + a * (x2 - x1),
|
||||
y: y1 + a * (y2 - y1),
|
||||
};
|
||||
}
|
||||
// End Helpers
|
||||
|
||||
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||
let d = u.data[seriesIdx];
|
||||
u.ctx.strokeStyle = getRGB(0);
|
||||
u.ctx.fillStyle = getRGB(0);
|
||||
let deg360 = 2 * Math.PI;
|
||||
let p = new Path2D();
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
p.moveTo(cx + size/2, cy);
|
||||
arc(p, cx, cy, size/2, 0, deg360);
|
||||
}
|
||||
}
|
||||
u.ctx.fill(p);
|
||||
});
|
||||
return null;
|
||||
};
|
||||
// Dot Renderers
|
||||
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
uPlot.orient(
|
||||
u,
|
||||
seriesIdx,
|
||||
(
|
||||
series,
|
||||
dataX,
|
||||
dataY,
|
||||
scaleX,
|
||||
scaleY,
|
||||
valToPosX,
|
||||
valToPosY,
|
||||
xOff,
|
||||
yOff,
|
||||
xDim,
|
||||
yDim,
|
||||
moveTo,
|
||||
lineTo,
|
||||
rect,
|
||||
arc,
|
||||
) => {
|
||||
let d = u.data[seriesIdx];
|
||||
let deg360 = 2 * Math.PI;
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let p = new Path2D();
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
u.ctx.strokeStyle = getRGB(u.data[2][i]);
|
||||
u.ctx.fillStyle = getRGB(u.data[2][i]);
|
||||
if (
|
||||
xVal >= scaleX.min &&
|
||||
xVal <= scaleX.max &&
|
||||
yVal >= scaleY.min &&
|
||||
yVal <= scaleY.max
|
||||
) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
// Main Function
|
||||
function render(plotData) {
|
||||
if (plotData) {
|
||||
const opts = {
|
||||
title: "",
|
||||
mode: 2,
|
||||
width: width,
|
||||
height: height,
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
cursor: { drag: { x: false, y: false } },
|
||||
axes: [
|
||||
{
|
||||
label: 'Intensity [FLOPS/Byte]',
|
||||
values: (u, vals) => vals.map(v => formatNumber(v))
|
||||
},
|
||||
{
|
||||
label: 'Performace [GFLOPS]',
|
||||
values: (u, vals) => vals.map(v => formatNumber(v))
|
||||
}
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
range: [0.01, 1000],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
y: {
|
||||
range: [1.0, cluster?.flopRateSimd?.value ? nearestThousand(cluster.flopRateSimd.value) : 10000],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{},
|
||||
{ paths: renderTime ? drawColorPoints : drawPoints }
|
||||
],
|
||||
hooks: {
|
||||
drawClear: [
|
||||
u => {
|
||||
u.series.forEach((s, i) => {
|
||||
if (i > 0)
|
||||
s._paths = null;
|
||||
});
|
||||
},
|
||||
],
|
||||
draw: [
|
||||
u => { // draw roofs when cluster set
|
||||
// console.log(u)
|
||||
if (cluster != null) {
|
||||
const padding = u._padding // [top, right, bottom, left]
|
||||
|
||||
u.ctx.strokeStyle = 'black'
|
||||
u.ctx.lineWidth = lineWidth
|
||||
u.ctx.beginPath()
|
||||
|
||||
const ycut = 0.01 * cluster.memoryBandwidth.value
|
||||
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
|
||||
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
|
||||
const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
|
||||
simdKneeX = u.valToPos(simdKnee, 'x', true),
|
||||
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
|
||||
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
|
||||
|
||||
// Debug get zoomLevel from browser
|
||||
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
|
||||
|
||||
if (scalarKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Lower horizontal roofline
|
||||
u.ctx.moveTo(scalarKneeX, flopRateScalarY)
|
||||
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateScalarY)
|
||||
}
|
||||
|
||||
if (simdKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Top horitontal roofline
|
||||
u.ctx.moveTo(simdKneeX, flopRateSimdY)
|
||||
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateSimdY)
|
||||
}
|
||||
|
||||
let x1 = u.valToPos(0.01, 'x', true),
|
||||
y1 = u.valToPos(ycut, 'y', true)
|
||||
|
||||
let x2 = u.valToPos(simdKnee, 'x', true),
|
||||
y2 = flopRateSimdY
|
||||
|
||||
let xAxisIntersect = lineIntersect(
|
||||
x1, y1, x2, y2,
|
||||
u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
|
||||
u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
|
||||
)
|
||||
|
||||
if (xAxisIntersect.x > x1) {
|
||||
x1 = xAxisIntersect.x
|
||||
y1 = xAxisIntersect.y
|
||||
}
|
||||
|
||||
// Diagonal
|
||||
u.ctx.moveTo(x1, y1)
|
||||
u.ctx.lineTo(x2, y2)
|
||||
|
||||
u.ctx.stroke()
|
||||
// Reset grid lineWidth
|
||||
u.ctx.lineWidth = 0.15
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// cursor: { drag: { x: true, y: true } } // Activate zoom
|
||||
};
|
||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||
} else {
|
||||
console.log('No data for roofline!')
|
||||
p.moveTo(cx + size / 2, cy);
|
||||
arc(p, cx, cy, size / 2, 0, deg360);
|
||||
}
|
||||
u.ctx.fill(p);
|
||||
}
|
||||
},
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
uPlot.orient(
|
||||
u,
|
||||
seriesIdx,
|
||||
(
|
||||
series,
|
||||
dataX,
|
||||
dataY,
|
||||
scaleX,
|
||||
scaleY,
|
||||
valToPosX,
|
||||
valToPosY,
|
||||
xOff,
|
||||
yOff,
|
||||
xDim,
|
||||
yDim,
|
||||
moveTo,
|
||||
lineTo,
|
||||
rect,
|
||||
arc,
|
||||
) => {
|
||||
let d = u.data[seriesIdx];
|
||||
u.ctx.strokeStyle = getRGB(0);
|
||||
u.ctx.fillStyle = getRGB(0);
|
||||
let deg360 = 2 * Math.PI;
|
||||
let p = new Path2D();
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
if (
|
||||
xVal >= scaleX.min &&
|
||||
xVal <= scaleX.max &&
|
||||
yVal >= scaleY.min &&
|
||||
yVal <= scaleY.max
|
||||
) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
p.moveTo(cx + size / 2, cy);
|
||||
arc(p, cx, cy, size / 2, 0, deg360);
|
||||
}
|
||||
}
|
||||
u.ctx.fill(p);
|
||||
},
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Main Function
|
||||
function render(plotData) {
|
||||
if (plotData) {
|
||||
const opts = {
|
||||
title: "",
|
||||
mode: 2,
|
||||
width: width,
|
||||
height: height,
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
cursor: { drag: { x: false, y: false } },
|
||||
axes: [
|
||||
{
|
||||
label: "Intensity [FLOPS/Byte]",
|
||||
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||
},
|
||||
{
|
||||
label: "Performace [GFLOPS]",
|
||||
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
range: [0.01, 1000],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
y: {
|
||||
range: [
|
||||
1.0,
|
||||
cluster?.flopRateSimd?.value
|
||||
? nearestThousand(cluster.flopRateSimd.value)
|
||||
: 10000,
|
||||
],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
},
|
||||
series: [{}, { paths: renderTime ? drawColorPoints : drawPoints }],
|
||||
hooks: {
|
||||
drawClear: [
|
||||
(u) => {
|
||||
u.series.forEach((s, i) => {
|
||||
if (i > 0) s._paths = null;
|
||||
});
|
||||
},
|
||||
],
|
||||
draw: [
|
||||
(u) => {
|
||||
// draw roofs when cluster set
|
||||
// console.log(u)
|
||||
if (cluster != null) {
|
||||
const padding = u._padding; // [top, right, bottom, left]
|
||||
|
||||
u.ctx.strokeStyle = "black";
|
||||
u.ctx.lineWidth = lineWidth;
|
||||
u.ctx.beginPath();
|
||||
|
||||
const ycut = 0.01 * cluster.memoryBandwidth.value;
|
||||
const scalarKnee =
|
||||
(cluster.flopRateScalar.value - ycut) /
|
||||
cluster.memoryBandwidth.value;
|
||||
const simdKnee =
|
||||
(cluster.flopRateSimd.value - ycut) /
|
||||
cluster.memoryBandwidth.value;
|
||||
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
|
||||
simdKneeX = u.valToPos(simdKnee, "x", true),
|
||||
flopRateScalarY = u.valToPos(
|
||||
cluster.flopRateScalar.value,
|
||||
"y",
|
||||
true,
|
||||
),
|
||||
flopRateSimdY = u.valToPos(
|
||||
cluster.flopRateSimd.value,
|
||||
"y",
|
||||
true,
|
||||
);
|
||||
|
||||
// Debug get zoomLevel from browser
|
||||
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
|
||||
|
||||
if (
|
||||
scalarKneeX <
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio
|
||||
) {
|
||||
// Lower horizontal roofline
|
||||
u.ctx.moveTo(scalarKneeX, flopRateScalarY);
|
||||
u.ctx.lineTo(
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio,
|
||||
flopRateScalarY,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
simdKneeX <
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio
|
||||
) {
|
||||
// Top horitontal roofline
|
||||
u.ctx.moveTo(simdKneeX, flopRateSimdY);
|
||||
u.ctx.lineTo(
|
||||
width * window.devicePixelRatio -
|
||||
padding[1] * window.devicePixelRatio,
|
||||
flopRateSimdY,
|
||||
);
|
||||
}
|
||||
|
||||
let x1 = u.valToPos(0.01, "x", true),
|
||||
y1 = u.valToPos(ycut, "y", true);
|
||||
|
||||
let x2 = u.valToPos(simdKnee, "x", true),
|
||||
y2 = flopRateSimdY;
|
||||
|
||||
let xAxisIntersect = lineIntersect(
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
u.valToPos(0.01, "x", true),
|
||||
u.valToPos(1.0, "y", true), // X-Axis Start Coords
|
||||
u.valToPos(1000, "x", true),
|
||||
u.valToPos(1.0, "y", true), // X-Axis End Coords
|
||||
);
|
||||
|
||||
if (xAxisIntersect.x > x1) {
|
||||
x1 = xAxisIntersect.x;
|
||||
y1 = xAxisIntersect.y;
|
||||
}
|
||||
|
||||
// Diagonal
|
||||
u.ctx.moveTo(x1, y1);
|
||||
u.ctx.lineTo(x2, y2);
|
||||
|
||||
u.ctx.stroke();
|
||||
// Reset grid lineWidth
|
||||
u.ctx.lineWidth = 0.15;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
// cursor: { drag: { x: true, y: true } } // Activate zoom
|
||||
};
|
||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||
} else {
|
||||
console.log("No data for roofline!");
|
||||
}
|
||||
}
|
||||
|
||||
// Svelte and Sizechange
|
||||
onMount(() => {
|
||||
render(data)
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
// Svelte and Sizechange
|
||||
onMount(() => {
|
||||
render(data);
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
if (uplot)
|
||||
uplot.destroy()
|
||||
render(data)
|
||||
}, 200)
|
||||
}
|
||||
$: if (allowSizeChange) sizeChanged(width, height)
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
if (uplot) uplot.destroy();
|
||||
render(data);
|
||||
}, 200);
|
||||
}
|
||||
$: if (allowSizeChange) sizeChanged(width, height);
|
||||
</script>
|
||||
|
||||
{#if data != null}
|
||||
<div bind:this={plotWrapper}/>
|
||||
<div bind:this={plotWrapper} />
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
|
||||
{/if}
|
||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
||||
>
|
||||
{/if}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user