mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-04-24 04:11:43 +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
|
var useArchive bool
|
||||||
|
|
||||||
func Init(disableArchive bool) error {
|
func Init(disableArchive bool) error {
|
||||||
|
|
||||||
useArchive = !disableArchive
|
useArchive = !disableArchive
|
||||||
for _, cluster := range config.Keys.Clusters {
|
for _, cluster := range config.Keys.Clusters {
|
||||||
if cluster.MetricDataRepository != nil {
|
if cluster.MetricDataRepository != nil {
|
||||||
@ -80,7 +79,8 @@ var cache *lrucache.Cache = lrucache.New(128 * 1024 * 1024)
|
|||||||
func LoadData(job *schema.Job,
|
func LoadData(job *schema.Job,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
scopes []schema.MetricScope,
|
scopes []schema.MetricScope,
|
||||||
ctx context.Context) (schema.JobData, error) {
|
ctx context.Context,
|
||||||
|
) (schema.JobData, error) {
|
||||||
data := cache.Get(cacheKey(job, metrics, scopes), func() (_ interface{}, ttl time.Duration, size int) {
|
data := cache.Get(cacheKey(job, metrics, scopes), func() (_ interface{}, ttl time.Duration, size int) {
|
||||||
var jd schema.JobData
|
var jd schema.JobData
|
||||||
var err error
|
var err error
|
||||||
@ -109,7 +109,8 @@ func LoadData(job *schema.Job,
|
|||||||
jd, err = repo.LoadData(job, metrics, scopes, ctx)
|
jd, err = repo.LoadData(job, metrics, scopes, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(jd) != 0 {
|
if len(jd) != 0 {
|
||||||
log.Warnf("partial error: %s", err.Error())
|
log.Errorf("partial error: %s", err.Error())
|
||||||
|
return err, 0, 0
|
||||||
} else {
|
} else {
|
||||||
log.Error("Error while loading job data from metric repository")
|
log.Error("Error while loading job data from metric repository")
|
||||||
return err, 0, 0
|
return err, 0, 0
|
||||||
@ -179,8 +180,8 @@ func LoadAverages(
|
|||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
data [][]schema.Float,
|
data [][]schema.Float,
|
||||||
ctx context.Context) error {
|
ctx context.Context,
|
||||||
|
) error {
|
||||||
if job.State != schema.JobStateRunning && useArchive {
|
if job.State != schema.JobStateRunning && useArchive {
|
||||||
return archive.LoadAveragesFromArchive(job, metrics, data) // #166 change also here?
|
return archive.LoadAveragesFromArchive(job, metrics, data) // #166 change also here?
|
||||||
}
|
}
|
||||||
@ -219,8 +220,8 @@ func LoadNodeData(
|
|||||||
metrics, nodes []string,
|
metrics, nodes []string,
|
||||||
scopes []schema.MetricScope,
|
scopes []schema.MetricScope,
|
||||||
from, to time.Time,
|
from, to time.Time,
|
||||||
ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) {
|
ctx context.Context,
|
||||||
|
) (map[string]map[string][]*schema.JobMetric, error) {
|
||||||
repo, ok := metricDataRepos[cluster]
|
repo, ok := metricDataRepos[cluster]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", cluster)
|
return nil, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", cluster)
|
||||||
@ -252,8 +253,8 @@ func LoadNodeData(
|
|||||||
func cacheKey(
|
func cacheKey(
|
||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
scopes []schema.MetricScope) string {
|
scopes []schema.MetricScope,
|
||||||
|
) string {
|
||||||
// Duration and StartTime do not need to be in the cache key as StartTime is less unique than
|
// Duration and StartTime do not need to be in the cache key as StartTime is less unique than
|
||||||
// job.ID and the TTL of the cache entry makes sure it does not stay there forever.
|
// job.ID and the TTL of the cache entry makes sure it does not stay there forever.
|
||||||
return fmt.Sprintf("%d(%s):[%v],[%v]",
|
return fmt.Sprintf("%d(%s):[%v],[%v]",
|
||||||
@ -267,8 +268,8 @@ func cacheKey(
|
|||||||
func prepareJobData(
|
func prepareJobData(
|
||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
jobData schema.JobData,
|
jobData schema.JobData,
|
||||||
scopes []schema.MetricScope) {
|
scopes []schema.MetricScope,
|
||||||
|
) {
|
||||||
const maxSeriesSize int = 15
|
const maxSeriesSize int = 15
|
||||||
for _, scopes := range jobData {
|
for _, scopes := range jobData {
|
||||||
for _, jm := range scopes {
|
for _, jm := range scopes {
|
||||||
@ -295,7 +296,6 @@ func prepareJobData(
|
|||||||
|
|
||||||
// Writes a running job to the job-archive
|
// Writes a running job to the job-archive
|
||||||
func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.JobMeta, error) {
|
func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.JobMeta, error) {
|
||||||
|
|
||||||
allMetrics := make([]string, 0)
|
allMetrics := make([]string, 0)
|
||||||
metricConfigs := archive.GetCluster(job.Cluster).MetricConfig
|
metricConfigs := archive.GetCluster(job.Cluster).MetricConfig
|
||||||
for _, mc := range metricConfigs {
|
for _, mc := range metricConfigs {
|
||||||
|
616
web/frontend/package-lock.json
generated
616
web/frontend/package-lock.json
generated
@ -9,27 +9,26 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@urql/svelte": "^4.0.1",
|
"@sveltestrap/sveltestrap": "^6.2.6",
|
||||||
"chart.js": "^4.3.3",
|
"@urql/svelte": "^4.1.0",
|
||||||
|
"chart.js": "^4.4.2",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"date-fns": "^2.30.0",
|
"graphql": "^16.8.1",
|
||||||
"graphql": "^16.6.0",
|
"mathjs": "^12.4.0",
|
||||||
"mathjs": "^12.0.0",
|
"svelte-chartjs": "^3.1.5",
|
||||||
"svelte-chartjs": "^3.1.2",
|
"uplot": "^1.6.30",
|
||||||
"sveltestrap": "^5.11.1",
|
"wonka": "^6.3.4"
|
||||||
"uplot": "^1.6.24",
|
|
||||||
"wonka": "^6.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^24.1.0",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-terser": "^0.4.1",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@timohausmann/quadtree-js": "^1.2.5",
|
"@timohausmann/quadtree-js": "^1.2.6",
|
||||||
"rollup": "^3.21.0",
|
"rollup": "^4.12.1",
|
||||||
"rollup-plugin-css-only": "^4.3.0",
|
"rollup-plugin-css-only": "^4.5.2",
|
||||||
"rollup-plugin-svelte": "^7.1.4",
|
"rollup-plugin-svelte": "^7.1.6",
|
||||||
"svelte": "^3.58.0"
|
"svelte": "^4.2.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@0no-co/graphql.web": {
|
"node_modules/@0no-co/graphql.web": {
|
||||||
@ -45,10 +44,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ampproject/remapping": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.23.5",
|
"version": "7.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
|
||||||
"integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
|
"integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
@ -57,33 +68,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||||
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
|
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.0.1",
|
"@jridgewell/set-array": "^1.2.1",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||||
"@jridgewell/trace-mapping": "^0.3.9"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/set-array": {
|
"node_modules/@jridgewell/set-array": {
|
||||||
"version": "1.1.2",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@ -104,13 +112,9 @@
|
|||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.20",
|
"version": "0.3.25",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||||
"version": "0.3.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
|
||||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
@ -131,9 +135,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-commonjs": {
|
"node_modules/@rollup/plugin-commonjs": {
|
||||||
"version": "24.1.0",
|
"version": "25.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz",
|
||||||
"integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==",
|
"integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/pluginutils": "^5.0.1",
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
@ -141,13 +145,13 @@
|
|||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"glob": "^8.0.3",
|
"glob": "^8.0.3",
|
||||||
"is-reference": "1.2.1",
|
"is-reference": "1.2.1",
|
||||||
"magic-string": "^0.27.0"
|
"magic-string": "^0.30.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rollup": "^2.68.0||^3.0.0"
|
"rollup": "^2.68.0||^3.0.0||^4.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"rollup": {
|
"rollup": {
|
||||||
@ -156,9 +160,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-node-resolve": {
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
"version": "15.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
|
||||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
|
||||||
"version": "15.2.3",
|
"version": "15.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||||
@ -176,7 +177,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"rollup": {
|
"rollup": {
|
||||||
@ -185,23 +185,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-replace": {
|
"node_modules/@rollup/plugin-replace": {
|
||||||
"version": "5.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
|
|
||||||
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
|
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
|
||||||
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
|
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/pluginutils": "^5.0.1",
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
"magic-string": "^0.30.3"
|
"magic-string": "^0.30.3"
|
||||||
"magic-string": "^0.30.3"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"rollup": {
|
"rollup": {
|
||||||
@ -209,32 +204,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
|
|
||||||
"version": "0.30.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
|
||||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
|
|
||||||
"version": "0.30.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
|
||||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/plugin-terser": {
|
"node_modules/@rollup/plugin-terser": {
|
||||||
"version": "0.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
|
|
||||||
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
|
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
|
||||||
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
|
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
|
||||||
@ -249,7 +219,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
||||||
"rollup": "^2.0.0||^3.0.0||^4.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"rollup": {
|
"rollup": {
|
||||||
@ -271,7 +240,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"rollup": {
|
"rollup": {
|
||||||
@ -279,6 +247,186 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@sveltestrap/sveltestrap": {
|
||||||
|
"version": "6.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-6.2.6.tgz",
|
||||||
|
"integrity": "sha512-iB50tbVzsFXp0M10pe3XywRkNxjKPIHXJzV44mb1FhajWNWwxme8MkBis9m2QNivM2hyw5zDHjgGuzwTOB76JQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^4.0.0 || ^5.0.0 || ^5.0.0-next.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@timohausmann/quadtree-js": {
|
"node_modules/@timohausmann/quadtree-js": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz",
|
||||||
@ -289,9 +437,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
@ -300,20 +445,20 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@urql/core": {
|
"node_modules/@urql/core": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.3.0.tgz",
|
||||||
"integrity": "sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==",
|
"integrity": "sha512-wT+FeL8DG4x5o6RfHEnONNFVDM3616ouzATMYUClB6CB+iIu2mwfBKd7xSUxYOZmwtxna5/hDRQdMl3nbQZlnw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@0no-co/graphql.web": "^1.0.1",
|
"@0no-co/graphql.web": "^1.0.1",
|
||||||
"wonka": "^6.3.2"
|
"wonka": "^6.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@urql/svelte": {
|
"node_modules/@urql/svelte": {
|
||||||
"version": "4.0.4",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.1.0.tgz",
|
||||||
"integrity": "sha512-HYz9dHdqEcs9d82WWczQ3XG+zuup3TS01H+txaij/QfQ+KHjrlrn0EkOHQQd1S+H8+nFjFU2x9+HE3+3fuwL1A==",
|
"integrity": "sha512-Ov3EclCjaXPPTjKNTcIDlAG3qY/jhLjl/J9yyz9FeLUQ9S2jEgsvlzNXibrY27f4ihD4gH36CNGuj1XOi5hEEQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@urql/core": "^4.1.0",
|
"@urql/core": "^4.3.0",
|
||||||
"wonka": "^6.3.2"
|
"wonka": "^6.3.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -321,13 +466,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.2",
|
"version": "8.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||||
"version": "8.11.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
|
||||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -335,6 +476,22 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-query": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axobject-query": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -369,14 +526,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
|
||||||
"integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
|
"integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": ">=7"
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/code-red": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
|
"@types/estree": "^1.0.1",
|
||||||
|
"acorn": "^8.10.0",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"periscopic": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/code-red/node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
@ -403,6 +580,18 @@
|
|||||||
"url": "https://www.patreon.com/infusion"
|
"url": "https://www.patreon.com/infusion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.0.30",
|
||||||
|
"source-map-js": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "2.30.0",
|
"version": "2.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||||
@ -432,6 +621,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-latex": {
|
"node_modules/escape-latex": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
|
||||||
@ -482,13 +679,6 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@ -510,9 +700,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graphql": {
|
"node_modules/graphql": {
|
||||||
"version": "16.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
|
||||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
|
||||||
"version": "16.8.1",
|
"version": "16.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||||
@ -521,21 +708,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
|
||||||
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
|
||||||
"node_modules/hasown": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
"function-bind": "^1.1.2"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
@ -570,16 +751,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
|
||||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
|
||||||
"version": "2.13.1",
|
"version": "2.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
||||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.0"
|
"hasown": "^2.0.0"
|
||||||
"hasown": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -605,25 +782,28 @@
|
|||||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
|
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/locate-character": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.27.0",
|
"version": "0.30.8",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
||||||
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
|
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
|
||||||
"dev": true,
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mathjs": {
|
"node_modules/mathjs": {
|
||||||
"version": "12.0.0",
|
"version": "12.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.0.tgz",
|
||||||
"integrity": "sha512-Oz3swPplNPe7taoP6WrkKhQzhDE2SwvOgLzu8H3EN+hEadw2GjEJUm6Xl+hrioHoB8g2BYb3gfw1glSzhdBKYw==",
|
"integrity": "sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.2",
|
"@babel/runtime": "^7.23.9",
|
||||||
"complex.js": "^2.1.1",
|
"complex.js": "^2.1.1",
|
||||||
"decimal.js": "^10.4.3",
|
"decimal.js": "^10.4.3",
|
||||||
"escape-latex": "^1.2.0",
|
"escape-latex": "^1.2.0",
|
||||||
@ -640,6 +820,11 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.0.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||||
|
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
@ -667,6 +852,32 @@
|
|||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/periscopic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^3.0.0",
|
||||||
|
"is-reference": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/periscopic/node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/periscopic/node_modules/is-reference": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
@ -688,19 +899,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regenerator-runtime": {
|
"node_modules/regenerator-runtime": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||||
},
|
|
||||||
"node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
|
||||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
|
||||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||||
@ -727,28 +930,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.29.4",
|
"version": "4.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz",
|
||||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
"integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==",
|
||||||
"version": "3.29.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
|
||||||
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "1.0.5"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0",
|
"node": ">=18.0.0",
|
||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.12.1",
|
||||||
|
"@rollup/rollup-android-arm64": "4.12.1",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.12.1",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.12.1",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.12.1",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.12.1",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.12.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "4.12.1",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.12.1",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.12.1",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.12.1",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.12.1",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.12.1",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup-plugin-css-only": {
|
"node_modules/rollup-plugin-css-only": {
|
||||||
"version": "4.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
|
|
||||||
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
|
|
||||||
"version": "4.5.2",
|
"version": "4.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
|
||||||
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
|
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
|
||||||
@ -761,7 +974,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rollup": "<5"
|
"rollup": "<5"
|
||||||
"rollup": "<5"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup-plugin-svelte": {
|
"node_modules/rollup-plugin-svelte": {
|
||||||
@ -820,18 +1032,15 @@
|
|||||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
||||||
},
|
},
|
||||||
"node_modules/serialize-javascript": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/smob": {
|
"node_modules/smob": {
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
|
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
|
||||||
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
|
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
|
||||||
@ -846,6 +1055,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-support": {
|
"node_modules/source-map-support": {
|
||||||
"version": "0.5.21",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
@ -869,40 +1086,58 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "3.59.2",
|
"version": "4.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
|
||||||
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
|
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ampproject/remapping": "^2.2.1",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.18",
|
||||||
|
"@types/estree": "^1.0.1",
|
||||||
|
"acorn": "^8.9.0",
|
||||||
|
"aria-query": "^5.3.0",
|
||||||
|
"axobject-query": "^4.0.0",
|
||||||
|
"code-red": "^1.0.3",
|
||||||
|
"css-tree": "^2.3.1",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"is-reference": "^3.0.1",
|
||||||
|
"locate-character": "^3.0.0",
|
||||||
|
"magic-string": "^0.30.4",
|
||||||
|
"periscopic": "^3.1.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-chartjs": {
|
"node_modules/svelte-chartjs": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz",
|
||||||
"integrity": "sha512-3+6gY2IJ9Ua8R9pk3iS1ypa7Z9OoXCJb9oPwIfTp7caJM+X+RrWnH2CTkGAq7FeSxc2nnmW08tYN88Q8Y+5M+w==",
|
"integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"chart.js": "^3.5.0 || ^4.0.0",
|
"chart.js": "^3.5.0 || ^4.0.0",
|
||||||
"svelte": "^3.45.0"
|
"svelte": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sveltestrap": {
|
"node_modules/svelte/node_modules/estree-walker": {
|
||||||
"version": "5.11.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
"version": "5.11.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
|
|
||||||
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8"
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"node_modules/svelte/node_modules/is-reference": {
|
||||||
"svelte": "^3.53.1"
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.25.0",
|
"version": "5.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
|
||||||
"integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==",
|
"integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
@ -931,12 +1166,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uplot": {
|
"node_modules/uplot": {
|
||||||
"version": "1.6.27",
|
"version": "1.6.30",
|
||||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
|
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.30.tgz",
|
||||||
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
|
"integrity": "sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q=="
|
||||||
"version": "1.6.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
|
|
||||||
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
|
|
||||||
},
|
},
|
||||||
"node_modules/wonka": {
|
"node_modules/wonka": {
|
||||||
"version": "6.3.4",
|
"version": "6.3.4",
|
||||||
|
@ -7,25 +7,25 @@
|
|||||||
"dev": "rollup -c -w"
|
"dev": "rollup -c -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^24.1.0",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-terser": "^0.4.1",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@timohausmann/quadtree-js": "^1.2.5",
|
"@timohausmann/quadtree-js": "^1.2.6",
|
||||||
"rollup": "^3.21.0",
|
"rollup": "^4.12.1",
|
||||||
"rollup-plugin-css-only": "^4.3.0",
|
"rollup-plugin-css-only": "^4.5.2",
|
||||||
"rollup-plugin-svelte": "^7.1.4",
|
"rollup-plugin-svelte": "^7.1.6",
|
||||||
"svelte": "^3.58.0"
|
"svelte": "^4.2.12"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@urql/svelte": "^4.0.1",
|
"@sveltestrap/sveltestrap": "^6.2.6",
|
||||||
"chart.js": "^4.3.3",
|
"@urql/svelte": "^4.1.0",
|
||||||
|
"chart.js": "^4.4.2",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.8.1",
|
||||||
"mathjs": "^12.0.0",
|
"mathjs": "^12.4.0",
|
||||||
"svelte-chartjs": "^3.1.2",
|
"svelte-chartjs": "^3.1.5",
|
||||||
"sveltestrap": "^5.11.1",
|
"uplot": "^1.6.30",
|
||||||
"uplot": "^1.6.24",
|
"wonka": "^6.3.4"
|
||||||
"wonka": "^6.3.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,44 @@
|
|||||||
<script>
|
<script>
|
||||||
import { init, convert2uplot } from './utils.js'
|
import { init, convert2uplot } from "./utils.js";
|
||||||
import { getContext, onMount } from 'svelte'
|
import { getContext, onMount } from "svelte";
|
||||||
import { queryStore, gql, getContextClient, mutationStore } from '@urql/svelte'
|
import {
|
||||||
import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
|
queryStore,
|
||||||
import Filters from './filters/Filters.svelte'
|
gql,
|
||||||
import PlotSelection from './PlotSelection.svelte'
|
getContextClient,
|
||||||
import Histogram from './plots/Histogram.svelte'
|
mutationStore,
|
||||||
import Pie, { colors } from './plots/Pie.svelte'
|
} from "@urql/svelte";
|
||||||
import { binsFromFootprint } from './utils.js'
|
import {
|
||||||
import ScatterPlot from './plots/Scatter.svelte'
|
Row,
|
||||||
import PlotTable from './PlotTable.svelte'
|
Col,
|
||||||
import RooflineHeatmap from './plots/RooflineHeatmap.svelte'
|
Spinner,
|
||||||
|
Card,
|
||||||
|
Table,
|
||||||
|
Icon,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import Filters from "./filters/Filters.svelte";
|
||||||
|
import PlotSelection from "./PlotSelection.svelte";
|
||||||
|
import Histogram from "./plots/Histogram.svelte";
|
||||||
|
import Pie, { colors } from "./plots/Pie.svelte";
|
||||||
|
import { binsFromFootprint } from "./utils.js";
|
||||||
|
import ScatterPlot from "./plots/Scatter.svelte";
|
||||||
|
import PlotTable from "./PlotTable.svelte";
|
||||||
|
import RooflineHeatmap from "./plots/RooflineHeatmap.svelte";
|
||||||
|
|
||||||
const { query: initq } = init()
|
const { query: initq } = init();
|
||||||
|
|
||||||
export let filterPresets
|
export let filterPresets;
|
||||||
|
|
||||||
// By default, look at the jobs of the last 6 hours:
|
// By default, look at the jobs of the last 6 hours:
|
||||||
if (filterPresets?.startTime == null) {
|
if (filterPresets?.startTime == null) {
|
||||||
if (filterPresets == null)
|
if (filterPresets == null) filterPresets = {};
|
||||||
filterPresets = {}
|
|
||||||
|
|
||||||
let now = new Date(Date.now())
|
let now = new Date(Date.now());
|
||||||
let hourAgo = new Date(now)
|
let hourAgo = new Date(now);
|
||||||
hourAgo.setHours(hourAgo.getHours() - 6)
|
hourAgo.setHours(hourAgo.getHours() - 6);
|
||||||
filterPresets.startTime = { from: hourAgo.toISOString(), to: now.toISOString() }
|
filterPresets.startTime = {
|
||||||
|
from: hourAgo.toISOString(),
|
||||||
|
to: now.toISOString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let cluster;
|
let cluster;
|
||||||
@ -34,44 +48,68 @@
|
|||||||
let colWidth1, colWidth2, colWidth3, colWidth4;
|
let colWidth1, colWidth2, colWidth3, colWidth4;
|
||||||
let numBins = 50;
|
let numBins = 50;
|
||||||
let maxY = -1;
|
let maxY = -1;
|
||||||
const ccconfig = getContext('cc-config')
|
const ccconfig = getContext("cc-config");
|
||||||
const metricConfig = getContext('metrics')
|
const metricConfig = getContext("metrics");
|
||||||
|
|
||||||
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
|
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
|
||||||
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics
|
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics;
|
||||||
|
|
||||||
$: metrics = [...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
|
$: metrics = [
|
||||||
|
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
|
||||||
|
];
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{key: 'totalWalltime', label: 'Walltime'},
|
{ key: "totalWalltime", label: "Walltime" },
|
||||||
{key: 'totalNodeHours', label: 'Node Hours'},
|
{ key: "totalNodeHours", label: "Node Hours" },
|
||||||
{key: 'totalCoreHours', label: 'Core Hours'},
|
{ key: "totalCoreHours", label: "Core Hours" },
|
||||||
{key: 'totalAccHours', label: 'Accelerator Hours'}
|
{ key: "totalAccHours", label: "Accelerator Hours" },
|
||||||
]
|
];
|
||||||
const groupOptions = [
|
const groupOptions = [
|
||||||
{key: 'user', label: 'User Name'},
|
{ key: "user", label: "User Name" },
|
||||||
{key: 'project', label: 'Project ID'}
|
{ key: "project", label: "Project ID" },
|
||||||
]
|
];
|
||||||
|
|
||||||
let sortSelection = sortOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`]) || sortOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopCategory)
|
let sortSelection =
|
||||||
let groupSelection = groupOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`]) || groupOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopEntity)
|
sortOptions.find(
|
||||||
|
(option) =>
|
||||||
|
option.key ==
|
||||||
|
ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`],
|
||||||
|
) ||
|
||||||
|
sortOptions.find(
|
||||||
|
(option) => option.key == ccconfig.analysis_view_selectedTopCategory,
|
||||||
|
);
|
||||||
|
let groupSelection =
|
||||||
|
groupOptions.find(
|
||||||
|
(option) =>
|
||||||
|
option.key ==
|
||||||
|
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`],
|
||||||
|
) ||
|
||||||
|
groupOptions.find(
|
||||||
|
(option) => option.key == ccconfig.analysis_view_selectedTopEntity,
|
||||||
|
);
|
||||||
|
|
||||||
getContext('on-init')(({ data }) => {
|
getContext("on-init")(({ data }) => {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
cluster = data.clusters.find(c => c.name == filterPresets.cluster)
|
cluster = data.clusters.find((c) => c.name == filterPresets.cluster);
|
||||||
console.assert(cluster != null, `This cluster could not be found: ${filterPresets.cluster}`)
|
console.assert(
|
||||||
|
cluster != null,
|
||||||
|
`This cluster could not be found: ${filterPresets.cluster}`,
|
||||||
|
);
|
||||||
|
|
||||||
rooflineMaxY = cluster.subClusters.reduce((max, part) => Math.max(max, part.flopRateSimd.value), 0)
|
rooflineMaxY = cluster.subClusters.reduce(
|
||||||
maxY = rooflineMaxY
|
(max, part) => Math.max(max, part.flopRateSimd.value),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
maxY = rooflineMaxY;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
|
|
||||||
$: statsQuery = queryStore({
|
$: statsQuery = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query($jobFilters: [JobFilter!]!) {
|
query ($jobFilters: [JobFilter!]!) {
|
||||||
stats: jobsStatistics(filter: $jobFilters) {
|
stats: jobsStatistics(filter: $jobFilters) {
|
||||||
totalJobs
|
totalJobs
|
||||||
shortJobs
|
shortJobs
|
||||||
@ -79,19 +117,35 @@
|
|||||||
totalNodeHours
|
totalNodeHours
|
||||||
totalCoreHours
|
totalCoreHours
|
||||||
totalAccHours
|
totalAccHours
|
||||||
histDuration { count, value }
|
histDuration {
|
||||||
histNumCores { count, value }
|
count
|
||||||
|
value
|
||||||
|
}
|
||||||
|
histNumCores {
|
||||||
|
count
|
||||||
|
value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { jobFilters }
|
variables: { jobFilters },
|
||||||
})
|
});
|
||||||
|
|
||||||
$: topQuery = queryStore({
|
$: topQuery = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query($jobFilters: [JobFilter!]!, $paging: PageRequest!, $sortBy: SortByAggregate!, $groupBy: Aggregate!) {
|
query (
|
||||||
topList: jobsStatistics(filter: $jobFilters, page: $paging, sortBy: $sortBy, groupBy: $groupBy) {
|
$jobFilters: [JobFilter!]!
|
||||||
|
$paging: PageRequest!
|
||||||
|
$sortBy: SortByAggregate!
|
||||||
|
$groupBy: Aggregate!
|
||||||
|
) {
|
||||||
|
topList: jobsStatistics(
|
||||||
|
filter: $jobFilters
|
||||||
|
page: $paging
|
||||||
|
sortBy: $sortBy
|
||||||
|
groupBy: $groupBy
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
totalWalltime
|
totalWalltime
|
||||||
totalNodeHours
|
totalNodeHours
|
||||||
@ -100,32 +154,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { jobFilters, paging: { itemsPerPage: 10, page: 1 }, sortBy: sortSelection.key.toUpperCase(), groupBy: groupSelection.key.toUpperCase() }
|
variables: {
|
||||||
})
|
jobFilters,
|
||||||
|
paging: { itemsPerPage: 10, page: 1 },
|
||||||
|
sortBy: sortSelection.key.toUpperCase(),
|
||||||
|
groupBy: groupSelection.key.toUpperCase(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
$: footprintsQuery = queryStore({
|
$: footprintsQuery = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
|
query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
|
||||||
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
|
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
|
||||||
timeWeights { nodeHours, accHours, coreHours },
|
timeWeights {
|
||||||
metrics { metric, data }
|
nodeHours
|
||||||
|
accHours
|
||||||
|
coreHours
|
||||||
}
|
}
|
||||||
}`,
|
metrics {
|
||||||
variables: { jobFilters, metrics }
|
metric
|
||||||
})
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { jobFilters, metrics },
|
||||||
|
});
|
||||||
|
|
||||||
$: rooflineQuery = queryStore({
|
$: rooflineQuery = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query($jobFilters: [JobFilter!]!, $rows: Int!, $cols: Int!,
|
query (
|
||||||
$minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) {
|
$jobFilters: [JobFilter!]!
|
||||||
rooflineHeatmap(filter: $jobFilters, rows: $rows, cols: $cols,
|
$rows: Int!
|
||||||
minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY)
|
$cols: Int!
|
||||||
|
$minX: Float!
|
||||||
|
$minY: Float!
|
||||||
|
$maxX: Float!
|
||||||
|
$maxY: Float!
|
||||||
|
) {
|
||||||
|
rooflineHeatmap(
|
||||||
|
filter: $jobFilters
|
||||||
|
rows: $rows
|
||||||
|
cols: $cols
|
||||||
|
minX: $minX
|
||||||
|
minY: $minY
|
||||||
|
maxX: $maxX
|
||||||
|
maxY: $maxY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { jobFilters, rows: 50, cols: 50, minX: 0.01, minY: 1., maxX: 1000., maxY }
|
variables: {
|
||||||
})
|
jobFilters,
|
||||||
|
rows: 50,
|
||||||
|
cols: 50,
|
||||||
|
minX: 0.01,
|
||||||
|
minY: 1,
|
||||||
|
maxX: 1000,
|
||||||
|
maxY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
@ -135,46 +224,54 @@
|
|||||||
updateConfiguration(name: $name, value: $value)
|
updateConfiguration(name: $name, value: $value)
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { name, value }
|
variables: { name, value },
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function updateEntityConfiguration(select) {
|
function updateEntityConfiguration(select) {
|
||||||
if (ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] != select) {
|
if (
|
||||||
updateConfigurationMutation({ name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`, value: JSON.stringify(select) })
|
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] !=
|
||||||
.subscribe(res => {
|
select
|
||||||
|
) {
|
||||||
|
updateConfigurationMutation({
|
||||||
|
name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`,
|
||||||
|
value: JSON.stringify(select),
|
||||||
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && !res.error) {
|
||||||
// console.log(`analysis_view_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
|
// console.log(`analysis_view_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
|
||||||
} else if (res.fetching === false && res.error) {
|
} else if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
// console.log('No Mutation Required: Entity')
|
// console.log('No Mutation Required: Entity')
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateCategoryConfiguration(select) {
|
function updateCategoryConfiguration(select) {
|
||||||
if (ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] != select) {
|
if (
|
||||||
updateConfigurationMutation({ name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`, value: JSON.stringify(select) })
|
ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] !=
|
||||||
.subscribe(res => {
|
select
|
||||||
|
) {
|
||||||
|
updateConfigurationMutation({
|
||||||
|
name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`,
|
||||||
|
value: JSON.stringify(select),
|
||||||
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && !res.error) {
|
||||||
// console.log(`analysis_view_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
|
// console.log(`analysis_view_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
|
||||||
} else if (res.fetching === false && res.error) {
|
} else if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
// console.log('No Mutation Required: Category')
|
// console.log('No Mutation Required: Category')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
$: updateEntityConfiguration(groupSelection.key);
|
||||||
|
$: updateCategoryConfiguration(sortSelection.key);
|
||||||
|
|
||||||
$: updateEntityConfiguration(groupSelection.key)
|
onMount(() => filterComponent.update());
|
||||||
$: updateCategoryConfiguration(sortSelection.key)
|
|
||||||
|
|
||||||
onMount(() => filterComponent.update())
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
@ -188,24 +285,26 @@
|
|||||||
<Card body color="danger">{$initq.error.message}</Card>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{:else if cluster}
|
{:else if cluster}
|
||||||
<PlotSelection
|
<PlotSelection
|
||||||
availableMetrics={cluster.metricConfig.map(mc => mc.name)}
|
availableMetrics={cluster.metricConfig.map((mc) => mc.name)}
|
||||||
bind:metricsInHistograms={metricsInHistograms}
|
bind:metricsInHistograms
|
||||||
bind:metricsInScatterplots={metricsInScatterplots} />
|
bind:metricsInScatterplots
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Filters
|
<Filters
|
||||||
bind:this={filterComponent}
|
bind:this={filterComponent}
|
||||||
filterPresets={filterPresets}
|
{filterPresets}
|
||||||
disableClusterSelection={true}
|
disableClusterSelection={true}
|
||||||
startTimeQuickSelect={true}
|
startTimeQuickSelect={true}
|
||||||
on:update={({ detail }) => {
|
on:update={({ detail }) => {
|
||||||
jobFilters = detail.filters;
|
jobFilters = detail.filters;
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<br/>
|
<br />
|
||||||
{#if $statsQuery.error}
|
{#if $statsQuery.error}
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
@ -244,7 +343,8 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div bind:clientWidth={colWidth1}>
|
<div bind:clientWidth={colWidth1}>
|
||||||
<h5>Top
|
<h5>
|
||||||
|
Top
|
||||||
<select class="p-0" bind:value={groupSelection}>
|
<select class="p-0" bind:value={groupSelection}>
|
||||||
{#each groupOptions as option}
|
{#each groupOptions as option}
|
||||||
<option value={option}>
|
<option value={option}>
|
||||||
@ -255,14 +355,16 @@
|
|||||||
</h5>
|
</h5>
|
||||||
{#key $topQuery.data}
|
{#key $topQuery.data}
|
||||||
{#if $topQuery.fetching}
|
{#if $topQuery.fetching}
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
{:else if $topQuery.error}
|
{:else if $topQuery.error}
|
||||||
<Card body color="danger">{$topQuery.error.message}</Card>
|
<Card body color="danger">{$topQuery.error.message}</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Pie
|
<Pie
|
||||||
size={colWidth1}
|
size={colWidth1}
|
||||||
sliceLabel={sortSelection.label}
|
sliceLabel={sortSelection.label}
|
||||||
quantities={$topQuery.data.topList.map((t) => t[sortSelection.key])}
|
quantities={$topQuery.data.topList.map(
|
||||||
|
(t) => t[sortSelection.key],
|
||||||
|
)}
|
||||||
entities={$topQuery.data.topList.map((t) => t.id)}
|
entities={$topQuery.data.topList.map((t) => t.id)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@ -272,7 +374,7 @@
|
|||||||
<Col>
|
<Col>
|
||||||
{#key $topQuery.data}
|
{#key $topQuery.data}
|
||||||
{#if $topQuery.fetching}
|
{#if $topQuery.fetching}
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
{:else if $topQuery.error}
|
{:else if $topQuery.error}
|
||||||
<Card body color="danger">{$topQuery.error.message}</Card>
|
<Card body color="danger">{$topQuery.error.message}</Card>
|
||||||
{:else}
|
{:else}
|
||||||
@ -292,11 +394,20 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{#each $topQuery.data.topList as te, i}
|
{#each $topQuery.data.topList as te, i}
|
||||||
<tr>
|
<tr>
|
||||||
<td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
|
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
||||||
{#if groupSelection.key == 'user'}
|
{#if groupSelection.key == "user"}
|
||||||
<th scope="col"><a href="/monitoring/user/{te.id}?cluster={cluster.name}">{te.id}</a></th>
|
<th scope="col"
|
||||||
|
><a href="/monitoring/user/{te.id}?cluster={cluster.name}"
|
||||||
|
>{te.id}</a
|
||||||
|
></th
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<th scope="col"><a href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq">{te.id}</a></th>
|
<th scope="col"
|
||||||
|
><a
|
||||||
|
href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq"
|
||||||
|
>{te.id}</a
|
||||||
|
></th
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<td>{te[sortSelection.key]}</td>
|
<td>{te[sortSelection.key]}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -316,10 +427,14 @@
|
|||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $rooflineQuery.data}
|
{#key $rooflineQuery.data}
|
||||||
<RooflineHeatmap
|
<RooflineHeatmap
|
||||||
width={colWidth2} height={300}
|
width={colWidth2}
|
||||||
|
height={300}
|
||||||
tiles={$rooflineQuery.data.rooflineHeatmap}
|
tiles={$rooflineQuery.data.rooflineHeatmap}
|
||||||
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
|
cluster={cluster.subClusters.length == 1
|
||||||
maxY={rooflineMaxY} />
|
? cluster.subClusters[0]
|
||||||
|
: null}
|
||||||
|
maxY={rooflineMaxY}
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -328,13 +443,15 @@
|
|||||||
<div bind:clientWidth={colWidth3}>
|
<div bind:clientWidth={colWidth3}>
|
||||||
{#key $statsQuery.data.stats[0].histDuration}
|
{#key $statsQuery.data.stats[0].histDuration}
|
||||||
<Histogram
|
<Histogram
|
||||||
width={colWidth3} height={300}
|
width={colWidth3}
|
||||||
|
height={300}
|
||||||
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
|
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
|
||||||
title="Duration Distribution"
|
title="Duration Distribution"
|
||||||
xlabel="Current Runtimes"
|
xlabel="Current Runtimes"
|
||||||
xunit="Hours"
|
xunit="Hours"
|
||||||
ylabel="Number of Jobs"
|
ylabel="Number of Jobs"
|
||||||
yunit="Jobs"/>
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
@ -342,20 +459,22 @@
|
|||||||
<div bind:clientWidth={colWidth4}>
|
<div bind:clientWidth={colWidth4}>
|
||||||
{#key $statsQuery.data.stats[0].histNumCores}
|
{#key $statsQuery.data.stats[0].histNumCores}
|
||||||
<Histogram
|
<Histogram
|
||||||
width={colWidth4} height={300}
|
width={colWidth4}
|
||||||
|
height={300}
|
||||||
data={convert2uplot($statsQuery.data.stats[0].histNumCores)}
|
data={convert2uplot($statsQuery.data.stats[0].histNumCores)}
|
||||||
title="Number of Cores Distribution"
|
title="Number of Cores Distribution"
|
||||||
xlabel="Allocated Cores"
|
xlabel="Allocated Cores"
|
||||||
xunit="Cores"
|
xunit="Cores"
|
||||||
ylabel="Number of Jobs"
|
ylabel="Number of Jobs"
|
||||||
yunit="Jobs"/>
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr class="my-6"/>
|
<hr class="my-6" />
|
||||||
|
|
||||||
{#if $footprintsQuery.error}
|
{#if $footprintsQuery.error}
|
||||||
<Row>
|
<Row>
|
||||||
@ -367,11 +486,14 @@
|
|||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<Card body>
|
<Card body>
|
||||||
These histograms show the distribution of the averages of all jobs matching the filters. Each job/average is weighted by its node hours by default
|
These histograms show the distribution of the averages of all jobs
|
||||||
(Accelerator hours for native accelerator scope metrics, coreHours for native core scope metrics).
|
matching the filters. Each job/average is weighted by its node hours by
|
||||||
Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
|
default (Accelerator hours for native accelerator scope metrics,
|
||||||
|
coreHours for native core scope metrics). Note that some metrics could
|
||||||
|
be disabled for specific subclusters as per metricConfig and thus could
|
||||||
|
affect shown average values.
|
||||||
</Card>
|
</Card>
|
||||||
<br/>
|
<br />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
@ -380,34 +502,58 @@
|
|||||||
let:item
|
let:item
|
||||||
let:width
|
let:width
|
||||||
renderFor="analysis"
|
renderFor="analysis"
|
||||||
items={metricsInHistograms.map(metric => ({ metric, ...binsFromFootprint(
|
items={metricsInHistograms.map((metric) => ({
|
||||||
|
metric,
|
||||||
|
...binsFromFootprint(
|
||||||
$footprintsQuery.data.footprints.timeWeights,
|
$footprintsQuery.data.footprints.timeWeights,
|
||||||
metricConfig(cluster.name, metric)?.scope,
|
metricConfig(cluster.name, metric)?.scope,
|
||||||
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
|
$footprintsQuery.data.footprints.metrics.find(
|
||||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
|
(f) => f.metric == metric,
|
||||||
|
).data,
|
||||||
|
numBins,
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||||
|
>
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot(item.bins)}
|
data={convert2uplot(item.bins)}
|
||||||
width={width} height={250}
|
{width}
|
||||||
|
height={250}
|
||||||
usesBins={true}
|
usesBins={true}
|
||||||
title="Average Distribution of '{item.metric}'"
|
title="Average Distribution of '{item.metric}'"
|
||||||
xlabel={`${item.metric} bin maximum ${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? '[' + metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
|
xlabel={`${item.metric} bin maximum ${
|
||||||
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base + ']' : '')}`}
|
(metricConfig(cluster.name, item.metric)?.unit?.prefix
|
||||||
xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
|
? "[" + metricConfig(cluster.name, item.metric)?.unit?.prefix
|
||||||
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}`}
|
: "") +
|
||||||
|
(metricConfig(cluster.name, item.metric)?.unit?.base
|
||||||
|
? metricConfig(cluster.name, item.metric)?.unit?.base + "]"
|
||||||
|
: "")
|
||||||
|
}`}
|
||||||
|
xunit={`${
|
||||||
|
(metricConfig(cluster.name, item.metric)?.unit?.prefix
|
||||||
|
? metricConfig(cluster.name, item.metric)?.unit?.prefix
|
||||||
|
: "") +
|
||||||
|
(metricConfig(cluster.name, item.metric)?.unit?.base
|
||||||
|
? metricConfig(cluster.name, item.metric)?.unit?.base
|
||||||
|
: "")
|
||||||
|
}`}
|
||||||
ylabel="Normalized Hours"
|
ylabel="Normalized Hours"
|
||||||
yunit="Hours"/>
|
yunit="Hours"
|
||||||
|
/>
|
||||||
</PlotTable>
|
</PlotTable>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br/>
|
<br />
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<Card body>
|
<Card body>
|
||||||
Each circle represents one job. The size of a circle is proportional to its node hours. Darker circles mean multiple jobs have the same averages for the respective metrics.
|
Each circle represents one job. The size of a circle is proportional to
|
||||||
Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
|
its node hours. Darker circles mean multiple jobs have the same averages
|
||||||
|
for the respective metrics. Note that some metrics could be disabled for
|
||||||
|
specific subclusters as per metricConfig and thus could affect shown
|
||||||
|
average values.
|
||||||
</Card>
|
</Card>
|
||||||
<br/>
|
<br />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
@ -417,17 +563,41 @@
|
|||||||
let:width
|
let:width
|
||||||
renderFor="analysis"
|
renderFor="analysis"
|
||||||
items={metricsInScatterplots.map(([m1, m2]) => ({
|
items={metricsInScatterplots.map(([m1, m2]) => ({
|
||||||
m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
|
m1,
|
||||||
m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
|
f1: $footprintsQuery.data.footprints.metrics.find(
|
||||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
|
(f) => f.metric == m1,
|
||||||
|
).data,
|
||||||
|
m2,
|
||||||
|
f2: $footprintsQuery.data.footprints.metrics.find(
|
||||||
|
(f) => f.metric == m2,
|
||||||
|
).data,
|
||||||
|
}))}
|
||||||
|
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||||
|
>
|
||||||
<ScatterPlot
|
<ScatterPlot
|
||||||
width={width} height={250} color={"rgba(0, 102, 204, 0.33)"}
|
{width}
|
||||||
xLabel={`${item.m1} [${(metricConfig(cluster.name, item.m1)?.unit?.prefix ? metricConfig(cluster.name, item.m1)?.unit?.prefix : '') +
|
height={250}
|
||||||
(metricConfig(cluster.name, item.m1)?.unit?.base ? metricConfig(cluster.name, item.m1)?.unit?.base : '')}]`}
|
color={"rgba(0, 102, 204, 0.33)"}
|
||||||
yLabel={`${item.m2} [${(metricConfig(cluster.name, item.m2)?.unit?.prefix ? metricConfig(cluster.name, item.m2)?.unit?.prefix : '') +
|
xLabel={`${item.m1} [${
|
||||||
(metricConfig(cluster.name, item.m2)?.unit?.base ? metricConfig(cluster.name, item.m2)?.unit?.base : '')}]`}
|
(metricConfig(cluster.name, item.m1)?.unit?.prefix
|
||||||
X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.timeWeights.nodeHours} />
|
? metricConfig(cluster.name, item.m1)?.unit?.prefix
|
||||||
|
: "") +
|
||||||
|
(metricConfig(cluster.name, item.m1)?.unit?.base
|
||||||
|
? metricConfig(cluster.name, item.m1)?.unit?.base
|
||||||
|
: "")
|
||||||
|
}]`}
|
||||||
|
yLabel={`${item.m2} [${
|
||||||
|
(metricConfig(cluster.name, item.m2)?.unit?.prefix
|
||||||
|
? metricConfig(cluster.name, item.m2)?.unit?.prefix
|
||||||
|
: "") +
|
||||||
|
(metricConfig(cluster.name, item.m2)?.unit?.base
|
||||||
|
? metricConfig(cluster.name, item.m2)?.unit?.base
|
||||||
|
: "")
|
||||||
|
}]`}
|
||||||
|
X={item.f1}
|
||||||
|
Y={item.f2}
|
||||||
|
S={$footprintsQuery.data.footprints.timeWeights.nodeHours}
|
||||||
|
/>
|
||||||
</PlotTable>
|
</PlotTable>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from "svelte";
|
||||||
import { init } from './utils.js'
|
import { init } from "./utils.js";
|
||||||
import { Card, CardHeader, CardTitle } from 'sveltestrap'
|
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
import PlotSettings from './config/PlotSettings.svelte'
|
import PlotSettings from "./config/PlotSettings.svelte";
|
||||||
import AdminSettings from './config/AdminSettings.svelte'
|
import AdminSettings from "./config/AdminSettings.svelte";
|
||||||
|
|
||||||
const { query: initq } = init()
|
const { query: initq } = init();
|
||||||
|
|
||||||
const ccconfig = getContext('cc-config')
|
const ccconfig = getContext("cc-config");
|
||||||
|
|
||||||
export let isAdmin
|
|
||||||
|
|
||||||
|
export let isAdmin;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAdmin == true}
|
{#if isAdmin == true}
|
||||||
<Card style="margin-bottom: 1.5em;">
|
<Card style="margin-bottom: 1.5em;">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<AdminSettings/>
|
<AdminSettings />
|
||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<PlotSettings config={ccconfig}/>
|
<PlotSettings config={ccconfig} />
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import NavbarLinks from "./NavbarLinks.svelte";
|
import NavbarLinks from "./NavbarLinks.svelte";
|
||||||
import NavbarTools from "./NavbarTools.svelte";
|
import NavbarTools from "./NavbarTools.svelte";
|
||||||
|
|
||||||
@ -116,17 +116,13 @@
|
|||||||
{#if screenSize > 1500 || screenSize < 768}
|
{#if screenSize > 1500 || screenSize < 768}
|
||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
links={views.filter(
|
links={views.filter((item) => item.requiredRole <= authlevel)}
|
||||||
(item) => item.requiredRole <= authlevel
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{:else if screenSize > 1300}
|
{:else if screenSize > 1300}
|
||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
links={views.filter(
|
links={views.filter(
|
||||||
(item) =>
|
(item) => item.requiredRole <= authlevel && item.menu != "Stats",
|
||||||
item.requiredRole <= authlevel &&
|
|
||||||
item.menu != "Stats"
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Dropdown nav>
|
<Dropdown nav>
|
||||||
@ -139,8 +135,7 @@
|
|||||||
{clusters}
|
{clusters}
|
||||||
links={views.filter(
|
links={views.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.requiredRole <= authlevel &&
|
item.requiredRole <= authlevel && item.menu == "Stats",
|
||||||
item.menu == "Stats"
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -149,9 +144,7 @@
|
|||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
links={views.filter(
|
links={views.filter(
|
||||||
(item) =>
|
(item) => item.requiredRole <= authlevel && item.menu == "none",
|
||||||
item.requiredRole <= authlevel &&
|
|
||||||
item.menu == "none"
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{#each Array("Groups", "Stats") as menu}
|
{#each Array("Groups", "Stats") as menu}
|
||||||
@ -163,9 +156,7 @@
|
|||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
links={views.filter(
|
links={views.filter(
|
||||||
(item) =>
|
(item) => item.requiredRole <= authlevel && item.menu == menu,
|
||||||
item.requiredRole <= authlevel &&
|
|
||||||
item.menu == menu
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
@ -1,65 +1,73 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Modal, ModalBody, ModalHeader, ModalFooter,
|
import {
|
||||||
Button, ListGroup, ListGroupItem } from 'sveltestrap'
|
Modal,
|
||||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
ListGroup,
|
||||||
|
ListGroupItem,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let cluster
|
export let cluster;
|
||||||
export let metricsInHistograms
|
export let metricsInHistograms;
|
||||||
export let isOpen
|
export let isOpen;
|
||||||
|
|
||||||
let availableMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // 'net_bw', 'file_bw'
|
let availableMetrics = ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // 'net_bw', 'file_bw'
|
||||||
let pendingMetrics = [...metricsInHistograms] // Copy
|
let pendingMetrics = [...metricsInHistograms]; // Copy
|
||||||
const client = getContextClient()
|
const client = getContextClient();
|
||||||
|
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`mutation($name: String!, $value: String!) {
|
query: gql`
|
||||||
|
mutation ($name: String!, $value: String!) {
|
||||||
updateConfiguration(name: $name, value: $value)
|
updateConfiguration(name: $name, value: $value)
|
||||||
}`,
|
|
||||||
variables: { name, value }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
variables: { name, value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function updateConfiguration(data) {
|
function updateConfiguration(data) {
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
value: JSON.stringify(data.value)
|
value: JSON.stringify(data.value),
|
||||||
}).subscribe(res => {
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAndApply() {
|
function closeAndApply() {
|
||||||
metricsInHistograms = [...pendingMetrics] // Set for parent
|
metricsInHistograms = [...pendingMetrics]; // Set for parent
|
||||||
isOpen = !isOpen
|
isOpen = !isOpen;
|
||||||
updateConfiguration({
|
updateConfiguration({
|
||||||
name: cluster ? `user_view_histogramMetrics:${cluster}` : 'user_view_histogramMetrics',
|
name: cluster
|
||||||
value: metricsInHistograms
|
? `user_view_histogramMetrics:${cluster}`
|
||||||
})
|
: "user_view_histogramMetrics",
|
||||||
|
value: metricsInHistograms,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen}
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
toggle={() => (isOpen = !isOpen)}>
|
<ModalHeader>Select metrics presented in histograms</ModalHeader>
|
||||||
<ModalHeader>
|
|
||||||
Select metrics presented in histograms
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#each availableMetrics as metric (metric)}
|
{#each availableMetrics as metric (metric)}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<input type="checkbox" bind:group={pendingMetrics} value={metric}>
|
<input type="checkbox" bind:group={pendingMetrics} value={metric} />
|
||||||
{metric}
|
{metric}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={closeAndApply}> Close & Apply </Button>
|
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
||||||
<Button color="secondary" on:click={() => (isOpen = !isOpen)}> Close </Button>
|
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
groupByScope,
|
groupByScope,
|
||||||
fetchMetricsStore,
|
fetchMetricsStore,
|
||||||
checkMetricDisabled,
|
checkMetricDisabled,
|
||||||
transformDataForRoofline
|
transformDataForRoofline,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
@ -18,7 +18,7 @@
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import PlotTable from "./PlotTable.svelte";
|
import PlotTable from "./PlotTable.svelte";
|
||||||
import Metric from "./Metric.svelte";
|
import Metric from "./Metric.svelte";
|
||||||
import Polar from "./plots/Polar.svelte";
|
import Polar from "./plots/Polar.svelte";
|
||||||
@ -34,8 +34,15 @@
|
|||||||
export let authlevel;
|
export let authlevel;
|
||||||
export let roles;
|
export let roles;
|
||||||
|
|
||||||
const accMetrics = ['acc_utilization', 'acc_mem_used', 'acc_power', 'nv_mem_util', 'nv_sm_clock', 'nv_temp'];
|
const accMetrics = [
|
||||||
let accNodeOnly
|
"acc_utilization",
|
||||||
|
"acc_mem_used",
|
||||||
|
"acc_power",
|
||||||
|
"nv_mem_util",
|
||||||
|
"nv_sm_clock",
|
||||||
|
"nv_temp",
|
||||||
|
];
|
||||||
|
let accNodeOnly;
|
||||||
|
|
||||||
const { query: initq } = init(`
|
const { query: initq } = init(`
|
||||||
job(id: "${dbid}") {
|
job(id: "${dbid}") {
|
||||||
@ -54,7 +61,7 @@
|
|||||||
|
|
||||||
const ccconfig = getContext("cc-config"),
|
const ccconfig = getContext("cc-config"),
|
||||||
clusters = getContext("clusters"),
|
clusters = getContext("clusters"),
|
||||||
metrics = getContext("metrics")
|
metrics = getContext("metrics");
|
||||||
|
|
||||||
let isMetricsSelectionOpen = false,
|
let isMetricsSelectionOpen = false,
|
||||||
selectedMetrics = [],
|
selectedMetrics = [],
|
||||||
@ -81,23 +88,21 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
|
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
|
||||||
accNodeOnly = [...toFetch].some(function(m) {
|
accNodeOnly = [...toFetch].some(function (m) {
|
||||||
if (accMetrics.includes(m)) {
|
if (accMetrics.includes(m)) {
|
||||||
const mc = metrics(job.cluster, m)
|
const mc = metrics(job.cluster, m);
|
||||||
return mc.scope !== 'accelerator'
|
return mc.scope !== "accelerator";
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (job.numAcc === 0 || accNodeOnly === true) {
|
if (job.numAcc === 0 || accNodeOnly === true) {
|
||||||
// No Accels or Accels on Node Scope
|
// No Accels or Accels on Node Scope
|
||||||
startFetching(
|
startFetching(
|
||||||
job,
|
job,
|
||||||
[...toFetch],
|
[...toFetch],
|
||||||
job.numNodes > 2
|
job.numNodes > 2 ? ["node"] : ["node", "socket", "core"],
|
||||||
? ["node"]
|
|
||||||
: ["node", "socket", "core"]
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Accels and not on node scope
|
// Accels and not on node scope
|
||||||
@ -106,7 +111,7 @@
|
|||||||
[...toFetch],
|
[...toFetch],
|
||||||
job.numNodes > 2
|
job.numNodes > 2
|
||||||
? ["node", "accelerator"]
|
? ["node", "accelerator"]
|
||||||
: ["node", "accelerator", "socket", "core"]
|
: ["node", "accelerator", "socket", "core"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +131,7 @@
|
|||||||
startFetching(
|
startFetching(
|
||||||
$initq.data.job,
|
$initq.data.job,
|
||||||
[...notYetFetched],
|
[...notYetFetched],
|
||||||
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"]
|
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -163,8 +168,8 @@
|
|||||||
!checkMetricDisabled(
|
!checkMetricDisabled(
|
||||||
metric,
|
metric,
|
||||||
$initq.data.job.cluster,
|
$initq.data.job.cluster,
|
||||||
$initq.data.job.subCluster
|
$initq.data.job.subCluster,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
missingHosts = job.resources
|
missingHosts = job.resources
|
||||||
.map(({ hostname }) => ({
|
.map(({ hostname }) => ({
|
||||||
@ -174,10 +179,8 @@
|
|||||||
!metrics.some(
|
!metrics.some(
|
||||||
(jm) =>
|
(jm) =>
|
||||||
jm.scope == "node" &&
|
jm.scope == "node" &&
|
||||||
jm.metric.series.some(
|
jm.metric.series.some((series) => series.hostname == hostname),
|
||||||
(series) => series.hostname == hostname
|
),
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
.filter(({ metrics }) => metrics.length > 0);
|
.filter(({ metrics }) => metrics.length > 0);
|
||||||
@ -191,7 +194,7 @@
|
|||||||
disabled: checkMetricDisabled(
|
disabled: checkMetricDisabled(
|
||||||
metric,
|
metric,
|
||||||
$initq.data.job.cluster,
|
$initq.data.job.cluster,
|
||||||
$initq.data.job.subCluster
|
$initq.data.job.subCluster,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
@ -231,16 +234,15 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="/monitoring/jobs/?{$initq.data.job
|
href="/monitoring/jobs/?{$initq.data.job.concurrentJobs
|
||||||
.concurrentJobs.listQuery}"
|
.listQuery}"
|
||||||
target="_blank">See All</a
|
target="_blank">See All</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{#each $initq.data.job.concurrentJobs.items as pjob, index}
|
{#each $initq.data.job.concurrentJobs.items as pjob, index}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="/monitoring/job/{pjob.id}" target="_blank"
|
||||||
href="/monitoring/job/{pjob.id}"
|
>{pjob.jobId}</a
|
||||||
target="_blank">{pjob.jobId}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@ -249,12 +251,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Col>
|
<Col>
|
||||||
<h5>
|
<h5>
|
||||||
{$initq.data.job.concurrentJobs.items.length} Concurrent
|
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
|
||||||
Jobs
|
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
Number of shared jobs on the same node with overlapping
|
Number of shared jobs on the same node with overlapping runtimes.
|
||||||
runtimes.
|
|
||||||
</p>
|
</p>
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
@ -273,15 +273,15 @@
|
|||||||
renderTime={true}
|
renderTime={true}
|
||||||
cluster={clusters
|
cluster={clusters
|
||||||
.find((c) => c.name == $initq.data.job.cluster)
|
.find((c) => c.name == $initq.data.job.cluster)
|
||||||
.subClusters.find(
|
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
|
||||||
(sc) => sc.name == $initq.data.job.subCluster
|
data={transformDataForRoofline(
|
||||||
|
$jobMetrics.data.jobMetrics.find(
|
||||||
|
(m) => m.name == "flops_any" && m.scope == "node",
|
||||||
|
).metric,
|
||||||
|
$jobMetrics.data.jobMetrics.find(
|
||||||
|
(m) => m.name == "mem_bw" && m.scope == "node",
|
||||||
|
).metric,
|
||||||
)}
|
)}
|
||||||
data={
|
|
||||||
transformDataForRoofline (
|
|
||||||
$jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric,
|
|
||||||
$jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
{:else}
|
{:else}
|
||||||
@ -302,7 +302,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<!-- <Col xs="auto">
|
<!-- <Col xs="auto">
|
||||||
<Zoom timeseriesPlots={plots} />
|
<Zoom timeseriesPlots={plots} />
|
||||||
</Col> -->
|
</Col> -->
|
||||||
</Row>
|
</Row>
|
||||||
@ -310,9 +310,7 @@
|
|||||||
<Col>
|
<Col>
|
||||||
{#if $jobMetrics.error}
|
{#if $jobMetrics.error}
|
||||||
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
|
{#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
|
||||||
<Card body color="warning"
|
<Card body color="warning">Not monitored or archiving failed</Card>
|
||||||
>Not monitored or archiving failed</Card
|
|
||||||
>
|
|
||||||
<br />
|
<br />
|
||||||
{/if}
|
{/if}
|
||||||
<Card body color="danger">{$jobMetrics.error.message}</Card>
|
<Card body color="danger">{$jobMetrics.error.message}</Card>
|
||||||
@ -325,15 +323,14 @@
|
|||||||
renderFor="job"
|
renderFor="job"
|
||||||
items={orderAndMap(
|
items={orderAndMap(
|
||||||
groupByScope($jobMetrics.data.jobMetrics),
|
groupByScope($jobMetrics.data.jobMetrics),
|
||||||
selectedMetrics
|
selectedMetrics,
|
||||||
)}
|
)}
|
||||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||||
>
|
>
|
||||||
{#if item.data}
|
{#if item.data}
|
||||||
<Metric
|
<Metric
|
||||||
bind:this={plots[item.metric]}
|
bind:this={plots[item.metric]}
|
||||||
on:more-loaded={({ detail }) =>
|
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
|
||||||
statsTable.moreLoaded(detail)}
|
|
||||||
job={$initq.data.job}
|
job={$initq.data.job}
|
||||||
metricName={item.metric}
|
metricName={item.metric}
|
||||||
rawData={item.data.map((x) => x.metric)}
|
rawData={item.data.map((x) => x.metric)}
|
||||||
@ -344,8 +341,7 @@
|
|||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning"
|
<Card body color="warning"
|
||||||
>No dataset returned for <code>{item.metric}</code
|
>No dataset returned for <code>{item.metric}</code></Card
|
||||||
></Card
|
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</PlotTable>
|
</PlotTable>
|
||||||
@ -357,36 +353,26 @@
|
|||||||
{#if $initq.data}
|
{#if $initq.data}
|
||||||
<TabContent>
|
<TabContent>
|
||||||
{#if somethingMissing}
|
{#if somethingMissing}
|
||||||
<TabPane
|
<TabPane tabId="resources" tab="Resources" active={somethingMissing}>
|
||||||
tabId="resources"
|
|
||||||
tab="Resources"
|
|
||||||
active={somethingMissing}
|
|
||||||
>
|
|
||||||
<div style="margin: 10px;">
|
<div style="margin: 10px;">
|
||||||
<Card color="warning">
|
<Card color="warning">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle
|
<CardTitle>Missing Metrics/Reseources</CardTitle>
|
||||||
>Missing Metrics/Reseources</CardTitle
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{#if missingMetrics.length > 0}
|
{#if missingMetrics.length > 0}
|
||||||
<p>
|
<p>
|
||||||
No data at all is available for the
|
No data at all is available for the metrics: {missingMetrics.join(
|
||||||
metrics: {missingMetrics.join(", ")}
|
", ",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if missingHosts.length > 0}
|
{#if missingHosts.length > 0}
|
||||||
<p>
|
<p>Some metrics are missing for the following hosts:</p>
|
||||||
Some metrics are missing for the
|
|
||||||
following hosts:
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each missingHosts as missing}
|
{#each missingHosts as missing}
|
||||||
<li>
|
<li>
|
||||||
{missing.hostname}: {missing.metrics.join(
|
{missing.hostname}: {missing.metrics.join(", ")}
|
||||||
", "
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@ -414,22 +400,16 @@
|
|||||||
<TabPane tabId="job-script" tab="Job Script">
|
<TabPane tabId="job-script" tab="Job Script">
|
||||||
<div class="pre-wrapper">
|
<div class="pre-wrapper">
|
||||||
{#if $initq.data.job.metaData?.jobScript}
|
{#if $initq.data.job.metaData?.jobScript}
|
||||||
<pre><code
|
<pre><code>{$initq.data.job.metaData?.jobScript}</code></pre>
|
||||||
>{$initq.data.job.metaData?.jobScript}</code
|
|
||||||
></pre>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning"
|
<Card body color="warning">No job script available</Card>
|
||||||
>No job script available</Card
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tabId="slurm-info" tab="Slurm Info">
|
<TabPane tabId="slurm-info" tab="Slurm Info">
|
||||||
<div class="pre-wrapper">
|
<div class="pre-wrapper">
|
||||||
{#if $initq.data.job.metaData?.slurmInfo}
|
{#if $initq.data.job.metaData?.slurmInfo}
|
||||||
<pre><code
|
<pre><code>{$initq.data.job.metaData?.slurmInfo}</code></pre>
|
||||||
>{$initq.data.job.metaData?.slurmInfo}</code
|
|
||||||
></pre>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning"
|
<Card body color="warning"
|
||||||
>No additional slurm information available</Card
|
>No additional slurm information available</Card
|
||||||
|
@ -1,5 +1,65 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function findJobThresholds(job, metricConfig, subClusterConfig) {
|
||||||
|
if (!job || !metricConfig || !subClusterConfig) {
|
||||||
|
console.warn("Argument missing for findJobThresholds!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subclusterThresholds = metricConfig.subClusters.find(
|
||||||
|
(sc) => sc.name == subClusterConfig.name,
|
||||||
|
);
|
||||||
|
const defaultThresholds = {
|
||||||
|
peak: subclusterThresholds
|
||||||
|
? subclusterThresholds.peak
|
||||||
|
: metricConfig.peak,
|
||||||
|
normal: subclusterThresholds
|
||||||
|
? subclusterThresholds.normal
|
||||||
|
: metricConfig.normal,
|
||||||
|
caution: subclusterThresholds
|
||||||
|
? subclusterThresholds.caution
|
||||||
|
: metricConfig.caution,
|
||||||
|
alert: subclusterThresholds
|
||||||
|
? subclusterThresholds.alert
|
||||||
|
: metricConfig.alert,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (job.exclusive === 1) {
|
||||||
|
// Exclusive: Use as defined
|
||||||
|
return defaultThresholds;
|
||||||
|
} else {
|
||||||
|
// Shared: Handle specifically
|
||||||
|
if (metricConfig.name === "cpu_load") {
|
||||||
|
// Special: Avg Aggregation BUT scaled based on #hwthreads
|
||||||
|
return {
|
||||||
|
peak: job.numHWThreads,
|
||||||
|
normal: job.numHWThreads,
|
||||||
|
caution: defaultThresholds.caution,
|
||||||
|
alert: defaultThresholds.alert,
|
||||||
|
};
|
||||||
|
} else if (metricConfig.aggregation === "avg") {
|
||||||
|
return defaultThresholds;
|
||||||
|
} else if (metricConfig.aggregation === "sum") {
|
||||||
|
const jobFraction =
|
||||||
|
job.numHWThreads / subClusterConfig.topology.node.length;
|
||||||
|
return {
|
||||||
|
peak: round(defaultThresholds.peak * jobFraction, 0),
|
||||||
|
normal: round(defaultThresholds.normal * jobFraction, 0),
|
||||||
|
caution: round(defaultThresholds.caution * jobFraction, 0),
|
||||||
|
alert: round(defaultThresholds.alert * jobFraction, 0),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||||
|
metricConfig,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} // Other job.exclusive cases?
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@ -7,121 +67,131 @@
|
|||||||
CardBody,
|
CardBody,
|
||||||
Progress,
|
Progress,
|
||||||
Icon,
|
Icon,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { mean, round } from 'mathjs'
|
import { mean, round } from "mathjs";
|
||||||
|
|
||||||
export let job
|
export let job;
|
||||||
export let jobMetrics
|
export let jobMetrics;
|
||||||
export let view = 'job'
|
export let view = "job";
|
||||||
export let width = 'auto'
|
export let width = "auto";
|
||||||
|
|
||||||
const clusters = getContext('clusters')
|
const clusters = getContext("clusters");
|
||||||
const subclusterConfig = clusters.find((c) => c.name == job.cluster).subClusters.find((sc) => sc.name == job.subCluster)
|
const subclusterConfig = clusters
|
||||||
|
.find((c) => c.name == job.cluster)
|
||||||
|
.subClusters.find((sc) => sc.name == job.subCluster);
|
||||||
|
|
||||||
const footprintMetrics = (job.numAcc !== 0)
|
const footprintMetrics =
|
||||||
? (job.exclusive !== 1) // GPU
|
job.numAcc !== 0
|
||||||
? ['acc_utilization', 'acc_mem_used', 'nv_sm_clock', 'nv_mem_util'] // Shared
|
? job.exclusive !== 1 // GPU
|
||||||
: ['acc_utilization', 'acc_mem_used', 'nv_sm_clock', 'nv_mem_util'] // Exclusive
|
? ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Shared
|
||||||
: (job.exclusive !== 1) // CPU only
|
: ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Exclusive
|
||||||
? ['flops_any', 'mem_used'] // Shared
|
: (job.exclusive !== 1) // CPU Only
|
||||||
: ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // Exclusive
|
? ["flops_any", "mem_used"] // Shared
|
||||||
|
: ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // Exclusive
|
||||||
|
|
||||||
const footprintData = footprintMetrics.map((fm) => {
|
const footprintData = footprintMetrics.map((fm) => {
|
||||||
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
|
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
|
||||||
let mv = null
|
let mv = null;
|
||||||
if (fm === 'cpu_load' && job.loadAvg !== 0) {
|
if (fm === "cpu_load" && job.loadAvg !== 0) {
|
||||||
mv = round(job.loadAvg, 2)
|
mv = round(job.loadAvg, 2);
|
||||||
} else if (fm === 'flops_any' && job.flopsAnyAvg !== 0) {
|
} else if (fm === "flops_any" && job.flopsAnyAvg !== 0) {
|
||||||
mv = round(job.flopsAnyAvg, 2)
|
mv = round(job.flopsAnyAvg, 2);
|
||||||
} else if (fm === 'mem_bw' && job.memBwAvg !== 0) {
|
} else if (fm === "mem_bw" && job.memBwAvg !== 0) {
|
||||||
mv = round(job.memBwAvg, 2)
|
mv = round(job.memBwAvg, 2);
|
||||||
} else { // Calculate from jobMetrics
|
|
||||||
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === 'node')
|
|
||||||
if (jm?.metric?.statisticsSeries) {
|
|
||||||
mv = round(mean(jm.metric.statisticsSeries.mean), 2)
|
|
||||||
} else if (jm?.metric?.series?.length > 1) {
|
|
||||||
const avgs = jm.metric.series.map(jms => jms.statistics.avg)
|
|
||||||
mv = round(mean(avgs), 2)
|
|
||||||
} else if (jm?.metric?.series) {
|
|
||||||
mv = round(jm.metric.series[0].statistics.avg, 2)
|
|
||||||
} else {
|
} else {
|
||||||
mv = 0.0
|
// Calculate from jobMetrics
|
||||||
|
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
|
||||||
|
if (jm?.metric?.statisticsSeries) {
|
||||||
|
mv = round(mean(jm.metric.statisticsSeries.mean), 2);
|
||||||
|
} else if (jm?.metric?.series?.length > 1) {
|
||||||
|
const avgs = jm.metric.series.map((jms) => jms.statistics.avg);
|
||||||
|
mv = round(mean(avgs), 2);
|
||||||
|
} else if (jm?.metric?.series) {
|
||||||
|
mv = round(jm.metric.series[0].statistics.avg, 2);
|
||||||
|
} else {
|
||||||
|
mv = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit
|
// Unit
|
||||||
const fmc = getContext('metrics')(job.cluster, fm)
|
const fmc = getContext("metrics")(job.cluster, fm);
|
||||||
let unit = ''
|
let unit = "";
|
||||||
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base
|
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base;
|
||||||
|
|
||||||
// Threshold / -Differences
|
// Threshold / -Differences
|
||||||
const fmt = findJobThresholds(job, fmc, subclusterConfig)
|
const fmt = findJobThresholds(job, fmc, subclusterConfig);
|
||||||
if (fm === 'flops_any') fmt.peak = round((fmt.peak * 0.85), 0)
|
if (fm === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0);
|
||||||
|
|
||||||
// Define basic data
|
// Define basic data
|
||||||
const fmBase = {
|
const fmBase = {
|
||||||
name: fm,
|
name: fm,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
avg: mv,
|
avg: mv,
|
||||||
max: fmt.peak
|
max: fmt.peak,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (evalFootprint(fm, mv, fmt, 'alert')) {
|
if (evalFootprint(fm, mv, fmt, "alert")) {
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
...fmBase,
|
||||||
color: 'danger',
|
color: "danger",
|
||||||
message:`Metric average way ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
|
message: `Metric average way ${fm === "mem_used" ? "above" : "below"} expected normal thresholds.`,
|
||||||
impact: 3
|
impact: 3,
|
||||||
}
|
};
|
||||||
} else if (evalFootprint(fm, mv, fmt, 'caution')) {
|
} else if (evalFootprint(fm, mv, fmt, "caution")) {
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
...fmBase,
|
||||||
color: 'warning',
|
color: "warning",
|
||||||
message: `Metric average ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
|
message: `Metric average ${fm === "mem_used" ? "above" : "below"} expected normal thresholds.`,
|
||||||
impact: 2
|
impact: 2,
|
||||||
}
|
};
|
||||||
} else if (evalFootprint(fm, mv, fmt, 'normal')) {
|
} else if (evalFootprint(fm, mv, fmt, "normal")) {
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
...fmBase,
|
||||||
color: 'success',
|
color: "success",
|
||||||
message: 'Metric average within expected thresholds.',
|
message: "Metric average within expected thresholds.",
|
||||||
impact: 1
|
impact: 1,
|
||||||
}
|
};
|
||||||
} else if (evalFootprint(fm, mv, fmt, 'peak')) {
|
} else if (evalFootprint(fm, mv, fmt, "peak")) {
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
...fmBase,
|
||||||
color: 'info',
|
color: "info",
|
||||||
message: 'Metric average above expected normal thresholds: Check for artifacts recommended.',
|
message:
|
||||||
impact: 0
|
"Metric average above expected normal thresholds: Check for artifacts recommended.",
|
||||||
}
|
impact: 0,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
...fmBase,
|
||||||
color: 'secondary',
|
color: "secondary",
|
||||||
message: 'Metric average above expected peak threshold: Check for artifacts!',
|
message:
|
||||||
impact: -1
|
"Metric average above expected peak threshold: Check for artifacts!",
|
||||||
|
impact: -1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
|
||||||
function evalFootprint(metric, mean, thresholds, level) {
|
function evalFootprint(metric, mean, thresholds, level) {
|
||||||
// mem_used has inverse logic regarding threshold levels, notify levels triggered if mean > threshold
|
// mem_used has inverse logic regarding threshold levels, notify levels triggered if mean > threshold
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'peak':
|
case "peak":
|
||||||
if (metric === 'mem_used') return false // mem_used over peak -> return false to trigger impact -1
|
if (metric === "mem_used")
|
||||||
else return (mean <= thresholds.peak && mean > thresholds.normal)
|
return false; // mem_used over peak -> return false to trigger impact -1
|
||||||
case 'alert':
|
else return mean <= thresholds.peak && mean > thresholds.normal;
|
||||||
if (metric === 'mem_used') return (mean <= thresholds.peak && mean >= thresholds.alert)
|
case "alert":
|
||||||
else return (mean <= thresholds.alert && mean >= 0)
|
if (metric === "mem_used")
|
||||||
case 'caution':
|
return mean <= thresholds.peak && mean >= thresholds.alert;
|
||||||
if (metric === 'mem_used') return (mean < thresholds.alert && mean >= thresholds.caution)
|
else return mean <= thresholds.alert && mean >= 0;
|
||||||
else return (mean <= thresholds.caution && mean > thresholds.alert)
|
case "caution":
|
||||||
case 'normal':
|
if (metric === "mem_used")
|
||||||
if (metric === 'mem_used') return (mean < thresholds.caution && mean >= 0)
|
return mean < thresholds.alert && mean >= thresholds.caution;
|
||||||
else return (mean <= thresholds.normal && mean > thresholds.caution)
|
else return mean <= thresholds.caution && mean > thresholds.alert;
|
||||||
|
case "normal":
|
||||||
|
if (metric === "mem_used")
|
||||||
|
return mean < thresholds.caution && mean >= 0;
|
||||||
|
else return mean <= thresholds.normal && mean > thresholds.caution;
|
||||||
default:
|
default:
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -155,13 +225,7 @@
|
|||||||
} else if (metricConfig.aggregation === 'avg' ){
|
} else if (metricConfig.aggregation === 'avg' ){
|
||||||
return defaultThresholds
|
return defaultThresholds
|
||||||
} else if (metricConfig.aggregation === 'sum' ){
|
} else if (metricConfig.aggregation === 'sum' ){
|
||||||
let jobFraction = 0.0
|
const jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
|
||||||
if (job.numAcc > 0) {
|
|
||||||
jobFraction = job.numAcc / subClusterConfig.topology.accelerators.length
|
|
||||||
} else if (job.numHWThreads > 0) {
|
|
||||||
jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peak: round((defaultThresholds.peak * jobFraction), 0),
|
peak: round((defaultThresholds.peak * jobFraction), 0),
|
||||||
normal: round((defaultThresholds.normal * jobFraction), 0),
|
normal: round((defaultThresholds.normal * jobFraction), 0),
|
||||||
@ -177,7 +241,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="h-auto mt-1" style="width: {width}px;">
|
<Card class="h-auto mt-1" style="width: {width}px;">
|
||||||
{#if view === 'job'}
|
{#if view === "job"}
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-0 d-flex justify-content-center">
|
<CardTitle class="mb-0 d-flex justify-content-center">
|
||||||
Core Metrics Footprint
|
Core Metrics Footprint
|
||||||
@ -187,45 +251,50 @@
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
{#each footprintData as fpd, index}
|
{#each footprintData as fpd, index}
|
||||||
<div class="mb-1 d-flex justify-content-between">
|
<div class="mb-1 d-flex justify-content-between">
|
||||||
<div> <b>{fpd.name}</b></div> <!-- For symmetry, see below ...-->
|
<div> <b>{fpd.name}</b></div>
|
||||||
<div class="cursor-help d-inline-flex" id={`footprint-${job.jobId}-${index}`}>
|
<!-- For symmetry, see below ...-->
|
||||||
|
<div
|
||||||
|
class="cursor-help d-inline-flex"
|
||||||
|
id={`footprint-${job.jobId}-${index}`}
|
||||||
|
>
|
||||||
<div class="mx-1">
|
<div class="mx-1">
|
||||||
<!-- Alerts Only -->
|
<!-- Alerts Only -->
|
||||||
{#if fpd.impact === 3 || fpd.impact === -1}
|
{#if fpd.impact === 3 || fpd.impact === -1}
|
||||||
<Icon name="exclamation-triangle-fill" class="text-danger"/>
|
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||||
{:else if fpd.impact === 2}
|
{:else if fpd.impact === 2}
|
||||||
<Icon name="exclamation-triangle" class="text-warning"/>
|
<Icon name="exclamation-triangle" class="text-warning" />
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Emoji for all states-->
|
<!-- Emoji for all states-->
|
||||||
{#if fpd.impact === 3}
|
{#if fpd.impact === 3}
|
||||||
<Icon name="emoji-frown" class="text-danger"/>
|
<Icon name="emoji-frown" class="text-danger" />
|
||||||
{:else if fpd.impact === 2}
|
{:else if fpd.impact === 2}
|
||||||
<Icon name="emoji-neutral" class="text-warning"/>
|
<Icon name="emoji-neutral" class="text-warning" />
|
||||||
{:else if fpd.impact === 1}
|
{:else if fpd.impact === 1}
|
||||||
<Icon name="emoji-smile" class="text-success"/>
|
<Icon name="emoji-smile" class="text-success" />
|
||||||
{:else if fpd.impact === 0}
|
{:else if fpd.impact === 0}
|
||||||
<Icon name="emoji-laughing" class="text-info"/>
|
<Icon name="emoji-laughing" class="text-info" />
|
||||||
{:else if fpd.impact === -1}
|
{:else if fpd.impact === -1}
|
||||||
<Icon name="emoji-dizzy" class="text-danger"/>
|
<Icon name="emoji-dizzy" class="text-danger" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<!-- Print Values -->
|
<!-- Print Values -->
|
||||||
{fpd.avg} / {fpd.max} {fpd.unit} <!-- To increase margin to tooltip: No other way manageable ... -->
|
{fpd.avg} / {fpd.max}
|
||||||
|
{fpd.unit} <!-- To increase margin to tooltip: No other way manageable ... -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip target={`footprint-${job.jobId}-${index}`} placement="right" offset={[0, 20]}>{fpd.message}</Tooltip>
|
<Tooltip
|
||||||
|
target={`footprint-${job.jobId}-${index}`}
|
||||||
|
placement="right"
|
||||||
|
offset={[0, 20]}>{fpd.message}</Tooltip
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<Progress
|
<Progress value={fpd.avg} max={fpd.max} color={fpd.color} />
|
||||||
value={fpd.avg}
|
|
||||||
max={fpd.max}
|
|
||||||
color={fpd.color}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if job?.metaData?.message}
|
{#if job?.metaData?.message}
|
||||||
<hr class="mt-1 mb-2"/>
|
<hr class="mt-1 mb-2" />
|
||||||
{@html job.metaData.message}
|
{@html job.metaData.message}
|
||||||
{/if}
|
{/if}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
@ -1,43 +1,54 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from 'svelte'
|
import { onMount, getContext } from "svelte";
|
||||||
import { init } from './utils.js'
|
import { init } from "./utils.js";
|
||||||
import { Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
|
import {
|
||||||
import Filters from './filters/Filters.svelte'
|
Row,
|
||||||
import JobList from './joblist/JobList.svelte'
|
Col,
|
||||||
import Refresher from './joblist/Refresher.svelte'
|
Button,
|
||||||
import Sorting from './joblist/SortSelection.svelte'
|
Icon,
|
||||||
import MetricSelection from './MetricSelection.svelte'
|
Card,
|
||||||
import UserOrProject from './filters/UserOrProject.svelte'
|
Spinner,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import Filters from "./filters/Filters.svelte";
|
||||||
|
import JobList from "./joblist/JobList.svelte";
|
||||||
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
|
import Sorting from "./joblist/SortSelection.svelte";
|
||||||
|
import MetricSelection from "./MetricSelection.svelte";
|
||||||
|
import UserOrProject from "./filters/UserOrProject.svelte";
|
||||||
|
|
||||||
const { query: initq } = init()
|
const { query: initq } = init();
|
||||||
|
|
||||||
const ccconfig = getContext('cc-config')
|
const ccconfig = getContext("cc-config");
|
||||||
|
|
||||||
export let filterPresets = {}
|
export let filterPresets = {};
|
||||||
export let authlevel
|
export let authlevel;
|
||||||
export let roles
|
export let roles;
|
||||||
|
|
||||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||||
let jobList, matchedJobs = null
|
let jobList,
|
||||||
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
|
matchedJobs = null;
|
||||||
|
let sorting = { field: "startTime", order: "DESC" },
|
||||||
|
isSortingOpen = false,
|
||||||
|
isMetricsSelectionOpen = false;
|
||||||
let metrics = filterPresets.cluster
|
let metrics = filterPresets.cluster
|
||||||
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics
|
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
|
||||||
: ccconfig.plot_list_selectedMetrics
|
ccconfig.plot_list_selectedMetrics
|
||||||
|
: ccconfig.plot_list_selectedMetrics;
|
||||||
let showFootprint = filterPresets.cluster
|
let showFootprint = filterPresets.cluster
|
||||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||||
: !!ccconfig.plot_list_showFootprint
|
: !!ccconfig.plot_list_showFootprint;
|
||||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
|
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
||||||
|
|
||||||
// The filterPresets are handled by the Filters component,
|
// The filterPresets are handled by the Filters component,
|
||||||
// so we need to wait for it to be ready before we can start a query.
|
// so we need to wait for it to be ready before we can start a query.
|
||||||
// This is also why JobList component starts out with a paused query.
|
// This is also why JobList component starts out with a paused query.
|
||||||
onMount(() => filterComponent.update())
|
onMount(() => filterComponent.update());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
{#if $initq.fetching}
|
{#if $initq.fetching}
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
</Col>
|
</Col>
|
||||||
{:else if $initq.error}
|
{:else if $initq.error}
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
@ -47,56 +58,64 @@
|
|||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Button
|
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
|
||||||
outline color="primary"
|
<Icon name="sort-up" /> Sorting
|
||||||
on:click={() => (isSortingOpen = true)}>
|
|
||||||
<Icon name="sort-up"/> Sorting
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
outline color="primary"
|
outline
|
||||||
on:click={() => (isMetricsSelectionOpen = true)}>
|
color="primary"
|
||||||
<Icon name="graph-up"/> Metrics
|
on:click={() => (isMetricsSelectionOpen = true)}
|
||||||
|
>
|
||||||
|
<Icon name="graph-up" /> Metrics
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button>
|
<Button disabled outline
|
||||||
|
>{matchedJobs == null ? "Loading..." : `${matchedJobs} jobs`}</Button
|
||||||
|
>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Filters
|
<Filters
|
||||||
filterPresets={filterPresets}
|
{filterPresets}
|
||||||
bind:this={filterComponent}
|
bind:this={filterComponent}
|
||||||
on:update={({ detail }) => {
|
on:update={({ detail }) => {
|
||||||
selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null
|
selectedCluster = detail.filters[0]?.cluster
|
||||||
jobList.update(detail.filters)
|
? detail.filters[0].cluster.eq
|
||||||
}
|
: null;
|
||||||
} />
|
jobList.update(detail.filters);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs="3" style="margin-left: auto;">
|
<Col xs="3" style="margin-left: auto;">
|
||||||
<UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filterComponent.update(detail)}/>
|
<UserOrProject
|
||||||
|
bind:authlevel
|
||||||
|
bind:roles
|
||||||
|
on:update={({ detail }) => filterComponent.update(detail)}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="2">
|
<Col xs="2">
|
||||||
<Refresher on:reload={() => jobList.refresh()} />
|
<Refresher on:reload={() => jobList.refresh()} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br/>
|
<br />
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<JobList
|
<JobList
|
||||||
bind:metrics={metrics}
|
bind:metrics
|
||||||
bind:sorting={sorting}
|
bind:sorting
|
||||||
bind:matchedJobs={matchedJobs}
|
bind:matchedJobs
|
||||||
bind:this={jobList}
|
bind:this={jobList}
|
||||||
bind:showFootprint={showFootprint} />
|
bind:showFootprint
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Sorting
|
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
|
||||||
bind:sorting={sorting}
|
|
||||||
bind:isOpen={isSortingOpen} />
|
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:cluster={selectedCluster}
|
||||||
configName="plot_list_selectedMetrics"
|
configName="plot_list_selectedMetrics"
|
||||||
bind:metrics={metrics}
|
bind:metrics
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
bind:showFootprint={showFootprint}
|
bind:showFootprint
|
||||||
view='list'/>
|
view="list"
|
||||||
|
/>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
Spinner,
|
Spinner,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Input,
|
Input,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import Filters from "./filters/Filters.svelte";
|
import Filters from "./filters/Filters.svelte";
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
||||||
@ -26,17 +26,23 @@
|
|||||||
|
|
||||||
// By default, look at the jobs of the last 30 days:
|
// By default, look at the jobs of the last 30 days:
|
||||||
if (filterPresets?.startTime == null) {
|
if (filterPresets?.startTime == null) {
|
||||||
if (filterPresets == null)
|
if (filterPresets == null) filterPresets = {};
|
||||||
filterPresets = {}
|
|
||||||
|
|
||||||
const lastMonth = (new Date(Date.now() - (30*24*60*60*1000))).toISOString()
|
const lastMonth = new Date(
|
||||||
const now = (new Date(Date.now())).toISOString()
|
Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||||
filterPresets.startTime = { from: lastMonth, to: now, text: 'Last 30 Days', url: 'last30d' }
|
).toISOString();
|
||||||
|
const now = new Date(Date.now()).toISOString();
|
||||||
|
filterPresets.startTime = {
|
||||||
|
from: lastMonth,
|
||||||
|
to: now,
|
||||||
|
text: "Last 30 Days",
|
||||||
|
url: "last30d",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.assert(
|
console.assert(
|
||||||
type == "USER" || type == "PROJECT",
|
type == "USER" || type == "PROJECT",
|
||||||
"Invalid list type provided!"
|
"Invalid list type provided!",
|
||||||
);
|
);
|
||||||
|
|
||||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||||
@ -58,16 +64,14 @@
|
|||||||
totalAccHours
|
totalAccHours
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
variables: { jobFilters }
|
variables: { jobFilters },
|
||||||
});
|
});
|
||||||
|
|
||||||
function changeSorting(event, field) {
|
function changeSorting(event, field) {
|
||||||
let target = event.target;
|
let target = event.target;
|
||||||
while (target.tagName != "BUTTON") target = target.parentElement;
|
while (target.tagName != "BUTTON") target = target.parentElement;
|
||||||
|
|
||||||
let direction = target.children[0].className.includes("up")
|
let direction = target.children[0].className.includes("up") ? "down" : "up";
|
||||||
? "down"
|
|
||||||
: "up";
|
|
||||||
target.children[0].className = `bi-sort-numeric-${direction}`;
|
target.children[0].className = `bi-sort-numeric-${direction}`;
|
||||||
sorting = { field, direction };
|
sorting = { field, direction };
|
||||||
}
|
}
|
||||||
@ -119,10 +123,10 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
{({
|
{{
|
||||||
USER: "Username",
|
USER: "Username",
|
||||||
PROJECT: "Project Name",
|
PROJECT: "Project Name",
|
||||||
})[type]}
|
}[type]}
|
||||||
<Button
|
<Button
|
||||||
color={sorting.field == "id" ? "primary" : "light"}
|
color={sorting.field == "id" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -156,9 +160,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
Total Walltime
|
Total Walltime
|
||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalWalltime"
|
color={sorting.field == "totalWalltime" ? "primary" : "light"}
|
||||||
? "primary"
|
|
||||||
: "light"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalWalltime")}
|
on:click={(e) => changeSorting(e, "totalWalltime")}
|
||||||
>
|
>
|
||||||
@ -168,9 +170,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
Total Core Hours
|
Total Core Hours
|
||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalCoreHours"
|
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
|
||||||
? "primary"
|
|
||||||
: "light"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalCoreHours")}
|
on:click={(e) => changeSorting(e, "totalCoreHours")}
|
||||||
>
|
>
|
||||||
@ -180,9 +180,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
Total Accelerator Hours
|
Total Accelerator Hours
|
||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalAccHours"
|
color={sorting.field == "totalAccHours" ? "primary" : "light"}
|
||||||
? "primary"
|
|
||||||
: "light"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalAccHours")}
|
on:click={(e) => changeSorting(e, "totalAccHours")}
|
||||||
>
|
>
|
||||||
@ -194,15 +192,12 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#if $stats.fetching}
|
{#if $stats.fetching}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" style="text-align: center;"
|
<td colspan="4" style="text-align: center;"><Spinner secondary /></td>
|
||||||
><Spinner secondary /></td
|
|
||||||
>
|
|
||||||
</tr>
|
</tr>
|
||||||
{:else if $stats.error}
|
{:else if $stats.error}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4"
|
<td colspan="4"
|
||||||
><Card body color="danger" class="mb-3"
|
><Card body color="danger" class="mb-3">{$stats.error.message}</Card
|
||||||
>{$stats.error.message}</Card
|
|
||||||
></td
|
></td
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
@ -223,7 +218,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{#if type == "USER"}
|
{#if type == "USER"}
|
||||||
<td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
|
<td
|
||||||
|
>{scrambleNames
|
||||||
|
? scramble(row?.name ? row.name : "-")
|
||||||
|
: row?.name
|
||||||
|
? row.name
|
||||||
|
: "-"}</td
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<td>{row.totalJobs}</td>
|
<td>{row.totalJobs}</td>
|
||||||
<td>{row.totalWalltime}</td>
|
<td>{row.totalWalltime}</td>
|
||||||
@ -232,9 +233,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4"
|
<td colspan="4"><i>No {type.toLowerCase()}s/jobs found</i></td>
|
||||||
><i>No {type.toLowerCase()}s/jobs found</i></td
|
|
||||||
>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,63 +1,82 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, createEventDispatcher } from 'svelte'
|
import { getContext, createEventDispatcher } from "svelte";
|
||||||
import Timeseries from './plots/MetricPlot.svelte'
|
import Timeseries from "./plots/MetricPlot.svelte";
|
||||||
import { InputGroup, InputGroupText, Spinner, Card } from 'sveltestrap'
|
import {
|
||||||
import { fetchMetrics, minScope } from './utils'
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
Spinner,
|
||||||
|
Card,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { fetchMetrics, minScope } from "./utils";
|
||||||
|
|
||||||
export let job
|
export let job;
|
||||||
export let metricName
|
export let metricName;
|
||||||
export let scopes
|
export let scopes;
|
||||||
export let width
|
export let width;
|
||||||
export let rawData
|
export let rawData;
|
||||||
export let isShared = false
|
export let isShared = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
const cluster = getContext('clusters').find(cluster => cluster.name == job.cluster)
|
const cluster = getContext("clusters").find(
|
||||||
const subCluster = cluster.subClusters.find(subCluster => subCluster.name == job.subCluster)
|
(cluster) => cluster.name == job.cluster,
|
||||||
const metricConfig = cluster.metricConfig.find(metricConfig => metricConfig.name == metricName)
|
);
|
||||||
|
const subCluster = cluster.subClusters.find(
|
||||||
|
(subCluster) => subCluster.name == job.subCluster,
|
||||||
|
);
|
||||||
|
const metricConfig = cluster.metricConfig.find(
|
||||||
|
(metricConfig) => metricConfig.name == metricName,
|
||||||
|
);
|
||||||
|
|
||||||
let selectedHost = null, plot, fetching = false, error = null
|
let selectedHost = null,
|
||||||
let selectedScope = minScope(scopes)
|
plot,
|
||||||
|
fetching = false,
|
||||||
|
error = null;
|
||||||
|
let selectedScope = minScope(scopes);
|
||||||
|
|
||||||
$: availableScopes = scopes
|
$: availableScopes = scopes;
|
||||||
$: selectedScopeIndex = scopes.findIndex(s => s == selectedScope)
|
$: selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
|
||||||
$: data = rawData[selectedScopeIndex]
|
$: data = rawData[selectedScopeIndex];
|
||||||
$: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost)
|
$: series = data?.series.filter(
|
||||||
|
(series) => selectedHost == null || series.hostname == selectedHost,
|
||||||
|
);
|
||||||
|
|
||||||
let from = null, to = null
|
let from = null,
|
||||||
|
to = null;
|
||||||
export function setTimeRange(f, t) {
|
export function setTimeRange(f, t) {
|
||||||
from = f, to = t
|
(from = f), (to = t);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (plot != null) plot.setTimeRange(from, to)
|
$: if (plot != null) plot.setTimeRange(from, to);
|
||||||
|
|
||||||
export async function loadMore() {
|
export async function loadMore() {
|
||||||
fetching = true
|
fetching = true;
|
||||||
let response = await fetchMetrics(job, [metricName], ["core"])
|
let response = await fetchMetrics(job, [metricName], ["core"]);
|
||||||
fetching = false
|
fetching = false;
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
error = response.error
|
error = response.error;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let jm of response.data.jobMetrics) {
|
for (let jm of response.data.jobMetrics) {
|
||||||
if (jm.scope != "node") {
|
if (jm.scope != "node") {
|
||||||
scopes = [...scopes, jm.scope]
|
scopes = [...scopes, jm.scope];
|
||||||
rawData.push(jm.metric)
|
rawData.push(jm.metric);
|
||||||
selectedScope = jm.scope
|
selectedScope = jm.scope;
|
||||||
selectedScopeIndex = scopes.findIndex(s => s == jm.scope)
|
selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
|
||||||
dispatch('more-loaded', jm)
|
dispatch("more-loaded", jm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (selectedScope == "load-more") loadMore()
|
$: if (selectedScope == "load-more") loadMore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText style="min-width: 150px;">
|
<InputGroupText style="min-width: 150px;">
|
||||||
{metricName} ({(metricConfig?.unit?.prefix ? metricConfig.unit.prefix : '') +
|
{metricName} ({(metricConfig?.unit?.prefix
|
||||||
(metricConfig?.unit?.base ? metricConfig.unit.base : '')})
|
? metricConfig.unit.prefix
|
||||||
|
: "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
<select class="form-select" bind:value={selectedScope}>
|
<select class="form-select" bind:value={selectedScope}>
|
||||||
{#each availableScopes as scope}
|
{#each availableScopes as scope}
|
||||||
@ -78,18 +97,22 @@
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
{#key series}
|
{#key series}
|
||||||
{#if fetching == true}
|
{#if fetching == true}
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
{:else if error != null}
|
{:else if error != null}
|
||||||
<Card body color="danger">{error.message}</Card>
|
<Card body color="danger">{error.message}</Card>
|
||||||
{:else if series != null}
|
{:else if series != null}
|
||||||
<Timeseries
|
<Timeseries
|
||||||
bind:this={plot}
|
bind:this={plot}
|
||||||
width={width} height={300}
|
{width}
|
||||||
cluster={cluster} subCluster={subCluster}
|
height={300}
|
||||||
|
{cluster}
|
||||||
|
{subCluster}
|
||||||
timestep={data.timestep}
|
timestep={data.timestep}
|
||||||
scope={selectedScope} metric={metricName}
|
scope={selectedScope}
|
||||||
series={series}
|
metric={metricName}
|
||||||
isShared={isShared}
|
{series}
|
||||||
resources={job.resources} />
|
{isShared}
|
||||||
|
resources={job.resources}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
@ -8,51 +8,55 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Modal, ModalBody, ModalHeader, ModalFooter, Button, ListGroup } from 'sveltestrap'
|
import {
|
||||||
import { getContext } from 'svelte'
|
Modal,
|
||||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
ListGroup,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let metrics
|
export let metrics;
|
||||||
export let isOpen
|
export let isOpen;
|
||||||
export let configName
|
export let configName;
|
||||||
export let allMetrics = null
|
export let allMetrics = null;
|
||||||
export let cluster = null
|
export let cluster = null;
|
||||||
export let showFootprint
|
export let showFootprint;
|
||||||
export let view = 'job'
|
export let view = "job";
|
||||||
|
|
||||||
const clusters = getContext('clusters'),
|
const clusters = getContext("clusters"),
|
||||||
onInit = getContext('on-init')
|
onInit = getContext("on-init");
|
||||||
|
|
||||||
let newMetricsOrder = []
|
let newMetricsOrder = [];
|
||||||
let unorderedMetrics = [...metrics]
|
let unorderedMetrics = [...metrics];
|
||||||
let pendingShowFootprint = !!showFootprint
|
let pendingShowFootprint = !!showFootprint;
|
||||||
|
|
||||||
onInit(() => {
|
onInit(() => {
|
||||||
if (allMetrics == null) allMetrics = new Set()
|
if (allMetrics == null) allMetrics = new Set();
|
||||||
for (let c of clusters)
|
for (let c of clusters)
|
||||||
for (let metric of c.metricConfig)
|
for (let metric of c.metricConfig) allMetrics.add(metric.name);
|
||||||
allMetrics.add(metric.name)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (allMetrics != null) {
|
if (allMetrics != null) {
|
||||||
if (cluster == null) {
|
if (cluster == null) {
|
||||||
// console.log('Reset to full metric list')
|
// console.log('Reset to full metric list')
|
||||||
for (let c of clusters)
|
for (let c of clusters)
|
||||||
for (let metric of c.metricConfig)
|
for (let metric of c.metricConfig) allMetrics.add(metric.name);
|
||||||
allMetrics.add(metric.name)
|
|
||||||
} else {
|
} else {
|
||||||
// console.log('Recalculate available metrics for ' + cluster)
|
// console.log('Recalculate available metrics for ' + cluster)
|
||||||
allMetrics.clear()
|
allMetrics.clear();
|
||||||
for (let c of clusters)
|
for (let c of clusters)
|
||||||
if (c.name == cluster)
|
if (c.name == cluster)
|
||||||
for (let metric of c.metricConfig)
|
for (let metric of c.metricConfig) allMetrics.add(metric.name);
|
||||||
allMetrics.add(metric.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newMetricsOrder = [...allMetrics].filter(m => !metrics.includes(m))
|
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
|
||||||
newMetricsOrder.unshift(...metrics.filter(m => allMetrics.has(m)))
|
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
|
||||||
unorderedMetrics = unorderedMetrics.filter(m => allMetrics.has(m))
|
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,62 +65,140 @@
|
|||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
mutation($name: String!, $value: String!) {
|
mutation ($name: String!, $value: String!) {
|
||||||
updateConfiguration(name: $name, value: $value)
|
updateConfiguration(name: $name, value: $value)
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { name, value }
|
variables: { name, value },
|
||||||
})}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let columnHovering = null
|
let columnHovering = null;
|
||||||
|
|
||||||
function columnsDragStart(event, i) {
|
function columnsDragStart(event, i) {
|
||||||
event.dataTransfer.effectAllowed = 'move'
|
event.dataTransfer.effectAllowed = "move";
|
||||||
event.dataTransfer.dropEffect = 'move'
|
event.dataTransfer.dropEffect = "move";
|
||||||
event.dataTransfer.setData('text/plain', i)
|
event.dataTransfer.setData("text/plain", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
function columnsDrag(event, target) {
|
function columnsDrag(event, target) {
|
||||||
event.dataTransfer.dropEffect = 'move'
|
event.dataTransfer.dropEffect = "move";
|
||||||
const start = Number.parseInt(event.dataTransfer.getData("text/plain"))
|
const start = Number.parseInt(event.dataTransfer.getData("text/plain"));
|
||||||
if (start < target) {
|
if (start < target) {
|
||||||
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start])
|
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start]);
|
||||||
newMetricsOrder.splice(start, 1)
|
newMetricsOrder.splice(start, 1);
|
||||||
} else {
|
} else {
|
||||||
newMetricsOrder.splice(target, 0, newMetricsOrder[start])
|
newMetricsOrder.splice(target, 0, newMetricsOrder[start]);
|
||||||
newMetricsOrder.splice(start + 1, 1)
|
newMetricsOrder.splice(start + 1, 1);
|
||||||
}
|
}
|
||||||
columnHovering = null
|
columnHovering = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAndApply() {
|
function closeAndApply() {
|
||||||
metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m))
|
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
|
||||||
isOpen = false
|
isOpen = false;
|
||||||
|
|
||||||
showFootprint = !!pendingShowFootprint
|
showFootprint = !!pendingShowFootprint;
|
||||||
|
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: cluster == null ? configName : `${configName}:${cluster}`,
|
name: cluster == null ? configName : `${configName}:${cluster}`,
|
||||||
value: JSON.stringify(metrics)
|
value: JSON.stringify(metrics),
|
||||||
}).subscribe(res => {
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: cluster == null ? 'plot_list_showFootprint' : `plot_list_showFootprint:${cluster}`,
|
name:
|
||||||
value: JSON.stringify(showFootprint)
|
cluster == null
|
||||||
}).subscribe(res => {
|
? "plot_list_showFootprint"
|
||||||
|
: `plot_list_showFootprint:${cluster}`,
|
||||||
|
value: JSON.stringify(showFootprint),
|
||||||
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
console.log('Error on footprint subscription: ' + res.error)
|
console.log("Error on footprint subscription: " + res.error);
|
||||||
throw res.error
|
throw res.error;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
|
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<ListGroup>
|
||||||
|
{#if view === "list"}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<input type="checkbox" bind:checked={pendingShowFootprint} /> Show Footprint
|
||||||
|
</li>
|
||||||
|
<hr />
|
||||||
|
{/if}
|
||||||
|
{#each newMetricsOrder as metric, index (metric)}
|
||||||
|
<li
|
||||||
|
class="cc-config-column list-group-item"
|
||||||
|
draggable={true}
|
||||||
|
ondragover="return false"
|
||||||
|
on:dragstart={(event) => columnsDragStart(event, index)}
|
||||||
|
on:drop|preventDefault={(event) => columnsDrag(event, index)}
|
||||||
|
on:dragenter={() => (columnHovering = index)}
|
||||||
|
class:is-active={columnHovering === index}
|
||||||
|
>
|
||||||
|
{#if unorderedMetrics.includes(metric)}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:group={unorderedMetrics}
|
||||||
|
value={metric}
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:group={unorderedMetrics}
|
||||||
|
value={metric}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{metric}
|
||||||
|
<span style="float: right;">
|
||||||
|
{cluster == null
|
||||||
|
? clusters // No single cluster specified: List Clusters with Metric
|
||||||
|
.filter(
|
||||||
|
(c) => c.metricConfig.find((m) => m.name == metric) != null,
|
||||||
|
)
|
||||||
|
.map((c) => c.name)
|
||||||
|
.join(", ")
|
||||||
|
: clusters // Single cluster requested: List Subclusters with do not have metric remove flag
|
||||||
|
.filter((c) => c.name == cluster)
|
||||||
|
.filter(
|
||||||
|
(c) => c.metricConfig.find((m) => m.name == metric) != null,
|
||||||
|
)
|
||||||
|
.map(function (c) {
|
||||||
|
let scNames = c.subClusters.map((sc) => sc.name);
|
||||||
|
scNames.forEach(function (scName) {
|
||||||
|
let met = c.metricConfig.find((m) => m.name == metric);
|
||||||
|
let msc = met.subClusters.find(
|
||||||
|
(msc) => msc.name == scName,
|
||||||
|
);
|
||||||
|
if (msc != null) {
|
||||||
|
if (msc.remove == true) {
|
||||||
|
scNames = scNames.filter((scn) => scn != msc.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return scNames;
|
||||||
|
})
|
||||||
|
.join(", ")}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ListGroup>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
li.cc-config-column {
|
li.cc-config-column {
|
||||||
display: block;
|
display: block;
|
||||||
@ -129,60 +211,3 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
|
||||||
<ModalHeader>
|
|
||||||
Configure columns (Metric availability shown)
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
|
||||||
<ListGroup>
|
|
||||||
{#if view === 'list'}
|
|
||||||
<li class="list-group-item">
|
|
||||||
<input type="checkbox" bind:checked={pendingShowFootprint}> Show Footprint
|
|
||||||
</li>
|
|
||||||
<hr/>
|
|
||||||
{/if}
|
|
||||||
{#each newMetricsOrder as metric, index (metric)}
|
|
||||||
<li class="cc-config-column list-group-item"
|
|
||||||
draggable={true} ondragover="return false"
|
|
||||||
on:dragstart={event => columnsDragStart(event, index)}
|
|
||||||
on:drop|preventDefault={event => columnsDrag(event, index)}
|
|
||||||
on:dragenter={() => columnHovering = index}
|
|
||||||
class:is-active={columnHovering === index}>
|
|
||||||
{#if unorderedMetrics.includes(metric)}
|
|
||||||
<input type="checkbox" bind:group={unorderedMetrics} value={metric} checked>
|
|
||||||
{:else}
|
|
||||||
<input type="checkbox" bind:group={unorderedMetrics} value={metric}>
|
|
||||||
{/if}
|
|
||||||
{metric}
|
|
||||||
<span style="float: right;">
|
|
||||||
{cluster == null ?
|
|
||||||
clusters // No single cluster specified: List Clusters with Metric
|
|
||||||
.filter(c => c.metricConfig.find(m => m.name == metric) != null)
|
|
||||||
.map(c => c.name).join(', ') :
|
|
||||||
clusters // Single cluster requested: List Subclusters with do not have metric remove flag
|
|
||||||
.filter(c => c.name == cluster)
|
|
||||||
.filter(c => c.metricConfig.find(m => m.name == metric) != null)
|
|
||||||
.map(function(c) {
|
|
||||||
let scNames = c.subClusters.map(sc => sc.name)
|
|
||||||
scNames.forEach(function(scName){
|
|
||||||
let met = c.metricConfig.find(m => m.name == metric)
|
|
||||||
let msc = met.subClusters.find(msc => msc.name == scName)
|
|
||||||
if (msc != null) {
|
|
||||||
if (msc.remove == true) {
|
|
||||||
scNames = scNames.filter(scn => scn != msc.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return scNames
|
|
||||||
})
|
|
||||||
.join(', ')}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ListGroup>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let clusters; // array of names
|
export let clusters; // array of names
|
||||||
export let links; // array of nav links
|
export let links; // array of nav links
|
||||||
@ -27,8 +27,7 @@
|
|||||||
{#each clusters as cluster}
|
{#each clusters as cluster}
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
href={item.href + cluster.name}
|
href={item.href + cluster.name}
|
||||||
active={window.location.pathname ==
|
active={window.location.pathname == item.href + cluster.name}
|
||||||
item.href + cluster.name}
|
|
||||||
>
|
>
|
||||||
{cluster.name}
|
{cluster.name}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
Container,
|
Container,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||||
export let authlevel; // Integer
|
export let authlevel; // Integer
|
||||||
@ -30,7 +30,8 @@
|
|||||||
style="margin-left: 10px;"
|
style="margin-left: 10px;"
|
||||||
/>
|
/>
|
||||||
<!-- bootstrap classes w/o effect -->
|
<!-- bootstrap classes w/o effect -->
|
||||||
<Button outline type="submit" title="Search"><Icon name="search" /></Button
|
<Button outline type="submit" title="Search"
|
||||||
|
><Icon name="search" /></Button
|
||||||
>
|
>
|
||||||
<InputGroupText
|
<InputGroupText
|
||||||
style="cursor:help;"
|
style="cursor:help;"
|
||||||
@ -43,7 +44,12 @@
|
|||||||
</form>
|
</form>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
|
<a
|
||||||
|
href="https://www.clustercockpit.org/docs/webinterface/"
|
||||||
|
title="Documentation"
|
||||||
|
rel="nofollow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<Button outline style="margin-left: 10px;">
|
<Button outline style="margin-left: 10px;">
|
||||||
<Icon name="book" />
|
<Icon name="book" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -70,9 +76,9 @@
|
|||||||
title="Logout {username}"
|
title="Logout {username}"
|
||||||
>
|
>
|
||||||
{#if screenSize > 1630}
|
{#if screenSize > 1630}
|
||||||
<Icon name="box-arrow-right"/> Logout {username}
|
<Icon name="box-arrow-right" /> Logout {username}
|
||||||
{:else}
|
{:else}
|
||||||
<Icon name="box-arrow-right"/>
|
<Icon name="box-arrow-right" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@ -83,7 +89,12 @@
|
|||||||
<Container>
|
<Container>
|
||||||
<Row cols={3}>
|
<Row cols={3}>
|
||||||
<Col xs="4">
|
<Col xs="4">
|
||||||
<a href="https://www.clustercockpit.org/docs/webinterface/" title="Documentation" rel="nofollow" target="_blank">
|
<a
|
||||||
|
href="https://www.clustercockpit.org/docs/webinterface/"
|
||||||
|
title="Documentation"
|
||||||
|
rel="nofollow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<Button outline size="sm" class="my-2 w-100">
|
<Button outline size="sm" class="my-2 w-100">
|
||||||
<Icon name="box-arrow-up-right" /> Documentation
|
<Icon name="box-arrow-up-right" /> Documentation
|
||||||
</Button>
|
</Button>
|
||||||
@ -127,8 +138,7 @@
|
|||||||
placeholder="Search 'type:<query>' ..."
|
placeholder="Search 'type:<query>' ..."
|
||||||
name="searchId"
|
name="searchId"
|
||||||
/>
|
/>
|
||||||
<Button outline type="submit"><Icon name="search" /></Button
|
<Button outline type="submit"><Icon name="search" /></Button>
|
||||||
>
|
|
||||||
<InputGroupText
|
<InputGroupText
|
||||||
style="cursor:help;"
|
style="cursor:help;"
|
||||||
title={authlevel >= roles.support
|
title={authlevel >= roles.support
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
Icon,
|
Icon,
|
||||||
Spinner,
|
Spinner,
|
||||||
Card,
|
Card,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import TimeSelection from "./filters/TimeSelection.svelte";
|
import TimeSelection from "./filters/TimeSelection.svelte";
|
||||||
import Refresher from './joblist/Refresher.svelte';
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
import PlotTable from "./PlotTable.svelte";
|
import PlotTable from "./PlotTable.svelte";
|
||||||
import MetricPlot from "./plots/MetricPlot.svelte";
|
import MetricPlot from "./plots/MetricPlot.svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
@ -34,12 +34,7 @@
|
|||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const nodeMetricsQuery = gql`
|
const nodeMetricsQuery = gql`
|
||||||
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
||||||
nodeMetrics(
|
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
|
||||||
cluster: $cluster
|
|
||||||
nodes: $nodes
|
|
||||||
from: $from
|
|
||||||
to: $to
|
|
||||||
) {
|
|
||||||
host
|
host
|
||||||
subCluster
|
subCluster
|
||||||
metrics {
|
metrics {
|
||||||
@ -150,8 +145,7 @@
|
|||||||
{#if $nodeJobsData.fetching}
|
{#if $nodeJobsData.fetching}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if $nodeJobsData.data}
|
{:else if $nodeJobsData.data}
|
||||||
Currently running jobs on this node: {$nodeJobsData.data.jobs
|
Currently running jobs on this node: {$nodeJobsData.data.jobs.count}
|
||||||
.count}
|
|
||||||
[
|
[
|
||||||
<a
|
<a
|
||||||
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
|
href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}"
|
||||||
@ -162,11 +156,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Refresher on:reload={() => {
|
<Refresher
|
||||||
const diff = Date.now() - to
|
on:reload={() => {
|
||||||
from = new Date(from.getTime() + diff)
|
const diff = Date.now() - to;
|
||||||
to = new Date(to.getTime() + diff)
|
from = new Date(from.getTime() + diff);
|
||||||
}} />
|
to = new Date(to.getTime() + diff);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<TimeSelection bind:from bind:to />
|
<TimeSelection bind:from bind:to />
|
||||||
@ -192,7 +188,7 @@
|
|||||||
disabled: checkMetricDisabled(
|
disabled: checkMetricDisabled(
|
||||||
m.name,
|
m.name,
|
||||||
cluster,
|
cluster,
|
||||||
$nodeMetricsData.data.nodeMetrics[0].subCluster
|
$nodeMetricsData.data.nodeMetrics[0].subCluster,
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))}
|
.sort((a, b) => a.name.localeCompare(b.name))}
|
||||||
@ -208,17 +204,13 @@
|
|||||||
metric={item.name}
|
metric={item.name}
|
||||||
timestep={item.metric.timestep}
|
timestep={item.metric.timestep}
|
||||||
cluster={clusters.find((c) => c.name == cluster)}
|
cluster={clusters.find((c) => c.name == cluster)}
|
||||||
subCluster={$nodeMetricsData.data.nodeMetrics[0]
|
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
|
||||||
.subCluster}
|
|
||||||
series={item.metric.series}
|
series={item.metric.series}
|
||||||
resources={[{hostname: hostname}]}
|
resources={[{ hostname: hostname }]}
|
||||||
forNode={true}
|
forNode={true}
|
||||||
/>
|
/>
|
||||||
{:else if item.disabled === true && item.metric}
|
{:else if item.disabled === true && item.metric}
|
||||||
<Card
|
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||||
style="margin-left: 2rem;margin-right: 2rem;"
|
|
||||||
body
|
|
||||||
color="info"
|
|
||||||
>Metric disabled for subcluster <code
|
>Metric disabled for subcluster <code
|
||||||
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
|
>{item.name}:{$nodeMetricsData.data.nodeMetrics[0]
|
||||||
.subCluster}</code
|
.subCluster}</code
|
||||||
|
@ -1,66 +1,81 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Modal, ModalBody, ModalHeader, ModalFooter, InputGroup,
|
import {
|
||||||
Button, ListGroup, ListGroupItem, Icon } from 'sveltestrap'
|
Modal,
|
||||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
InputGroup,
|
||||||
|
Button,
|
||||||
|
ListGroup,
|
||||||
|
ListGroupItem,
|
||||||
|
Icon,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let availableMetrics
|
export let availableMetrics;
|
||||||
export let metricsInHistograms
|
export let metricsInHistograms;
|
||||||
export let metricsInScatterplots
|
export let metricsInScatterplots;
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`mutation($name: String!, $value: String!) {
|
query: gql`
|
||||||
|
mutation ($name: String!, $value: String!) {
|
||||||
updateConfiguration(name: $name, value: $value)
|
updateConfiguration(name: $name, value: $value)
|
||||||
}`,
|
|
||||||
variables: { name, value }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
variables: { name, value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let isHistogramConfigOpen = false, isScatterPlotConfigOpen = false
|
let isHistogramConfigOpen = false,
|
||||||
let selectedMetric1 = null, selectedMetric2 = null
|
isScatterPlotConfigOpen = false;
|
||||||
|
let selectedMetric1 = null,
|
||||||
|
selectedMetric2 = null;
|
||||||
|
|
||||||
function updateConfiguration(data) {
|
function updateConfiguration(data) {
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
value: JSON.stringify(data.value)
|
value: JSON.stringify(data.value),
|
||||||
}).subscribe(res => {
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button outline
|
<Button outline on:click={() => (isHistogramConfigOpen = true)}>
|
||||||
on:click={() => (isHistogramConfigOpen = true)}>
|
<Icon name="" />
|
||||||
<Icon name=""/>
|
|
||||||
Select Plots for Histograms
|
Select Plots for Histograms
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button outline
|
<Button outline on:click={() => (isScatterPlotConfigOpen = true)}>
|
||||||
on:click={() => (isScatterPlotConfigOpen = true)}>
|
<Icon name="" />
|
||||||
<Icon name=""/>
|
|
||||||
Select Plots in Scatter Plots
|
Select Plots in Scatter Plots
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Modal isOpen={isHistogramConfigOpen}
|
<Modal
|
||||||
toggle={() => (isHistogramConfigOpen = !isHistogramConfigOpen)}>
|
isOpen={isHistogramConfigOpen}
|
||||||
<ModalHeader>
|
toggle={() => (isHistogramConfigOpen = !isHistogramConfigOpen)}
|
||||||
Select metrics presented in histograms
|
>
|
||||||
</ModalHeader>
|
<ModalHeader>Select metrics presented in histograms</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#each availableMetrics as metric (metric)}
|
{#each availableMetrics as metric (metric)}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<input type="checkbox" bind:group={metricsInHistograms}
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:group={metricsInHistograms}
|
||||||
value={metric}
|
value={metric}
|
||||||
on:change={() => updateConfiguration({
|
on:change={() =>
|
||||||
name: 'analysis_view_histogramMetrics',
|
updateConfiguration({
|
||||||
value: metricsInHistograms
|
name: "analysis_view_histogramMetrics",
|
||||||
})} />
|
value: metricsInHistograms,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
{metric}
|
{metric}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
@ -68,39 +83,44 @@
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary"
|
<Button color="primary" on:click={() => (isHistogramConfigOpen = false)}>
|
||||||
on:click={() => (isHistogramConfigOpen = false)}>
|
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal isOpen={isScatterPlotConfigOpen}
|
<Modal
|
||||||
toggle={() => (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}>
|
isOpen={isScatterPlotConfigOpen}
|
||||||
<ModalHeader>
|
toggle={() => (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}
|
||||||
Select metric pairs presented in scatter plots
|
>
|
||||||
</ModalHeader>
|
<ModalHeader>Select metric pairs presented in scatter plots</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#each metricsInScatterplots as pair}
|
{#each metricsInScatterplots as pair}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<b>{pair[0]}</b> / <b>{pair[1]}</b>
|
<b>{pair[0]}</b> / <b>{pair[1]}</b>
|
||||||
|
|
||||||
<Button style="float: right;" outline color="danger"
|
<Button
|
||||||
|
style="float: right;"
|
||||||
|
outline
|
||||||
|
color="danger"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
metricsInScatterplots = metricsInScatterplots.filter(p => pair != p)
|
metricsInScatterplots = metricsInScatterplots.filter(
|
||||||
|
(p) => pair != p,
|
||||||
|
);
|
||||||
updateConfiguration({
|
updateConfiguration({
|
||||||
name: 'analysis_view_scatterPlotMetrics',
|
name: "analysis_view_scatterPlotMetrics",
|
||||||
value: metricsInScatterplots
|
value: metricsInScatterplots,
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Icon name="x" />
|
<Icon name="x" />
|
||||||
</Button>
|
</Button>
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
<br/>
|
<br />
|
||||||
|
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<select bind:value={selectedMetric1} class="form-group form-select">
|
<select bind:value={selectedMetric1} class="form-group form-select">
|
||||||
@ -115,24 +135,28 @@
|
|||||||
<option value={metric}>{metric}</option>
|
<option value={metric}>{metric}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<Button outline disabled={selectedMetric1 == null || selectedMetric2 == null}
|
<Button
|
||||||
|
outline
|
||||||
|
disabled={selectedMetric1 == null || selectedMetric2 == null}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
metricsInScatterplots = [...metricsInScatterplots, [selectedMetric1, selectedMetric2]]
|
metricsInScatterplots = [
|
||||||
selectedMetric1 = null
|
...metricsInScatterplots,
|
||||||
selectedMetric2 = null
|
[selectedMetric1, selectedMetric2],
|
||||||
|
];
|
||||||
|
selectedMetric1 = null;
|
||||||
|
selectedMetric2 = null;
|
||||||
updateConfiguration({
|
updateConfiguration({
|
||||||
name: 'analysis_view_scatterPlotMetrics',
|
name: "analysis_view_scatterPlotMetrics",
|
||||||
value: metricsInScatterplots
|
value: metricsInScatterplots,
|
||||||
})
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Add Plot
|
Add Plot
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary"
|
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}>
|
||||||
on:click={() => (isScatterPlotConfigOpen = false)}>
|
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
@ -1,75 +1,82 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from "svelte";
|
||||||
import { Button, Table, InputGroup, InputGroupText, Icon } from 'sveltestrap'
|
import {
|
||||||
import MetricSelection from './MetricSelection.svelte'
|
Button,
|
||||||
import StatsTableEntry from './StatsTableEntry.svelte'
|
Table,
|
||||||
import { maxScope } from './utils.js'
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
Icon,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import MetricSelection from "./MetricSelection.svelte";
|
||||||
|
import StatsTableEntry from "./StatsTableEntry.svelte";
|
||||||
|
import { maxScope } from "./utils.js";
|
||||||
|
|
||||||
export let job
|
export let job;
|
||||||
export let jobMetrics
|
export let jobMetrics;
|
||||||
|
|
||||||
const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(),
|
const allMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort(),
|
||||||
scopesForMetric = (metric) => jobMetrics
|
scopesForMetric = (metric) =>
|
||||||
.filter(jm => jm.name == metric)
|
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
|
||||||
.map(jm => jm.scope)
|
|
||||||
|
|
||||||
let hosts = job.resources.map(r => r.hostname).sort(),
|
let hosts = job.resources.map((r) => r.hostname).sort(),
|
||||||
selectedScopes = {},
|
selectedScopes = {},
|
||||||
sorting = {},
|
sorting = {},
|
||||||
isMetricSelectionOpen = false,
|
isMetricSelectionOpen = false,
|
||||||
selectedMetrics = getContext('cc-config')[`job_view_nodestats_selectedMetrics:${job.cluster}`]
|
selectedMetrics =
|
||||||
|| getContext('cc-config')['job_view_nodestats_selectedMetrics']
|
getContext("cc-config")[
|
||||||
|
`job_view_nodestats_selectedMetrics:${job.cluster}`
|
||||||
|
] || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
||||||
|
|
||||||
for (let metric of allMetrics) {
|
for (let metric of allMetrics) {
|
||||||
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
||||||
// -> Else: Load smallest available granularity as default as per availability
|
// -> Else: Load smallest available granularity as default as per availability
|
||||||
const availableScopes = scopesForMetric(metric)
|
const availableScopes = scopesForMetric(metric);
|
||||||
if (job.exclusive != 1 || job.numNodes == 1) {
|
if (job.exclusive != 1 || job.numNodes == 1) {
|
||||||
if (availableScopes.includes('accelerator')) {
|
if (availableScopes.includes("accelerator")) {
|
||||||
selectedScopes[metric] = 'accelerator'
|
selectedScopes[metric] = "accelerator";
|
||||||
} else if (availableScopes.includes('core')) {
|
} else if (availableScopes.includes("core")) {
|
||||||
selectedScopes[metric] = 'core'
|
selectedScopes[metric] = "core";
|
||||||
} else if (availableScopes.includes('socket')) {
|
} else if (availableScopes.includes("socket")) {
|
||||||
selectedScopes[metric] = 'socket'
|
selectedScopes[metric] = "socket";
|
||||||
} else {
|
} else {
|
||||||
selectedScopes[metric] = 'node'
|
selectedScopes[metric] = "node";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedScopes[metric] = maxScope(availableScopes)
|
selectedScopes[metric] = maxScope(availableScopes);
|
||||||
}
|
}
|
||||||
|
|
||||||
sorting[metric] = {
|
sorting[metric] = {
|
||||||
min: { dir: 'up', active: false },
|
min: { dir: "up", active: false },
|
||||||
avg: { dir: 'up', active: false },
|
avg: { dir: "up", active: false },
|
||||||
max: { dir: 'up', active: false },
|
max: { dir: "up", active: false },
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortBy(metric, stat) {
|
export function sortBy(metric, stat) {
|
||||||
let s = sorting[metric][stat]
|
let s = sorting[metric][stat];
|
||||||
if (s.active) {
|
if (s.active) {
|
||||||
s.dir = s.dir == 'up' ? 'down' : 'up'
|
s.dir = s.dir == "up" ? "down" : "up";
|
||||||
} else {
|
} else {
|
||||||
for (let metric in sorting)
|
for (let metric in sorting)
|
||||||
for (let stat in sorting[metric])
|
for (let stat in sorting[metric]) sorting[metric][stat].active = false;
|
||||||
sorting[metric][stat].active = false
|
s.active = true;
|
||||||
s.active = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let series = jobMetrics.find(jm => jm.name == metric && jm.scope == 'node')?.metric.series
|
let series = jobMetrics.find(
|
||||||
sorting = {...sorting}
|
(jm) => jm.name == metric && jm.scope == "node",
|
||||||
|
)?.metric.series;
|
||||||
|
sorting = { ...sorting };
|
||||||
hosts = hosts.sort((h1, h2) => {
|
hosts = hosts.sort((h1, h2) => {
|
||||||
let s1 = series.find(s => s.hostname == h1)?.statistics
|
let s1 = series.find((s) => s.hostname == h1)?.statistics;
|
||||||
let s2 = series.find(s => s.hostname == h2)?.statistics
|
let s2 = series.find((s) => s.hostname == h2)?.statistics;
|
||||||
if (s1 == null || s2 == null)
|
if (s1 == null || s2 == null) return -1;
|
||||||
return -1
|
|
||||||
|
|
||||||
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
|
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moreLoaded(jobMetric) {
|
export function moreLoaded(jobMetric) {
|
||||||
jobMetrics = [...jobMetrics, jobMetric]
|
jobMetrics = [...jobMetrics, jobMetric];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -77,18 +84,18 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' -->
|
<Button outline on:click={() => (isMetricSelectionOpen = true)}>
|
||||||
|
<!-- log to click ', console.log(isMetricSelectionOpen)' -->
|
||||||
Metrics
|
Metrics
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
{#each selectedMetrics as metric}
|
{#each selectedMetrics as metric}
|
||||||
<th colspan={selectedScopes[metric] == 'node' ? 3 : 4}>
|
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
{metric}
|
{metric}
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
<select class="form-select"
|
<select class="form-select" bind:value={selectedScopes[metric]}>
|
||||||
bind:value={selectedScopes[metric]}>
|
|
||||||
{#each scopesForMetric(metric, jobMetrics) as scope}
|
{#each scopesForMetric(metric, jobMetrics) as scope}
|
||||||
<option value={scope}>{scope}</option>
|
<option value={scope}>{scope}</option>
|
||||||
{/each}
|
{/each}
|
||||||
@ -100,14 +107,19 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Node</th>
|
<th>Node</th>
|
||||||
{#each selectedMetrics as metric}
|
{#each selectedMetrics as metric}
|
||||||
{#if selectedScopes[metric] != 'node'}
|
{#if selectedScopes[metric] != "node"}
|
||||||
<th>Id</th>
|
<th>Id</th>
|
||||||
{/if}
|
{/if}
|
||||||
{#each ['min', 'avg', 'max'] as stat}
|
{#each ["min", "avg", "max"] as stat}
|
||||||
<th on:click={() => sortBy(metric, stat)}>
|
<th on:click={() => sortBy(metric, stat)}>
|
||||||
{stat}
|
{stat}
|
||||||
{#if selectedScopes[metric] == 'node'}
|
{#if selectedScopes[metric] == "node"}
|
||||||
<Icon name="caret-{sorting[metric][stat].dir}{sorting[metric][stat].active ? '-fill' : ''}" />
|
<Icon
|
||||||
|
name="caret-{sorting[metric][stat].dir}{sorting[metric][stat]
|
||||||
|
.active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
@ -120,20 +132,23 @@
|
|||||||
<th scope="col">{host}</th>
|
<th scope="col">{host}</th>
|
||||||
{#each selectedMetrics as metric (metric)}
|
{#each selectedMetrics as metric (metric)}
|
||||||
<StatsTableEntry
|
<StatsTableEntry
|
||||||
host={host} metric={metric}
|
{host}
|
||||||
|
{metric}
|
||||||
scope={selectedScopes[metric]}
|
scope={selectedScopes[metric]}
|
||||||
jobMetrics={jobMetrics} />
|
{jobMetrics}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<br/>
|
<br />
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
configName='job_view_nodestats_selectedMetrics'
|
configName="job_view_nodestats_selectedMetrics"
|
||||||
allMetrics={new Set(allMetrics)}
|
allMetrics={new Set(allMetrics)}
|
||||||
bind:metrics={selectedMetrics}
|
bind:metrics={selectedMetrics}
|
||||||
bind:isOpen={isMetricSelectionOpen} />
|
bind:isOpen={isMetricSelectionOpen}
|
||||||
|
/>
|
||||||
|
@ -1,54 +1,54 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Icon } from 'sveltestrap'
|
import { Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let host
|
export let host;
|
||||||
export let metric
|
export let metric;
|
||||||
export let scope
|
export let scope;
|
||||||
export let jobMetrics
|
export let jobMetrics;
|
||||||
|
|
||||||
function compareNumbers(a, b) {
|
function compareNumbers(a, b) {
|
||||||
return a.id - b.id;
|
return a.id - b.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByField(field) {
|
function sortByField(field) {
|
||||||
let s = sorting[field]
|
let s = sorting[field];
|
||||||
if (s.active) {
|
if (s.active) {
|
||||||
s.dir = s.dir == 'up' ? 'down' : 'up'
|
s.dir = s.dir == "up" ? "down" : "up";
|
||||||
} else {
|
} else {
|
||||||
for (let field in sorting)
|
for (let field in sorting) sorting[field].active = false;
|
||||||
sorting[field].active = false
|
s.active = true;
|
||||||
s.active = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sorting = {...sorting}
|
sorting = { ...sorting };
|
||||||
series = series.sort((a, b) => {
|
series = series.sort((a, b) => {
|
||||||
if (a == null || b == null)
|
if (a == null || b == null) return -1;
|
||||||
return -1
|
|
||||||
|
|
||||||
if (field === 'id') {
|
if (field === "id") {
|
||||||
return s.dir != 'up' ? a[field] - b[field] : b[field] - a[field]
|
return s.dir != "up" ? a[field] - b[field] : b[field] - a[field];
|
||||||
} else {
|
} else {
|
||||||
return s.dir != 'up' ? a.statistics[field] - b.statistics[field] : b.statistics[field] - a.statistics[field]
|
return s.dir != "up"
|
||||||
|
? a.statistics[field] - b.statistics[field]
|
||||||
|
: b.statistics[field] - a.statistics[field];
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sorting = {
|
let sorting = {
|
||||||
id: { dir: 'down', active: true },
|
id: { dir: "down", active: true },
|
||||||
min: { dir: 'up', active: false },
|
min: { dir: "up", active: false },
|
||||||
avg: { dir: 'up', active: false },
|
avg: { dir: "up", active: false },
|
||||||
max: { dir: 'up', active: false },
|
max: { dir: "up", active: false },
|
||||||
}
|
};
|
||||||
|
|
||||||
$: series = jobMetrics
|
$: series = jobMetrics
|
||||||
.find(jm => jm.name == metric && jm.scope == scope)
|
.find((jm) => jm.name == metric && jm.scope == scope)
|
||||||
?.metric.series.filter(s => s.hostname == host && s.statistics != null)
|
?.metric.series.filter((s) => s.hostname == host && s.statistics != null)
|
||||||
?.sort(compareNumbers)
|
?.sort(compareNumbers);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if series == null || series.length == 0}
|
{#if series == null || series.length == 0}
|
||||||
<td colspan={scope == 'node' ? 3 : 4}><i>No data</i></td>
|
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
|
||||||
{:else if series.length == 1 && scope == 'node'}
|
{:else if series.length == 1 && scope == "node"}
|
||||||
<td>
|
<td>
|
||||||
{series[0].statistics.min}
|
{series[0].statistics.min}
|
||||||
</td>
|
</td>
|
||||||
@ -62,10 +62,14 @@
|
|||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
<table style="width: 100%;">
|
<table style="width: 100%;">
|
||||||
<tr>
|
<tr>
|
||||||
{#each ['id', 'min', 'avg', 'max'] as field}
|
{#each ["id", "min", "avg", "max"] as field}
|
||||||
<th on:click={() => sortByField(field)}>
|
<th on:click={() => sortByField(field)}>
|
||||||
Sort
|
Sort
|
||||||
<Icon name="caret-{sorting[field].dir}{sorting[field].active ? '-fill' : ''}" />
|
<Icon
|
||||||
|
name="caret-{sorting[field].dir}{sorting[field].active
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -15,9 +15,13 @@
|
|||||||
Table,
|
Table,
|
||||||
Progress,
|
Progress,
|
||||||
Icon,
|
Icon,
|
||||||
Button
|
Button,
|
||||||
} from "sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js";
|
import {
|
||||||
|
init,
|
||||||
|
convert2uplot,
|
||||||
|
transformPerNodeDataForRoofline,
|
||||||
|
} from "./utils.js";
|
||||||
import { scaleNumbers } from "./units.js";
|
import { scaleNumbers } from "./units.js";
|
||||||
import {
|
import {
|
||||||
queryStore,
|
queryStore,
|
||||||
@ -25,8 +29,8 @@
|
|||||||
getContextClient,
|
getContextClient,
|
||||||
mutationStore,
|
mutationStore,
|
||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import PlotTable from './PlotTable.svelte'
|
import PlotTable from "./PlotTable.svelte";
|
||||||
import HistogramSelection from './HistogramSelection.svelte'
|
import HistogramSelection from "./HistogramSelection.svelte";
|
||||||
|
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
@ -35,7 +39,7 @@
|
|||||||
|
|
||||||
let plotWidths = [],
|
let plotWidths = [],
|
||||||
colWidth1,
|
colWidth1,
|
||||||
colWidth2
|
colWidth2;
|
||||||
let from = new Date(Date.now() - 5 * 60 * 1000),
|
let from = new Date(Date.now() - 5 * 60 * 1000),
|
||||||
to = new Date(Date.now());
|
to = new Date(Date.now());
|
||||||
const topOptions = [
|
const topOptions = [
|
||||||
@ -49,25 +53,25 @@
|
|||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.key ==
|
option.key ==
|
||||||
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`]
|
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`],
|
||||||
) ||
|
) ||
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) =>
|
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
|
||||||
option.key == ccconfig.status_view_selectedTopProjectCategory
|
|
||||||
);
|
);
|
||||||
let topUserSelection =
|
let topUserSelection =
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.key ==
|
option.key ==
|
||||||
ccconfig[`status_view_selectedTopUserCategory:${cluster}`]
|
ccconfig[`status_view_selectedTopUserCategory:${cluster}`],
|
||||||
) ||
|
) ||
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) =>
|
(option) => option.key == ccconfig.status_view_selectedTopUserCategory,
|
||||||
option.key == ccconfig.status_view_selectedTopUserCategory
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let isHistogramSelectionOpen = false
|
let isHistogramSelectionOpen = false;
|
||||||
$: metricsInHistograms = cluster ? (ccconfig[`user_view_histogramMetrics:${cluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
|
$: metricsInHistograms = cluster
|
||||||
|
? ccconfig[`user_view_histogramMetrics:${cluster}`] || []
|
||||||
|
: ccconfig.user_view_histogramMetrics || [];
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
$: mainQuery = queryStore({
|
$: mainQuery = queryStore({
|
||||||
@ -146,7 +150,7 @@
|
|||||||
from: from.toISOString(),
|
from: from.toISOString(),
|
||||||
to: to.toISOString(),
|
to: to.toISOString(),
|
||||||
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
||||||
metricsInHistograms: metricsInHistograms
|
metricsInHistograms: metricsInHistograms,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -217,12 +221,11 @@
|
|||||||
(node.metrics
|
(node.metrics
|
||||||
.find((m) => m.name == metric)
|
.find((m) => m.name == metric)
|
||||||
?.metric.series.reduce(
|
?.metric.series.reduce(
|
||||||
(sum, series) =>
|
(sum, series) => sum + series.data[series.data.length - 1],
|
||||||
sum + series.data[series.data.length - 1],
|
0,
|
||||||
0
|
|
||||||
) || 0)
|
) || 0)
|
||||||
: sum,
|
: sum,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let allocatedNodes = {},
|
let allocatedNodes = {},
|
||||||
@ -234,37 +237,27 @@
|
|||||||
memBwRateUnitBase = {};
|
memBwRateUnitBase = {};
|
||||||
$: if ($initq.data && $mainQuery.data) {
|
$: if ($initq.data && $mainQuery.data) {
|
||||||
let subClusters = $initq.data.clusters.find(
|
let subClusters = $initq.data.clusters.find(
|
||||||
(c) => c.name == cluster
|
(c) => c.name == cluster,
|
||||||
).subClusters;
|
).subClusters;
|
||||||
for (let subCluster of subClusters) {
|
for (let subCluster of subClusters) {
|
||||||
allocatedNodes[subCluster.name] =
|
allocatedNodes[subCluster.name] =
|
||||||
$mainQuery.data.allocatedNodes.find(
|
$mainQuery.data.allocatedNodes.find(
|
||||||
({ name }) => name == subCluster.name
|
({ name }) => name == subCluster.name,
|
||||||
)?.count || 0;
|
)?.count || 0;
|
||||||
flopRate[subCluster.name] =
|
flopRate[subCluster.name] =
|
||||||
Math.floor(
|
Math.floor(
|
||||||
sumUp(
|
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "flops_any") *
|
||||||
$mainQuery.data.nodeMetrics,
|
100,
|
||||||
subCluster.name,
|
|
||||||
"flops_any"
|
|
||||||
) * 100
|
|
||||||
) / 100;
|
) / 100;
|
||||||
flopRateUnitPrefix[subCluster.name] =
|
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix;
|
||||||
subCluster.flopRateSimd.unit.prefix;
|
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base;
|
||||||
flopRateUnitBase[subCluster.name] =
|
|
||||||
subCluster.flopRateSimd.unit.base;
|
|
||||||
memBwRate[subCluster.name] =
|
memBwRate[subCluster.name] =
|
||||||
Math.floor(
|
Math.floor(
|
||||||
sumUp(
|
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "mem_bw") * 100,
|
||||||
$mainQuery.data.nodeMetrics,
|
|
||||||
subCluster.name,
|
|
||||||
"mem_bw"
|
|
||||||
) * 100
|
|
||||||
) / 100;
|
) / 100;
|
||||||
memBwRateUnitPrefix[subCluster.name] =
|
memBwRateUnitPrefix[subCluster.name] =
|
||||||
subCluster.memoryBandwidth.unit.prefix;
|
subCluster.memoryBandwidth.unit.prefix;
|
||||||
memBwRateUnitBase[subCluster.name] =
|
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base;
|
||||||
subCluster.memoryBandwidth.unit.base;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,9 +274,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function updateTopUserConfiguration(select) {
|
function updateTopUserConfiguration(select) {
|
||||||
if (
|
if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) {
|
||||||
ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select
|
|
||||||
) {
|
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: `status_view_selectedTopUserCategory:${cluster}`,
|
name: `status_view_selectedTopUserCategory:${cluster}`,
|
||||||
value: JSON.stringify(select),
|
value: JSON.stringify(select),
|
||||||
@ -301,8 +292,7 @@
|
|||||||
|
|
||||||
function updateTopProjectConfiguration(select) {
|
function updateTopProjectConfiguration(select) {
|
||||||
if (
|
if (
|
||||||
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`] !=
|
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`] != select
|
||||||
select
|
|
||||||
) {
|
) {
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: `status_view_selectedTopProjectCategory:${cluster}`,
|
name: `status_view_selectedTopProjectCategory:${cluster}`,
|
||||||
@ -340,9 +330,11 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto" style="margin-left: auto;">
|
<Col xs="auto" style="margin-left: auto;">
|
||||||
<Button
|
<Button
|
||||||
outline color="secondary"
|
outline
|
||||||
on:click={() => (isHistogramSelectionOpen = true)}>
|
color="secondary"
|
||||||
<Icon name="bar-chart-line"/> Select Histograms
|
on:click={() => (isHistogramSelectionOpen = true)}
|
||||||
|
>
|
||||||
|
<Icon name="bar-chart-line" /> Select Histograms
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto" style="margin-left: 0.25rem;">
|
<Col xs="auto" style="margin-left: 0.25rem;">
|
||||||
@ -373,9 +365,7 @@
|
|||||||
<Col md="4" class="px-3">
|
<Col md="4" class="px-3">
|
||||||
<Card class="h-auto mt-1">
|
<Card class="h-auto mt-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-0"
|
<CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
|
||||||
>SubCluster "{subCluster.name}"</CardTitle
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Table borderless>
|
<Table borderless>
|
||||||
@ -384,9 +374,7 @@
|
|||||||
<td style="min-width: 100px;"
|
<td style="min-width: 100px;"
|
||||||
><div class="col">
|
><div class="col">
|
||||||
<Progress
|
<Progress
|
||||||
value={allocatedNodes[
|
value={allocatedNodes[subCluster.name]}
|
||||||
subCluster.name
|
|
||||||
]}
|
|
||||||
max={subCluster.numberOfNodes}
|
max={subCluster.numberOfNodes}
|
||||||
/>
|
/>
|
||||||
</div></td
|
</div></td
|
||||||
@ -417,9 +405,8 @@
|
|||||||
<td>
|
<td>
|
||||||
{scaleNumbers(
|
{scaleNumbers(
|
||||||
flopRate[subCluster.name],
|
flopRate[subCluster.name],
|
||||||
subCluster.flopRateSimd.value *
|
subCluster.flopRateSimd.value * subCluster.numberOfNodes,
|
||||||
subCluster.numberOfNodes,
|
flopRateUnitPrefix[subCluster.name],
|
||||||
flopRateUnitPrefix[subCluster.name]
|
|
||||||
)}{flopRateUnitBase[subCluster.name]} [Max]
|
)}{flopRateUnitBase[subCluster.name]} [Max]
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -429,8 +416,7 @@
|
|||||||
><div class="col">
|
><div class="col">
|
||||||
<Progress
|
<Progress
|
||||||
value={memBwRate[subCluster.name]}
|
value={memBwRate[subCluster.name]}
|
||||||
max={subCluster.memoryBandwidth
|
max={subCluster.memoryBandwidth.value *
|
||||||
.value *
|
|
||||||
subCluster.numberOfNodes}
|
subCluster.numberOfNodes}
|
||||||
/>
|
/>
|
||||||
</div></td
|
</div></td
|
||||||
@ -438,9 +424,8 @@
|
|||||||
<td>
|
<td>
|
||||||
{scaleNumbers(
|
{scaleNumbers(
|
||||||
memBwRate[subCluster.name],
|
memBwRate[subCluster.name],
|
||||||
subCluster.memoryBandwidth.value *
|
subCluster.memoryBandwidth.value * subCluster.numberOfNodes,
|
||||||
subCluster.numberOfNodes,
|
memBwRateUnitPrefix[subCluster.name],
|
||||||
memBwRateUnitPrefix[subCluster.name]
|
|
||||||
)}{memBwRateUnitBase[subCluster.name]} [Max]
|
)}{memBwRateUnitBase[subCluster.name]} [Max]
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -456,13 +441,11 @@
|
|||||||
width={plotWidths[i] - 10}
|
width={plotWidths[i] - 10}
|
||||||
height={300}
|
height={300}
|
||||||
cluster={subCluster}
|
cluster={subCluster}
|
||||||
data={
|
data={transformPerNodeDataForRoofline(
|
||||||
transformPerNodeDataForRoofline(
|
|
||||||
$mainQuery.data.nodeMetrics.filter(
|
$mainQuery.data.nodeMetrics.filter(
|
||||||
(data) => data.subCluster == subCluster.name
|
(data) => data.subCluster == subCluster.name,
|
||||||
)
|
),
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
@ -470,7 +453,7 @@
|
|||||||
</Row>
|
</Row>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<hr/>
|
<hr />
|
||||||
|
|
||||||
<!-- Usage Stats as Histograms -->
|
<!-- Usage Stats as Histograms -->
|
||||||
|
|
||||||
@ -478,26 +461,21 @@
|
|||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
<div bind:clientWidth={colWidth1}>
|
<div bind:clientWidth={colWidth1}>
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
Top Users on {cluster.charAt(0).toUpperCase() +
|
Top Users on {cluster.charAt(0).toUpperCase() + cluster.slice(1)}
|
||||||
cluster.slice(1)}
|
|
||||||
</h4>
|
</h4>
|
||||||
{#key $topUserQuery.data}
|
{#key $topUserQuery.data}
|
||||||
{#if $topUserQuery.fetching}
|
{#if $topUserQuery.fetching}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if $topUserQuery.error}
|
{:else if $topUserQuery.error}
|
||||||
<Card body color="danger"
|
<Card body color="danger">{$topUserQuery.error.message}</Card>
|
||||||
>{$topUserQuery.error.message}</Card
|
|
||||||
>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Pie
|
<Pie
|
||||||
size={colWidth1}
|
size={colWidth1}
|
||||||
sliceLabel={topUserSelection.label}
|
sliceLabel={topUserSelection.label}
|
||||||
quantities={$topUserQuery.data.topUser.map(
|
quantities={$topUserQuery.data.topUser.map(
|
||||||
(tu) => tu[topUserSelection.key]
|
(tu) => tu[topUserSelection.key],
|
||||||
)}
|
|
||||||
entities={$topUserQuery.data.topUser.map(
|
|
||||||
(tu) => tu.id
|
|
||||||
)}
|
)}
|
||||||
|
entities={$topUserQuery.data.topUser.map((tu) => tu.id)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
@ -508,9 +486,7 @@
|
|||||||
{#if $topUserQuery.fetching}
|
{#if $topUserQuery.fetching}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if $topUserQuery.error}
|
{:else if $topUserQuery.error}
|
||||||
<Card body color="danger"
|
<Card body color="danger">{$topUserQuery.error.message}</Card>
|
||||||
>{$topUserQuery.error.message}</Card
|
|
||||||
>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Table>
|
<Table>
|
||||||
<tr class="mb-2">
|
<tr class="mb-2">
|
||||||
@ -518,10 +494,7 @@
|
|||||||
<th>User Name</th>
|
<th>User Name</th>
|
||||||
<th
|
<th
|
||||||
>Number of
|
>Number of
|
||||||
<select
|
<select class="p-0" bind:value={topUserSelection}>
|
||||||
class="p-0"
|
|
||||||
bind:value={topUserSelection}
|
|
||||||
>
|
|
||||||
{#each topOptions as option}
|
{#each topOptions as option}
|
||||||
<option value={option}>
|
<option value={option}>
|
||||||
{option.label}
|
{option.label}
|
||||||
@ -532,12 +505,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{#each $topUserQuery.data.topUser as tu, i}
|
{#each $topUserQuery.data.topUser as tu, i}
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
||||||
><Icon
|
|
||||||
name="circle-fill"
|
|
||||||
style="color: {colors[i]};"
|
|
||||||
/></td
|
|
||||||
>
|
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
><a
|
><a
|
||||||
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
|
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
|
||||||
@ -553,26 +521,21 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
Top Projects on {cluster.charAt(0).toUpperCase() +
|
Top Projects on {cluster.charAt(0).toUpperCase() + cluster.slice(1)}
|
||||||
cluster.slice(1)}
|
|
||||||
</h4>
|
</h4>
|
||||||
{#key $topProjectQuery.data}
|
{#key $topProjectQuery.data}
|
||||||
{#if $topProjectQuery.fetching}
|
{#if $topProjectQuery.fetching}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if $topProjectQuery.error}
|
{:else if $topProjectQuery.error}
|
||||||
<Card body color="danger"
|
<Card body color="danger">{$topProjectQuery.error.message}</Card>
|
||||||
>{$topProjectQuery.error.message}</Card
|
|
||||||
>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Pie
|
<Pie
|
||||||
size={colWidth1}
|
size={colWidth1}
|
||||||
sliceLabel={topProjectSelection.label}
|
sliceLabel={topProjectSelection.label}
|
||||||
quantities={$topProjectQuery.data.topProjects.map(
|
quantities={$topProjectQuery.data.topProjects.map(
|
||||||
(tp) => tp[topProjectSelection.key]
|
(tp) => tp[topProjectSelection.key],
|
||||||
)}
|
|
||||||
entities={$topProjectQuery.data.topProjects.map(
|
|
||||||
(tp) => tp.id
|
|
||||||
)}
|
)}
|
||||||
|
entities={$topProjectQuery.data.topProjects.map((tp) => tp.id)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
@ -582,9 +545,7 @@
|
|||||||
{#if $topProjectQuery.fetching}
|
{#if $topProjectQuery.fetching}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if $topProjectQuery.error}
|
{:else if $topProjectQuery.error}
|
||||||
<Card body color="danger"
|
<Card body color="danger">{$topProjectQuery.error.message}</Card>
|
||||||
>{$topProjectQuery.error.message}</Card
|
|
||||||
>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Table>
|
<Table>
|
||||||
<tr class="mb-2">
|
<tr class="mb-2">
|
||||||
@ -592,10 +553,7 @@
|
|||||||
<th>Project Code</th>
|
<th>Project Code</th>
|
||||||
<th
|
<th
|
||||||
>Number of
|
>Number of
|
||||||
<select
|
<select class="p-0" bind:value={topProjectSelection}>
|
||||||
class="p-0"
|
|
||||||
bind:value={topProjectSelection}
|
|
||||||
>
|
|
||||||
{#each topOptions as option}
|
{#each topOptions as option}
|
||||||
<option value={option}>
|
<option value={option}>
|
||||||
{option.label}
|
{option.label}
|
||||||
@ -606,12 +564,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{#each $topProjectQuery.data.topProjects as tp, i}
|
{#each $topProjectQuery.data.topProjects as tp, i}
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
||||||
><Icon
|
|
||||||
name="circle-fill"
|
|
||||||
style="color: {colors[i]};"
|
|
||||||
/></td
|
|
||||||
>
|
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
><a
|
><a
|
||||||
href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
|
href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
|
||||||
@ -632,9 +585,7 @@
|
|||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $mainQuery.data.stats}
|
{#key $mainQuery.data.stats}
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot(
|
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
|
||||||
$mainQuery.data.stats[0].histDuration
|
|
||||||
)}
|
|
||||||
width={colWidth2 - 25}
|
width={colWidth2 - 25}
|
||||||
title="Duration Distribution"
|
title="Duration Distribution"
|
||||||
xlabel="Current Runtimes"
|
xlabel="Current Runtimes"
|
||||||
@ -664,9 +615,7 @@
|
|||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $mainQuery.data.stats}
|
{#key $mainQuery.data.stats}
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot(
|
data={convert2uplot($mainQuery.data.stats[0].histNumCores)}
|
||||||
$mainQuery.data.stats[0].histNumCores
|
|
||||||
)}
|
|
||||||
width={colWidth2 - 25}
|
width={colWidth2 - 25}
|
||||||
title="Number of Cores Distribution"
|
title="Number of Cores Distribution"
|
||||||
xlabel="Allocated Cores"
|
xlabel="Allocated Cores"
|
||||||
@ -701,17 +650,19 @@
|
|||||||
let:width
|
let:width
|
||||||
renderFor="user"
|
renderFor="user"
|
||||||
items={$mainQuery.data.stats[0].histMetrics}
|
items={$mainQuery.data.stats[0].histMetrics}
|
||||||
itemsPerRow={3}>
|
itemsPerRow={3}
|
||||||
|
>
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot(item.data)}
|
data={convert2uplot(item.data)}
|
||||||
usesBins={true}
|
usesBins={true}
|
||||||
width={width} height={250}
|
{width}
|
||||||
|
height={250}
|
||||||
title="Distribution of '{item.metric}' averages"
|
title="Distribution of '{item.metric}' averages"
|
||||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||||
xunit={item.unit}
|
xunit={item.unit}
|
||||||
ylabel="Number of Jobs"
|
ylabel="Number of Jobs"
|
||||||
yunit="Jobs"/>
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
</PlotTable>
|
</PlotTable>
|
||||||
{/key}
|
{/key}
|
||||||
</Col>
|
</Col>
|
||||||
@ -720,6 +671,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<HistogramSelection
|
<HistogramSelection
|
||||||
bind:cluster={cluster}
|
bind:cluster
|
||||||
bind:metricsInHistograms={metricsInHistograms}
|
bind:metricsInHistograms
|
||||||
bind:isOpen={isHistogramSelectionOpen} />
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
|
/>
|
||||||
|
@ -1,38 +1,53 @@
|
|||||||
<script>
|
<script>
|
||||||
import { init, checkMetricDisabled } from './utils.js'
|
import { init, checkMetricDisabled } from "./utils.js";
|
||||||
import Refresher from './joblist/Refresher.svelte'
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
|
import {
|
||||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
Row,
|
||||||
import TimeSelection from './filters/TimeSelection.svelte'
|
Col,
|
||||||
import PlotTable from './PlotTable.svelte'
|
Input,
|
||||||
import MetricPlot from './plots/MetricPlot.svelte'
|
InputGroup,
|
||||||
import { getContext } from 'svelte'
|
InputGroupText,
|
||||||
|
Icon,
|
||||||
|
Spinner,
|
||||||
|
Card,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
|
import TimeSelection from "./filters/TimeSelection.svelte";
|
||||||
|
import PlotTable from "./PlotTable.svelte";
|
||||||
|
import MetricPlot from "./plots/MetricPlot.svelte";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
export let cluster
|
export let cluster;
|
||||||
export let from = null
|
export let from = null;
|
||||||
export let to = null
|
export let to = null;
|
||||||
|
|
||||||
const { query: initq } = init()
|
const { query: initq } = init();
|
||||||
|
|
||||||
if (from == null || to == null) {
|
if (from == null || to == null) {
|
||||||
to = new Date(Date.now())
|
to = new Date(Date.now());
|
||||||
from = new Date(to.getTime())
|
from = new Date(to.getTime());
|
||||||
from.setMinutes(from.getMinutes() - 30)
|
from.setMinutes(from.getMinutes() - 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clusters = getContext('clusters')
|
const clusters = getContext("clusters");
|
||||||
const ccconfig = getContext('cc-config')
|
const ccconfig = getContext("cc-config");
|
||||||
const metricConfig = getContext('metrics')
|
const metricConfig = getContext("metrics");
|
||||||
|
|
||||||
let plotHeight = 300
|
let plotHeight = 300;
|
||||||
let hostnameFilter = ''
|
let hostnameFilter = "";
|
||||||
let selectedMetric = ccconfig.system_view_selectedMetric
|
let selectedMetric = ccconfig.system_view_selectedMetric;
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
$: nodesQuery = queryStore({
|
$: nodesQuery = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
query: gql`
|
||||||
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
|
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
||||||
|
nodeMetrics(
|
||||||
|
cluster: $cluster
|
||||||
|
metrics: $metrics
|
||||||
|
from: $from
|
||||||
|
to: $to
|
||||||
|
) {
|
||||||
host
|
host
|
||||||
subCluster
|
subCluster
|
||||||
metrics {
|
metrics {
|
||||||
@ -40,64 +55,78 @@
|
|||||||
scope
|
scope
|
||||||
metric {
|
metric {
|
||||||
timestep
|
timestep
|
||||||
unit { base, prefix }
|
unit {
|
||||||
|
base
|
||||||
|
prefix
|
||||||
|
}
|
||||||
series {
|
series {
|
||||||
statistics { min, avg, max }
|
statistics {
|
||||||
|
min
|
||||||
|
avg
|
||||||
|
max
|
||||||
|
}
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}
|
||||||
|
`,
|
||||||
variables: {
|
variables: {
|
||||||
cluster: cluster,
|
cluster: cluster,
|
||||||
metrics: [selectedMetric],
|
metrics: [selectedMetric],
|
||||||
from: from.toISOString(),
|
from: from.toISOString(),
|
||||||
to: to.toISOString()
|
to: to.toISOString(),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
let metricUnits = {}
|
let metricUnits = {};
|
||||||
$: if ($nodesQuery.data) {
|
$: if ($nodesQuery.data) {
|
||||||
let thisCluster = clusters.find(c => c.name == cluster)
|
let thisCluster = clusters.find((c) => c.name == cluster);
|
||||||
if (thisCluster) {
|
if (thisCluster) {
|
||||||
for (let metric of thisCluster.metricConfig) {
|
for (let metric of thisCluster.metricConfig) {
|
||||||
if (metric.unit.prefix || metric.unit.base) {
|
if (metric.unit.prefix || metric.unit.base) {
|
||||||
metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
|
metricUnits[metric.name] =
|
||||||
} else { // If no unit defined: Omit Unit Display
|
"(" +
|
||||||
metricUnits[metric.name] = ''
|
(metric.unit.prefix ? metric.unit.prefix : "") +
|
||||||
|
(metric.unit.base ? metric.unit.base : "") +
|
||||||
|
")";
|
||||||
|
} else {
|
||||||
|
// If no unit defined: Omit Unit Display
|
||||||
|
metricUnits[metric.name] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
{#if $initq.error}
|
{#if $initq.error}
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{:else if $initq.fetching}
|
{:else if $initq.fetching}
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
{:else}
|
{:else}
|
||||||
<Col>
|
<Col>
|
||||||
<Refresher on:reload={() => {
|
<Refresher
|
||||||
const diff = Date.now() - to
|
on:reload={() => {
|
||||||
from = new Date(from.getTime() + diff)
|
const diff = Date.now() - to;
|
||||||
to = new Date(to.getTime() + diff)
|
from = new Date(from.getTime() + diff);
|
||||||
}} />
|
to = new Date(to.getTime() + diff);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<TimeSelection
|
<TimeSelection bind:from bind:to />
|
||||||
bind:from={from}
|
|
||||||
bind:to={to} />
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||||
<InputGroupText>Metric</InputGroupText>
|
<InputGroupText>Metric</InputGroupText>
|
||||||
<select class="form-select" bind:value={selectedMetric}>
|
<select class="form-select" bind:value={selectedMetric}>
|
||||||
{#each clusters.find(c => c.name == cluster).metricConfig as metric}
|
{#each clusters.find((c) => c.name == cluster).metricConfig as metric}
|
||||||
<option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
|
<option value={metric.name}
|
||||||
|
>{metric.name} {metricUnits[metric.name]}</option
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
@ -106,18 +135,22 @@
|
|||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
||||||
<InputGroupText>Find Node</InputGroupText>
|
<InputGroupText>Find Node</InputGroupText>
|
||||||
<Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
|
<Input
|
||||||
|
placeholder="hostname..."
|
||||||
|
type="text"
|
||||||
|
bind:value={hostnameFilter}
|
||||||
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
</Row>
|
</Row>
|
||||||
<br/>
|
<br />
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
{#if $nodesQuery.error}
|
{#if $nodesQuery.error}
|
||||||
<Card body color="danger">{$nodesQuery.error.message}</Card>
|
<Card body color="danger">{$nodesQuery.error.message}</Card>
|
||||||
{:else if $nodesQuery.fetching || $initq.fetching}
|
{:else if $nodesQuery.fetching || $initq.fetching}
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
{:else}
|
{:else}
|
||||||
<PlotTable
|
<PlotTable
|
||||||
let:item
|
let:item
|
||||||
@ -125,35 +158,61 @@
|
|||||||
renderFor="systems"
|
renderFor="systems"
|
||||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||||
items={$nodesQuery.data.nodeMetrics
|
items={$nodesQuery.data.nodeMetrics
|
||||||
.filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
|
.filter(
|
||||||
.map(h => ({
|
(h) =>
|
||||||
|
h.host.includes(hostnameFilter) &&
|
||||||
|
h.metrics.some(
|
||||||
|
(m) => m.name == selectedMetric && m.scope == "node",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((h) => ({
|
||||||
host: h.host,
|
host: h.host,
|
||||||
subCluster: h.subCluster,
|
subCluster: h.subCluster,
|
||||||
data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node'),
|
data: h.metrics.find(
|
||||||
disabled: checkMetricDisabled(selectedMetric, cluster, h.subCluster)
|
(m) => m.name == selectedMetric && m.scope == "node",
|
||||||
|
),
|
||||||
|
disabled: checkMetricDisabled(
|
||||||
|
selectedMetric,
|
||||||
|
cluster,
|
||||||
|
h.subCluster,
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.host.localeCompare(b.host))
|
.sort((a, b) => a.host.localeCompare(b.host))}
|
||||||
}>
|
>
|
||||||
|
<h4 style="width: 100%; text-align: center;">
|
||||||
<h4 style="width: 100%; text-align: center;"><a style="display: block;padding-top: 15px;" href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>
|
<a
|
||||||
|
style="display: block;padding-top: 15px;"
|
||||||
|
href="/monitoring/node/{cluster}/{item.host}"
|
||||||
|
>{item.host} ({item.subCluster})</a
|
||||||
|
>
|
||||||
|
</h4>
|
||||||
{#if item.disabled === false && item.data}
|
{#if item.disabled === false && item.data}
|
||||||
<MetricPlot
|
<MetricPlot
|
||||||
width={width}
|
{width}
|
||||||
height={plotHeight}
|
height={plotHeight}
|
||||||
timestep={item.data.metric.timestep}
|
timestep={item.data.metric.timestep}
|
||||||
series={item.data.metric.series}
|
series={item.data.metric.series}
|
||||||
metric={item.data.name}
|
metric={item.data.name}
|
||||||
cluster={clusters.find(c => c.name == cluster)}
|
cluster={clusters.find((c) => c.name == cluster)}
|
||||||
subCluster={item.subCluster}
|
subCluster={item.subCluster}
|
||||||
resources={[{hostname: item.host}]}
|
resources={[{ hostname: item.host }]}
|
||||||
forNode={true}/>
|
forNode={true}
|
||||||
|
/>
|
||||||
{:else if item.disabled === true && item.data}
|
{:else if item.disabled === true && item.data}
|
||||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
|
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||||
|
>Metric disabled for subcluster <code
|
||||||
|
>{selectedMetric}:{item.subCluster}</code
|
||||||
|
></Card
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
|
<Card
|
||||||
|
style="margin-left: 2rem;margin-right: 2rem;"
|
||||||
|
body
|
||||||
|
color="warning"
|
||||||
|
>No dataset returned for <code>{selectedMetric}</code></Card
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</PlotTable>
|
</PlotTable>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
@ -1,126 +1,146 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from "svelte";
|
||||||
import { gql, getContextClient , mutationStore } from '@urql/svelte'
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
import { Icon, Button, ListGroupItem, Spinner, Modal, Input,
|
import {
|
||||||
ModalBody, ModalHeader, ModalFooter, Alert } from 'sveltestrap'
|
Icon,
|
||||||
import { fuzzySearchTags } from './utils.js'
|
Button,
|
||||||
import Tag from './Tag.svelte'
|
ListGroupItem,
|
||||||
|
Spinner,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
Alert,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { fuzzySearchTags } from "./utils.js";
|
||||||
|
import Tag from "./Tag.svelte";
|
||||||
|
|
||||||
export let job
|
export let job;
|
||||||
export let jobTags = job.tags
|
export let jobTags = job.tags;
|
||||||
|
|
||||||
let allTags = getContext('tags'), initialized = getContext('initialized')
|
let allTags = getContext("tags"),
|
||||||
let newTagType = '', newTagName = ''
|
initialized = getContext("initialized");
|
||||||
let filterTerm = ''
|
let newTagType = "",
|
||||||
let pendingChange = false
|
newTagName = "";
|
||||||
let isOpen = false
|
let filterTerm = "";
|
||||||
|
let pendingChange = false;
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
|
|
||||||
const createTagMutation = ({ type, name }) => {
|
const createTagMutation = ({ type, name }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`mutation($type: String!, $name: String!) {
|
query: gql`
|
||||||
createTag(type: $type, name: $name) { id, type, name }
|
mutation ($type: String!, $name: String!) {
|
||||||
}`,
|
createTag(type: $type, name: $name) {
|
||||||
variables: { type, name}
|
id
|
||||||
})
|
type
|
||||||
|
name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { type, name },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const addTagsToJobMutation = ({ job, tagIds }) => {
|
const addTagsToJobMutation = ({ job, tagIds }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
|
query: gql`
|
||||||
addTagsToJob(job: $job, tagIds: $tagIds) { id, type, name }
|
mutation ($job: ID!, $tagIds: [ID!]!) {
|
||||||
}`,
|
addTagsToJob(job: $job, tagIds: $tagIds) {
|
||||||
variables: {job, tagIds}
|
id
|
||||||
})
|
type
|
||||||
|
name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { job, tagIds },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const removeTagsFromJobMutation = ({ job, tagIds }) => {
|
const removeTagsFromJobMutation = ({ job, tagIds }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
|
query: gql`
|
||||||
removeTagsFromJob(job: $job, tagIds: $tagIds) { id, type, name }
|
mutation ($job: ID!, $tagIds: [ID!]!) {
|
||||||
}`,
|
removeTagsFromJob(job: $job, tagIds: $tagIds) {
|
||||||
variables: {job, tagIds}
|
id
|
||||||
})
|
type
|
||||||
|
name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { job, tagIds },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let allTagsFiltered // $initialized is in there because when it becomes true, allTags is initailzed.
|
let allTagsFiltered; // $initialized is in there because when it becomes true, allTags is initailzed.
|
||||||
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags))
|
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
newTagType = '';
|
newTagType = "";
|
||||||
newTagName = '';
|
newTagName = "";
|
||||||
let parts = filterTerm.split(':').map(s => s.trim())
|
let parts = filterTerm.split(":").map((s) => s.trim());
|
||||||
if (parts.length == 2 && parts.every(s => s.length > 0)) {
|
if (parts.length == 2 && parts.every((s) => s.length > 0)) {
|
||||||
newTagType = parts[0]
|
newTagType = parts[0];
|
||||||
newTagName = parts[1]
|
newTagName = parts[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNewTag(type, name) {
|
function isNewTag(type, name) {
|
||||||
for (let tag of allTagsFiltered)
|
for (let tag of allTagsFiltered)
|
||||||
if (tag.type == type && tag.name == name)
|
if (tag.type == type && tag.name == name) return false;
|
||||||
return false
|
return true;
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTag(type, name) {
|
function createTag(type, name) {
|
||||||
pendingChange = true
|
pendingChange = true;
|
||||||
createTagMutation({ type: type, name: name })
|
createTagMutation({ type: type, name: name }).subscribe((res) => {
|
||||||
.subscribe(res => {
|
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && !res.error) {
|
||||||
pendingChange = false
|
pendingChange = false;
|
||||||
allTags = [...allTags, res.data.createTag]
|
allTags = [...allTags, res.data.createTag];
|
||||||
newTagType = ''
|
newTagType = "";
|
||||||
newTagName = ''
|
newTagName = "";
|
||||||
addTagToJob(res.data.createTag)
|
addTagToJob(res.data.createTag);
|
||||||
} else if (res.fetching === false && res.error) {
|
} else if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTagToJob(tag) {
|
function addTagToJob(tag) {
|
||||||
pendingChange = tag.id
|
pendingChange = tag.id;
|
||||||
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] })
|
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe((res) => {
|
||||||
.subscribe(res => {
|
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && !res.error) {
|
||||||
jobTags = job.tags = res.data.addTagsToJob;
|
jobTags = job.tags = res.data.addTagsToJob;
|
||||||
pendingChange = false;
|
pendingChange = false;
|
||||||
} else if (res.fetching === false && res.error) {
|
} else if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTagFromJob(tag) {
|
function removeTagFromJob(tag) {
|
||||||
pendingChange = tag.id
|
pendingChange = tag.id;
|
||||||
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] })
|
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe(
|
||||||
.subscribe(res => {
|
(res) => {
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && !res.error) {
|
||||||
jobTags = job.tags = res.data.removeTagsFromJob
|
jobTags = job.tags = res.data.removeTagsFromJob;
|
||||||
pendingChange = false
|
pendingChange = false;
|
||||||
} else if (res.fetching === false && res.error) {
|
} else if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
ul.list-group {
|
|
||||||
max-height: 450px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manage Tags
|
Manage Tags
|
||||||
@ -131,33 +151,44 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Input style="width: 100%;"
|
<Input
|
||||||
type="text" placeholder="Search Tags"
|
style="width: 100%;"
|
||||||
bind:value={filterTerm} />
|
type="text"
|
||||||
|
placeholder="Search Tags"
|
||||||
|
bind:value={filterTerm}
|
||||||
|
/>
|
||||||
|
|
||||||
<br/>
|
<br />
|
||||||
|
|
||||||
<Alert color="info">
|
<Alert color="info">
|
||||||
Search using "<code>type: name</code>". If no tag matches your search,
|
Search using "<code>type: name</code>". If no tag matches your search, a
|
||||||
a button for creating a new one will appear.
|
button for creating a new one will appear.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{#each allTagsFiltered as tag}
|
{#each allTagsFiltered as tag}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<Tag tag={tag}/>
|
<Tag {tag} />
|
||||||
|
|
||||||
<span style="float: right;">
|
<span style="float: right;">
|
||||||
{#if pendingChange === tag.id}
|
{#if pendingChange === tag.id}
|
||||||
<Spinner size="sm" secondary />
|
<Spinner size="sm" secondary />
|
||||||
{:else if job.tags.find(t => t.id == tag.id)}
|
{:else if job.tags.find((t) => t.id == tag.id)}
|
||||||
<Button size="sm" outline color="danger"
|
<Button
|
||||||
on:click={() => removeTagFromJob(tag)}>
|
size="sm"
|
||||||
|
outline
|
||||||
|
color="danger"
|
||||||
|
on:click={() => removeTagFromJob(tag)}
|
||||||
|
>
|
||||||
<Icon name="x" />
|
<Icon name="x" />
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button size="sm" outline color="success"
|
<Button
|
||||||
on:click={() => addTagToJob(tag)}>
|
size="sm"
|
||||||
|
outline
|
||||||
|
color="success"
|
||||||
|
on:click={() => addTagToJob(tag)}
|
||||||
|
>
|
||||||
<Icon name="plus" />
|
<Icon name="plus" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
@ -169,12 +200,17 @@
|
|||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<br/>
|
<br />
|
||||||
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
|
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
|
||||||
<Button outline color="success"
|
<Button
|
||||||
on:click={e => (e.preventDefault(), createTag(newTagType, newTagName))}>
|
outline
|
||||||
|
color="success"
|
||||||
|
on:click={(e) => (
|
||||||
|
e.preventDefault(), createTag(newTagType, newTagName)
|
||||||
|
)}
|
||||||
|
>
|
||||||
Create & Add Tag:
|
Create & Add Tag:
|
||||||
<Tag tag={({ type: newTagType, name: newTagName })} clickable={false}/>
|
<Tag tag={{ type: newTagType, name: newTagName }} clickable={false} />
|
||||||
</Button>
|
</Button>
|
||||||
{:else if allTagsFiltered.length == 0}
|
{:else if allTagsFiltered.length == 0}
|
||||||
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
|
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
|
||||||
@ -188,3 +224,11 @@
|
|||||||
<Button outline on:click={() => (isOpen = true)}>
|
<Button outline on:click={() => (isOpen = true)}>
|
||||||
Manage Tags <Icon name="tags" />
|
Manage Tags <Icon name="tags" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul.list-group {
|
||||||
|
max-height: 450px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,62 +1,94 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from 'svelte'
|
import { onMount, getContext } from "svelte";
|
||||||
import { init, convert2uplot } from './utils.js'
|
import { init, convert2uplot } from "./utils.js";
|
||||||
import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
|
import {
|
||||||
import { queryStore, gql, getContextClient } from '@urql/svelte'
|
Table,
|
||||||
import Filters from './filters/Filters.svelte'
|
Row,
|
||||||
import JobList from './joblist/JobList.svelte'
|
Col,
|
||||||
import Sorting from './joblist/SortSelection.svelte'
|
Button,
|
||||||
import Refresher from './joblist/Refresher.svelte'
|
Icon,
|
||||||
import Histogram from './plots/Histogram.svelte'
|
Card,
|
||||||
import MetricSelection from './MetricSelection.svelte'
|
Spinner,
|
||||||
import HistogramSelection from './HistogramSelection.svelte'
|
} from "@sveltestrap/sveltestrap";
|
||||||
import PlotTable from './PlotTable.svelte'
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
|
import Filters from "./filters/Filters.svelte";
|
||||||
|
import JobList from "./joblist/JobList.svelte";
|
||||||
|
import Sorting from "./joblist/SortSelection.svelte";
|
||||||
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
|
import Histogram from "./plots/Histogram.svelte";
|
||||||
|
import MetricSelection from "./MetricSelection.svelte";
|
||||||
|
import HistogramSelection from "./HistogramSelection.svelte";
|
||||||
|
import PlotTable from "./PlotTable.svelte";
|
||||||
|
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
||||||
|
|
||||||
const { query: initq } = init()
|
const { query: initq } = init();
|
||||||
|
|
||||||
const ccconfig = getContext('cc-config')
|
const ccconfig = getContext("cc-config");
|
||||||
|
|
||||||
export let user
|
export let user;
|
||||||
export let filterPresets
|
export let filterPresets;
|
||||||
|
|
||||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||||
let jobList;
|
let jobList;
|
||||||
let jobFilters = [];
|
let jobFilters = [];
|
||||||
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false
|
let sorting = { field: "startTime", order: "DESC" },
|
||||||
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
|
isSortingOpen = false;
|
||||||
let w1, w2, histogramHeight = 250, isHistogramSelectionOpen = false
|
let metrics = ccconfig.plot_list_selectedMetrics,
|
||||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
|
isMetricsSelectionOpen = false;
|
||||||
|
let w1,
|
||||||
|
w2,
|
||||||
|
histogramHeight = 250,
|
||||||
|
isHistogramSelectionOpen = false;
|
||||||
|
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
||||||
let showFootprint = filterPresets.cluster
|
let showFootprint = filterPresets.cluster
|
||||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||||
: !!ccconfig.plot_list_showFootprint
|
: !!ccconfig.plot_list_showFootprint;
|
||||||
|
|
||||||
$: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
|
$: metricsInHistograms = selectedCluster
|
||||||
|
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []
|
||||||
|
: ccconfig.user_view_histogramMetrics || [];
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
$: stats = queryStore({
|
$: stats = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
|
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
|
||||||
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
|
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
|
||||||
totalJobs
|
totalJobs
|
||||||
shortJobs
|
shortJobs
|
||||||
totalWalltime
|
totalWalltime
|
||||||
totalCoreHours
|
totalCoreHours
|
||||||
histDuration { count, value }
|
histDuration {
|
||||||
histNumNodes { count, value }
|
count
|
||||||
histMetrics { metric, unit, data { min, max, count, bin } }
|
value
|
||||||
}}`,
|
}
|
||||||
variables: { jobFilters, metricsInHistograms }
|
histNumNodes {
|
||||||
})
|
count
|
||||||
|
value
|
||||||
|
}
|
||||||
|
histMetrics {
|
||||||
|
metric
|
||||||
|
unit
|
||||||
|
data {
|
||||||
|
min
|
||||||
|
max
|
||||||
|
count
|
||||||
|
bin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { jobFilters, metricsInHistograms },
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => filterComponent.update())
|
onMount(() => filterComponent.update());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
{#if $initq.fetching}
|
{#if $initq.fetching}
|
||||||
<Col>
|
<Col>
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
</Col>
|
</Col>
|
||||||
{:else if $initq.error}
|
{:else if $initq.error}
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
@ -65,40 +97,45 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Button
|
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
|
||||||
outline color="primary"
|
<Icon name="sort-up" /> Sorting
|
||||||
on:click={() => (isSortingOpen = true)}>
|
|
||||||
<Icon name="sort-up"/> Sorting
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
outline color="primary"
|
outline
|
||||||
on:click={() => (isMetricsSelectionOpen = true)}>
|
color="primary"
|
||||||
<Icon name="graph-up"/> Metrics
|
on:click={() => (isMetricsSelectionOpen = true)}
|
||||||
|
>
|
||||||
|
<Icon name="graph-up" /> Metrics
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
outline color="secondary"
|
outline
|
||||||
on:click={() => (isHistogramSelectionOpen = true)}>
|
color="secondary"
|
||||||
<Icon name="bar-chart-line"/> Select Histograms
|
on:click={() => (isHistogramSelectionOpen = true)}
|
||||||
|
>
|
||||||
|
<Icon name="bar-chart-line" /> Select Histograms
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Filters
|
<Filters
|
||||||
filterPresets={filterPresets}
|
{filterPresets}
|
||||||
startTimeQuickSelect={true}
|
startTimeQuickSelect={true}
|
||||||
bind:this={filterComponent}
|
bind:this={filterComponent}
|
||||||
on:update={({ detail }) => {
|
on:update={({ detail }) => {
|
||||||
jobFilters = [...detail.filters, { user: { eq: user.username } }]
|
jobFilters = [...detail.filters, { user: { eq: user.username } }];
|
||||||
selectedCluster = jobFilters[0]?.cluster ? jobFilters[0].cluster.eq : null
|
selectedCluster = jobFilters[0]?.cluster
|
||||||
jobList.update(jobFilters)
|
? jobFilters[0].cluster.eq
|
||||||
}} />
|
: null;
|
||||||
|
jobList.update(jobFilters);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto" style="margin-left: auto;">
|
<Col xs="auto" style="margin-left: auto;">
|
||||||
<Refresher on:reload={() => jobList.refresh()} />
|
<Refresher on:reload={() => jobList.refresh()} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br/>
|
<br />
|
||||||
<Row>
|
<Row>
|
||||||
{#if $stats.error}
|
{#if $stats.error}
|
||||||
<Col>
|
<Col>
|
||||||
@ -151,24 +188,28 @@
|
|||||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||||
width={w1 - 25} height={histogramHeight}
|
width={w1 - 25}
|
||||||
|
height={histogramHeight}
|
||||||
title="Duration Distribution"
|
title="Duration Distribution"
|
||||||
xlabel="Current Runtimes"
|
xlabel="Current Runtimes"
|
||||||
xunit="Hours"
|
xunit="Hours"
|
||||||
ylabel="Number of Jobs"
|
ylabel="Number of Jobs"
|
||||||
yunit="Jobs"/>
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 text-center" bind:clientWidth={w2}>
|
<div class="col-4 text-center" bind:clientWidth={w2}>
|
||||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||||
width={w2 - 25} height={histogramHeight}
|
width={w2 - 25}
|
||||||
|
height={histogramHeight}
|
||||||
title="Number of Nodes Distribution"
|
title="Number of Nodes Distribution"
|
||||||
xlabel="Allocated Nodes"
|
xlabel="Allocated Nodes"
|
||||||
xunit="Nodes"
|
xunit="Nodes"
|
||||||
ylabel="Number of Jobs"
|
ylabel="Number of Jobs"
|
||||||
yunit="Jobs"/>
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -191,47 +232,45 @@
|
|||||||
let:width
|
let:width
|
||||||
renderFor="user"
|
renderFor="user"
|
||||||
items={$stats.data.jobsStatistics[0].histMetrics}
|
items={$stats.data.jobsStatistics[0].histMetrics}
|
||||||
itemsPerRow={3}>
|
itemsPerRow={3}
|
||||||
|
>
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot(item.data)}
|
data={convert2uplot(item.data)}
|
||||||
usesBins={true}
|
usesBins={true}
|
||||||
width={width} height={250}
|
{width}
|
||||||
|
height={250}
|
||||||
title="Distribution of '{item.metric}' averages"
|
title="Distribution of '{item.metric}' averages"
|
||||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||||
xunit={item.unit}
|
xunit={item.unit}
|
||||||
ylabel="Number of Jobs"
|
ylabel="Number of Jobs"
|
||||||
yunit="Jobs"/>
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
</PlotTable>
|
</PlotTable>
|
||||||
{/key}
|
{/key}
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
</Row>
|
</Row>
|
||||||
{/if}
|
{/if}
|
||||||
<br/>
|
<br />
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<JobList
|
<JobList bind:metrics bind:sorting bind:this={jobList} bind:showFootprint />
|
||||||
bind:metrics={metrics}
|
|
||||||
bind:sorting={sorting}
|
|
||||||
bind:this={jobList}
|
|
||||||
bind:showFootprint={showFootprint} />
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Sorting
|
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
|
||||||
bind:sorting={sorting}
|
|
||||||
bind:isOpen={isSortingOpen} />
|
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:cluster={selectedCluster}
|
||||||
configName="plot_list_selectedMetrics"
|
configName="plot_list_selectedMetrics"
|
||||||
bind:metrics={metrics}
|
bind:metrics
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
bind:showFootprint={showFootprint}
|
bind:showFootprint
|
||||||
view='list'/>
|
view="list"
|
||||||
|
/>
|
||||||
|
|
||||||
<HistogramSelection
|
<HistogramSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:cluster={selectedCluster}
|
||||||
bind:metricsInHistograms={metricsInHistograms}
|
bind:metricsInHistograms
|
||||||
bind:isOpen={isHistogramSelectionOpen} />
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
|
/>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Icon, InputGroup, InputGroupText } from 'sveltestrap';
|
import { Icon, InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let timeseriesPlots;
|
export let timeseriesPlots;
|
||||||
|
|
||||||
@ -9,19 +9,18 @@
|
|||||||
function updatePlots() {
|
function updatePlots() {
|
||||||
let ws = windowSize / (100 * 2),
|
let ws = windowSize / (100 * 2),
|
||||||
wp = windowPosition / 100;
|
wp = windowPosition / 100;
|
||||||
let from = (wp - ws),
|
let from = wp - ws,
|
||||||
to = (wp + ws);
|
to = wp + ws;
|
||||||
Object
|
Object.values(timeseriesPlots).forEach((plot) =>
|
||||||
.values(timeseriesPlots)
|
plot.setTimeRange(from, to),
|
||||||
.forEach(plot => plot.setTimeRange(from, to));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rendering a big job can take a long time, so we
|
// Rendering a big job can take a long time, so we
|
||||||
// throttle the rerenders to every 100ms here.
|
// throttle the rerenders to every 100ms here.
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
function requestUpdatePlots() {
|
function requestUpdatePlots() {
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) window.cancelAnimationFrame(timeoutId);
|
||||||
window.cancelAnimationFrame(timeoutId);
|
|
||||||
|
|
||||||
timeoutId = window.requestAnimationFrame(() => {
|
timeoutId = window.requestAnimationFrame(() => {
|
||||||
updatePlots();
|
updatePlots();
|
||||||
@ -35,7 +34,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
<Icon name="zoom-in"/>
|
<Icon name="zoom-in" />
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
Window Size:
|
Window Size:
|
||||||
@ -43,7 +42,10 @@
|
|||||||
style="margin: 0em 0em 0em 1em"
|
style="margin: 0em 0em 0em 1em"
|
||||||
type="range"
|
type="range"
|
||||||
bind:value={windowSize}
|
bind:value={windowSize}
|
||||||
min=1 max=100 step=1 />
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
<span style="width: 5em;">
|
<span style="width: 5em;">
|
||||||
({windowSize}%)
|
({windowSize}%)
|
||||||
</span>
|
</span>
|
||||||
@ -54,7 +56,10 @@
|
|||||||
style="margin: 0em 0em 0em 1em"
|
style="margin: 0em 0em 0em 1em"
|
||||||
type="range"
|
type="range"
|
||||||
bind:value={windowPosition}
|
bind:value={windowPosition}
|
||||||
min=0 max=100 step=1 />
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,54 +1,53 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Row, Col } from 'sveltestrap'
|
import { Row, Col } from "@sveltestrap/sveltestrap";
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from "svelte";
|
||||||
import EditRole from './admin/EditRole.svelte'
|
import EditRole from "./admin/EditRole.svelte";
|
||||||
import EditProject from './admin/EditProject.svelte'
|
import EditProject from "./admin/EditProject.svelte";
|
||||||
import AddUser from './admin/AddUser.svelte'
|
import AddUser from "./admin/AddUser.svelte";
|
||||||
import ShowUsers from './admin/ShowUsers.svelte'
|
import ShowUsers from "./admin/ShowUsers.svelte";
|
||||||
import Options from './admin/Options.svelte'
|
import Options from "./admin/Options.svelte";
|
||||||
|
|
||||||
let users = []
|
let users = [];
|
||||||
let roles = []
|
let roles = [];
|
||||||
|
|
||||||
function getUserList() {
|
function getUserList() {
|
||||||
fetch('/api/users/?via-ldap=false¬-just-user=true')
|
fetch("/api/users/?via-ldap=false¬-just-user=true")
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(usersRaw => {
|
.then((usersRaw) => {
|
||||||
users = usersRaw
|
users = usersRaw;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValidRoles() {
|
function getValidRoles() {
|
||||||
fetch('/api/roles/')
|
fetch("/api/roles/")
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(rolesRaw => {
|
.then((rolesRaw) => {
|
||||||
roles = rolesRaw
|
roles = rolesRaw;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAdmin() {
|
function initAdmin() {
|
||||||
getUserList()
|
getUserList();
|
||||||
getValidRoles()
|
getValidRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => initAdmin())
|
onMount(() => initAdmin());
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={2} class="p-2 g-2" >
|
<Row cols={2} class="p-2 g-2">
|
||||||
<Col class="mb-1">
|
<Col class="mb-1">
|
||||||
<AddUser roles={roles} on:reload={getUserList}/>
|
<AddUser {roles} on:reload={getUserList} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-1">
|
<Col class="mb-1">
|
||||||
<ShowUsers on:reload={getUserList} bind:users={users}/>
|
<ShowUsers on:reload={getUserList} bind:users />
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<EditRole roles={roles} on:reload={getUserList}/>
|
<EditRole {roles} on:reload={getUserList} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<EditProject on:reload={getUserList}/>
|
<EditProject on:reload={getUserList} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Options/>
|
<Options />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,64 +1,300 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button, Table, Row, Col, Card, CardBody, CardTitle } from 'sveltestrap'
|
import {
|
||||||
import { fade } from 'svelte/transition'
|
Button,
|
||||||
|
Table,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
export let config
|
export let config;
|
||||||
|
|
||||||
let message = {msg: '', target: '', color: '#d63384'}
|
let message = { msg: "", target: "", color: "#d63384" };
|
||||||
let displayMessage = false
|
let displayMessage = false;
|
||||||
|
|
||||||
const colorschemes = {
|
const colorschemes = {
|
||||||
'Default': ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"],
|
Default: [
|
||||||
'Autumn': ['rgb(255,0,0)','rgb(255,11,0)','rgb(255,20,0)','rgb(255,30,0)','rgb(255,41,0)','rgb(255,50,0)','rgb(255,60,0)','rgb(255,71,0)','rgb(255,80,0)','rgb(255,90,0)','rgb(255,101,0)','rgb(255,111,0)','rgb(255,120,0)','rgb(255,131,0)','rgb(255,141,0)','rgb(255,150,0)','rgb(255,161,0)','rgb(255,171,0)','rgb(255,180,0)','rgb(255,190,0)','rgb(255,201,0)','rgb(255,210,0)','rgb(255,220,0)','rgb(255,231,0)','rgb(255,240,0)','rgb(255,250,0)'],
|
"#00bfff",
|
||||||
'Beach': ['rgb(0,252,0)','rgb(0,233,0)','rgb(0,212,0)','rgb(0,189,0)','rgb(0,169,0)','rgb(0,148,0)','rgb(0,129,4)','rgb(0,145,46)','rgb(0,162,90)','rgb(0,180,132)','rgb(29,143,136)','rgb(73,88,136)','rgb(115,32,136)','rgb(81,9,64)','rgb(124,51,23)','rgb(162,90,0)','rgb(194,132,0)','rgb(220,171,0)','rgb(231,213,0)','rgb(0,0,13)','rgb(0,0,55)','rgb(0,0,92)','rgb(0,0,127)','rgb(0,0,159)','rgb(0,0,196)','rgb(0,0,233)'],
|
"#0000ff",
|
||||||
'BlueRed': ['rgb(0,0,131)','rgb(0,0,168)','rgb(0,0,208)','rgb(0,0,247)','rgb(0,27,255)','rgb(0,67,255)','rgb(0,108,255)','rgb(0,148,255)','rgb(0,187,255)','rgb(0,227,255)','rgb(8,255,247)','rgb(48,255,208)','rgb(87,255,168)','rgb(127,255,127)','rgb(168,255,87)','rgb(208,255,48)','rgb(247,255,8)','rgb(255,224,0)','rgb(255,183,0)','rgb(255,143,0)','rgb(255,104,0)','rgb(255,64,0)','rgb(255,23,0)','rgb(238,0,0)','rgb(194,0,0)','rgb(150,0,0)'],
|
"#ff00ff",
|
||||||
'Rainbow': ['rgb(125,0,255)','rgb(85,0,255)','rgb(39,0,255)','rgb(0,6,255)','rgb(0,51,255)','rgb(0,97,255)','rgb(0,141,255)','rgb(0,187,255)','rgb(0,231,255)','rgb(0,255,233)','rgb(0,255,189)','rgb(0,255,143)','rgb(0,255,99)','rgb(0,255,53)','rgb(0,255,9)','rgb(37,255,0)','rgb(83,255,0)','rgb(127,255,0)','rgb(173,255,0)','rgb(217,255,0)','rgb(255,248,0)','rgb(255,203,0)','rgb(255,159,0)','rgb(255,113,0)','rgb(255,69,0)','rgb(255,23,0)'],
|
"#ff0000",
|
||||||
'Binary': ['rgb(215,215,215)','rgb(206,206,206)','rgb(196,196,196)','rgb(185,185,185)','rgb(176,176,176)','rgb(166,166,166)','rgb(155,155,155)','rgb(145,145,145)','rgb(136,136,136)','rgb(125,125,125)','rgb(115,115,115)','rgb(106,106,106)','rgb(95,95,95)','rgb(85,85,85)','rgb(76,76,76)','rgb(66,66,66)','rgb(55,55,55)','rgb(46,46,46)','rgb(36,36,36)','rgb(25,25,25)','rgb(16,16,16)','rgb(6,6,6)'],
|
"#ff8000",
|
||||||
'GistEarth': ['rgb(0,0,0)','rgb(2,7,117)','rgb(9,30,118)','rgb(16,53,120)','rgb(23,73,122)','rgb(31,93,124)','rgb(39,110,125)','rgb(47,126,127)','rgb(51,133,119)','rgb(57,138,106)','rgb(62,145,94)','rgb(66,150,82)','rgb(74,157,71)','rgb(97,162,77)','rgb(121,168,83)','rgb(136,173,85)','rgb(153,176,88)','rgb(170,180,92)','rgb(185,182,94)','rgb(189,173,99)','rgb(192,164,101)','rgb(203,169,124)','rgb(215,178,149)','rgb(226,192,176)','rgb(238,212,204)','rgb(248,236,236)'],
|
"#ffff00",
|
||||||
'BlueWaves': ['rgb(83,0,215)','rgb(43,6,108)','rgb(9,16,16)','rgb(8,32,25)','rgb(0,50,8)','rgb(27,64,66)','rgb(69,67,178)','rgb(115,62,210)','rgb(155,50,104)','rgb(178,43,41)','rgb(180,51,34)','rgb(161,78,87)','rgb(124,117,187)','rgb(78,155,203)','rgb(34,178,85)','rgb(4,176,2)','rgb(9,152,27)','rgb(4,118,2)','rgb(34,92,85)','rgb(78,92,203)','rgb(124,127,187)','rgb(161,187,87)','rgb(180,248,34)','rgb(178,220,41)','rgb(155,217,104)','rgb(115,254,210)'],
|
"#80ff00",
|
||||||
'BlueGreenRedYellow': ['rgb(0,0,0)','rgb(0,0,20)','rgb(0,0,41)','rgb(0,0,62)','rgb(0,25,83)','rgb(0,57,101)','rgb(0,87,101)','rgb(0,118,101)','rgb(0,150,101)','rgb(0,150,69)','rgb(0,148,37)','rgb(0,141,6)','rgb(60,120,0)','rgb(131,87,0)','rgb(180,25,0)','rgb(203,13,0)','rgb(208,36,0)','rgb(213,60,0)','rgb(219,83,0)','rgb(224,106,0)','rgb(229,129,0)','rgb(233,152,0)','rgb(238,176,0)','rgb(243,199,0)','rgb(248,222,0)','rgb(254,245,0)']
|
],
|
||||||
|
Autumn: [
|
||||||
|
"rgb(255,0,0)",
|
||||||
|
"rgb(255,11,0)",
|
||||||
|
"rgb(255,20,0)",
|
||||||
|
"rgb(255,30,0)",
|
||||||
|
"rgb(255,41,0)",
|
||||||
|
"rgb(255,50,0)",
|
||||||
|
"rgb(255,60,0)",
|
||||||
|
"rgb(255,71,0)",
|
||||||
|
"rgb(255,80,0)",
|
||||||
|
"rgb(255,90,0)",
|
||||||
|
"rgb(255,101,0)",
|
||||||
|
"rgb(255,111,0)",
|
||||||
|
"rgb(255,120,0)",
|
||||||
|
"rgb(255,131,0)",
|
||||||
|
"rgb(255,141,0)",
|
||||||
|
"rgb(255,150,0)",
|
||||||
|
"rgb(255,161,0)",
|
||||||
|
"rgb(255,171,0)",
|
||||||
|
"rgb(255,180,0)",
|
||||||
|
"rgb(255,190,0)",
|
||||||
|
"rgb(255,201,0)",
|
||||||
|
"rgb(255,210,0)",
|
||||||
|
"rgb(255,220,0)",
|
||||||
|
"rgb(255,231,0)",
|
||||||
|
"rgb(255,240,0)",
|
||||||
|
"rgb(255,250,0)",
|
||||||
|
],
|
||||||
|
Beach: [
|
||||||
|
"rgb(0,252,0)",
|
||||||
|
"rgb(0,233,0)",
|
||||||
|
"rgb(0,212,0)",
|
||||||
|
"rgb(0,189,0)",
|
||||||
|
"rgb(0,169,0)",
|
||||||
|
"rgb(0,148,0)",
|
||||||
|
"rgb(0,129,4)",
|
||||||
|
"rgb(0,145,46)",
|
||||||
|
"rgb(0,162,90)",
|
||||||
|
"rgb(0,180,132)",
|
||||||
|
"rgb(29,143,136)",
|
||||||
|
"rgb(73,88,136)",
|
||||||
|
"rgb(115,32,136)",
|
||||||
|
"rgb(81,9,64)",
|
||||||
|
"rgb(124,51,23)",
|
||||||
|
"rgb(162,90,0)",
|
||||||
|
"rgb(194,132,0)",
|
||||||
|
"rgb(220,171,0)",
|
||||||
|
"rgb(231,213,0)",
|
||||||
|
"rgb(0,0,13)",
|
||||||
|
"rgb(0,0,55)",
|
||||||
|
"rgb(0,0,92)",
|
||||||
|
"rgb(0,0,127)",
|
||||||
|
"rgb(0,0,159)",
|
||||||
|
"rgb(0,0,196)",
|
||||||
|
"rgb(0,0,233)",
|
||||||
|
],
|
||||||
|
BlueRed: [
|
||||||
|
"rgb(0,0,131)",
|
||||||
|
"rgb(0,0,168)",
|
||||||
|
"rgb(0,0,208)",
|
||||||
|
"rgb(0,0,247)",
|
||||||
|
"rgb(0,27,255)",
|
||||||
|
"rgb(0,67,255)",
|
||||||
|
"rgb(0,108,255)",
|
||||||
|
"rgb(0,148,255)",
|
||||||
|
"rgb(0,187,255)",
|
||||||
|
"rgb(0,227,255)",
|
||||||
|
"rgb(8,255,247)",
|
||||||
|
"rgb(48,255,208)",
|
||||||
|
"rgb(87,255,168)",
|
||||||
|
"rgb(127,255,127)",
|
||||||
|
"rgb(168,255,87)",
|
||||||
|
"rgb(208,255,48)",
|
||||||
|
"rgb(247,255,8)",
|
||||||
|
"rgb(255,224,0)",
|
||||||
|
"rgb(255,183,0)",
|
||||||
|
"rgb(255,143,0)",
|
||||||
|
"rgb(255,104,0)",
|
||||||
|
"rgb(255,64,0)",
|
||||||
|
"rgb(255,23,0)",
|
||||||
|
"rgb(238,0,0)",
|
||||||
|
"rgb(194,0,0)",
|
||||||
|
"rgb(150,0,0)",
|
||||||
|
],
|
||||||
|
Rainbow: [
|
||||||
|
"rgb(125,0,255)",
|
||||||
|
"rgb(85,0,255)",
|
||||||
|
"rgb(39,0,255)",
|
||||||
|
"rgb(0,6,255)",
|
||||||
|
"rgb(0,51,255)",
|
||||||
|
"rgb(0,97,255)",
|
||||||
|
"rgb(0,141,255)",
|
||||||
|
"rgb(0,187,255)",
|
||||||
|
"rgb(0,231,255)",
|
||||||
|
"rgb(0,255,233)",
|
||||||
|
"rgb(0,255,189)",
|
||||||
|
"rgb(0,255,143)",
|
||||||
|
"rgb(0,255,99)",
|
||||||
|
"rgb(0,255,53)",
|
||||||
|
"rgb(0,255,9)",
|
||||||
|
"rgb(37,255,0)",
|
||||||
|
"rgb(83,255,0)",
|
||||||
|
"rgb(127,255,0)",
|
||||||
|
"rgb(173,255,0)",
|
||||||
|
"rgb(217,255,0)",
|
||||||
|
"rgb(255,248,0)",
|
||||||
|
"rgb(255,203,0)",
|
||||||
|
"rgb(255,159,0)",
|
||||||
|
"rgb(255,113,0)",
|
||||||
|
"rgb(255,69,0)",
|
||||||
|
"rgb(255,23,0)",
|
||||||
|
],
|
||||||
|
Binary: [
|
||||||
|
"rgb(215,215,215)",
|
||||||
|
"rgb(206,206,206)",
|
||||||
|
"rgb(196,196,196)",
|
||||||
|
"rgb(185,185,185)",
|
||||||
|
"rgb(176,176,176)",
|
||||||
|
"rgb(166,166,166)",
|
||||||
|
"rgb(155,155,155)",
|
||||||
|
"rgb(145,145,145)",
|
||||||
|
"rgb(136,136,136)",
|
||||||
|
"rgb(125,125,125)",
|
||||||
|
"rgb(115,115,115)",
|
||||||
|
"rgb(106,106,106)",
|
||||||
|
"rgb(95,95,95)",
|
||||||
|
"rgb(85,85,85)",
|
||||||
|
"rgb(76,76,76)",
|
||||||
|
"rgb(66,66,66)",
|
||||||
|
"rgb(55,55,55)",
|
||||||
|
"rgb(46,46,46)",
|
||||||
|
"rgb(36,36,36)",
|
||||||
|
"rgb(25,25,25)",
|
||||||
|
"rgb(16,16,16)",
|
||||||
|
"rgb(6,6,6)",
|
||||||
|
],
|
||||||
|
GistEarth: [
|
||||||
|
"rgb(0,0,0)",
|
||||||
|
"rgb(2,7,117)",
|
||||||
|
"rgb(9,30,118)",
|
||||||
|
"rgb(16,53,120)",
|
||||||
|
"rgb(23,73,122)",
|
||||||
|
"rgb(31,93,124)",
|
||||||
|
"rgb(39,110,125)",
|
||||||
|
"rgb(47,126,127)",
|
||||||
|
"rgb(51,133,119)",
|
||||||
|
"rgb(57,138,106)",
|
||||||
|
"rgb(62,145,94)",
|
||||||
|
"rgb(66,150,82)",
|
||||||
|
"rgb(74,157,71)",
|
||||||
|
"rgb(97,162,77)",
|
||||||
|
"rgb(121,168,83)",
|
||||||
|
"rgb(136,173,85)",
|
||||||
|
"rgb(153,176,88)",
|
||||||
|
"rgb(170,180,92)",
|
||||||
|
"rgb(185,182,94)",
|
||||||
|
"rgb(189,173,99)",
|
||||||
|
"rgb(192,164,101)",
|
||||||
|
"rgb(203,169,124)",
|
||||||
|
"rgb(215,178,149)",
|
||||||
|
"rgb(226,192,176)",
|
||||||
|
"rgb(238,212,204)",
|
||||||
|
"rgb(248,236,236)",
|
||||||
|
],
|
||||||
|
BlueWaves: [
|
||||||
|
"rgb(83,0,215)",
|
||||||
|
"rgb(43,6,108)",
|
||||||
|
"rgb(9,16,16)",
|
||||||
|
"rgb(8,32,25)",
|
||||||
|
"rgb(0,50,8)",
|
||||||
|
"rgb(27,64,66)",
|
||||||
|
"rgb(69,67,178)",
|
||||||
|
"rgb(115,62,210)",
|
||||||
|
"rgb(155,50,104)",
|
||||||
|
"rgb(178,43,41)",
|
||||||
|
"rgb(180,51,34)",
|
||||||
|
"rgb(161,78,87)",
|
||||||
|
"rgb(124,117,187)",
|
||||||
|
"rgb(78,155,203)",
|
||||||
|
"rgb(34,178,85)",
|
||||||
|
"rgb(4,176,2)",
|
||||||
|
"rgb(9,152,27)",
|
||||||
|
"rgb(4,118,2)",
|
||||||
|
"rgb(34,92,85)",
|
||||||
|
"rgb(78,92,203)",
|
||||||
|
"rgb(124,127,187)",
|
||||||
|
"rgb(161,187,87)",
|
||||||
|
"rgb(180,248,34)",
|
||||||
|
"rgb(178,220,41)",
|
||||||
|
"rgb(155,217,104)",
|
||||||
|
"rgb(115,254,210)",
|
||||||
|
],
|
||||||
|
BlueGreenRedYellow: [
|
||||||
|
"rgb(0,0,0)",
|
||||||
|
"rgb(0,0,20)",
|
||||||
|
"rgb(0,0,41)",
|
||||||
|
"rgb(0,0,62)",
|
||||||
|
"rgb(0,25,83)",
|
||||||
|
"rgb(0,57,101)",
|
||||||
|
"rgb(0,87,101)",
|
||||||
|
"rgb(0,118,101)",
|
||||||
|
"rgb(0,150,101)",
|
||||||
|
"rgb(0,150,69)",
|
||||||
|
"rgb(0,148,37)",
|
||||||
|
"rgb(0,141,6)",
|
||||||
|
"rgb(60,120,0)",
|
||||||
|
"rgb(131,87,0)",
|
||||||
|
"rgb(180,25,0)",
|
||||||
|
"rgb(203,13,0)",
|
||||||
|
"rgb(208,36,0)",
|
||||||
|
"rgb(213,60,0)",
|
||||||
|
"rgb(219,83,0)",
|
||||||
|
"rgb(224,106,0)",
|
||||||
|
"rgb(229,129,0)",
|
||||||
|
"rgb(233,152,0)",
|
||||||
|
"rgb(238,176,0)",
|
||||||
|
"rgb(243,199,0)",
|
||||||
|
"rgb(248,222,0)",
|
||||||
|
"rgb(254,245,0)",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleSettingSubmit(selector, target) {
|
async function handleSettingSubmit(selector, target) {
|
||||||
let form = document.querySelector(selector)
|
let form = document.querySelector(selector);
|
||||||
let formData = new FormData(form)
|
let formData = new FormData(form);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(form.action, { method: 'POST', body: formData });
|
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
popMessage(text, target, '#048109')
|
popMessage(text, target, "#048109");
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
// console.log(res.statusText)
|
// console.log(res.statusText)
|
||||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
popMessage(err, target, '#d63384')
|
popMessage(err, target, "#d63384");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function popMessage(response, restarget, rescolor) {
|
function popMessage(response, restarget, rescolor) {
|
||||||
message = {msg: response, target: restarget, color: rescolor}
|
message = { msg: response, target: restarget, color: rescolor };
|
||||||
displayMessage = true
|
displayMessage = true;
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
displayMessage = false
|
displayMessage = false;
|
||||||
}, 3500)
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={3} class="p-2 g-2">
|
<Row cols={3} class="p-2 g-2">
|
||||||
<!-- LINE WIDTH -->
|
<!-- LINE WIDTH -->
|
||||||
<Col><Card class="h-100">
|
<Col
|
||||||
|
><Card class="h-100">
|
||||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
||||||
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#line-width-form', 'lw')}>
|
<form
|
||||||
|
id="line-width-form"
|
||||||
|
method="post"
|
||||||
|
action="/api/configuration/"
|
||||||
|
class="card-body"
|
||||||
|
on:submit|preventDefault={() =>
|
||||||
|
handleSettingSubmit("#line-width-form", "lw")}
|
||||||
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
<CardTitle
|
||||||
|
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||||
|
>
|
||||||
<div>Line Width</div>
|
<div>Line Width</div>
|
||||||
<!-- Expand If-Clause for clarity once -->
|
<!-- Expand If-Clause for clarity once -->
|
||||||
{#if displayMessage && message.target == 'lw'}
|
{#if displayMessage && message.target == "lw"}
|
||||||
<div style="margin-left: auto; font-size: 0.9em;">
|
<div style="margin-left: auto; font-size: 0.9em;">
|
||||||
<code style="color: {message.color};" out:fade>
|
<code style="color: {message.color};" out:fade>
|
||||||
Update: {message.msg}
|
Update: {message.msg}
|
||||||
@ -66,47 +302,102 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<input type="hidden" name="key" value="plot_general_lineWidth"/>
|
<input type="hidden" name="key" value="plot_general_lineWidth" />
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="value" class="form-label">Line Width</label>
|
<label for="value" class="form-label">Line Width</label>
|
||||||
<input type="number" class="form-control" id="lwvalue" name="value" aria-describedby="lineWidthHelp" value="{config.plot_general_lineWidth}" min="1"/>
|
<input
|
||||||
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div>
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="lwvalue"
|
||||||
|
name="value"
|
||||||
|
aria-describedby="lineWidthHelp"
|
||||||
|
value={config.plot_general_lineWidth}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<div id="lineWidthHelp" class="form-text">
|
||||||
|
Width of the lines in the timeseries plots.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button color="primary" type="submit">Submit</Button>
|
<Button color="primary" type="submit">Submit</Button>
|
||||||
</form>
|
</form>
|
||||||
</Card></Col>
|
</Card></Col
|
||||||
|
>
|
||||||
|
|
||||||
<!-- PLOTS PER ROW -->
|
<!-- PLOTS PER ROW -->
|
||||||
<Col><Card class="h-100">
|
<Col
|
||||||
<form id="plots-per-row-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#plots-per-row-form', 'ppr')}>
|
><Card class="h-100">
|
||||||
|
<form
|
||||||
|
id="plots-per-row-form"
|
||||||
|
method="post"
|
||||||
|
action="/api/configuration/"
|
||||||
|
class="card-body"
|
||||||
|
on:submit|preventDefault={() =>
|
||||||
|
handleSettingSubmit("#plots-per-row-form", "ppr")}
|
||||||
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
<CardTitle
|
||||||
|
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||||
|
>
|
||||||
<div>Plots per Row</div>
|
<div>Plots per Row</div>
|
||||||
{#if displayMessage && message.target == 'ppr'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
{#if displayMessage && message.target == "ppr"}<div
|
||||||
|
style="margin-left: auto; font-size: 0.9em;"
|
||||||
|
>
|
||||||
|
<code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
>
|
||||||
|
</div>{/if}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<input type="hidden" name="key" value="plot_view_plotsPerRow"/>
|
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="value" class="form-label">Plots per Row</label>
|
<label for="value" class="form-label">Plots per Row</label>
|
||||||
<input type="number" class="form-control" id="pprvalue" name="value" aria-describedby="plotsperrowHelp" value="{config.plot_view_plotsPerRow }" min="1"/>
|
<input
|
||||||
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div>
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="pprvalue"
|
||||||
|
name="value"
|
||||||
|
aria-describedby="plotsperrowHelp"
|
||||||
|
value={config.plot_view_plotsPerRow}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<div id="plotsperrowHelp" class="form-text">
|
||||||
|
How many plots to show next to each other on pages such as
|
||||||
|
/monitoring/job/, /monitoring/system/...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button color="primary" type="submit">Submit</Button>
|
<Button color="primary" type="submit">Submit</Button>
|
||||||
</form>
|
</form>
|
||||||
</Card></Col>
|
</Card></Col
|
||||||
|
>
|
||||||
|
|
||||||
<!-- BACKGROUND -->
|
<!-- BACKGROUND -->
|
||||||
<Col><Card class="h-100">
|
<Col
|
||||||
<form id="backgrounds-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#backgrounds-form', 'bg')}>
|
><Card class="h-100">
|
||||||
|
<form
|
||||||
|
id="backgrounds-form"
|
||||||
|
method="post"
|
||||||
|
action="/api/configuration/"
|
||||||
|
class="card-body"
|
||||||
|
on:submit|preventDefault={() =>
|
||||||
|
handleSettingSubmit("#backgrounds-form", "bg")}
|
||||||
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
<CardTitle
|
||||||
|
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||||
|
>
|
||||||
<div>Colored Backgrounds</div>
|
<div>Colored Backgrounds</div>
|
||||||
{#if displayMessage && message.target == 'bg'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
{#if displayMessage && message.target == "bg"}<div
|
||||||
|
style="margin-left: auto; font-size: 0.9em;"
|
||||||
|
>
|
||||||
|
<code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
>
|
||||||
|
</div>{/if}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<input type="hidden" name="key" value="plot_general_colorBackground"/>
|
<input type="hidden" name="key" value="plot_general_colorBackground" />
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div>
|
<div>
|
||||||
{#if config.plot_general_colorBackground}
|
{#if config.plot_general_colorBackground}
|
||||||
<input type="radio" id="true" name="value" value="true" checked/>
|
<input type="radio" id="true" name="value" value="true" checked />
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" id="true" name="value" value="true" />
|
<input type="radio" id="true" name="value" value="true" />
|
||||||
{/if}
|
{/if}
|
||||||
@ -116,41 +407,76 @@
|
|||||||
{#if config.plot_general_colorBackground}
|
{#if config.plot_general_colorBackground}
|
||||||
<input type="radio" id="false" name="value" value="false" />
|
<input type="radio" id="false" name="value" value="false" />
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" id="false" name="value" value="false" checked/>
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="false"
|
||||||
|
name="value"
|
||||||
|
value="false"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<label for="false">No</label>
|
<label for="false">No</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button color="primary" type="submit">Submit</Button>
|
<Button color="primary" type="submit">Submit</Button>
|
||||||
</form>
|
</form>
|
||||||
</Card></Col>
|
</Card></Col
|
||||||
|
>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row cols={1} class="p-2 g-2">
|
<Row cols={1} class="p-2 g-2">
|
||||||
<!-- COLORSCHEME -->
|
<!-- COLORSCHEME -->
|
||||||
<Col><Card>
|
<Col
|
||||||
<form id="colorscheme-form" method="post" action="/api/configuration/" class="card-body">
|
><Card>
|
||||||
|
<form
|
||||||
|
id="colorscheme-form"
|
||||||
|
method="post"
|
||||||
|
action="/api/configuration/"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
<CardTitle
|
||||||
|
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||||
|
>
|
||||||
<div>Color Scheme for Timeseries Plots</div>
|
<div>Color Scheme for Timeseries Plots</div>
|
||||||
{#if displayMessage && message.target == 'cs'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
{#if displayMessage && message.target == "cs"}<div
|
||||||
|
style="margin-left: auto; font-size: 0.9em;"
|
||||||
|
>
|
||||||
|
<code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
>
|
||||||
|
</div>{/if}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<input type="hidden" name="key" value="plot_general_colorscheme"/>
|
<input type="hidden" name="key" value="plot_general_colorscheme" />
|
||||||
<Table hover>
|
<Table hover>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{name}</th>
|
<th scope="col">{name}</th>
|
||||||
<td>
|
<td>
|
||||||
{#if rgbrow.join(',') == config.plot_general_colorscheme}
|
{#if rgbrow.join(",") == config.plot_general_colorscheme}
|
||||||
<input type="radio" name="value" value={JSON.stringify(rgbrow)} checked on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="value"
|
||||||
|
value={JSON.stringify(rgbrow)}
|
||||||
|
checked
|
||||||
|
on:click={() =>
|
||||||
|
handleSettingSubmit("#colorscheme-form", "cs")}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" name="value" value={JSON.stringify(rgbrow)} on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="value"
|
||||||
|
value={JSON.stringify(rgbrow)}
|
||||||
|
on:click={() =>
|
||||||
|
handleSettingSubmit("#colorscheme-form", "cs")}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{#each rgbrow as rgb}
|
{#each rgbrow as rgb}
|
||||||
<span class="color-dot" style="background-color: {rgb};"></span>
|
<span class="color-dot" style="background-color: {rgb};"
|
||||||
|
></span>
|
||||||
{/each}
|
{/each}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -158,7 +484,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</form>
|
</form>
|
||||||
</Card></Col>
|
</Card></Col
|
||||||
|
>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,103 +1,156 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button, Card, CardTitle } from 'sveltestrap'
|
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let message = {msg: '', color: '#d63384'}
|
let message = { msg: "", color: "#d63384" };
|
||||||
let displayMessage = false
|
let displayMessage = false;
|
||||||
|
|
||||||
export let roles = []
|
export let roles = [];
|
||||||
|
|
||||||
async function handleUserSubmit() {
|
async function handleUserSubmit() {
|
||||||
let form = document.querySelector('#create-user-form')
|
let form = document.querySelector("#create-user-form");
|
||||||
let formData = new FormData(form)
|
let formData = new FormData(form);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(form.action, { method: 'POST', body: formData });
|
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
popMessage(text, '#048109')
|
popMessage(text, "#048109");
|
||||||
reloadUserList()
|
reloadUserList();
|
||||||
form.reset()
|
form.reset();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
// console.log(res.statusText)
|
// console.log(res.statusText)
|
||||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
popMessage(err, '#d63384')
|
popMessage(err, "#d63384");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function popMessage(response, rescolor) {
|
function popMessage(response, rescolor) {
|
||||||
message = {msg: response, color: rescolor}
|
message = { msg: response, color: rescolor };
|
||||||
displayMessage = true
|
displayMessage = true;
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
displayMessage = false
|
displayMessage = false;
|
||||||
}, 3500)
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadUserList() {
|
function reloadUserList() {
|
||||||
dispatch('reload')
|
dispatch("reload");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}>
|
<form
|
||||||
|
id="create-user-form"
|
||||||
|
method="post"
|
||||||
|
action="/api/users/"
|
||||||
|
class="card-body"
|
||||||
|
on:submit|preventDefault={handleUserSubmit}
|
||||||
|
>
|
||||||
<CardTitle class="mb-3">Create User</CardTitle>
|
<CardTitle class="mb-3">Create User</CardTitle>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Username (ID)</label>
|
<label for="username" class="form-label">Username (ID)</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
aria-describedby="usernameHelp"
|
||||||
|
/>
|
||||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Password</label>
|
<label for="password" class="form-label">Password</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
|
<input
|
||||||
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
aria-describedby="passwordHelp"
|
||||||
|
/>
|
||||||
|
<div id="passwordHelp" class="form-text">
|
||||||
|
Only API users are allowed to have a blank password. Users with a blank
|
||||||
|
password can only authenticate via Tokens.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Project</label>
|
<label for="name" class="form-label">Project</label>
|
||||||
<input type="text" class="form-control" id="project" name="project" aria-describedby="projectHelp"/>
|
<input
|
||||||
<div id="projectHelp" class="form-text">Only Manager users can have a project. Allows to inspect jobs and users of given project.</div>
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="project"
|
||||||
|
name="project"
|
||||||
|
aria-describedby="projectHelp"
|
||||||
|
/>
|
||||||
|
<div id="projectHelp" class="form-text">
|
||||||
|
Only Manager users can have a project. Allows to inspect jobs and users
|
||||||
|
of given project.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
aria-describedby="nameHelp"
|
||||||
|
/>
|
||||||
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email address</label>
|
<label for="email" class="form-label">Email address</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
aria-describedby="emailHelp"
|
||||||
|
/>
|
||||||
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p>Role:</p>
|
<p>Role:</p>
|
||||||
{#each roles as role, i}
|
{#each roles as role, i}
|
||||||
{#if i == 0}
|
{#if i == 0}
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id={role} name="role" value={role} checked/>
|
<input type="radio" id={role} name="role" value={role} checked />
|
||||||
<label for={role}>{role.toUpperCase()} (Allowed to interact with REST API.)</label>
|
<label for={role}
|
||||||
|
>{role.toUpperCase()} (Allowed to interact with REST API.)</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else if i == 1}
|
{:else if i == 1}
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id={role} name="role" value={role} checked/>
|
<input type="radio" id={role} name="role" value={role} checked />
|
||||||
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created via LDAP sync.)</label>
|
<label for={role}
|
||||||
|
>{role.charAt(0).toUpperCase() + role.slice(1)} (Same as if created
|
||||||
|
via LDAP sync.)</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id={role} name="role" value={role}/>
|
<input type="radio" id={role} name="role" value={role} />
|
||||||
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label>
|
<label for={role}
|
||||||
|
>{role.charAt(0).toUpperCase() + role.slice(1)}</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<p style="display: flex; align-items: center;">
|
<p style="display: flex; align-items: center;">
|
||||||
<Button type="submit" color="primary">Submit</Button>
|
<Button type="submit" color="primary">Submit</Button>
|
||||||
{#if displayMessage}<div style="margin-left: 1.5em;"><b><code style="color: {message.color};" out:fade>{message.msg}</code></b></div>{/if}
|
{#if displayMessage}<div style="margin-left: 1.5em;">
|
||||||
|
<b
|
||||||
|
><code style="color: {message.color};" out:fade>{message.msg}</code
|
||||||
|
></b
|
||||||
|
>
|
||||||
|
</div>{/if}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,97 +1,129 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Card, CardTitle, CardBody } from 'sveltestrap'
|
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let message = {msg: '', color: '#d63384'}
|
let message = { msg: "", color: "#d63384" };
|
||||||
let displayMessage = false
|
let displayMessage = false;
|
||||||
|
|
||||||
async function handleAddProject() {
|
async function handleAddProject() {
|
||||||
const username = document.querySelector('#project-username').value
|
const username = document.querySelector("#project-username").value;
|
||||||
const project = document.querySelector('#project-id').value
|
const project = document.querySelector("#project-id").value;
|
||||||
|
|
||||||
if (username == "" || project == "") {
|
if (username == "" || project == "") {
|
||||||
alert('Please fill in a username and select a project.')
|
alert("Please fill in a username and select a project.");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let formData = new FormData()
|
let formData = new FormData();
|
||||||
formData.append('username', username)
|
formData.append("username", username);
|
||||||
formData.append('add-project', project)
|
formData.append("add-project", project);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
const res = await fetch(`/api/user/${username}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
popMessage(text, '#048109')
|
popMessage(text, "#048109");
|
||||||
reloadUserList()
|
reloadUserList();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
// console.log(res.statusText)
|
// console.log(res.statusText)
|
||||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
popMessage(err, '#d63384')
|
popMessage(err, "#d63384");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveProject() {
|
async function handleRemoveProject() {
|
||||||
const username = document.querySelector('#project-username').value
|
const username = document.querySelector("#project-username").value;
|
||||||
const project = document.querySelector('#project-id').value
|
const project = document.querySelector("#project-id").value;
|
||||||
|
|
||||||
if (username == "" || project == "") {
|
if (username == "" || project == "") {
|
||||||
alert('Please fill in a username and select a project.')
|
alert("Please fill in a username and select a project.");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let formData = new FormData()
|
let formData = new FormData();
|
||||||
formData.append('username', username)
|
formData.append("username", username);
|
||||||
formData.append('remove-project', project)
|
formData.append("remove-project", project);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
const res = await fetch(`/api/user/${username}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
popMessage(text, '#048109')
|
popMessage(text, "#048109");
|
||||||
reloadUserList()
|
reloadUserList();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
// console.log(res.statusText)
|
// console.log(res.statusText)
|
||||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
popMessage(err, '#d63384')
|
popMessage(err, "#d63384");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function popMessage(response, rescolor) {
|
function popMessage(response, rescolor) {
|
||||||
message = {msg: response, color: rescolor}
|
message = { msg: response, color: rescolor };
|
||||||
displayMessage = true
|
displayMessage = true;
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
displayMessage = false
|
displayMessage = false;
|
||||||
}, 3500)
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadUserList() {
|
function reloadUserList() {
|
||||||
dispatch('reload')
|
dispatch("reload");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<CardTitle class="mb-3">Edit Project Managed By User (Manager Only)</CardTitle>
|
<CardTitle class="mb-3"
|
||||||
|
>Edit Project Managed By User (Manager Only)</CardTitle
|
||||||
|
>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control" placeholder="username" id="project-username"/>
|
<input
|
||||||
<input type="text" class="form-control" placeholder="project-id" id="project-id"/>
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username"
|
||||||
|
id="project-username"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="project-id"
|
||||||
|
id="project-id"
|
||||||
|
/>
|
||||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||||
<button class="btn btn-primary" type="button" id="add-project-button" on:click|preventDefault={handleAddProject}>Add</button>
|
<button
|
||||||
<button class="btn btn-danger" type="button" id="remove-project-button" on:click|preventDefault={handleRemoveProject}>Remove</button>
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
id="add-project-button"
|
||||||
|
on:click|preventDefault={handleAddProject}>Add</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger"
|
||||||
|
type="button"
|
||||||
|
id="remove-project-button"
|
||||||
|
on:click|preventDefault={handleRemoveProject}>Remove</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
|
{#if displayMessage}<b
|
||||||
|
><code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
></b
|
||||||
|
>{/if}
|
||||||
</p>
|
</p>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,83 +1,89 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Card, CardTitle, CardBody } from 'sveltestrap'
|
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let message = {msg: '', color: '#d63384'}
|
let message = { msg: "", color: "#d63384" };
|
||||||
let displayMessage = false
|
let displayMessage = false;
|
||||||
|
|
||||||
export let roles = []
|
export let roles = [];
|
||||||
|
|
||||||
async function handleAddRole() {
|
async function handleAddRole() {
|
||||||
const username = document.querySelector('#role-username').value
|
const username = document.querySelector("#role-username").value;
|
||||||
const role = document.querySelector('#role-select').value
|
const role = document.querySelector("#role-select").value;
|
||||||
|
|
||||||
if (username == "" || role == "") {
|
if (username == "" || role == "") {
|
||||||
alert('Please fill in a username and select a role.')
|
alert("Please fill in a username and select a role.");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let formData = new FormData()
|
let formData = new FormData();
|
||||||
formData.append('username', username)
|
formData.append("username", username);
|
||||||
formData.append('add-role', role)
|
formData.append("add-role", role);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
const res = await fetch(`/api/user/${username}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
popMessage(text, '#048109')
|
popMessage(text, "#048109");
|
||||||
reloadUserList()
|
reloadUserList();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
// console.log(res.statusText)
|
// console.log(res.statusText)
|
||||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
popMessage(err, '#d63384')
|
popMessage(err, "#d63384");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveRole() {
|
async function handleRemoveRole() {
|
||||||
const username = document.querySelector('#role-username').value
|
const username = document.querySelector("#role-username").value;
|
||||||
const role = document.querySelector('#role-select').value
|
const role = document.querySelector("#role-select").value;
|
||||||
|
|
||||||
if (username == "" || role == "") {
|
if (username == "" || role == "") {
|
||||||
alert('Please fill in a username and select a role.')
|
alert("Please fill in a username and select a role.");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let formData = new FormData()
|
let formData = new FormData();
|
||||||
formData.append('username', username)
|
formData.append("username", username);
|
||||||
formData.append('remove-role', role)
|
formData.append("remove-role", role);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
const res = await fetch(`/api/user/${username}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
popMessage(text, '#048109')
|
popMessage(text, "#048109");
|
||||||
reloadUserList()
|
reloadUserList();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text()
|
let text = await res.text();
|
||||||
// console.log(res.statusText)
|
// console.log(res.statusText)
|
||||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
popMessage(err, '#d63384')
|
popMessage(err, "#d63384");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function popMessage(response, rescolor) {
|
function popMessage(response, rescolor) {
|
||||||
message = {msg: response, color: rescolor}
|
message = { msg: response, color: rescolor };
|
||||||
displayMessage = true
|
displayMessage = true;
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
displayMessage = false
|
displayMessage = false;
|
||||||
}, 3500)
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadUserList() {
|
function reloadUserList() {
|
||||||
dispatch('reload')
|
dispatch("reload");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -85,20 +91,41 @@
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<CardTitle class="mb-3">Edit User Roles</CardTitle>
|
<CardTitle class="mb-3">Edit User Roles</CardTitle>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control" placeholder="username" id="role-username"/>
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username"
|
||||||
|
id="role-username"
|
||||||
|
/>
|
||||||
<select class="form-select" id="role-select">
|
<select class="form-select" id="role-select">
|
||||||
<option selected value="">Role...</option>
|
<option selected value="">Role...</option>
|
||||||
{#each roles as role}
|
{#each roles as role}
|
||||||
<option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option>
|
<option value={role}
|
||||||
|
>{role.charAt(0).toUpperCase() + role.slice(1)}</option
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||||
<button class="btn btn-primary" type="button" id="add-role-button" on:click|preventDefault={handleAddRole}>Add</button>
|
<button
|
||||||
<button class="btn btn-danger" type="button" id="remove-role-button" on:click|preventDefault={handleRemoveRole}>Remove</button>
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
id="add-role-button"
|
||||||
|
on:click|preventDefault={handleAddRole}>Add</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger"
|
||||||
|
type="button"
|
||||||
|
id="remove-role-button"
|
||||||
|
on:click|preventDefault={handleRemoveRole}>Remove</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
|
{#if displayMessage}<b
|
||||||
|
><code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
></b
|
||||||
|
>{/if}
|
||||||
</p>
|
</p>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,29 +1,34 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from "svelte";
|
||||||
import { Card, CardBody, CardTitle } from 'sveltestrap'
|
import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
let scrambled
|
let scrambled;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
scrambled = window.localStorage.getItem("cc-scramble-names") != null
|
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
|
||||||
})
|
});
|
||||||
|
|
||||||
function handleScramble() {
|
function handleScramble() {
|
||||||
if (!scrambled) {
|
if (!scrambled) {
|
||||||
scrambled = true
|
scrambled = true;
|
||||||
window.localStorage.setItem("cc-scramble-names", "true")
|
window.localStorage.setItem("cc-scramble-names", "true");
|
||||||
} else {
|
} else {
|
||||||
scrambled = false
|
scrambled = false;
|
||||||
window.localStorage.removeItem("cc-scramble-names")
|
window.localStorage.removeItem("cc-scramble-names");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="h-100">
|
<Card class="h-100">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
||||||
<input type="checkbox" id="scramble-names-checkbox" style="margin-right: 1em;" on:click={handleScramble} bind:checked={scrambled}/>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="scramble-names-checkbox"
|
||||||
|
style="margin-right: 1em;"
|
||||||
|
on:click={handleScramble}
|
||||||
|
bind:checked={scrambled}
|
||||||
|
/>
|
||||||
Active?
|
Active?
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,39 +1,51 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button, Table, Card, CardTitle, CardBody } from 'sveltestrap'
|
import {
|
||||||
import { onMount, createEventDispatcher } from "svelte";
|
Button,
|
||||||
import ShowUsersRow from './ShowUsersRow.svelte'
|
Table,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
CardBody,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import ShowUsersRow from "./ShowUsersRow.svelte";
|
||||||
|
|
||||||
export let users = []
|
export let users = [];
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
function reloadUserList() {
|
function reloadUserList() {
|
||||||
dispatch('reload')
|
dispatch("reload");
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteUser(username) {
|
function deleteUser(username) {
|
||||||
if (confirm('Are you sure?')) {
|
if (confirm("Are you sure?")) {
|
||||||
let formData = new FormData()
|
let formData = new FormData();
|
||||||
formData.append('username', username)
|
formData.append("username", username);
|
||||||
fetch('/api/users/', { method: 'DELETE', body: formData }).then(res => {
|
fetch("/api/users/", { method: "DELETE", body: formData }).then((res) => {
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
reloadUserList()
|
reloadUserList();
|
||||||
} else {
|
} else {
|
||||||
confirm(res.statusText)
|
confirm(res.statusText);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: userList = users
|
$: userList = users;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="h-100">
|
<Card class="h-100">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<CardTitle class="mb-3">Special Users</CardTitle>
|
<CardTitle class="mb-3">Special Users</CardTitle>
|
||||||
<p>
|
<p>
|
||||||
Not created by an LDAP sync and/or having a role other than <code>user</code>
|
Not created by an LDAP sync and/or having a role other than <code
|
||||||
<Button color="secondary" size="sm" on:click={reloadUserList} style="float: right;">Reload</Button>
|
>user</code
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
on:click={reloadUserList}
|
||||||
|
style="float: right;">Reload</Button
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
||||||
<Table hover>
|
<Table hover>
|
||||||
@ -51,13 +63,20 @@
|
|||||||
<tbody id="users-list">
|
<tbody id="users-list">
|
||||||
{#each userList as user}
|
{#each userList as user}
|
||||||
<tr id="user-{user.username}">
|
<tr id="user-{user.username}">
|
||||||
<ShowUsersRow {user}/>
|
<ShowUsersRow {user} />
|
||||||
<td><button class="btn btn-danger del-user" on:click={deleteUser(user.username)}>Delete</button></td>
|
<td
|
||||||
|
><button
|
||||||
|
class="btn btn-danger del-user"
|
||||||
|
on:click={deleteUser(user.username)}>Delete</button
|
||||||
|
></td
|
||||||
|
>
|
||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button } from 'sveltestrap'
|
import { Button } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let user
|
export let user;
|
||||||
let jwt = ""
|
let jwt = "";
|
||||||
|
|
||||||
function getUserJwt(username) {
|
function getUserJwt(username) {
|
||||||
fetch(`/api/jwt/?username=${username}`)
|
fetch(`/api/jwt/?username=${username}`)
|
||||||
.then(res => res.text())
|
.then((res) => res.text())
|
||||||
.then(text => {
|
.then((text) => {
|
||||||
jwt = text
|
jwt = text;
|
||||||
navigator.clipboard.writeText(text).catch(reason => console.error(reason))
|
navigator.clipboard
|
||||||
})
|
.writeText(text)
|
||||||
|
.catch((reason) => console.error(reason));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -18,10 +20,12 @@
|
|||||||
<td>{user.name}</td>
|
<td>{user.name}</td>
|
||||||
<td>{user.projects}</td>
|
<td>{user.projects}</td>
|
||||||
<td>{user.email}</td>
|
<td>{user.email}</td>
|
||||||
<td><code>{user.roles.join(', ')}</code></td>
|
<td><code>{user.roles.join(", ")}</code></td>
|
||||||
<td>
|
<td>
|
||||||
{#if ! jwt}
|
{#if !jwt}
|
||||||
<Button color="success" on:click={getUserJwt(user.username)}>Gen. JWT</Button>
|
<Button color="success" on:click={getUserJwt(user.username)}
|
||||||
|
>Gen. JWT</Button
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<textarea rows="3" cols="20">{jwt}</textarea>
|
<textarea rows="3" cols="20">{jwt}</textarea>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from 'svelte'
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import { Button, ListGroup, ListGroupItem,
|
import {
|
||||||
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
Button,
|
||||||
|
ListGroup,
|
||||||
|
ListGroupItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const clusters = getContext('clusters'),
|
const clusters = getContext("clusters"),
|
||||||
initialized = getContext('initialized'),
|
initialized = getContext("initialized"),
|
||||||
dispatch = createEventDispatcher()
|
dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let disableClusterSelection = false
|
export let disableClusterSelection = false;
|
||||||
export let isModified = false
|
export let isModified = false;
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let cluster = null
|
export let cluster = null;
|
||||||
export let partition = null
|
export let partition = null;
|
||||||
let pendingCluster = cluster, pendingPartition = partition
|
let pendingCluster = cluster,
|
||||||
$: isModified = pendingCluster != cluster || pendingPartition != partition
|
pendingPartition = partition;
|
||||||
|
$: isModified = pendingCluster != cluster || pendingPartition != partition;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Select Cluster & Slurm Partition</ModalHeader>
|
||||||
Select Cluster & Slurm Partition
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if $initialized}
|
{#if $initialized}
|
||||||
<h4>Cluster</h4>
|
<h4>Cluster</h4>
|
||||||
@ -27,32 +33,38 @@
|
|||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == null}
|
active={pendingCluster == null}
|
||||||
on:click={() => (pendingCluster = null, pendingPartition = null)}>
|
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
|
||||||
|
>
|
||||||
Any Cluster
|
Any Cluster
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{#each clusters as cluster}
|
{#each clusters as cluster}
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == cluster.name}
|
active={pendingCluster == cluster.name}
|
||||||
on:click={() => (pendingCluster = cluster.name, pendingPartition = null)}>
|
on:click={() => (
|
||||||
|
(pendingCluster = cluster.name), (pendingPartition = null)
|
||||||
|
)}
|
||||||
|
>
|
||||||
{cluster.name}
|
{cluster.name}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $initialized && pendingCluster != null}
|
{#if $initialized && pendingCluster != null}
|
||||||
<br/>
|
<br />
|
||||||
<h4>Partiton</h4>
|
<h4>Partiton</h4>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
active={pendingPartition == null}
|
active={pendingPartition == null}
|
||||||
on:click={() => (pendingPartition = null)}>
|
on:click={() => (pendingPartition = null)}
|
||||||
|
>
|
||||||
Any Partition
|
Any Partition
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{#each clusters.find(c => c.name == pendingCluster).partitions as partition}
|
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition}
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
active={pendingPartition == partition}
|
active={pendingPartition == partition}
|
||||||
on:click={() => (pendingPartition = partition)}>
|
on:click={() => (pendingPartition = partition)}
|
||||||
|
>
|
||||||
{partition}
|
{partition}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
@ -60,18 +72,24 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => {
|
<Button
|
||||||
isOpen = false
|
color="primary"
|
||||||
cluster = pendingCluster
|
on:click={() => {
|
||||||
partition = pendingPartition
|
isOpen = false;
|
||||||
dispatch('update', { cluster, partition })
|
cluster = pendingCluster;
|
||||||
}}>Close & Apply</Button>
|
partition = pendingPartition;
|
||||||
<Button color="danger" on:click={() => {
|
dispatch("update", { cluster, partition });
|
||||||
isOpen = false
|
}}>Close & Apply</Button
|
||||||
cluster = pendingCluster = null
|
>
|
||||||
partition = pendingPartition = null
|
<Button
|
||||||
dispatch('update', { cluster, partition })
|
color="danger"
|
||||||
}}>Reset</Button>
|
on:click={() => {
|
||||||
|
isOpen = false;
|
||||||
|
cluster = pendingCluster = null;
|
||||||
|
partition = pendingPartition = null;
|
||||||
|
dispatch("update", { cluster, partition });
|
||||||
|
}}>Reset</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,54 +1,86 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { Row, Col, Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let lessThan = null
|
export let lessThan = null;
|
||||||
export let moreThan = null
|
export let moreThan = null;
|
||||||
export let from = null
|
export let from = null;
|
||||||
export let to = null
|
export let to = null;
|
||||||
|
|
||||||
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo
|
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo;
|
||||||
let lessDisabled = false, moreDisabled = false, betweenDisabled = false
|
let lessDisabled = false,
|
||||||
|
moreDisabled = false,
|
||||||
|
betweenDisabled = false;
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
pendingLessThan = lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan)
|
pendingLessThan =
|
||||||
pendingMoreThan = moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan)
|
lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan);
|
||||||
pendingFrom = from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from)
|
pendingMoreThan =
|
||||||
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to)
|
moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan);
|
||||||
|
pendingFrom =
|
||||||
|
from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from);
|
||||||
|
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset()
|
reset();
|
||||||
|
|
||||||
function secsToHoursAndMins(duration) {
|
function secsToHoursAndMins(duration) {
|
||||||
const hours = Math.floor(duration / 3600)
|
const hours = Math.floor(duration / 3600);
|
||||||
duration -= hours * 3600
|
duration -= hours * 3600;
|
||||||
const mins = Math.floor(duration / 60)
|
const mins = Math.floor(duration / 60);
|
||||||
return { hours, mins }
|
return { hours, mins };
|
||||||
}
|
}
|
||||||
|
|
||||||
function hoursAndMinsToSecs({ hours, mins }) {
|
function hoursAndMinsToSecs({ hours, mins }) {
|
||||||
return hours * 3600 + mins * 60
|
return hours * 3600 + mins * 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: lessDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
|
$: lessDisabled =
|
||||||
$: moreDisabled = pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
|
pendingMoreThan.hours !== 0 ||
|
||||||
$: betweenDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0
|
pendingMoreThan.mins !== 0 ||
|
||||||
|
pendingFrom.hours !== 0 ||
|
||||||
|
pendingFrom.mins !== 0 ||
|
||||||
|
pendingTo.hours !== 0 ||
|
||||||
|
pendingTo.mins !== 0;
|
||||||
|
$: moreDisabled =
|
||||||
|
pendingLessThan.hours !== 0 ||
|
||||||
|
pendingLessThan.mins !== 0 ||
|
||||||
|
pendingFrom.hours !== 0 ||
|
||||||
|
pendingFrom.mins !== 0 ||
|
||||||
|
pendingTo.hours !== 0 ||
|
||||||
|
pendingTo.mins !== 0;
|
||||||
|
$: betweenDisabled =
|
||||||
|
pendingMoreThan.hours !== 0 ||
|
||||||
|
pendingMoreThan.mins !== 0 ||
|
||||||
|
pendingLessThan.hours !== 0 ||
|
||||||
|
pendingLessThan.mins !== 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Select Job Duration</ModalHeader>
|
||||||
Select Job Duration
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h4>Duration more than</h4>
|
<h4>Duration more than</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" class="form-control" bind:value={pendingMoreThan.hours} disabled={moreDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingMoreThan.hours}
|
||||||
|
disabled={moreDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">h</div>
|
<div class="input-group-text">h</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,20 +88,33 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingMoreThan.mins} disabled={moreDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingMoreThan.mins}
|
||||||
|
disabled={moreDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">m</div>
|
<div class="input-group-text">m</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<hr/>
|
<hr />
|
||||||
|
|
||||||
<h4>Duration less than</h4>
|
<h4>Duration less than</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" class="form-control" bind:value={pendingLessThan.hours} disabled={lessDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingLessThan.hours}
|
||||||
|
disabled={lessDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">h</div>
|
<div class="input-group-text">h</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,20 +122,33 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingLessThan.mins} disabled={lessDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingLessThan.mins}
|
||||||
|
disabled={lessDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">m</div>
|
<div class="input-group-text">m</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<hr/>
|
<hr />
|
||||||
|
|
||||||
<h4>Duration between</h4>
|
<h4>Duration between</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" class="form-control" bind:value={pendingFrom.hours} disabled={betweenDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingFrom.hours}
|
||||||
|
disabled={betweenDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">h</div>
|
<div class="input-group-text">h</div>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +156,14 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingFrom.mins} disabled={betweenDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingFrom.mins}
|
||||||
|
disabled={betweenDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">m</div>
|
<div class="input-group-text">m</div>
|
||||||
</div>
|
</div>
|
||||||
@ -109,7 +174,13 @@
|
|||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" class="form-control" bind:value={pendingTo.hours} disabled={betweenDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingTo.hours}
|
||||||
|
disabled={betweenDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">h</div>
|
<div class="input-group-text">h</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,7 +188,14 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<div class="input-group mb-2 mr-sm-2">
|
<div class="input-group mb-2 mr-sm-2">
|
||||||
<input type="number" min="0" max="59" class="form-control" bind:value={pendingTo.mins} disabled={betweenDisabled}>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={pendingTo.mins}
|
||||||
|
disabled={betweenDisabled}
|
||||||
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">m</div>
|
<div class="input-group-text">m</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,33 +204,41 @@
|
|||||||
</Row>
|
</Row>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary"
|
<Button
|
||||||
|
color="primary"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
isOpen = false
|
isOpen = false;
|
||||||
lessThan = hoursAndMinsToSecs(pendingLessThan)
|
lessThan = hoursAndMinsToSecs(pendingLessThan);
|
||||||
moreThan = hoursAndMinsToSecs(pendingMoreThan)
|
moreThan = hoursAndMinsToSecs(pendingMoreThan);
|
||||||
from = hoursAndMinsToSecs(pendingFrom)
|
from = hoursAndMinsToSecs(pendingFrom);
|
||||||
to = hoursAndMinsToSecs(pendingTo)
|
to = hoursAndMinsToSecs(pendingTo);
|
||||||
dispatch('update', { lessThan, moreThan, from, to })
|
dispatch("update", { lessThan, moreThan, from, to });
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Close & Apply
|
Close & Apply
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="warning" on:click={() => {
|
<Button
|
||||||
lessThan = null
|
color="warning"
|
||||||
moreThan = null
|
on:click={() => {
|
||||||
from = null
|
lessThan = null;
|
||||||
to = null
|
moreThan = null;
|
||||||
reset()
|
from = null;
|
||||||
}}>Reset Values</Button>
|
to = null;
|
||||||
<Button color="danger" on:click={() => {
|
reset();
|
||||||
isOpen = false
|
}}>Reset Values</Button
|
||||||
lessThan = null
|
>
|
||||||
moreThan = null
|
<Button
|
||||||
from = null
|
color="danger"
|
||||||
to = null
|
on:click={() => {
|
||||||
reset()
|
isOpen = false;
|
||||||
dispatch('update', { lessThan, moreThan, from, to })
|
lessThan = null;
|
||||||
}}>Reset Filter</Button>
|
moreThan = null;
|
||||||
|
from = null;
|
||||||
|
to = null;
|
||||||
|
reset();
|
||||||
|
dispatch("update", { lessThan, moreThan, from, to });
|
||||||
|
}}>Reset Filter</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -10,43 +10,58 @@
|
|||||||
- void update(additionalFilters: Object?): Triggers an update
|
- void update(additionalFilters: Object?): Triggers an update
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
import { Row, Col, DropdownItem, DropdownMenu,
|
import {
|
||||||
DropdownToggle, ButtonDropdown, Icon } from 'sveltestrap'
|
Row,
|
||||||
import { createEventDispatcher } from 'svelte'
|
Col,
|
||||||
import Info from './InfoBox.svelte'
|
DropdownItem,
|
||||||
import Cluster from './Cluster.svelte'
|
DropdownMenu,
|
||||||
import JobStates, { allJobStates } from './JobStates.svelte'
|
DropdownToggle,
|
||||||
import StartTime from './StartTime.svelte'
|
ButtonDropdown,
|
||||||
import Tags from './Tags.svelte'
|
Icon,
|
||||||
import Tag from '../Tag.svelte'
|
} from "@sveltestrap/sveltestrap";
|
||||||
import Duration from './Duration.svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import Resources from './Resources.svelte'
|
import Info from "./InfoBox.svelte";
|
||||||
import Statistics from './Stats.svelte'
|
import Cluster from "./Cluster.svelte";
|
||||||
|
import JobStates, { allJobStates } from "./JobStates.svelte";
|
||||||
|
import StartTime from "./StartTime.svelte";
|
||||||
|
import Tags from "./Tags.svelte";
|
||||||
|
import Tag from "../Tag.svelte";
|
||||||
|
import Duration from "./Duration.svelte";
|
||||||
|
import Resources from "./Resources.svelte";
|
||||||
|
import Statistics from "./Stats.svelte";
|
||||||
// import TimeSelection from './TimeSelection.svelte'
|
// import TimeSelection from './TimeSelection.svelte'
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let menuText = null
|
export let menuText = null;
|
||||||
export let filterPresets = {}
|
export let filterPresets = {};
|
||||||
export let disableClusterSelection = false
|
export let disableClusterSelection = false;
|
||||||
export let startTimeQuickSelect = false
|
export let startTimeQuickSelect = false;
|
||||||
|
|
||||||
let filters = {
|
let filters = {
|
||||||
projectMatch: filterPresets.projectMatch || 'contains',
|
projectMatch: filterPresets.projectMatch || "contains",
|
||||||
userMatch: filterPresets.userMatch || 'contains',
|
userMatch: filterPresets.userMatch || "contains",
|
||||||
jobIdMatch: filterPresets.jobIdMatch || 'eq',
|
jobIdMatch: filterPresets.jobIdMatch || "eq",
|
||||||
|
|
||||||
cluster: filterPresets.cluster || null,
|
cluster: filterPresets.cluster || null,
|
||||||
partition: filterPresets.partition || null,
|
partition: filterPresets.partition || null,
|
||||||
states: filterPresets.states || filterPresets.state ? [filterPresets.state].flat() : allJobStates,
|
states:
|
||||||
|
filterPresets.states || filterPresets.state
|
||||||
|
? [filterPresets.state].flat()
|
||||||
|
: allJobStates,
|
||||||
startTime: filterPresets.startTime || { from: null, to: null },
|
startTime: filterPresets.startTime || { from: null, to: null },
|
||||||
tags: filterPresets.tags || [],
|
tags: filterPresets.tags || [],
|
||||||
duration: filterPresets.duration || { lessThan: null, moreThan: null, from: null, to: null },
|
duration: filterPresets.duration || {
|
||||||
jobId: filterPresets.jobId || '',
|
lessThan: null,
|
||||||
|
moreThan: null,
|
||||||
|
from: null,
|
||||||
|
to: null,
|
||||||
|
},
|
||||||
|
jobId: filterPresets.jobId || "",
|
||||||
arrayJobId: filterPresets.arrayJobId || null,
|
arrayJobId: filterPresets.arrayJobId || null,
|
||||||
user: filterPresets.user || '',
|
user: filterPresets.user || "",
|
||||||
project: filterPresets.project || '',
|
project: filterPresets.project || "",
|
||||||
jobName: filterPresets.jobName || '',
|
jobName: filterPresets.jobName || "",
|
||||||
|
|
||||||
node: filterPresets.node || null,
|
node: filterPresets.node || null,
|
||||||
numNodes: filterPresets.numNodes || { from: null, to: null },
|
numNodes: filterPresets.numNodes || { from: null, to: null },
|
||||||
@ -54,7 +69,7 @@
|
|||||||
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
||||||
|
|
||||||
stats: [],
|
stats: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
let isClusterOpen = false,
|
let isClusterOpen = false,
|
||||||
isJobStatesOpen = false,
|
isJobStatesOpen = false,
|
||||||
@ -65,118 +80,126 @@
|
|||||||
isStatsOpen = false,
|
isStatsOpen = false,
|
||||||
isNodesModified = false,
|
isNodesModified = false,
|
||||||
isHwthreadsModified = false,
|
isHwthreadsModified = false,
|
||||||
isAccsModified = false
|
isAccsModified = false;
|
||||||
|
|
||||||
// Can be called from the outside to trigger a 'update' event from this component.
|
// Can be called from the outside to trigger a 'update' event from this component.
|
||||||
export function update(additionalFilters = null) {
|
export function update(additionalFilters = null) {
|
||||||
if (additionalFilters != null)
|
if (additionalFilters != null)
|
||||||
for (let key in additionalFilters)
|
for (let key in additionalFilters) filters[key] = additionalFilters[key];
|
||||||
filters[key] = additionalFilters[key]
|
|
||||||
|
|
||||||
let items = []
|
let items = [];
|
||||||
if (filters.cluster)
|
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
||||||
items.push({ cluster: { eq: filters.cluster } })
|
if (filters.node) items.push({ node: { contains: filters.node } });
|
||||||
if (filters.node)
|
if (filters.partition) items.push({ partition: { eq: filters.partition } });
|
||||||
items.push({ node: { contains: filters.node } })
|
|
||||||
if (filters.partition)
|
|
||||||
items.push({ partition: { eq: filters.partition } })
|
|
||||||
if (filters.states.length != allJobStates.length)
|
if (filters.states.length != allJobStates.length)
|
||||||
items.push({ state: filters.states })
|
items.push({ state: filters.states });
|
||||||
if (filters.startTime.from || filters.startTime.to)
|
if (filters.startTime.from || filters.startTime.to)
|
||||||
items.push({ startTime: { from: filters.startTime.from, to: filters.startTime.to } })
|
items.push({
|
||||||
if (filters.tags.length != 0)
|
startTime: { from: filters.startTime.from, to: filters.startTime.to },
|
||||||
items.push({ tags: filters.tags })
|
});
|
||||||
|
if (filters.tags.length != 0) items.push({ tags: filters.tags });
|
||||||
if (filters.duration.from || filters.duration.to)
|
if (filters.duration.from || filters.duration.to)
|
||||||
items.push({ duration: { from: filters.duration.from, to: filters.duration.to } })
|
items.push({
|
||||||
|
duration: { from: filters.duration.from, to: filters.duration.to },
|
||||||
|
});
|
||||||
if (filters.duration.lessThan)
|
if (filters.duration.lessThan)
|
||||||
items.push({ duration: { from: 0, to: filters.duration.lessThan } })
|
items.push({ duration: { from: 0, to: filters.duration.lessThan } });
|
||||||
if (filters.duration.moreThan)
|
if (filters.duration.moreThan)
|
||||||
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }) // 7 days to include special jobs with long runtimes
|
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
|
||||||
if (filters.jobId)
|
if (filters.jobId)
|
||||||
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } })
|
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
||||||
if (filters.arrayJobId != null)
|
if (filters.arrayJobId != null)
|
||||||
items.push({ arrayJobId: filters.arrayJobId })
|
items.push({ arrayJobId: filters.arrayJobId });
|
||||||
if (filters.numNodes.from != null || filters.numNodes.to != null)
|
if (filters.numNodes.from != null || filters.numNodes.to != null)
|
||||||
items.push({ numNodes: { from: filters.numNodes.from, to: filters.numNodes.to } })
|
items.push({
|
||||||
|
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
|
||||||
|
});
|
||||||
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
|
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
|
||||||
items.push({ numHWThreads: { from: filters.numHWThreads.from, to: filters.numHWThreads.to } })
|
items.push({
|
||||||
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null)
|
numHWThreads: {
|
||||||
items.push({ numAccelerators: { from: filters.numAccelerators.from, to: filters.numAccelerators.to } })
|
from: filters.numHWThreads.from,
|
||||||
|
to: filters.numHWThreads.to,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
filters.numAccelerators.from != null ||
|
||||||
|
filters.numAccelerators.to != null
|
||||||
|
)
|
||||||
|
items.push({
|
||||||
|
numAccelerators: {
|
||||||
|
from: filters.numAccelerators.from,
|
||||||
|
to: filters.numAccelerators.to,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (filters.user)
|
if (filters.user)
|
||||||
items.push({ user: { [filters.userMatch]: filters.user } })
|
items.push({ user: { [filters.userMatch]: filters.user } });
|
||||||
if (filters.project)
|
if (filters.project)
|
||||||
items.push({ project: { [filters.projectMatch]: filters.project } })
|
items.push({ project: { [filters.projectMatch]: filters.project } });
|
||||||
if (filters.jobName)
|
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
|
||||||
items.push({ jobName: { contains: filters.jobName } })
|
|
||||||
for (let stat of filters.stats)
|
for (let stat of filters.stats)
|
||||||
items.push({ [stat.field]: { from: stat.from, to: stat.to } })
|
items.push({ [stat.field]: { from: stat.from, to: stat.to } });
|
||||||
|
|
||||||
dispatch('update', { filters: items })
|
dispatch("update", { filters: items });
|
||||||
changeURL()
|
changeURL();
|
||||||
return items
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeURL() {
|
function changeURL() {
|
||||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000)
|
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||||
|
|
||||||
let opts = []
|
let opts = [];
|
||||||
if (filters.cluster)
|
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
||||||
opts.push(`cluster=${filters.cluster}`)
|
if (filters.node) opts.push(`node=${filters.node}`);
|
||||||
if (filters.node)
|
if (filters.partition) opts.push(`partition=${filters.partition}`);
|
||||||
opts.push(`node=${filters.node}`)
|
|
||||||
if (filters.partition)
|
|
||||||
opts.push(`partition=${filters.partition}`)
|
|
||||||
if (filters.states.length != allJobStates.length)
|
if (filters.states.length != allJobStates.length)
|
||||||
for (let state of filters.states)
|
for (let state of filters.states) opts.push(`state=${state}`);
|
||||||
opts.push(`state=${state}`)
|
|
||||||
if (filters.startTime.from && filters.startTime.to)
|
if (filters.startTime.from && filters.startTime.to)
|
||||||
// if (filters.startTime.url) {
|
// if (filters.startTime.url) {
|
||||||
// opts.push(`startTime=${filters.startTime.url}`)
|
// opts.push(`startTime=${filters.startTime.url}`)
|
||||||
// } else {
|
// } else {
|
||||||
opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`)
|
opts.push(
|
||||||
|
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
|
||||||
|
);
|
||||||
// }
|
// }
|
||||||
if (filters.jobId.length != 0)
|
if (filters.jobId.length != 0)
|
||||||
if (filters.jobIdMatch != 'in') {
|
if (filters.jobIdMatch != "in") {
|
||||||
opts.push(`jobId=${filters.jobId}`)
|
opts.push(`jobId=${filters.jobId}`);
|
||||||
} else {
|
} else {
|
||||||
for (let singleJobId of filters.jobId)
|
for (let singleJobId of filters.jobId)
|
||||||
opts.push(`jobId=${singleJobId}`)
|
opts.push(`jobId=${singleJobId}`);
|
||||||
}
|
}
|
||||||
if (filters.jobIdMatch != 'eq')
|
if (filters.jobIdMatch != "eq")
|
||||||
opts.push(`jobIdMatch=${filters.jobIdMatch}`)
|
opts.push(`jobIdMatch=${filters.jobIdMatch}`);
|
||||||
for (let tag of filters.tags)
|
for (let tag of filters.tags) opts.push(`tag=${tag}`);
|
||||||
opts.push(`tag=${tag}`)
|
|
||||||
if (filters.duration.from && filters.duration.to)
|
if (filters.duration.from && filters.duration.to)
|
||||||
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`)
|
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
|
||||||
if (filters.duration.lessThan)
|
if (filters.duration.lessThan)
|
||||||
opts.push(`duration=0-${filters.duration.lessThan}`)
|
opts.push(`duration=0-${filters.duration.lessThan}`);
|
||||||
if (filters.duration.moreThan)
|
if (filters.duration.moreThan)
|
||||||
opts.push(`duration=${filters.duration.moreThan}-604800`)
|
opts.push(`duration=${filters.duration.moreThan}-604800`);
|
||||||
if (filters.numNodes.from && filters.numNodes.to)
|
if (filters.numNodes.from && filters.numNodes.to)
|
||||||
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`)
|
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
|
||||||
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
||||||
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`)
|
opts.push(
|
||||||
|
`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`,
|
||||||
|
);
|
||||||
if (filters.user.length != 0)
|
if (filters.user.length != 0)
|
||||||
if (filters.userMatch != 'in') {
|
if (filters.userMatch != "in") {
|
||||||
opts.push(`user=${filters.user}`)
|
opts.push(`user=${filters.user}`);
|
||||||
} else {
|
} else {
|
||||||
for (let singleUser of filters.user)
|
for (let singleUser of filters.user) opts.push(`user=${singleUser}`);
|
||||||
opts.push(`user=${singleUser}`)
|
|
||||||
}
|
}
|
||||||
if (filters.userMatch != 'contains')
|
if (filters.userMatch != "contains")
|
||||||
opts.push(`userMatch=${filters.userMatch}`)
|
opts.push(`userMatch=${filters.userMatch}`);
|
||||||
if (filters.project)
|
if (filters.project) opts.push(`project=${filters.project}`);
|
||||||
opts.push(`project=${filters.project}`)
|
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
|
||||||
if (filters.jobName)
|
if (filters.projectMatch != "contains")
|
||||||
opts.push(`jobName=${filters.jobName}`)
|
opts.push(`projectMatch=${filters.projectMatch}`);
|
||||||
if (filters.projectMatch != 'contains')
|
|
||||||
opts.push(`projectMatch=${filters.projectMatch}`)
|
|
||||||
|
|
||||||
if (opts.length == 0 && window.location.search.length <= 1)
|
if (opts.length == 0 && window.location.search.length <= 1) return;
|
||||||
return
|
|
||||||
|
|
||||||
let newurl = `${window.location.pathname}?${opts.join('&')}`
|
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
||||||
window.history.replaceState(null, '', newurl)
|
window.history.replaceState(null, "", newurl);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -184,57 +207,52 @@
|
|||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<ButtonDropdown class="cc-dropdown-on-hover">
|
<ButtonDropdown class="cc-dropdown-on-hover">
|
||||||
<DropdownToggle outline caret color="success">
|
<DropdownToggle outline caret color="success">
|
||||||
<Icon name="sliders"/>
|
<Icon name="sliders" />
|
||||||
Filters
|
Filters
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownItem header>
|
<DropdownItem header>Manage Filters</DropdownItem>
|
||||||
Manage Filters
|
|
||||||
</DropdownItem>
|
|
||||||
{#if menuText}
|
{#if menuText}
|
||||||
<DropdownItem disabled>{menuText}</DropdownItem>
|
<DropdownItem disabled>{menuText}</DropdownItem>
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
{/if}
|
{/if}
|
||||||
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
||||||
<Icon name="cpu"/> Cluster/Partition
|
<Icon name="cpu" /> Cluster/Partition
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
||||||
<Icon name="gear-fill"/> Job States
|
<Icon name="gear-fill" /> Job States
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
||||||
<Icon name="calendar-range"/> Start Time
|
<Icon name="calendar-range" /> Start Time
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
||||||
<Icon name="stopwatch"/> Duration
|
<Icon name="stopwatch" /> Duration
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
||||||
<Icon name="tags"/> Tags
|
<Icon name="tags" /> Tags
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
||||||
<Icon name="hdd-stack"/> Resources
|
<Icon name="hdd-stack" /> Resources
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
||||||
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
|
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{#if startTimeQuickSelect}
|
{#if startTimeQuickSelect}
|
||||||
<DropdownItem divider/>
|
<DropdownItem divider />
|
||||||
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
|
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
|
||||||
{#each [
|
{#each [{ text: "Last 6hrs", url: "last6h", seconds: 6 * 60 * 60 }, { text: "Last 24hrs", url: "last24h", seconds: 24 * 60 * 60 }, { text: "Last 7 days", url: "last7d", seconds: 7 * 24 * 60 * 60 }, { text: "Last 30 days", url: "last30d", seconds: 30 * 24 * 60 * 60 }] as { text, url, seconds }}
|
||||||
{ text: 'Last 6hrs', url: 'last6h', seconds: 6*60*60 },
|
<DropdownItem
|
||||||
// { text: 'Last 12hrs', seconds: 12*60*60 },
|
on:click={() => {
|
||||||
{ text: 'Last 24hrs', url: 'last24h', seconds: 24*60*60 },
|
filters.startTime.from = new Date(
|
||||||
// { text: 'Last 48hrs', seconds: 48*60*60 },
|
Date.now() - seconds * 1000,
|
||||||
{ text: 'Last 7 days', url: 'last7d', seconds: 7*24*60*60 },
|
).toISOString();
|
||||||
{ text: 'Last 30 days', url: 'last30d', seconds: 30*24*60*60 }
|
filters.startTime.to = new Date(Date.now()).toISOString();
|
||||||
] as {text, url, seconds}}
|
(filters.startTime.text = text), (filters.startTime.url = url);
|
||||||
<DropdownItem on:click={() => {
|
update();
|
||||||
filters.startTime.from = (new Date(Date.now() - seconds * 1000)).toISOString()
|
}}
|
||||||
filters.startTime.to = (new Date(Date.now())).toISOString()
|
>
|
||||||
filters.startTime.text = text,
|
<Icon name="calendar-range" />
|
||||||
filters.startTime.url = url
|
{text}
|
||||||
update()
|
|
||||||
}}>
|
|
||||||
<Icon name="calendar-range"/> {text}
|
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@ -253,7 +271,7 @@
|
|||||||
|
|
||||||
{#if filters.states.length != allJobStates.length}
|
{#if filters.states.length != allJobStates.length}
|
||||||
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
||||||
{filters.states.join(', ')}
|
{filters.states.join(", ")}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -262,28 +280,37 @@
|
|||||||
{#if filters.startTime.text}
|
{#if filters.startTime.text}
|
||||||
{filters.startTime.text}
|
{filters.startTime.text}
|
||||||
{:else}
|
{:else}
|
||||||
{new Date(filters.startTime.from).toLocaleString()} - {new Date(filters.startTime.to).toLocaleString()}
|
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
|
||||||
|
filters.startTime.to,
|
||||||
|
).toLocaleString()}
|
||||||
{/if}
|
{/if}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.from || filters.duration.to}
|
{#if filters.duration.from || filters.duration.to}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||||
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(filters.duration.from % 3600 / 60)}m
|
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
|
||||||
-
|
(filters.duration.from % 3600) / 60,
|
||||||
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(filters.duration.to % 3600 / 60)}m
|
)}m -
|
||||||
|
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(
|
||||||
|
(filters.duration.to % 3600) / 60,
|
||||||
|
)}m
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.lessThan}
|
{#if filters.duration.lessThan}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||||
Duration less than {Math.floor(filters.duration.lessThan / 3600)}h:{Math.floor(filters.duration.lessThan % 3600 / 60)}m
|
Duration less than {Math.floor(
|
||||||
|
filters.duration.lessThan / 3600,
|
||||||
|
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.moreThan}
|
{#if filters.duration.moreThan}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
||||||
Duration more than {Math.floor(filters.duration.moreThan / 3600)}h:{Math.floor(filters.duration.moreThan % 3600 / 60)}m
|
Duration more than {Math.floor(
|
||||||
|
filters.duration.moreThan / 3600,
|
||||||
|
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -295,19 +322,26 @@
|
|||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.numNodes.from != null || filters.numNodes.to != null ||
|
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
|
||||||
filters.numHWThreads.from != null || filters.numHWThreads.to != null ||
|
|
||||||
filters.numAccelerators.from != null || filters.numAccelerators.to != null }
|
|
||||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||||
{#if isNodesModified } Nodes: {filters.numNodes.from} - {filters.numNodes.to} {/if}
|
{#if isNodesModified}
|
||||||
{#if isNodesModified && isHwthreadsModified }, {/if}
|
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
|
||||||
{#if isHwthreadsModified } HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} {/if}
|
{/if}
|
||||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified }, {/if}
|
{#if isNodesModified && isHwthreadsModified},
|
||||||
{#if isAccsModified } Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} {/if}
|
{/if}
|
||||||
|
{#if isHwthreadsModified}
|
||||||
|
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
||||||
|
{/if}
|
||||||
|
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
||||||
|
{/if}
|
||||||
|
{#if isAccsModified}
|
||||||
|
Accelerators: {filters.numAccelerators.from} - {filters
|
||||||
|
.numAccelerators.to}
|
||||||
|
{/if}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.node != null }
|
{#if filters.node != null}
|
||||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
||||||
Node: {filters.node}
|
Node: {filters.node}
|
||||||
</Info>
|
</Info>
|
||||||
@ -315,33 +349,38 @@
|
|||||||
|
|
||||||
{#if filters.stats.length > 0}
|
{#if filters.stats.length > 0}
|
||||||
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
||||||
{filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')}
|
{filters.stats
|
||||||
|
.map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
|
||||||
|
.join(", ")}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Cluster
|
<Cluster
|
||||||
disableClusterSelection={disableClusterSelection}
|
{disableClusterSelection}
|
||||||
bind:isOpen={isClusterOpen}
|
bind:isOpen={isClusterOpen}
|
||||||
bind:cluster={filters.cluster}
|
bind:cluster={filters.cluster}
|
||||||
bind:partition={filters.partition}
|
bind:partition={filters.partition}
|
||||||
on:update={() => update()} />
|
on:update={() => update()}
|
||||||
|
/>
|
||||||
|
|
||||||
<JobStates
|
<JobStates
|
||||||
bind:isOpen={isJobStatesOpen}
|
bind:isOpen={isJobStatesOpen}
|
||||||
bind:states={filters.states}
|
bind:states={filters.states}
|
||||||
on:update={() => update()} />
|
on:update={() => update()}
|
||||||
|
/>
|
||||||
|
|
||||||
<StartTime
|
<StartTime
|
||||||
bind:isOpen={isStartTimeOpen}
|
bind:isOpen={isStartTimeOpen}
|
||||||
bind:from={filters.startTime.from}
|
bind:from={filters.startTime.from}
|
||||||
bind:to={filters.startTime.to}
|
bind:to={filters.startTime.to}
|
||||||
on:update={() => {
|
on:update={() => {
|
||||||
delete filters.startTime['text']
|
delete filters.startTime["text"];
|
||||||
delete filters.startTime['url']
|
delete filters.startTime["url"];
|
||||||
update()
|
update();
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Duration
|
<Duration
|
||||||
bind:isOpen={isDurationOpen}
|
bind:isOpen={isDurationOpen}
|
||||||
@ -349,28 +388,34 @@
|
|||||||
bind:moreThan={filters.duration.moreThan}
|
bind:moreThan={filters.duration.moreThan}
|
||||||
bind:from={filters.duration.from}
|
bind:from={filters.duration.from}
|
||||||
bind:to={filters.duration.to}
|
bind:to={filters.duration.to}
|
||||||
on:update={() => update()} />
|
on:update={() => update()}
|
||||||
|
/>
|
||||||
|
|
||||||
<Tags
|
<Tags
|
||||||
bind:isOpen={isTagsOpen}
|
bind:isOpen={isTagsOpen}
|
||||||
bind:tags={filters.tags}
|
bind:tags={filters.tags}
|
||||||
on:update={() => update()} />
|
on:update={() => update()}
|
||||||
|
/>
|
||||||
|
|
||||||
<Resources cluster={filters.cluster}
|
<Resources
|
||||||
|
cluster={filters.cluster}
|
||||||
bind:isOpen={isResourcesOpen}
|
bind:isOpen={isResourcesOpen}
|
||||||
bind:numNodes={filters.numNodes}
|
bind:numNodes={filters.numNodes}
|
||||||
bind:numHWThreads={filters.numHWThreads}
|
bind:numHWThreads={filters.numHWThreads}
|
||||||
bind:numAccelerators={filters.numAccelerators}
|
bind:numAccelerators={filters.numAccelerators}
|
||||||
bind:namedNode={filters.node}
|
bind:namedNode={filters.node}
|
||||||
bind:isNodesModified={isNodesModified}
|
bind:isNodesModified
|
||||||
bind:isHwthreadsModified={isHwthreadsModified}
|
bind:isHwthreadsModified
|
||||||
bind:isAccsModified={isAccsModified}
|
bind:isAccsModified
|
||||||
on:update={() => update()} />
|
on:update={() => update()}
|
||||||
|
/>
|
||||||
|
|
||||||
<Statistics cluster={filters.cluster}
|
<Statistics
|
||||||
|
cluster={filters.cluster}
|
||||||
bind:isOpen={isStatsOpen}
|
bind:isOpen={isStatsOpen}
|
||||||
bind:stats={filters.stats}
|
bind:stats={filters.stats}
|
||||||
on:update={() => update()} />
|
on:update={() => update()}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
|
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button, Icon } from 'sveltestrap'
|
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let icon
|
export let icon;
|
||||||
export let modified = false
|
export let modified = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button outline color={modified ? 'warning' : 'primary'} on:click>
|
<Button outline color={modified ? "warning" : "primary"} on:click>
|
||||||
<Icon name={icon}/>
|
<Icon name={icon} />
|
||||||
<slot />
|
<slot />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,47 +1,76 @@
|
|||||||
<script context="module">
|
<script context="module">
|
||||||
export const allJobStates = [ 'running', 'completed', 'failed', 'cancelled', 'stopped', 'timeout', 'preempted', 'out_of_memory' ]
|
export const allJobStates = [
|
||||||
|
"running",
|
||||||
|
"completed",
|
||||||
|
"failed",
|
||||||
|
"cancelled",
|
||||||
|
"stopped",
|
||||||
|
"timeout",
|
||||||
|
"preempted",
|
||||||
|
"out_of_memory",
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { Button, ListGroup, ListGroupItem,
|
import {
|
||||||
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
Button,
|
||||||
|
ListGroup,
|
||||||
|
ListGroupItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let isModified = false
|
export let isModified = false;
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let states = [...allJobStates]
|
export let states = [...allJobStates];
|
||||||
|
|
||||||
let pendingStates = [...states]
|
let pendingStates = [...states];
|
||||||
$: isModified = states.length != pendingStates.length || !states.every(state => pendingStates.includes(state))
|
$: isModified =
|
||||||
|
states.length != pendingStates.length ||
|
||||||
|
!states.every((state) => pendingStates.includes(state));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Select Job States</ModalHeader>
|
||||||
Select Job States
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#each allJobStates as state}
|
{#each allJobStates as state}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<input type=checkbox bind:group={pendingStates} name="flavours" value={state}>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:group={pendingStates}
|
||||||
|
name="flavours"
|
||||||
|
value={state}
|
||||||
|
/>
|
||||||
{state}
|
{state}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" disabled={pendingStates.length == 0} on:click={() => {
|
<Button
|
||||||
isOpen = false
|
color="primary"
|
||||||
states = [...pendingStates]
|
disabled={pendingStates.length == 0}
|
||||||
dispatch('update', { states })
|
on:click={() => {
|
||||||
}}>Close & Apply</Button>
|
isOpen = false;
|
||||||
<Button color="danger" on:click={() => {
|
states = [...pendingStates];
|
||||||
isOpen = false
|
dispatch("update", { states });
|
||||||
states = [...allJobStates]
|
}}>Close & Apply</Button
|
||||||
pendingStates = [...allJobStates]
|
>
|
||||||
dispatch('update', { states })
|
<Button
|
||||||
}}>Reset</Button>
|
color="danger"
|
||||||
|
on:click={() => {
|
||||||
|
isOpen = false;
|
||||||
|
states = [...allJobStates];
|
||||||
|
pendingStates = [...allJobStates];
|
||||||
|
dispatch("update", { states });
|
||||||
|
}}>Reset</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,145 +1,242 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from 'svelte'
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
import {
|
||||||
import Header from '../Header.svelte';
|
Button,
|
||||||
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
|
||||||
|
|
||||||
const clusters = getContext('clusters'),
|
const clusters = getContext("clusters"),
|
||||||
initialized = getContext('initialized'),
|
initialized = getContext("initialized"),
|
||||||
dispatch = createEventDispatcher()
|
dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let cluster = null
|
export let cluster = null;
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let numNodes = { from: null, to: null }
|
export let numNodes = { from: null, to: null };
|
||||||
export let numHWThreads = { from: null, to: null }
|
export let numHWThreads = { from: null, to: null };
|
||||||
export let numAccelerators = { from: null, to: null }
|
export let numAccelerators = { from: null, to: null };
|
||||||
export let isNodesModified = false
|
export let isNodesModified = false;
|
||||||
export let isHwthreadsModified = false
|
export let isHwthreadsModified = false;
|
||||||
export let isAccsModified = false
|
export let isAccsModified = false;
|
||||||
export let namedNode = null
|
export let namedNode = null;
|
||||||
|
|
||||||
let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators, pendingNamedNode = namedNode
|
let pendingNumNodes = numNodes,
|
||||||
|
pendingNumHWThreads = numHWThreads,
|
||||||
|
pendingNumAccelerators = numAccelerators,
|
||||||
|
pendingNamedNode = namedNode;
|
||||||
|
|
||||||
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
|
const findMaxNumAccels = (clusters) =>
|
||||||
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
|
clusters.reduce(
|
||||||
|
(max, cluster) =>
|
||||||
|
Math.max(
|
||||||
|
max,
|
||||||
|
cluster.subClusters.reduce(
|
||||||
|
(max, sc) => Math.max(max, sc.topology.accelerators?.length || 0),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
// Limited to Single-Node Thread Count
|
// Limited to Single-Node Thread Count
|
||||||
const findMaxNumHWTreadsPerNode = clusters => clusters.reduce((max, cluster) => Math.max(max,
|
const findMaxNumHWTreadsPerNode = (clusters) =>
|
||||||
cluster.subClusters.reduce((max, sc) => Math.max(max, (sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode) || 0), 0)), 0)
|
clusters.reduce(
|
||||||
|
(max, cluster) =>
|
||||||
|
Math.max(
|
||||||
|
max,
|
||||||
|
cluster.subClusters.reduce(
|
||||||
|
(max, sc) =>
|
||||||
|
Math.max(
|
||||||
|
max,
|
||||||
|
sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode || 0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
// console.log(header)
|
// console.log(header)
|
||||||
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
|
let minNumNodes = 1,
|
||||||
|
maxNumNodes = 0,
|
||||||
|
minNumHWThreads = 1,
|
||||||
|
maxNumHWThreads = 0,
|
||||||
|
minNumAccelerators = 0,
|
||||||
|
maxNumAccelerators = 0;
|
||||||
$: {
|
$: {
|
||||||
if ($initialized) {
|
if ($initialized) {
|
||||||
if (cluster != null) {
|
if (cluster != null) {
|
||||||
const { subClusters } = clusters.find(c => c.name == cluster)
|
const { subClusters } = clusters.find((c) => c.name == cluster);
|
||||||
const { filterRanges } = header.clusters.find(c => c.name == cluster)
|
const { filterRanges } = header.clusters.find((c) => c.name == cluster);
|
||||||
minNumNodes = filterRanges.numNodes.from
|
minNumNodes = filterRanges.numNodes.from;
|
||||||
maxNumNodes = filterRanges.numNodes.to
|
maxNumNodes = filterRanges.numNodes.to;
|
||||||
maxNumAccelerators = findMaxNumAccels([{ subClusters }])
|
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
||||||
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }])
|
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]);
|
||||||
} else if (clusters.length > 0) {
|
} else if (clusters.length > 0) {
|
||||||
const { filterRanges } = header.clusters[0]
|
const { filterRanges } = header.clusters[0];
|
||||||
minNumNodes = filterRanges.numNodes.from
|
minNumNodes = filterRanges.numNodes.from;
|
||||||
maxNumNodes = filterRanges.numNodes.to
|
maxNumNodes = filterRanges.numNodes.to;
|
||||||
maxNumAccelerators = findMaxNumAccels(clusters)
|
maxNumAccelerators = findMaxNumAccels(clusters);
|
||||||
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters)
|
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters);
|
||||||
for (let cluster of header.clusters) {
|
for (let cluster of header.clusters) {
|
||||||
const { filterRanges } = cluster
|
const { filterRanges } = cluster;
|
||||||
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from)
|
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
|
||||||
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to)
|
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (isOpen && $initialized && pendingNumNodes.from == null && pendingNumNodes.to == null) {
|
if (
|
||||||
pendingNumNodes = { from: 0, to: maxNumNodes }
|
isOpen &&
|
||||||
|
$initialized &&
|
||||||
|
pendingNumNodes.from == null &&
|
||||||
|
pendingNumNodes.to == null
|
||||||
|
) {
|
||||||
|
pendingNumNodes = { from: 0, to: maxNumNodes };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (isOpen && $initialized && ((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) || (isHwthreadsModified == false))) {
|
if (
|
||||||
pendingNumHWThreads = { from: 0, to: maxNumHWThreads }
|
isOpen &&
|
||||||
|
$initialized &&
|
||||||
|
((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) ||
|
||||||
|
isHwthreadsModified == false)
|
||||||
|
) {
|
||||||
|
pendingNumHWThreads = { from: 0, to: maxNumHWThreads };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ( maxNumAccelerators != null && maxNumAccelerators > 1 ) {
|
$: if (maxNumAccelerators != null && maxNumAccelerators > 1) {
|
||||||
if (isOpen && $initialized && pendingNumAccelerators.from == null && pendingNumAccelerators.to == null) {
|
if (
|
||||||
pendingNumAccelerators = { from: 0, to: maxNumAccelerators }
|
isOpen &&
|
||||||
|
$initialized &&
|
||||||
|
pendingNumAccelerators.from == null &&
|
||||||
|
pendingNumAccelerators.to == null
|
||||||
|
) {
|
||||||
|
pendingNumAccelerators = { from: 0, to: maxNumAccelerators };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Select number of utilized Resources</ModalHeader>
|
||||||
Select number of utilized Resources
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h6>Named Node</h6>
|
<h6>Named Node</h6>
|
||||||
<input type="text" class="form-control" bind:value={pendingNamedNode}>
|
<input type="text" class="form-control" bind:value={pendingNamedNode} />
|
||||||
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
|
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
|
||||||
<DoubleRangeSlider
|
<DoubleRangeSlider
|
||||||
on:change={({ detail }) => {
|
on:change={({ detail }) => {
|
||||||
pendingNumNodes = { from: detail[0], to: detail[1] }
|
pendingNumNodes = { from: detail[0], to: detail[1] };
|
||||||
isNodesModified = true
|
isNodesModified = true;
|
||||||
}}
|
}}
|
||||||
min={minNumNodes} max={maxNumNodes}
|
min={minNumNodes}
|
||||||
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to}
|
max={maxNumNodes}
|
||||||
inputFieldFrom={pendingNumNodes.from} inputFieldTo={pendingNumNodes.to}/>
|
firstSlider={pendingNumNodes.from}
|
||||||
<h6 style="margin-top: 1rem;">Number of HWThreads (Use for Single-Node Jobs)</h6>
|
secondSlider={pendingNumNodes.to}
|
||||||
|
inputFieldFrom={pendingNumNodes.from}
|
||||||
|
inputFieldTo={pendingNumNodes.to}
|
||||||
|
/>
|
||||||
|
<h6 style="margin-top: 1rem;">
|
||||||
|
Number of HWThreads (Use for Single-Node Jobs)
|
||||||
|
</h6>
|
||||||
<DoubleRangeSlider
|
<DoubleRangeSlider
|
||||||
on:change={({ detail }) => {
|
on:change={({ detail }) => {
|
||||||
pendingNumHWThreads = { from: detail[0], to: detail[1] }
|
pendingNumHWThreads = { from: detail[0], to: detail[1] };
|
||||||
isHwthreadsModified = true
|
isHwthreadsModified = true;
|
||||||
}}
|
}}
|
||||||
min={minNumHWThreads} max={maxNumHWThreads}
|
min={minNumHWThreads}
|
||||||
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to}
|
max={maxNumHWThreads}
|
||||||
inputFieldFrom={pendingNumHWThreads.from} inputFieldTo={pendingNumHWThreads.to}/>
|
firstSlider={pendingNumHWThreads.from}
|
||||||
|
secondSlider={pendingNumHWThreads.to}
|
||||||
|
inputFieldFrom={pendingNumHWThreads.from}
|
||||||
|
inputFieldTo={pendingNumHWThreads.to}
|
||||||
|
/>
|
||||||
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
|
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
|
||||||
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
|
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
|
||||||
<DoubleRangeSlider
|
<DoubleRangeSlider
|
||||||
on:change={({ detail }) => {
|
on:change={({ detail }) => {
|
||||||
pendingNumAccelerators = { from: detail[0], to: detail[1] }
|
pendingNumAccelerators = { from: detail[0], to: detail[1] };
|
||||||
isAccsModified = true
|
isAccsModified = true;
|
||||||
}}
|
}}
|
||||||
min={minNumAccelerators} max={maxNumAccelerators}
|
min={minNumAccelerators}
|
||||||
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to}
|
max={maxNumAccelerators}
|
||||||
inputFieldFrom={pendingNumAccelerators.from} inputFieldTo={pendingNumAccelerators.to}/>
|
firstSlider={pendingNumAccelerators.from}
|
||||||
|
secondSlider={pendingNumAccelerators.to}
|
||||||
|
inputFieldFrom={pendingNumAccelerators.from}
|
||||||
|
inputFieldTo={pendingNumAccelerators.to}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary"
|
<Button
|
||||||
|
color="primary"
|
||||||
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
|
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
isOpen = false
|
isOpen = false;
|
||||||
pendingNumNodes = isNodesModified ? pendingNumNodes : { from: null, to: null }
|
pendingNumNodes = isNodesModified
|
||||||
pendingNumHWThreads = isHwthreadsModified ? pendingNumHWThreads : { from: null, to: null }
|
? pendingNumNodes
|
||||||
pendingNumAccelerators = isAccsModified ? pendingNumAccelerators : { from: null, to: null }
|
: { from: null, to: null };
|
||||||
numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to }
|
pendingNumHWThreads = isHwthreadsModified
|
||||||
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
|
? pendingNumHWThreads
|
||||||
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
|
: { from: null, to: null };
|
||||||
namedNode = pendingNamedNode
|
pendingNumAccelerators = isAccsModified
|
||||||
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode })
|
? pendingNumAccelerators
|
||||||
}}>
|
: { from: null, to: null };
|
||||||
|
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
|
||||||
|
numHWThreads = {
|
||||||
|
from: pendingNumHWThreads.from,
|
||||||
|
to: pendingNumHWThreads.to,
|
||||||
|
};
|
||||||
|
numAccelerators = {
|
||||||
|
from: pendingNumAccelerators.from,
|
||||||
|
to: pendingNumAccelerators.to,
|
||||||
|
};
|
||||||
|
namedNode = pendingNamedNode;
|
||||||
|
dispatch("update", {
|
||||||
|
numNodes,
|
||||||
|
numHWThreads,
|
||||||
|
numAccelerators,
|
||||||
|
namedNode,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
Close & Apply
|
Close & Apply
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="danger" on:click={() => {
|
<Button
|
||||||
isOpen = false
|
color="danger"
|
||||||
pendingNumNodes = { from: null, to: null }
|
on:click={() => {
|
||||||
pendingNumHWThreads = { from: null, to: null }
|
isOpen = false;
|
||||||
pendingNumAccelerators = { from: null, to: null }
|
pendingNumNodes = { from: null, to: null };
|
||||||
pendingNamedNode = null
|
pendingNumHWThreads = { from: null, to: null };
|
||||||
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
|
pendingNumAccelerators = { from: null, to: null };
|
||||||
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
|
pendingNamedNode = null;
|
||||||
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
|
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
|
||||||
isNodesModified = false
|
numHWThreads = {
|
||||||
isHwthreadsModified = false
|
from: pendingNumHWThreads.from,
|
||||||
isAccsModified = false
|
to: pendingNumHWThreads.to,
|
||||||
namedNode = pendingNamedNode
|
};
|
||||||
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode})
|
numAccelerators = {
|
||||||
}}>Reset</Button>
|
from: pendingNumAccelerators.from,
|
||||||
|
to: pendingNumAccelerators.to,
|
||||||
|
};
|
||||||
|
isNodesModified = false;
|
||||||
|
isHwthreadsModified = false;
|
||||||
|
isAccsModified = false;
|
||||||
|
namedNode = pendingNamedNode;
|
||||||
|
dispatch("update", {
|
||||||
|
numNodes,
|
||||||
|
numHWThreads,
|
||||||
|
numAccelerators,
|
||||||
|
namedNode,
|
||||||
|
});
|
||||||
|
}}>Reset</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,86 +1,121 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { parse, format, sub } from 'date-fns'
|
import { parse, format, sub } from "date-fns";
|
||||||
import { Row, Button, Input, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap'
|
import {
|
||||||
|
Row,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
FormGroup,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let isModified = false
|
export let isModified = false;
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let from = null
|
export let from = null;
|
||||||
export let to = null
|
export let to = null;
|
||||||
|
|
||||||
let pendingFrom, pendingTo
|
let pendingFrom, pendingTo;
|
||||||
|
|
||||||
const now = new Date(Date.now())
|
const now = new Date(Date.now());
|
||||||
const ago = sub(now, {months: 1})
|
const ago = sub(now, { months: 1 });
|
||||||
const defaultFrom = {date: format(ago, 'yyyy-MM-dd'), time: format(ago, 'HH:mm')}
|
const defaultFrom = {
|
||||||
const defaultTo = {date: format(now, 'yyyy-MM-dd'), time: format(now, 'HH:mm')}
|
date: format(ago, "yyyy-MM-dd"),
|
||||||
|
time: format(ago, "HH:mm"),
|
||||||
|
};
|
||||||
|
const defaultTo = {
|
||||||
|
date: format(now, "yyyy-MM-dd"),
|
||||||
|
time: format(now, "HH:mm"),
|
||||||
|
};
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
pendingFrom = from == null ? defaultFrom : fromRFC3339(from)
|
pendingFrom = from == null ? defaultFrom : fromRFC3339(from);
|
||||||
pendingTo = to == null ? defaultTo : fromRFC3339(to)
|
pendingTo = to == null ? defaultTo : fromRFC3339(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset()
|
reset();
|
||||||
|
|
||||||
function toRFC3339({ date, time }, secs = '00') {
|
function toRFC3339({ date, time }, secs = "00") {
|
||||||
const parsedDate = parse(date+' '+time+':'+secs, 'yyyy-MM-dd HH:mm:ss', new Date())
|
const parsedDate = parse(
|
||||||
return parsedDate.toISOString()
|
date + " " + time + ":" + secs,
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
return parsedDate.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromRFC3339(rfc3339) {
|
function fromRFC3339(rfc3339) {
|
||||||
const parsedDate = new Date(rfc3339)
|
const parsedDate = new Date(rfc3339);
|
||||||
return { date: format(parsedDate, 'yyyy-MM-dd'), time: format(parsedDate, 'HH:mm') }
|
return {
|
||||||
|
date: format(parsedDate, "yyyy-MM-dd"),
|
||||||
|
time: format(parsedDate, "HH:mm"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$: isModified = (from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, '59'))
|
$: isModified =
|
||||||
&& !(from == null && pendingFrom.date == '0000-00-00' && pendingFrom.time == '00:00')
|
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
|
||||||
&& !(to == null && pendingTo.date == '0000-00-00' && pendingTo.time == '00:00')
|
!(
|
||||||
|
from == null &&
|
||||||
|
pendingFrom.date == "0000-00-00" &&
|
||||||
|
pendingFrom.time == "00:00"
|
||||||
|
) &&
|
||||||
|
!(
|
||||||
|
to == null &&
|
||||||
|
pendingTo.date == "0000-00-00" &&
|
||||||
|
pendingTo.time == "00:00"
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Select Start Time</ModalHeader>
|
||||||
Select Start Time
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h4>From</h4>
|
<h4>From</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="date" bind:value={pendingFrom.date}/>
|
<Input type="date" bind:value={pendingFrom.date} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="time" bind:value={pendingFrom.time}/>
|
<Input type="time" bind:value={pendingFrom.time} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Row>
|
</Row>
|
||||||
<h4>To</h4>
|
<h4>To</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="date" bind:value={pendingTo.date}/>
|
<Input type="date" bind:value={pendingTo.date} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="time" bind:value={pendingTo.time}/>
|
<Input type="time" bind:value={pendingTo.time} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Row>
|
</Row>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary"
|
<Button
|
||||||
disabled={pendingFrom.date == '0000-00-00' || pendingTo.date == '0000-00-00'}
|
color="primary"
|
||||||
|
disabled={pendingFrom.date == "0000-00-00" ||
|
||||||
|
pendingTo.date == "0000-00-00"}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
isOpen = false
|
isOpen = false;
|
||||||
from = toRFC3339(pendingFrom)
|
from = toRFC3339(pendingFrom);
|
||||||
to = toRFC3339(pendingTo, '59')
|
to = toRFC3339(pendingTo, "59");
|
||||||
dispatch('update', { from, to })
|
dispatch("update", { from, to });
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Close & Apply
|
Close & Apply
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="danger" on:click={() => {
|
<Button
|
||||||
isOpen = false
|
color="danger"
|
||||||
from = null
|
on:click={() => {
|
||||||
to = null
|
isOpen = false;
|
||||||
reset()
|
from = null;
|
||||||
dispatch('update', { from, to })
|
to = null;
|
||||||
}}>Reset</Button>
|
reset();
|
||||||
|
dispatch("update", { from, to });
|
||||||
|
}}>Reset</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,115 +1,137 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from 'svelte'
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
import {
|
||||||
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
|
||||||
|
|
||||||
const clusters = getContext('clusters'),
|
const clusters = getContext("clusters"),
|
||||||
initialized = getContext('initialized'),
|
initialized = getContext("initialized"),
|
||||||
dispatch = createEventDispatcher()
|
dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let cluster = null
|
export let cluster = null;
|
||||||
export let isModified = false
|
export let isModified = false;
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let stats = []
|
export let stats = [];
|
||||||
|
|
||||||
let statistics = [
|
let statistics = [
|
||||||
{
|
{
|
||||||
field: 'flopsAnyAvg',
|
field: "flopsAnyAvg",
|
||||||
text: 'FLOPs (Avg.)',
|
text: "FLOPs (Avg.)",
|
||||||
metric: 'flops_any',
|
metric: "flops_any",
|
||||||
from: 0, to: 0, peak: 0,
|
from: 0,
|
||||||
enabled: false
|
to: 0,
|
||||||
|
peak: 0,
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'memBwAvg',
|
field: "memBwAvg",
|
||||||
text: 'Mem. Bw. (Avg.)',
|
text: "Mem. Bw. (Avg.)",
|
||||||
metric: 'mem_bw',
|
metric: "mem_bw",
|
||||||
from: 0, to: 0, peak: 0,
|
from: 0,
|
||||||
enabled: false
|
to: 0,
|
||||||
|
peak: 0,
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'loadAvg',
|
field: "loadAvg",
|
||||||
text: 'Load (Avg.)',
|
text: "Load (Avg.)",
|
||||||
metric: 'cpu_load',
|
metric: "cpu_load",
|
||||||
from: 0, to: 0, peak: 0,
|
from: 0,
|
||||||
enabled: false
|
to: 0,
|
||||||
|
peak: 0,
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'memUsedMax',
|
field: "memUsedMax",
|
||||||
text: 'Mem. used (Max.)',
|
text: "Mem. used (Max.)",
|
||||||
metric: 'mem_used',
|
metric: "mem_used",
|
||||||
from: 0, to: 0, peak: 0,
|
from: 0,
|
||||||
enabled: false
|
to: 0,
|
||||||
}
|
peak: 0,
|
||||||
]
|
enabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
$: isModified = !statistics.every(a => {
|
$: isModified = !statistics.every((a) => {
|
||||||
let b = stats.find(s => s.field == a.field)
|
let b = stats.find((s) => s.field == a.field);
|
||||||
if (b == null)
|
if (b == null) return !a.enabled;
|
||||||
return !a.enabled
|
|
||||||
|
|
||||||
return a.from == b.from && a.to == b.to
|
return a.from == b.from && a.to == b.to;
|
||||||
})
|
});
|
||||||
|
|
||||||
function getPeak(cluster, metric) {
|
function getPeak(cluster, metric) {
|
||||||
const mc = cluster.metricConfig.find(mc => mc.name == metric)
|
const mc = cluster.metricConfig.find((mc) => mc.name == metric);
|
||||||
return mc ? mc.peak : 0
|
return mc ? mc.peak : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetRange(isInitialized, cluster) {
|
function resetRange(isInitialized, cluster) {
|
||||||
if (!isInitialized)
|
if (!isInitialized) return;
|
||||||
return
|
|
||||||
|
|
||||||
if (cluster != null) {
|
if (cluster != null) {
|
||||||
let c = clusters.find(c => c.name == cluster)
|
let c = clusters.find((c) => c.name == cluster);
|
||||||
for (let stat of statistics) {
|
for (let stat of statistics) {
|
||||||
stat.peak = getPeak(c, stat.metric)
|
stat.peak = getPeak(c, stat.metric);
|
||||||
stat.from = 0
|
stat.from = 0;
|
||||||
stat.to = stat.peak
|
stat.to = stat.peak;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let stat of statistics) {
|
for (let stat of statistics) {
|
||||||
for (let c of clusters) {
|
for (let c of clusters) {
|
||||||
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric))
|
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric));
|
||||||
}
|
}
|
||||||
stat.from = 0
|
stat.from = 0;
|
||||||
stat.to = stat.peak
|
stat.to = stat.peak;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics = [...statistics]
|
statistics = [...statistics];
|
||||||
}
|
}
|
||||||
|
|
||||||
$: resetRange($initialized, cluster)
|
$: resetRange($initialized, cluster);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Filter based on statistics (of non-running jobs)</ModalHeader>
|
||||||
Filter based on statistics (of non-running jobs)
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#each statistics as stat}
|
{#each statistics as stat}
|
||||||
<h4>{stat.text}</h4>
|
<h4>{stat.text}</h4>
|
||||||
<DoubleRangeSlider
|
<DoubleRangeSlider
|
||||||
on:change={({ detail }) => (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)}
|
on:change={({ detail }) => (
|
||||||
min={0} max={stat.peak}
|
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
|
||||||
firstSlider={stat.from} secondSlider={stat.to}
|
)}
|
||||||
inputFieldFrom={stat.from} inputFieldTo={stat.to}/>
|
min={0}
|
||||||
|
max={stat.peak}
|
||||||
|
firstSlider={stat.from}
|
||||||
|
secondSlider={stat.to}
|
||||||
|
inputFieldFrom={stat.from}
|
||||||
|
inputFieldTo={stat.to}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => {
|
<Button
|
||||||
isOpen = false
|
color="primary"
|
||||||
stats = statistics.filter(stat => stat.enabled)
|
on:click={() => {
|
||||||
dispatch('update', { stats })
|
isOpen = false;
|
||||||
}}>Close & Apply</Button>
|
stats = statistics.filter((stat) => stat.enabled);
|
||||||
<Button color="danger" on:click={() => {
|
dispatch("update", { stats });
|
||||||
isOpen = false
|
}}>Close & Apply</Button
|
||||||
resetRange($initialized, cluster)
|
>
|
||||||
statistics.forEach(stat => (stat.enabled = false))
|
<Button
|
||||||
stats = []
|
color="danger"
|
||||||
dispatch('update', { stats })
|
on:click={() => {
|
||||||
}}>Reset</Button>
|
isOpen = false;
|
||||||
|
resetRange($initialized, cluster);
|
||||||
|
statistics.forEach((stat) => (stat.enabled = false));
|
||||||
|
stats = [];
|
||||||
|
dispatch("update", { stats });
|
||||||
|
}}>Reset</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,48 +1,64 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from 'svelte'
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
import { Button, ListGroup, ListGroupItem, Input,
|
import {
|
||||||
Modal, ModalBody, ModalHeader, ModalFooter, Icon } from 'sveltestrap'
|
Button,
|
||||||
import { fuzzySearchTags } from '../utils.js'
|
ListGroup,
|
||||||
import Tag from '../Tag.svelte'
|
ListGroupItem,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
Icon,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { fuzzySearchTags } from "../utils.js";
|
||||||
|
import Tag from "../Tag.svelte";
|
||||||
|
|
||||||
const allTags = getContext('tags'),
|
const allTags = getContext("tags"),
|
||||||
initialized = getContext('initialized'),
|
initialized = getContext("initialized"),
|
||||||
dispatch = createEventDispatcher()
|
dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let isModified = false
|
export let isModified = false;
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let tags = []
|
export let tags = [];
|
||||||
|
|
||||||
let pendingTags = [...tags]
|
let pendingTags = [...tags];
|
||||||
$: isModified = tags.length != pendingTags.length || !tags.every(tagId => pendingTags.includes(tagId))
|
$: isModified =
|
||||||
|
tags.length != pendingTags.length ||
|
||||||
|
!tags.every((tagId) => pendingTags.includes(tagId));
|
||||||
|
|
||||||
let searchTerm = ''
|
let searchTerm = "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>
|
<ModalHeader>Select Tags</ModalHeader>
|
||||||
Select Tags
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Input type="text" placeholder="Search" bind:value={searchTerm} />
|
<Input type="text" placeholder="Search" bind:value={searchTerm} />
|
||||||
<br/>
|
<br />
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#if $initialized}
|
{#if $initialized}
|
||||||
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
|
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
{#if pendingTags.includes(tag.id)}
|
{#if pendingTags.includes(tag.id)}
|
||||||
<Button outline color="danger"
|
<Button
|
||||||
on:click={() => (pendingTags = pendingTags.filter(id => id != tag.id))}>
|
outline
|
||||||
|
color="danger"
|
||||||
|
on:click={() =>
|
||||||
|
(pendingTags = pendingTags.filter((id) => id != tag.id))}
|
||||||
|
>
|
||||||
<Icon name="dash-circle" />
|
<Icon name="dash-circle" />
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button outline color="success"
|
<Button
|
||||||
on:click={() => (pendingTags = [...pendingTags, tag.id])}>
|
outline
|
||||||
|
color="success"
|
||||||
|
on:click={() => (pendingTags = [...pendingTags, tag.id])}
|
||||||
|
>
|
||||||
<Icon name="plus-circle" />
|
<Icon name="plus-circle" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tag tag={tag} />
|
<Tag {tag} />
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{:else}
|
{:else}
|
||||||
<ListGroupItem disabled>No Tags</ListGroupItem>
|
<ListGroupItem disabled>No Tags</ListGroupItem>
|
||||||
@ -51,17 +67,23 @@
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => {
|
<Button
|
||||||
isOpen = false
|
color="primary"
|
||||||
tags = [...pendingTags]
|
on:click={() => {
|
||||||
dispatch('update', { tags })
|
isOpen = false;
|
||||||
}}>Close & Apply</Button>
|
tags = [...pendingTags];
|
||||||
<Button color="danger" on:click={() => {
|
dispatch("update", { tags });
|
||||||
isOpen = false
|
}}>Close & Apply</Button
|
||||||
tags = []
|
>
|
||||||
pendingTags = []
|
<Button
|
||||||
dispatch('update', { tags })
|
color="danger"
|
||||||
}}>Reset</Button>
|
on:click={() => {
|
||||||
|
isOpen = false;
|
||||||
|
tags = [];
|
||||||
|
pendingTags = [];
|
||||||
|
dispatch("update", { tags });
|
||||||
|
}}>Reset</Button
|
||||||
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,67 +1,76 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Icon, Input, InputGroup, InputGroupText } from 'sveltestrap'
|
import {
|
||||||
import { createEventDispatcher } from "svelte"
|
Icon,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupText,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let from
|
export let from;
|
||||||
export let to
|
export let to;
|
||||||
export let customEnabled = true
|
export let customEnabled = true;
|
||||||
export let anyEnabled = false
|
export let anyEnabled = false;
|
||||||
export let options = {
|
export let options = {
|
||||||
'Last quarter hour': 15*60,
|
"Last quarter hour": 15 * 60,
|
||||||
'Last half hour': 30*60,
|
"Last half hour": 30 * 60,
|
||||||
'Last hour': 60*60,
|
"Last hour": 60 * 60,
|
||||||
'Last 2hrs': 2*60*60,
|
"Last 2hrs": 2 * 60 * 60,
|
||||||
'Last 4hrs': 4*60*60,
|
"Last 4hrs": 4 * 60 * 60,
|
||||||
'Last 12hrs': 12*60*60,
|
"Last 12hrs": 12 * 60 * 60,
|
||||||
'Last 24hrs': 24*60*60
|
"Last 24hrs": 24 * 60 * 60,
|
||||||
}
|
};
|
||||||
|
|
||||||
$: pendingFrom = from
|
$: pendingFrom = from;
|
||||||
$: pendingTo = to
|
$: pendingTo = to;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
let timeRange = to && from
|
let timeRange =
|
||||||
? (to.getTime() - from.getTime()) / 1000
|
to && from ? (to.getTime() - from.getTime()) / 1000 : anyEnabled ? -2 : -1;
|
||||||
: (anyEnabled ? -2 : -1)
|
|
||||||
|
|
||||||
function updateTimeRange(event) {
|
function updateTimeRange(event) {
|
||||||
if (timeRange == -1) {
|
if (timeRange == -1) {
|
||||||
pendingFrom = null
|
pendingFrom = null;
|
||||||
pendingTo = null
|
pendingTo = null;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (timeRange == -2) {
|
if (timeRange == -2) {
|
||||||
from = pendingFrom = null
|
from = pendingFrom = null;
|
||||||
to = pendingTo = null
|
to = pendingTo = null;
|
||||||
dispatch('change', { from, to })
|
dispatch("change", { from, to });
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Date.now(), t = timeRange * 1000
|
let now = Date.now(),
|
||||||
from = pendingFrom = new Date(now - t)
|
t = timeRange * 1000;
|
||||||
to = pendingTo = new Date(now)
|
from = pendingFrom = new Date(now - t);
|
||||||
dispatch('change', { from, to })
|
to = pendingTo = new Date(now);
|
||||||
|
dispatch("change", { from, to });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateExplicitTimeRange(type, event) {
|
function updateExplicitTimeRange(type, event) {
|
||||||
let d = new Date(Date.parse(event.target.value));
|
let d = new Date(Date.parse(event.target.value));
|
||||||
if (type == 'from') pendingFrom = d
|
if (type == "from") pendingFrom = d;
|
||||||
else pendingTo = d
|
else pendingTo = d;
|
||||||
|
|
||||||
if (pendingFrom != null && pendingTo != null) {
|
if (pendingFrom != null && pendingTo != null) {
|
||||||
from = pendingFrom
|
from = pendingFrom;
|
||||||
to = pendingTo
|
to = pendingTo;
|
||||||
dispatch('change', { from, to })
|
dispatch("change", { from, to });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InputGroup class="inline-from">
|
<InputGroup class="inline-from">
|
||||||
<InputGroupText><Icon name="clock-history"/></InputGroupText>
|
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
||||||
<!-- <InputGroupText>
|
<!-- <InputGroupText>
|
||||||
Time
|
Time
|
||||||
</InputGroupText> -->
|
</InputGroupText> -->
|
||||||
<select class="form-select" bind:value={timeRange} on:change={updateTimeRange}>
|
<select
|
||||||
|
class="form-select"
|
||||||
|
bind:value={timeRange}
|
||||||
|
on:change={updateTimeRange}
|
||||||
|
>
|
||||||
{#if customEnabled}
|
{#if customEnabled}
|
||||||
<option value={-1}>Custom</option>
|
<option value={-1}>Custom</option>
|
||||||
{/if}
|
{/if}
|
||||||
@ -74,8 +83,14 @@
|
|||||||
</select>
|
</select>
|
||||||
{#if timeRange == -1}
|
{#if timeRange == -1}
|
||||||
<InputGroupText>from</InputGroupText>
|
<InputGroupText>from</InputGroupText>
|
||||||
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('from', event)}></Input>
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
on:change={(event) => updateExplicitTimeRange("from", event)}
|
||||||
|
></Input>
|
||||||
<InputGroupText>to</InputGroupText>
|
<InputGroupText>to</InputGroupText>
|
||||||
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('to', event)}></Input>
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
on:change={(event) => updateExplicitTimeRange("to", event)}
|
||||||
|
></Input>
|
||||||
{/if}
|
{/if}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
@ -1,75 +1,84 @@
|
|||||||
<script>
|
<script>
|
||||||
import { InputGroup, Input } from 'sveltestrap'
|
import { InputGroup, Input } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let user = ''
|
export let user = "";
|
||||||
export let project = ''
|
export let project = "";
|
||||||
export let authlevel
|
export let authlevel;
|
||||||
export let roles
|
export let roles;
|
||||||
let mode = 'user', term = ''
|
let mode = "user",
|
||||||
const throttle = 500
|
term = "";
|
||||||
|
const throttle = 500;
|
||||||
|
|
||||||
function modeChanged() {
|
function modeChanged() {
|
||||||
if (mode == 'user') {
|
if (mode == "user") {
|
||||||
project = term
|
project = term;
|
||||||
term = user
|
term = user;
|
||||||
} else {
|
} else {
|
||||||
user = term
|
user = term;
|
||||||
term = project
|
term = project;
|
||||||
}
|
}
|
||||||
termChanged(0)
|
termChanged(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutId = null
|
let timeoutId = null;
|
||||||
// Compatibility: Handle "user role" and "no role" identically
|
// Compatibility: Handle "user role" and "no role" identically
|
||||||
function termChanged(sleep = throttle) {
|
function termChanged(sleep = throttle) {
|
||||||
if (authlevel >= roles.manager) {
|
if (authlevel >= roles.manager) {
|
||||||
if (mode == 'user')
|
if (mode == "user") user = term;
|
||||||
user = term
|
else project = term;
|
||||||
else
|
|
||||||
project = term
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
dispatch('update', {
|
dispatch("update", {
|
||||||
user,
|
user,
|
||||||
project
|
project,
|
||||||
})
|
});
|
||||||
}, sleep)
|
}, sleep);
|
||||||
} else {
|
} else {
|
||||||
project = term
|
project = term;
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
dispatch('update', {
|
dispatch("update", {
|
||||||
project
|
project,
|
||||||
})
|
});
|
||||||
}, sleep)
|
}, sleep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if authlevel >= roles.manager}
|
{#if authlevel >= roles.manager}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<select style="max-width: 175px;" class="form-select"
|
<select
|
||||||
bind:value={mode} on:change={modeChanged}>
|
style="max-width: 175px;"
|
||||||
<option value={'user'}>Search User</option>
|
class="form-select"
|
||||||
<option value={'project'}>Search Project</option>
|
bind:value={mode}
|
||||||
|
on:change={modeChanged}
|
||||||
|
>
|
||||||
|
<option value={"user"}>Search User</option>
|
||||||
|
<option value={"project"}>Search Project</option>
|
||||||
</select>
|
</select>
|
||||||
<Input
|
<Input
|
||||||
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
|
type="text"
|
||||||
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
|
bind:value={term}
|
||||||
|
on:change={() => termChanged()}
|
||||||
|
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
||||||
|
placeholder={mode == "user" ? "filter username..." : "filter project..."}
|
||||||
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Compatibility: Handle "user role" and "no role" identically-->
|
<!-- Compatibility: Handle "user role" and "no role" identically-->
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...'
|
type="text"
|
||||||
|
bind:value={term}
|
||||||
|
on:change={() => termChanged()}
|
||||||
|
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
||||||
|
placeholder="filter project..."
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -6,15 +6,20 @@
|
|||||||
- jobTags: Defaults to job.tags, usefull for dynamically updating the tags.
|
- jobTags: Defaults to job.tags, usefull for dynamically updating the tags.
|
||||||
-->
|
-->
|
||||||
<script context="module">
|
<script context="module">
|
||||||
export const scrambleNames = window.localStorage.getItem("cc-scramble-names")
|
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
|
||||||
export const scramble = function(str) {
|
export const scramble = function (str) {
|
||||||
if (str === '-') return str
|
if (str === "-") return str;
|
||||||
else return [...str].reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5).toString(32).substr(0, 6)
|
else
|
||||||
}
|
return [...str]
|
||||||
|
.reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5)
|
||||||
|
.toString(32)
|
||||||
|
.substr(0, 6);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Tag from '../Tag.svelte';
|
import Tag from "../Tag.svelte";
|
||||||
import { Badge, Icon } from 'sveltestrap';
|
import { Badge, Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let job;
|
export let job;
|
||||||
export let jobTags = job.tags;
|
export let jobTags = job.tags;
|
||||||
@ -25,50 +30,65 @@
|
|||||||
const minutes = Math.floor(duration / 60);
|
const minutes = Math.floor(duration / 60);
|
||||||
duration -= minutes * 60;
|
duration -= minutes * 60;
|
||||||
const seconds = duration;
|
const seconds = duration;
|
||||||
return `${hours}:${('0' + minutes).slice(-2)}:${('0' + seconds).slice(-2)}`;
|
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStateColor(state) {
|
function getStateColor(state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'running':
|
case "running":
|
||||||
return 'success'
|
return "success";
|
||||||
case 'completed':
|
case "completed":
|
||||||
return 'primary'
|
return "primary";
|
||||||
default:
|
default:
|
||||||
return 'danger'
|
return "danger";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<span class="fw-bold"><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> ({job.cluster})</span>
|
<span class="fw-bold"
|
||||||
|
><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
||||||
|
({job.cluster})</span
|
||||||
|
>
|
||||||
{#if job.metaData?.jobName}
|
{#if job.metaData?.jobName}
|
||||||
<br/>
|
<br />
|
||||||
{#if job.metaData?.jobName.length <= 25}
|
{#if job.metaData?.jobName.length <= 25}
|
||||||
<div>{job.metaData.jobName}</div>
|
<div>{job.metaData.jobName}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="truncate" style="cursor:help; width:230px;" title={job.metaData.jobName}>{job.metaData.jobName}</div>
|
<div
|
||||||
|
class="truncate"
|
||||||
|
style="cursor:help; width:230px;"
|
||||||
|
title={job.metaData.jobName}
|
||||||
|
>
|
||||||
|
{job.metaData.jobName}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if job.arrayJobId}
|
{#if job.arrayJobId}
|
||||||
Array Job: <a href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" target="_blank">#{job.arrayJobId}</a>
|
Array Job: <a
|
||||||
|
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
|
||||||
|
target="_blank">#{job.arrayJobId}</a
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Icon name="person-fill"/>
|
<Icon name="person-fill" />
|
||||||
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
|
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
|
||||||
{scrambleNames ? scramble(job.user) : job.user}
|
{scrambleNames ? scramble(job.user) : job.user}
|
||||||
</a>
|
</a>
|
||||||
{#if job.userData && job.userData.name}
|
{#if job.userData && job.userData.name}
|
||||||
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
|
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
|
||||||
{/if}
|
{/if}
|
||||||
{#if job.project && job.project != 'no project'}
|
{#if job.project && job.project != "no project"}
|
||||||
<br/>
|
<br />
|
||||||
<Icon name="people-fill"/>
|
<Icon name="people-fill" />
|
||||||
<a class="fst-italic" href="/monitoring/jobs/?project={job.project}&projectMatch=eq" target="_blank">
|
<a
|
||||||
|
class="fst-italic"
|
||||||
|
href="/monitoring/jobs/?project={job.project}&projectMatch=eq"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
{scrambleNames ? scramble(job.project) : job.project}
|
{scrambleNames ? scramble(job.project) : job.project}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
@ -80,33 +100,36 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{job.numNodes}
|
{job.numNodes}
|
||||||
{/if}
|
{/if}
|
||||||
<Icon name="pc-horizontal"/>
|
<Icon name="pc-horizontal" />
|
||||||
{#if job.exclusive != 1}
|
{#if job.exclusive != 1}
|
||||||
(shared)
|
(shared)
|
||||||
{/if}
|
{/if}
|
||||||
{#if job.numAcc > 0}
|
{#if job.numAcc > 0}
|
||||||
, {job.numAcc} <Icon name="gpu-card"/>
|
, {job.numAcc} <Icon name="gpu-card" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if job.numHWThreads > 0}
|
{#if job.numHWThreads > 0}
|
||||||
, {job.numHWThreads} <Icon name="cpu"/>
|
, {job.numHWThreads} <Icon name="cpu" />
|
||||||
{/if}
|
{/if}
|
||||||
<br/>
|
<br />
|
||||||
{job.subCluster}
|
{job.subCluster}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Start: <span class="fw-bold">{(new Date(job.startTime)).toLocaleString()}</span>
|
Start: <span class="fw-bold"
|
||||||
<br/>
|
>{new Date(job.startTime).toLocaleString()}</span
|
||||||
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span> <Badge color="{getStateColor(job.state)}">{job.state}</Badge>
|
>
|
||||||
|
<br />
|
||||||
|
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span>
|
||||||
|
<Badge color={getStateColor(job.state)}>{job.state}</Badge>
|
||||||
{#if job.walltime}
|
{#if job.walltime}
|
||||||
<br/>
|
<br />
|
||||||
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
|
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{#each jobTags as tag}
|
{#each jobTags as tag}
|
||||||
<Tag tag={tag}/>
|
<Tag {tag} />
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
mutationStore,
|
mutationStore,
|
||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { Row, Table, Card, Spinner } from "sveltestrap";
|
import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import Pagination from "./Pagination.svelte";
|
import Pagination from "./Pagination.svelte";
|
||||||
import JobListRow from "./Row.svelte";
|
import JobListRow from "./Row.svelte";
|
||||||
import { stickyHeader } from "../utils.js";
|
import { stickyHeader } from "../utils.js";
|
||||||
@ -86,7 +86,7 @@
|
|||||||
$: jobs = queryStore({
|
$: jobs = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: query,
|
query: query,
|
||||||
variables: { paging, sorting, filter }
|
variables: { paging, sorting, filter },
|
||||||
});
|
});
|
||||||
|
|
||||||
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
|
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
|
||||||
@ -97,7 +97,7 @@
|
|||||||
client: client,
|
client: client,
|
||||||
query: query,
|
query: query,
|
||||||
variables: { paging, sorting, filter },
|
variables: { paging, sorting, filter },
|
||||||
requestPolicy: 'network-only'
|
requestPolicy: "network-only",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,21 +122,23 @@
|
|||||||
updateConfiguration(name: $name, value: $value)
|
updateConfiguration(name: $name, value: $value)
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { name, value }
|
variables: { name, value },
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function updateConfiguration(value, page) {
|
function updateConfiguration(value, page) {
|
||||||
updateConfigurationMutation({ name: 'plot_list_jobsPerPage', value: value })
|
updateConfigurationMutation({
|
||||||
.subscribe(res => {
|
name: "plot_list_jobsPerPage",
|
||||||
|
value: value,
|
||||||
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && !res.error) {
|
||||||
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
|
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
|
||||||
} else if (res.fetching === false && res.error) {
|
} else if (res.fetching === false && res.error) {
|
||||||
throw res.error
|
throw res.error;
|
||||||
// console.log('Error on subscription: ' + res.error)
|
// console.log('Error on subscription: ' + res.error)
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
let plotWidth = null;
|
let plotWidth = null;
|
||||||
let tableWidth = null;
|
let tableWidth = null;
|
||||||
@ -144,18 +146,18 @@
|
|||||||
|
|
||||||
$: if (showFootprint) {
|
$: if (showFootprint) {
|
||||||
plotWidth = Math.floor(
|
plotWidth = Math.floor(
|
||||||
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10
|
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
plotWidth = Math.floor(
|
plotWidth = Math.floor(
|
||||||
(tableWidth - jobInfoColumnWidth) / metrics.length - 10
|
(tableWidth - jobInfoColumnWidth) / metrics.length - 10,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let headerPaddingTop = 0;
|
let headerPaddingTop = 0;
|
||||||
stickyHeader(
|
stickyHeader(
|
||||||
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
||||||
(x) => (headerPaddingTop = x)
|
(x) => (headerPaddingTop = x),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -190,24 +192,18 @@
|
|||||||
{#if $initialized}
|
{#if $initialized}
|
||||||
({clusters
|
({clusters
|
||||||
.map((cluster) =>
|
.map((cluster) =>
|
||||||
cluster.metricConfig.find(
|
cluster.metricConfig.find((m) => m.name == metric),
|
||||||
(m) => m.name == metric
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.filter((m) => m != null)
|
.filter((m) => m != null)
|
||||||
.map(
|
.map(
|
||||||
(m) =>
|
(m) =>
|
||||||
(m.unit?.prefix
|
(m.unit?.prefix ? m.unit?.prefix : "") +
|
||||||
? m.unit?.prefix
|
(m.unit?.base ? m.unit?.base : ""),
|
||||||
: "") +
|
|
||||||
(m.unit?.base ? m.unit?.base : "")
|
|
||||||
) // Build unitStr
|
) // Build unitStr
|
||||||
.reduce(
|
.reduce(
|
||||||
(arr, unitStr) =>
|
(arr, unitStr) =>
|
||||||
arr.includes(unitStr)
|
arr.includes(unitStr) ? arr : [...arr, unitStr],
|
||||||
? arr
|
[],
|
||||||
: [...arr, unitStr],
|
|
||||||
[]
|
|
||||||
) // w/o this, output would be [unitStr, unitStr]
|
) // w/o this, output would be [unitStr, unitStr]
|
||||||
.join(", ")})
|
.join(", ")})
|
||||||
{/if}
|
{/if}
|
||||||
@ -232,12 +228,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{:else if $jobs.data && $initialized}
|
{:else if $jobs.data && $initialized}
|
||||||
{#each $jobs.data.jobs.items as job (job)}
|
{#each $jobs.data.jobs.items as job (job)}
|
||||||
<JobListRow {job} {metrics} {plotWidth} {showFootprint}/>
|
<JobListRow {job} {metrics} {plotWidth} {showFootprint} />
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={metrics.length + 1}>
|
<td colspan={metrics.length + 1}> No jobs found </td>
|
||||||
No jobs found
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@ -253,12 +247,9 @@
|
|||||||
totalItems={matchedJobs}
|
totalItems={matchedJobs}
|
||||||
on:update={({ detail }) => {
|
on:update={({ detail }) => {
|
||||||
if (detail.itemsPerPage != itemsPerPage) {
|
if (detail.itemsPerPage != itemsPerPage) {
|
||||||
updateConfiguration(
|
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
||||||
detail.itemsPerPage.toString(),
|
|
||||||
detail.page
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
|
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -5,39 +5,46 @@
|
|||||||
- 'reload': When fired, the parent component shoud refresh its contents
|
- 'reload': When fired, the parent component shoud refresh its contents
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from "svelte";
|
||||||
import { Button, Icon, InputGroup } from 'sveltestrap'
|
import { Button, Icon, InputGroup } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let refreshInterval = null;
|
let refreshInterval = null;
|
||||||
let refreshIntervalId = null;
|
let refreshIntervalId = null;
|
||||||
function refreshIntervalChanged() {
|
function refreshIntervalChanged() {
|
||||||
if (refreshIntervalId != null)
|
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
|
||||||
clearInterval(refreshIntervalId);
|
|
||||||
|
|
||||||
if (refreshInterval == null)
|
if (refreshInterval == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
|
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
export let initially = null
|
export let initially = null;
|
||||||
if (initially != null) {
|
if (initially != null) {
|
||||||
refreshInterval = initially * 1000
|
refreshInterval = initially * 1000;
|
||||||
refreshIntervalChanged()
|
refreshIntervalChanged();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Button outline on:click={() => dispatch("reload")} disabled={refreshInterval != null}>
|
<Button
|
||||||
|
outline
|
||||||
|
on:click={() => dispatch("reload")}
|
||||||
|
disabled={refreshInterval != null}
|
||||||
|
>
|
||||||
<Icon name="arrow-clockwise" /> Reload
|
<Icon name="arrow-clockwise" /> Reload
|
||||||
</Button>
|
</Button>
|
||||||
<select class="form-select" bind:value={refreshInterval} on:change={refreshIntervalChanged}>
|
<select
|
||||||
|
class="form-select"
|
||||||
|
bind:value={refreshInterval}
|
||||||
|
on:change={refreshIntervalChanged}
|
||||||
|
>
|
||||||
<option value={null}>No periodic reload</option>
|
<option value={null}>No periodic reload</option>
|
||||||
<option value={ 30 * 1000}>Update every 30 seconds</option>
|
<option value={30 * 1000}>Update every 30 seconds</option>
|
||||||
<option value={ 60 * 1000}>Update every minute</option>
|
<option value={60 * 1000}>Update every minute</option>
|
||||||
<option value={2 * 60 * 1000}>Update every two minutes</option>
|
<option value={2 * 60 * 1000}>Update every two minutes</option>
|
||||||
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
|
<option value={5 * 60 * 1000}>Update every 5 minutes</option>
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { Card, Spinner } from "sveltestrap";
|
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import MetricPlot from "../plots/MetricPlot.svelte";
|
import MetricPlot from "../plots/MetricPlot.svelte";
|
||||||
import JobInfo from "./JobInfo.svelte";
|
import JobInfo from "./JobInfo.svelte";
|
||||||
import JobFootprint from "../JobFootprint.svelte";
|
import JobFootprint from "../JobFootprint.svelte";
|
||||||
@ -67,16 +67,23 @@
|
|||||||
$: metricsQuery = queryStore({
|
$: metricsQuery = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: query,
|
query: query,
|
||||||
variables: { id, queryMetrics, scopes }
|
variables: { id, queryMetrics, scopes },
|
||||||
});
|
});
|
||||||
|
|
||||||
let queryMetrics = null
|
let queryMetrics = null;
|
||||||
$: if (showFootprint) {
|
$: if (showFootprint) {
|
||||||
queryMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw', 'acc_utilization', ...metrics].filter(distinct)
|
queryMetrics = [
|
||||||
scopes = ["node"]
|
"cpu_load",
|
||||||
|
"flops_any",
|
||||||
|
"mem_used",
|
||||||
|
"mem_bw",
|
||||||
|
"acc_utilization",
|
||||||
|
...metrics,
|
||||||
|
].filter(distinct);
|
||||||
|
scopes = ["node"];
|
||||||
} else {
|
} else {
|
||||||
queryMetrics = [...metrics]
|
queryMetrics = [...metrics];
|
||||||
scopes = [job.numNodes == 1 ? "core" : "node"]
|
scopes = [job.numNodes == 1 ? "core" : "node"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refresh() {
|
export function refresh() {
|
||||||
@ -98,20 +105,30 @@
|
|||||||
: job.numNodes > 1
|
: job.numNodes > 1
|
||||||
? b
|
? b
|
||||||
: a,
|
: a,
|
||||||
jobMetrics[0]
|
jobMetrics[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sortAndSelectScope = (jobMetrics) =>
|
||||||
const sortAndSelectScope = (jobMetrics) => metrics
|
metrics
|
||||||
.map(name => jobMetrics.filter(jobMetric => jobMetric.name == name))
|
.map((name) => jobMetrics.filter((jobMetric) => jobMetric.name == name))
|
||||||
.map(jobMetrics => ({ disabled: false, data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null }))
|
.map((jobMetrics) => ({
|
||||||
.map(jobMetric => {
|
disabled: false,
|
||||||
|
data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null,
|
||||||
|
}))
|
||||||
|
.map((jobMetric) => {
|
||||||
if (jobMetric.data) {
|
if (jobMetric.data) {
|
||||||
return { disabled: checkMetricDisabled(jobMetric.data.name, job.cluster, job.subCluster), data: jobMetric.data }
|
return {
|
||||||
|
disabled: checkMetricDisabled(
|
||||||
|
jobMetric.data.name,
|
||||||
|
job.cluster,
|
||||||
|
job.subCluster,
|
||||||
|
),
|
||||||
|
data: jobMetric.data,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return jobMetric
|
return jobMetric;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (job.monitoringStatus) refresh();
|
if (job.monitoringStatus) refresh();
|
||||||
</script>
|
</script>
|
||||||
@ -140,7 +157,7 @@
|
|||||||
{#if showFootprint}
|
{#if showFootprint}
|
||||||
<td>
|
<td>
|
||||||
<JobFootprint
|
<JobFootprint
|
||||||
job={job}
|
{job}
|
||||||
jobMetrics={$metricsQuery.data.jobMetrics}
|
jobMetrics={$metricsQuery.data.jobMetrics}
|
||||||
width={plotWidth}
|
width={plotWidth}
|
||||||
view="list"
|
view="list"
|
||||||
@ -161,13 +178,17 @@
|
|||||||
metric={metric.data.name}
|
metric={metric.data.name}
|
||||||
{cluster}
|
{cluster}
|
||||||
subCluster={job.subCluster}
|
subCluster={job.subCluster}
|
||||||
isShared={(job.exclusive != 1)}
|
isShared={job.exclusive != 1}
|
||||||
resources={job.resources}
|
resources={job.resources}
|
||||||
numhwthreads={job.numHWThreads}
|
numhwthreads={job.numHWThreads}
|
||||||
numaccs={job.numAcc}
|
numaccs={job.numAcc}
|
||||||
/>
|
/>
|
||||||
{:else if metric.disabled == true && metric.data}
|
{:else if metric.disabled == true && metric.data}
|
||||||
<Card body color="info">Metric disabled for subcluster <code>{metric.data.name}:{job.subCluster}</code></Card>
|
<Card body color="info"
|
||||||
|
>Metric disabled for subcluster <code
|
||||||
|
>{metric.data.name}:{job.subCluster}</code
|
||||||
|
></Card
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning">No dataset returned</Card>
|
<Card body color="warning">No dataset returned</Card>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -7,47 +7,70 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Button, ListGroup, ListGroupItem,
|
import {
|
||||||
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
Icon,
|
||||||
|
Button,
|
||||||
|
ListGroup,
|
||||||
|
ListGroupItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let isOpen = false
|
export let isOpen = false;
|
||||||
export let sorting = { field: 'startTime', order: 'DESC' }
|
export let sorting = { field: "startTime", order: "DESC" };
|
||||||
|
|
||||||
let sortableColumns = [
|
let sortableColumns = [
|
||||||
{ field: 'startTime', text: 'Start Time', order: 'DESC' },
|
{ field: "startTime", text: "Start Time", order: "DESC" },
|
||||||
{ field: 'duration', text: 'Duration', order: 'DESC' },
|
{ field: "duration", text: "Duration", order: "DESC" },
|
||||||
{ field: 'numNodes', text: 'Number of Nodes', order: 'DESC' },
|
{ field: "numNodes", text: "Number of Nodes", order: "DESC" },
|
||||||
{ field: 'memUsedMax', text: 'Max. Memory Used', order: 'DESC' },
|
{ field: "memUsedMax", text: "Max. Memory Used", order: "DESC" },
|
||||||
{ field: 'flopsAnyAvg', text: 'Avg. FLOPs', order: 'DESC' },
|
{ field: "flopsAnyAvg", text: "Avg. FLOPs", order: "DESC" },
|
||||||
{ field: 'memBwAvg', text: 'Avg. Memory Bandwidth', order: 'DESC' },
|
{ field: "memBwAvg", text: "Avg. Memory Bandwidth", order: "DESC" },
|
||||||
{ field: 'netBwAvg', text: 'Avg. Network Bandwidth', order: 'DESC' }
|
{ field: "netBwAvg", text: "Avg. Network Bandwidth", order: "DESC" },
|
||||||
]
|
];
|
||||||
|
|
||||||
let activeColumnIdx = sortableColumns.findIndex(col => col.field == sorting.field)
|
let activeColumnIdx = sortableColumns.findIndex(
|
||||||
sortableColumns[activeColumnIdx].order = sorting.order
|
(col) => col.field == sorting.field,
|
||||||
|
);
|
||||||
|
sortableColumns[activeColumnIdx].order = sorting.order;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={() => { isOpen = !isOpen }}>
|
<Modal
|
||||||
<ModalHeader>
|
{isOpen}
|
||||||
Sort rows
|
toggle={() => {
|
||||||
</ModalHeader>
|
isOpen = !isOpen;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalHeader>Sort rows</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#each sortableColumns as col, i (col)}
|
{#each sortableColumns as col, i (col)}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<button class="sort" on:click={() => {
|
<button
|
||||||
|
class="sort"
|
||||||
|
on:click={() => {
|
||||||
if (activeColumnIdx == i) {
|
if (activeColumnIdx == i) {
|
||||||
col.order = col.order == 'DESC' ? 'ASC' : 'DESC'
|
col.order = col.order == "DESC" ? "ASC" : "DESC";
|
||||||
} else {
|
} else {
|
||||||
sortableColumns[activeColumnIdx] = { ...sortableColumns[activeColumnIdx] }
|
sortableColumns[activeColumnIdx] = {
|
||||||
|
...sortableColumns[activeColumnIdx],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
sortableColumns[i] = { ...sortableColumns[i] }
|
sortableColumns[i] = { ...sortableColumns[i] };
|
||||||
activeColumnIdx = i
|
activeColumnIdx = i;
|
||||||
sortableColumns = [...sortableColumns]
|
sortableColumns = [...sortableColumns];
|
||||||
sorting = { field: col.field, order: col.order }
|
sorting = { field: col.field, order: col.order };
|
||||||
}}>
|
}}
|
||||||
<Icon name="arrow-{col.order == 'DESC' ? 'down' : 'up'}-circle{i == activeColumnIdx ? '-fill' : ''}"/>
|
>
|
||||||
|
<Icon
|
||||||
|
name="arrow-{col.order == 'DESC' ? 'down' : 'up'}-circle{i ==
|
||||||
|
activeColumnIdx
|
||||||
|
? '-fill'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{col.text}
|
{col.text}
|
||||||
@ -56,7 +79,12 @@
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => { isOpen = false }}>Close</Button>
|
<Button
|
||||||
|
color="primary"
|
||||||
|
on:click={() => {
|
||||||
|
isOpen = false;
|
||||||
|
}}>Close</Button
|
||||||
|
>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@ -69,3 +97,4 @@
|
|||||||
transition: all 70ms;
|
transition: all 70ms;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -5,22 +5,22 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import uPlot from 'uplot'
|
import uPlot from "uplot";
|
||||||
import { formatNumber } from '../units.js'
|
import { formatNumber } from "../units.js";
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { Card } from 'sveltestrap'
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let data
|
export let data;
|
||||||
export let usesBins = false
|
export let usesBins = false;
|
||||||
export let width = 500
|
export let width = 500;
|
||||||
export let height = 300
|
export let height = 300;
|
||||||
export let title = ''
|
export let title = "";
|
||||||
export let xlabel = ''
|
export let xlabel = "";
|
||||||
export let xunit = 'X'
|
export let xunit = "X";
|
||||||
export let ylabel = ''
|
export let ylabel = "";
|
||||||
export let yunit = 'Y'
|
export let yunit = "Y";
|
||||||
|
|
||||||
const { bars } = uPlot.paths
|
const { bars } = uPlot.paths;
|
||||||
|
|
||||||
const drawStyles = {
|
const drawStyles = {
|
||||||
bars: 1,
|
bars: 1,
|
||||||
@ -31,18 +31,17 @@
|
|||||||
let s = u.series[seriesIdx];
|
let s = u.series[seriesIdx];
|
||||||
let style = s.drawStyle;
|
let style = s.drawStyle;
|
||||||
|
|
||||||
let renderer = ( // If bars to wide, change here
|
let renderer = // If bars to wide, change here
|
||||||
style == drawStyles.bars ? (
|
style == drawStyles.bars ? bars({ size: [0.75, 100] }) : () => null;
|
||||||
bars({size: [0.75, 100]})
|
|
||||||
) :
|
|
||||||
() => null
|
|
||||||
)
|
|
||||||
|
|
||||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
// converts the legend into a simple tooltip
|
// converts the legend into a simple tooltip
|
||||||
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
function legendAsTooltipPlugin({
|
||||||
|
className,
|
||||||
|
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||||
|
} = {}) {
|
||||||
let legendEl;
|
let legendEl;
|
||||||
|
|
||||||
function init(u, opts) {
|
function init(u, opts) {
|
||||||
@ -60,14 +59,13 @@
|
|||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||||
...style
|
...style,
|
||||||
});
|
});
|
||||||
|
|
||||||
// hide series color markers
|
// hide series color markers
|
||||||
const idents = legendEl.querySelectorAll(".u-marker");
|
const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
|
|
||||||
for (let i = 0; i < idents.length; i++)
|
for (let i = 0; i < idents.length; i++) idents[i].style.display = "none";
|
||||||
idents[i].style.display = "none";
|
|
||||||
|
|
||||||
const overEl = u.over;
|
const overEl = u.over;
|
||||||
overEl.style.overflow = "visible";
|
overEl.style.overflow = "visible";
|
||||||
@ -76,8 +74,12 @@
|
|||||||
overEl.appendChild(legendEl);
|
overEl.appendChild(legendEl);
|
||||||
|
|
||||||
// show/hide tooltip on enter/exit
|
// show/hide tooltip on enter/exit
|
||||||
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
overEl.addEventListener("mouseenter", () => {
|
||||||
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
legendEl.style.display = null;
|
||||||
|
});
|
||||||
|
overEl.addEventListener("mouseleave", () => {
|
||||||
|
legendEl.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
// let tooltip exit plot
|
// let tooltip exit plot
|
||||||
// overEl.style.overflow = "visible";
|
// overEl.style.overflow = "visible";
|
||||||
@ -85,40 +87,40 @@
|
|||||||
|
|
||||||
function update(u) {
|
function update(u) {
|
||||||
const { left, top } = u.cursor;
|
const { left, top } = u.cursor;
|
||||||
legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
legendEl.style.transform =
|
||||||
|
"translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hooks: {
|
hooks: {
|
||||||
init: init,
|
init: init,
|
||||||
setCursor: update,
|
setCursor: update,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let plotWrapper = null
|
let plotWrapper = null;
|
||||||
let uplot = null
|
let uplot = null;
|
||||||
let timeoutId = null
|
let timeoutId = null;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
let opts = {
|
let opts = {
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
title: title,
|
title: title,
|
||||||
plugins: [
|
plugins: [legendAsTooltipPlugin()],
|
||||||
legendAsTooltipPlugin()
|
|
||||||
],
|
|
||||||
cursor: {
|
cursor: {
|
||||||
points: {
|
points: {
|
||||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
||||||
width: (u, seriesIdx, size) => size / 4,
|
width: (u, seriesIdx, size) => size / 4,
|
||||||
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '90',
|
stroke: (u, seriesIdx) =>
|
||||||
|
u.series[seriesIdx].points.stroke(u, seriesIdx) + "90",
|
||||||
fill: (u, seriesIdx) => "#fff",
|
fill: (u, seriesIdx) => "#fff",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
time: false
|
time: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axes: [
|
axes: [
|
||||||
@ -138,7 +140,7 @@
|
|||||||
size: 5 / devicePixelRatio,
|
size: 5 / devicePixelRatio,
|
||||||
stroke: "#000000",
|
stroke: "#000000",
|
||||||
},
|
},
|
||||||
values: (_, t) => t.map(v => formatNumber(v)),
|
values: (_, t) => t.map((v) => formatNumber(v)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stroke: "#000000",
|
stroke: "#000000",
|
||||||
@ -155,71 +157,70 @@
|
|||||||
size: 5 / devicePixelRatio,
|
size: 5 / devicePixelRatio,
|
||||||
stroke: "#000000",
|
stroke: "#000000",
|
||||||
},
|
},
|
||||||
values: (_, t) => t.map(v => formatNumber(v)),
|
values: (_, t) => t.map((v) => formatNumber(v)),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
label: xunit !== '' ? xunit : null,
|
label: xunit !== "" ? xunit : null,
|
||||||
value: (u, ts, sidx, didx) => {
|
value: (u, ts, sidx, didx) => {
|
||||||
if (usesBins) {
|
if (usesBins) {
|
||||||
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0
|
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0;
|
||||||
const max = u.data[sidx][didx]
|
const max = u.data[sidx][didx];
|
||||||
ts = min + ' - ' + max // narrow spaces
|
ts = min + " - " + max; // narrow spaces
|
||||||
}
|
|
||||||
return ts
|
|
||||||
}
|
}
|
||||||
|
return ts;
|
||||||
},
|
},
|
||||||
Object.assign({
|
},
|
||||||
label: yunit !== '' ? yunit : null,
|
Object.assign(
|
||||||
|
{
|
||||||
|
label: yunit !== "" ? yunit : null,
|
||||||
width: 1 / devicePixelRatio,
|
width: 1 / devicePixelRatio,
|
||||||
drawStyle: drawStyles.points,
|
drawStyle: drawStyles.points,
|
||||||
lineInterpolation: null,
|
lineInterpolation: null,
|
||||||
paths,
|
paths,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
drawStyle: drawStyles.bars,
|
drawStyle: drawStyles.bars,
|
||||||
lineInterpolation: null,
|
lineInterpolation: null,
|
||||||
stroke: "#85abce",
|
stroke: "#85abce",
|
||||||
fill: "#85abce", // + "1A", // Transparent Fill
|
fill: "#85abce", // + "1A", // Transparent Fill
|
||||||
}),
|
},
|
||||||
]
|
),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
uplot = new uPlot(opts, data, plotWrapper)
|
uplot = new uPlot(opts, data, plotWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
render()
|
render();
|
||||||
})
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (uplot)
|
if (uplot) uplot.destroy();
|
||||||
uplot.destroy()
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
function sizeChanged() {
|
function sizeChanged() {
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
timeoutId = null
|
timeoutId = null;
|
||||||
if (uplot)
|
if (uplot) uplot.destroy();
|
||||||
uplot.destroy()
|
|
||||||
|
|
||||||
render()
|
render();
|
||||||
}, 200)
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: sizeChanged(width, height)
|
$: sizeChanged(width, height);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data.length > 0}
|
{#if data.length > 0}
|
||||||
<div bind:this={plotWrapper}/>
|
<div bind:this={plotWrapper} />
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="mx-4" body color="warning">Cannot render histogram: No data!</Card>
|
<Card class="mx-4" body color="warning"
|
||||||
|
>Cannot render histogram: No data!</Card
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,121 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function formatTime(t, forNode = false) {
|
||||||
|
if (t !== null) {
|
||||||
|
if (isNaN(t)) {
|
||||||
|
return t;
|
||||||
|
} else {
|
||||||
|
const tAbs = Math.abs(t);
|
||||||
|
const h = Math.floor(tAbs / 3600);
|
||||||
|
const m = Math.floor((tAbs % 3600) / 60);
|
||||||
|
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
||||||
|
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
|
||||||
|
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
|
||||||
|
else return `${forNode ? "-" : ""}${h}:${m}h`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeIncrs(timestep, maxX, forNode) {
|
||||||
|
if (forNode === true) {
|
||||||
|
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
||||||
|
} else {
|
||||||
|
let incrs = [];
|
||||||
|
for (let t = timestep; t < maxX; t *= 10)
|
||||||
|
incrs.push(t, t * 2, t * 3, t * 5);
|
||||||
|
|
||||||
|
return incrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findThresholds(
|
||||||
|
metricConfig,
|
||||||
|
scope,
|
||||||
|
subCluster,
|
||||||
|
isShared,
|
||||||
|
hwthreads,
|
||||||
|
) {
|
||||||
|
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
|
||||||
|
if (!metricConfig || !scope || !subCluster) {
|
||||||
|
console.warn("Argument missing for findThresholds!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(scope == "node" && isShared == false) ||
|
||||||
|
metricConfig.aggregation == "avg"
|
||||||
|
) {
|
||||||
|
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
|
||||||
|
// console.log('subClusterConfigs array empty, use metricConfig defaults')
|
||||||
|
return {
|
||||||
|
normal: metricConfig.normal,
|
||||||
|
caution: metricConfig.caution,
|
||||||
|
alert: metricConfig.alert,
|
||||||
|
peak: metricConfig.peak,
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
metricConfig.subClusters &&
|
||||||
|
metricConfig.subClusters.length > 0
|
||||||
|
) {
|
||||||
|
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
|
||||||
|
let forSubCluster = metricConfig.subClusters.find(
|
||||||
|
(sc) => sc.name == subCluster.name,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
forSubCluster &&
|
||||||
|
forSubCluster.normal &&
|
||||||
|
forSubCluster.caution &&
|
||||||
|
forSubCluster.alert &&
|
||||||
|
forSubCluster.peak
|
||||||
|
)
|
||||||
|
return forSubCluster;
|
||||||
|
else
|
||||||
|
return {
|
||||||
|
normal: metricConfig.normal,
|
||||||
|
caution: metricConfig.caution,
|
||||||
|
alert: metricConfig.alert,
|
||||||
|
peak: metricConfig.peak,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn("metricConfig.subClusters not found!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metricConfig.aggregation != "sum") {
|
||||||
|
console.warn(
|
||||||
|
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||||
|
metricConfig,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let divisor = 1
|
||||||
|
if (isShared == true) { // Shared
|
||||||
|
if (numaccs > 0) divisor = subCluster.topology.accelerators.length / numaccs;
|
||||||
|
else if (numhwthreads > 0) divisor = subCluster.topology.node.length / numhwthreads;
|
||||||
|
}
|
||||||
|
else if (scope == 'socket') divisor = subCluster.topology.socket.length;
|
||||||
|
else if (scope == "core") divisor = subCluster.topology.core.length;
|
||||||
|
else if (scope == "accelerator")
|
||||||
|
divisor = subCluster.topology.accelerators.length;
|
||||||
|
else if (scope == "hwthread") divisor = subCluster.topology.node.length;
|
||||||
|
else {
|
||||||
|
// console.log('TODO: how to calc thresholds for ', scope)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mc =
|
||||||
|
metricConfig?.subClusters?.find((sc) => sc.name == subCluster.name) ||
|
||||||
|
metricConfig;
|
||||||
|
return {
|
||||||
|
peak: mc.peak / divisor,
|
||||||
|
normal: mc.normal / divisor,
|
||||||
|
caution: mc.caution / divisor,
|
||||||
|
alert: mc.alert / divisor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@component
|
@component
|
||||||
|
|
||||||
@ -21,46 +139,59 @@
|
|||||||
// TODO: Move helper functions to module context?
|
// TODO: Move helper functions to module context?
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
import uPlot from 'uplot'
|
import uPlot from "uplot";
|
||||||
import { formatNumber } from '../units.js'
|
import { formatNumber } from "../units.js";
|
||||||
import { getContext, onMount, onDestroy } from 'svelte'
|
import { getContext, onMount, onDestroy } from "svelte";
|
||||||
import { Card } from 'sveltestrap'
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let metric
|
export let metric;
|
||||||
export let scope = 'node'
|
export let scope = "node";
|
||||||
export let resources = []
|
export let resources = [];
|
||||||
export let width
|
export let width;
|
||||||
export let height
|
export let height;
|
||||||
export let timestep
|
export let timestep;
|
||||||
export let series
|
export let series;
|
||||||
export let useStatsSeries = null
|
export let useStatsSeries = null;
|
||||||
export let statisticsSeries = null
|
export let statisticsSeries = null;
|
||||||
export let cluster
|
export let cluster;
|
||||||
export let subCluster
|
export let subCluster;
|
||||||
export let isShared = false
|
export let isShared = false;
|
||||||
export let forNode = false
|
export let forNode = false;
|
||||||
export let numhwthreads = 0
|
export let hwthreads = 0;
|
||||||
export let numaccs = 0
|
|
||||||
|
|
||||||
if (useStatsSeries == null)
|
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
||||||
useStatsSeries = statisticsSeries != null
|
|
||||||
|
|
||||||
if (useStatsSeries == false && series == null)
|
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
||||||
useStatsSeries = true
|
|
||||||
|
|
||||||
const metricConfig = getContext('metrics')(cluster, metric)
|
const metricConfig = getContext("metrics")(cluster, metric);
|
||||||
const clusterCockpitConfig = getContext('cc-config')
|
const clusterCockpitConfig = getContext("cc-config");
|
||||||
const resizeSleepTime = 250
|
const resizeSleepTime = 250;
|
||||||
const normalLineColor = '#000000'
|
const normalLineColor = "#000000";
|
||||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio
|
const lineWidth =
|
||||||
const lineColors = clusterCockpitConfig.plot_general_colorscheme
|
clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
||||||
const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' }
|
const lineColors = clusterCockpitConfig.plot_general_colorscheme;
|
||||||
const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster, isShared, numhwthreads, numaccs)
|
const backgroundColors = {
|
||||||
|
normal: "rgba(255, 255, 255, 1.0)",
|
||||||
|
caution: "rgba(255, 128, 0, 0.3)",
|
||||||
|
alert: "rgba(255, 0, 0, 0.3)",
|
||||||
|
};
|
||||||
|
const thresholds = findThresholds(
|
||||||
|
metricConfig,
|
||||||
|
scope,
|
||||||
|
typeof subCluster == "string"
|
||||||
|
? cluster.subClusters.find((sc) => sc.name == subCluster)
|
||||||
|
: subCluster,
|
||||||
|
isShared,
|
||||||
|
hwthreads,
|
||||||
|
);
|
||||||
|
|
||||||
// converts the legend into a simple tooltip
|
// converts the legend into a simple tooltip
|
||||||
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
|
function legendAsTooltipPlugin({
|
||||||
|
className,
|
||||||
|
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||||
|
} = {}) {
|
||||||
let legendEl;
|
let legendEl;
|
||||||
const dataSize = series.length
|
const dataSize = series.length;
|
||||||
|
|
||||||
function init(u, opts) {
|
function init(u, opts) {
|
||||||
legendEl = u.root.querySelector(".u-legend");
|
legendEl = u.root.querySelector(".u-legend");
|
||||||
@ -77,13 +208,16 @@
|
|||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||||
...style
|
...style,
|
||||||
});
|
});
|
||||||
|
|
||||||
// conditional hide series color markers:
|
// conditional hide series color markers:
|
||||||
if (useStatsSeries === true || // Min/Max/Avg Self-Explanatory
|
if (
|
||||||
|
useStatsSeries === true || // Min/Max/Avg Self-Explanatory
|
||||||
dataSize === 1 || // Only one Y-Dataseries
|
dataSize === 1 || // Only one Y-Dataseries
|
||||||
dataSize > 6 ){ // More than 6 Y-Dataseries
|
dataSize > 6
|
||||||
|
) {
|
||||||
|
// More than 6 Y-Dataseries
|
||||||
const idents = legendEl.querySelectorAll(".u-marker");
|
const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
for (let i = 0; i < idents.length; i++)
|
for (let i = 0; i < idents.length; i++)
|
||||||
idents[i].style.display = "none";
|
idents[i].style.display = "none";
|
||||||
@ -96,8 +230,12 @@
|
|||||||
overEl.appendChild(legendEl);
|
overEl.appendChild(legendEl);
|
||||||
|
|
||||||
// show/hide tooltip on enter/exit
|
// show/hide tooltip on enter/exit
|
||||||
overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
|
overEl.addEventListener("mouseenter", () => {
|
||||||
overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
|
legendEl.style.display = null;
|
||||||
|
});
|
||||||
|
overEl.addEventListener("mouseleave", () => {
|
||||||
|
legendEl.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
// let tooltip exit plot
|
// let tooltip exit plot
|
||||||
// overEl.style.overflow = "visible";
|
// overEl.style.overflow = "visible";
|
||||||
@ -106,7 +244,8 @@
|
|||||||
function update(u) {
|
function update(u) {
|
||||||
const { left, top } = u.cursor;
|
const { left, top } = u.cursor;
|
||||||
const width = u.over.querySelector(".u-legend").offsetWidth;
|
const width = u.over.querySelector(".u-legend").offsetWidth;
|
||||||
legendEl.style.transform = "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
legendEl.style.transform =
|
||||||
|
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataSize <= 12 || useStatsSeries === true) {
|
if (dataSize <= 12 || useStatsSeries === true) {
|
||||||
@ -114,336 +253,289 @@
|
|||||||
hooks: {
|
hooks: {
|
||||||
init: init,
|
init: init,
|
||||||
setCursor: update,
|
setCursor: update,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
} else { // Setting legend-opts show/live as object with false here will not work ...
|
} else {
|
||||||
return {}
|
// Setting legend-opts show/live as object with false here will not work ...
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function backgroundColor() {
|
function backgroundColor() {
|
||||||
if (clusterCockpitConfig.plot_general_colorBackground == false
|
if (
|
||||||
|| !thresholds
|
clusterCockpitConfig.plot_general_colorBackground == false ||
|
||||||
|| !(series && series.every(s => s.statistics != null)))
|
!thresholds ||
|
||||||
return backgroundColors.normal
|
!(series && series.every((s) => s.statistics != null))
|
||||||
|
)
|
||||||
|
return backgroundColors.normal;
|
||||||
|
|
||||||
let cond = thresholds.alert < thresholds.caution
|
let cond =
|
||||||
|
thresholds.alert < thresholds.caution
|
||||||
? (a, b) => a <= b
|
? (a, b) => a <= b
|
||||||
: (a, b) => a >= b
|
: (a, b) => a >= b;
|
||||||
|
|
||||||
let avg = series.reduce((sum, series) => sum + series.statistics.avg, 0) / series.length
|
let avg =
|
||||||
|
series.reduce((sum, series) => sum + series.statistics.avg, 0) /
|
||||||
|
series.length;
|
||||||
|
|
||||||
if (Number.isNaN(avg))
|
if (Number.isNaN(avg)) return backgroundColors.normal;
|
||||||
return backgroundColors.normal
|
|
||||||
|
|
||||||
if (cond(avg, thresholds.alert))
|
if (cond(avg, thresholds.alert)) return backgroundColors.alert;
|
||||||
return backgroundColors.alert
|
|
||||||
|
|
||||||
if (cond(avg, thresholds.caution))
|
if (cond(avg, thresholds.caution)) return backgroundColors.caution;
|
||||||
return backgroundColors.caution
|
|
||||||
|
|
||||||
return backgroundColors.normal
|
return backgroundColors.normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineColor(i, n) {
|
function lineColor(i, n) {
|
||||||
if (n >= lineColors.length)
|
if (n >= lineColors.length) return lineColors[i % lineColors.length];
|
||||||
return lineColors[i % lineColors.length];
|
else return lineColors[Math.floor((i / n) * lineColors.length)];
|
||||||
else
|
|
||||||
return lineColors[Math.floor((i / n) * lineColors.length)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const longestSeries = useStatsSeries
|
const longestSeries = useStatsSeries
|
||||||
? statisticsSeries.mean.length
|
? statisticsSeries.mean.length
|
||||||
: series.reduce((n, series) => Math.max(n, series.data.length), 0)
|
: series.reduce((n, series) => Math.max(n, series.data.length), 0);
|
||||||
const maxX = longestSeries * timestep
|
const maxX = longestSeries * timestep;
|
||||||
let maxY = null
|
let maxY = null;
|
||||||
|
|
||||||
if (thresholds !== null) {
|
if (thresholds !== null) {
|
||||||
maxY = useStatsSeries
|
maxY = useStatsSeries
|
||||||
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal)
|
? statisticsSeries.max.reduce(
|
||||||
: (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal)
|
(max, x) => Math.max(max, x),
|
||||||
|
thresholds.normal,
|
||||||
|
) || thresholds.normal
|
||||||
|
: series.reduce(
|
||||||
|
(max, series) => Math.max(max, series.statistics?.max),
|
||||||
|
thresholds.normal,
|
||||||
|
) || thresholds.normal;
|
||||||
|
|
||||||
if (maxY >= (10 * thresholds.peak)) { // Hard y-range render limit if outliers in series data
|
if (maxY >= 10 * thresholds.peak) {
|
||||||
maxY = (10 * thresholds.peak)
|
// Hard y-range render limit if outliers in series data
|
||||||
|
maxY = 10 * thresholds.peak;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts, forNode)}]
|
const plotSeries = [
|
||||||
const plotData = [new Array(longestSeries)]
|
{
|
||||||
|
label: "Runtime",
|
||||||
|
value: (u, ts, sidx, didx) =>
|
||||||
|
didx == null ? null : formatTime(ts, forNode),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const plotData = [new Array(longestSeries)];
|
||||||
|
|
||||||
if (forNode === true) {
|
if (forNode === true) {
|
||||||
// Negative Timestamp Buildup
|
// Negative Timestamp Buildup
|
||||||
for (let i = 0; i <= longestSeries; i++) {
|
for (let i = 0; i <= longestSeries; i++) {
|
||||||
plotData[0][i] = (longestSeries - i) * timestep * -1
|
plotData[0][i] = (longestSeries - i) * timestep * -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Positive Timestamp Buildup
|
// Positive Timestamp Buildup
|
||||||
for (let j = 0; j < longestSeries; j++) // TODO: Cache/Reuse this array?
|
for (
|
||||||
plotData[0][j] = j * timestep
|
let j = 0;
|
||||||
|
j < longestSeries;
|
||||||
|
j++ // TODO: Cache/Reuse this array?
|
||||||
|
)
|
||||||
|
plotData[0][j] = j * timestep;
|
||||||
}
|
}
|
||||||
|
|
||||||
let plotBands = undefined
|
let plotBands = undefined;
|
||||||
if (useStatsSeries) {
|
if (useStatsSeries) {
|
||||||
plotData.push(statisticsSeries.min)
|
plotData.push(statisticsSeries.min);
|
||||||
plotData.push(statisticsSeries.max)
|
plotData.push(statisticsSeries.max);
|
||||||
plotData.push(statisticsSeries.mean)
|
plotData.push(statisticsSeries.mean);
|
||||||
|
|
||||||
if (forNode === true) { // timestamp 0 with null value for reversed time axis
|
if (forNode === true) {
|
||||||
if (plotData[1].length != 0) plotData[1].push(null)
|
// timestamp 0 with null value for reversed time axis
|
||||||
if (plotData[2].length != 0) plotData[2].push(null)
|
if (plotData[1].length != 0) plotData[1].push(null);
|
||||||
if (plotData[3].length != 0) plotData[3].push(null)
|
if (plotData[2].length != 0) plotData[2].push(null);
|
||||||
|
if (plotData[3].length != 0) plotData[3].push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
plotSeries.push({ label: 'min', scale: 'y', width: lineWidth, stroke: 'red' })
|
plotSeries.push({
|
||||||
plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' })
|
label: "min",
|
||||||
plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' })
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "red",
|
||||||
|
});
|
||||||
|
plotSeries.push({
|
||||||
|
label: "max",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "green",
|
||||||
|
});
|
||||||
|
plotSeries.push({
|
||||||
|
label: "mean",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: "black",
|
||||||
|
});
|
||||||
|
|
||||||
plotBands = [
|
plotBands = [
|
||||||
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' },
|
{ series: [2, 3], fill: "rgba(0,255,0,0.1)" },
|
||||||
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' }
|
{ series: [3, 1], fill: "rgba(255,0,0,0.1)" },
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < series.length; i++) {
|
for (let i = 0; i < series.length; i++) {
|
||||||
plotData.push(series[i].data)
|
plotData.push(series[i].data);
|
||||||
if (forNode === true && plotData[1].length != 0) plotData[1].push(null) // timestamp 0 with null value for reversed time axis
|
if (forNode === true && plotData[1].length != 0) plotData[1].push(null); // timestamp 0 with null value for reversed time axis
|
||||||
plotSeries.push({
|
plotSeries.push({
|
||||||
label: scope === 'node' ? resources[i].hostname :
|
label:
|
||||||
// scope === 'accelerator' ? resources[0].accelerators[i] :
|
scope === "node"
|
||||||
scope + ' #' + (i+1),
|
? resources[i].hostname
|
||||||
scale: 'y',
|
: // scope === 'accelerator' ? resources[0].accelerators[i] :
|
||||||
|
scope + " #" + (i + 1),
|
||||||
|
scale: "y",
|
||||||
width: lineWidth,
|
width: lineWidth,
|
||||||
stroke: lineColor(i, series.length)
|
stroke: lineColor(i, series.length),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
plugins: [
|
plugins: [legendAsTooltipPlugin()],
|
||||||
legendAsTooltipPlugin()
|
|
||||||
],
|
|
||||||
series: plotSeries,
|
series: plotSeries,
|
||||||
axes: [
|
axes: [
|
||||||
{
|
{
|
||||||
scale: 'x',
|
scale: "x",
|
||||||
space: 35,
|
space: 35,
|
||||||
incrs: timeIncrs(timestep, maxX, forNode),
|
incrs: timeIncrs(timestep, maxX, forNode),
|
||||||
values: (_, vals) => vals.map(v => formatTime(v, forNode))
|
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scale: 'y',
|
scale: "y",
|
||||||
grid: { show: true },
|
grid: { show: true },
|
||||||
labelFont: 'sans-serif',
|
labelFont: "sans-serif",
|
||||||
values: (u, vals) => vals.map(v => formatNumber(v))
|
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
bands: plotBands,
|
bands: plotBands,
|
||||||
padding: [5, 10, -20, 0],
|
padding: [5, 10, -20, 0],
|
||||||
hooks: {
|
hooks: {
|
||||||
draw: [(u) => {
|
draw: [
|
||||||
|
(u) => {
|
||||||
// Draw plot type label:
|
// Draw plot type label:
|
||||||
let textl = `${scope}${plotSeries.length > 2 ? 's' : ''}${
|
let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
|
||||||
useStatsSeries ? ': min/avg/max' : (metricConfig != null && scope != metricConfig.scope ? ` (${metricConfig.aggregation})` : '')}`
|
useStatsSeries
|
||||||
let textr = `${(isShared && (scope != 'core' && scope != 'accelerator')) ? '[Shared]' : '' }`
|
? ": min/avg/max"
|
||||||
u.ctx.save()
|
: metricConfig != null && scope != metricConfig.scope
|
||||||
u.ctx.textAlign = 'start' // 'end'
|
? ` (${metricConfig.aggregation})`
|
||||||
u.ctx.fillStyle = 'black'
|
: ""
|
||||||
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10)
|
}`;
|
||||||
u.ctx.textAlign = 'end'
|
let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
|
||||||
u.ctx.fillStyle = 'black'
|
u.ctx.save();
|
||||||
u.ctx.fillText(textr, u.bbox.left + u.bbox.width - 10, u.bbox.top + 10)
|
u.ctx.textAlign = "start"; // 'end'
|
||||||
|
u.ctx.fillStyle = "black";
|
||||||
|
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10);
|
||||||
|
u.ctx.textAlign = "end";
|
||||||
|
u.ctx.fillStyle = "black";
|
||||||
|
u.ctx.fillText(
|
||||||
|
textr,
|
||||||
|
u.bbox.left + u.bbox.width - 10,
|
||||||
|
u.bbox.top + 10,
|
||||||
|
);
|
||||||
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
||||||
|
|
||||||
if (!thresholds) {
|
if (!thresholds) {
|
||||||
u.ctx.restore()
|
u.ctx.restore();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let y = u.valToPos(thresholds.normal, 'y', true)
|
let y = u.valToPos(thresholds.normal, "y", true);
|
||||||
u.ctx.save()
|
u.ctx.save();
|
||||||
u.ctx.lineWidth = lineWidth
|
u.ctx.lineWidth = lineWidth;
|
||||||
u.ctx.strokeStyle = normalLineColor
|
u.ctx.strokeStyle = normalLineColor;
|
||||||
u.ctx.setLineDash([5, 5])
|
u.ctx.setLineDash([5, 5]);
|
||||||
u.ctx.beginPath()
|
u.ctx.beginPath();
|
||||||
u.ctx.moveTo(u.bbox.left, y)
|
u.ctx.moveTo(u.bbox.left, y);
|
||||||
u.ctx.lineTo(u.bbox.left + u.bbox.width, y)
|
u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
|
||||||
u.ctx.stroke()
|
u.ctx.stroke();
|
||||||
u.ctx.restore()
|
u.ctx.restore();
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { time: false },
|
x: { time: false },
|
||||||
y: maxY ? { range: [0., maxY * 1.1] } : {}
|
y: maxY ? { range: [0, maxY * 1.1] } : {},
|
||||||
},
|
},
|
||||||
legend : { // Display legend until max 12 Y-dataseries
|
legend: {
|
||||||
show: (series.length <= 12 || useStatsSeries === true) ? true : false,
|
// Display legend until max 12 Y-dataseries
|
||||||
live: (series.length <= 12 || useStatsSeries === true) ? true : false
|
show: series.length <= 12 || useStatsSeries === true ? true : false,
|
||||||
|
live: series.length <= 12 || useStatsSeries === true ? true : false,
|
||||||
},
|
},
|
||||||
cursor: { drag: { x: true, y: true } }
|
cursor: { drag: { x: true, y: true } },
|
||||||
}
|
};
|
||||||
|
|
||||||
// console.log(opts)
|
// console.log(opts)
|
||||||
|
|
||||||
let plotWrapper = null
|
let plotWrapper = null;
|
||||||
let uplot = null
|
let uplot = null;
|
||||||
let timeoutId = null
|
let timeoutId = null;
|
||||||
let prevWidth = null, prevHeight = null
|
let prevWidth = null,
|
||||||
|
prevHeight = null;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (!width || Number.isNaN(width) || width < 0)
|
if (!width || Number.isNaN(width) || width < 0) return;
|
||||||
return
|
|
||||||
|
|
||||||
if (prevWidth != null && Math.abs(prevWidth - width) < 10)
|
if (prevWidth != null && Math.abs(prevWidth - width) < 10) return;
|
||||||
return
|
|
||||||
|
|
||||||
prevWidth = width
|
prevWidth = width;
|
||||||
prevHeight = height
|
prevHeight = height;
|
||||||
|
|
||||||
if (!uplot) {
|
if (!uplot) {
|
||||||
opts.width = width
|
opts.width = width;
|
||||||
opts.height = height
|
opts.height = height;
|
||||||
uplot = new uPlot(opts, plotData, plotWrapper)
|
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||||
} else {
|
} else {
|
||||||
uplot.setSize({ width, height })
|
uplot.setSize({ width, height });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSizeChange() {
|
function onSizeChange() {
|
||||||
if (!uplot)
|
if (!uplot) return;
|
||||||
return
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
timeoutId = null
|
timeoutId = null;
|
||||||
render()
|
render();
|
||||||
}, resizeSleepTime)
|
}, resizeSleepTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (series[0].data.length > 0) {
|
$: if (series[0].data.length > 0) {
|
||||||
onSizeChange(width, height)
|
onSizeChange(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (series[0].data.length > 0) {
|
if (series[0].data.length > 0) {
|
||||||
plotWrapper.style.backgroundColor = backgroundColor()
|
plotWrapper.style.backgroundColor = backgroundColor();
|
||||||
render()
|
render();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (uplot)
|
if (uplot) uplot.destroy();
|
||||||
uplot.destroy()
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
// `from` and `to` must be numbers between 0 and 1.
|
// `from` and `to` must be numbers between 0 and 1.
|
||||||
export function setTimeRange(from, to) {
|
export function setTimeRange(from, to) {
|
||||||
if (!uplot || from > to)
|
if (!uplot || from > to) return false;
|
||||||
return false
|
|
||||||
|
|
||||||
uplot.setScale('x', { min: from * maxX, max: to * maxX })
|
uplot.setScale("x", { min: from * maxX, max: to * maxX });
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script context="module">
|
|
||||||
export function formatTime(t, forNode = false) {
|
|
||||||
if (t !== null) {
|
|
||||||
if (isNaN(t)) {
|
|
||||||
return t
|
|
||||||
} else {
|
|
||||||
const tAbs = Math.abs(t)
|
|
||||||
const h = Math.floor(tAbs / 3600)
|
|
||||||
const m = Math.floor((tAbs % 3600) / 60)
|
|
||||||
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
|
||||||
if (h == 0)
|
|
||||||
return `${forNode && m != 0 ? '-' : ''}${m}m`
|
|
||||||
else if (m == 0)
|
|
||||||
return `${forNode?'-':''}${h}h`
|
|
||||||
else
|
|
||||||
return `${forNode?'-':''}${h}:${m}h`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function timeIncrs(timestep, maxX, forNode) {
|
|
||||||
if (forNode === true) {
|
|
||||||
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600] // forNode fixed increments
|
|
||||||
} else {
|
|
||||||
let incrs = []
|
|
||||||
for (let t = timestep; t < maxX; t *= 10)
|
|
||||||
incrs.push(t, t * 2, t * 3, t * 5)
|
|
||||||
|
|
||||||
return incrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findThresholds(metricConfig, scope, subCluster, isShared, numhwthreads, numaccs) {
|
|
||||||
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
|
|
||||||
if (!metricConfig || !scope || !subCluster) {
|
|
||||||
console.warn('Argument missing for findThresholds!')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((scope == 'node' && isShared == false) || metricConfig.aggregation == 'avg') {
|
|
||||||
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
|
|
||||||
// console.log('subClusterConfigs array empty, use metricConfig defaults')
|
|
||||||
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak }
|
|
||||||
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
|
|
||||||
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
|
|
||||||
let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
|
|
||||||
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert && forSubCluster.peak) return forSubCluster
|
|
||||||
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak}
|
|
||||||
} else {
|
|
||||||
console.warn('metricConfig.subClusters not found!')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metricConfig.aggregation != 'sum') {
|
|
||||||
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let divisor = 1
|
|
||||||
if (isShared == true) { // Shared
|
|
||||||
if (numaccs > 0) {
|
|
||||||
divisor = subCluster.topology.accelerators.length / numaccs
|
|
||||||
} else if (numhwthreads > 0) {
|
|
||||||
divisor = subCluster.topology.node.length / numhwthreads
|
|
||||||
}
|
|
||||||
else if (scope == 'socket')
|
|
||||||
divisor = subCluster.topology.socket.length
|
|
||||||
else if (scope == 'core')
|
|
||||||
divisor = subCluster.topology.core.length
|
|
||||||
else if (scope == 'accelerator')
|
|
||||||
divisor = subCluster.topology.accelerators.length
|
|
||||||
else if (scope == 'hwthread')
|
|
||||||
divisor = subCluster.topology.node.length
|
|
||||||
else
|
|
||||||
// console.log('TODO: how to calc thresholds for ', scope)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig
|
|
||||||
return {
|
|
||||||
peak: mc.peak / divisor,
|
|
||||||
normal: mc.normal / divisor,
|
|
||||||
caution: mc.caution / divisor,
|
|
||||||
alert: mc.alert / divisor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if series[0].data.length > 0}
|
{#if series[0].data.length > 0}
|
||||||
<div bind:this={plotWrapper} class="cc-plot"></div>
|
<div bind:this={plotWrapper} class="cc-plot"></div>
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="mx-4" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
|
<Card class="mx-4" body color="warning"
|
||||||
|
>Cannot render plot: No series data returned for <code>{metric}</code></Card
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import uPlot from 'uplot'
|
import uPlot from "uplot";
|
||||||
import { formatNumber } from '../units.js'
|
import { formatNumber } from "../units.js";
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { Card } from 'sveltestrap'
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let data = null
|
export let data = null;
|
||||||
export let renderTime = false
|
export let renderTime = false;
|
||||||
export let allowSizeChange = false
|
export let allowSizeChange = false;
|
||||||
export let cluster = null
|
export let cluster = null;
|
||||||
export let width = 600
|
export let width = 600;
|
||||||
export let height = 350
|
export let height = 350;
|
||||||
|
|
||||||
let plotWrapper = null
|
let plotWrapper = null;
|
||||||
let uplot = null
|
let uplot = null;
|
||||||
let timeoutId = null
|
let timeoutId = null;
|
||||||
|
|
||||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth
|
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
|
||||||
|
|
||||||
/* Data Format
|
/* Data Format
|
||||||
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
|
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
|
||||||
@ -26,67 +26,111 @@
|
|||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function getGradientR(x) {
|
function getGradientR(x) {
|
||||||
if (x < 0.5) return 0
|
if (x < 0.5) return 0;
|
||||||
if (x > 0.75) return 255
|
if (x > 0.75) return 255;
|
||||||
x = (x - 0.5) * 4.0
|
x = (x - 0.5) * 4.0;
|
||||||
return Math.floor(x * 255.0)
|
return Math.floor(x * 255.0);
|
||||||
}
|
}
|
||||||
function getGradientG(x) {
|
function getGradientG(x) {
|
||||||
if (x > 0.25 && x < 0.75) return 255
|
if (x > 0.25 && x < 0.75) return 255;
|
||||||
if (x < 0.25) x = x * 4.0
|
if (x < 0.25) x = x * 4.0;
|
||||||
else x = 1.0 - (x - 0.75) * 4.0
|
else x = 1.0 - (x - 0.75) * 4.0;
|
||||||
return Math.floor(x * 255.0)
|
return Math.floor(x * 255.0);
|
||||||
}
|
}
|
||||||
function getGradientB(x) {
|
function getGradientB(x) {
|
||||||
if (x < 0.25) return 255
|
if (x < 0.25) return 255;
|
||||||
if (x > 0.5) return 0
|
if (x > 0.5) return 0;
|
||||||
x = 1.0 - (x - 0.25) * 4.0
|
x = 1.0 - (x - 0.25) * 4.0;
|
||||||
return Math.floor(x * 255.0)
|
return Math.floor(x * 255.0);
|
||||||
}
|
}
|
||||||
function getRGB(c) {
|
function getRGB(c) {
|
||||||
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
|
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`;
|
||||||
}
|
}
|
||||||
function nearestThousand (num) {
|
function nearestThousand(num) {
|
||||||
return Math.ceil(num/1000) * 1000
|
return Math.ceil(num / 1000) * 1000;
|
||||||
}
|
}
|
||||||
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||||
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
||||||
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
|
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l;
|
||||||
return {
|
return {
|
||||||
x: x1 + a * (x2 - x1),
|
x: x1 + a * (x2 - x1),
|
||||||
y: y1 + a * (y2 - y1)
|
y: y1 + a * (y2 - y1),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
// End Helpers
|
// End Helpers
|
||||||
|
|
||||||
// Dot Renderers
|
// Dot Renderers
|
||||||
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
|
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
|
||||||
const size = 5 * devicePixelRatio;
|
const size = 5 * devicePixelRatio;
|
||||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
uPlot.orient(
|
||||||
|
u,
|
||||||
|
seriesIdx,
|
||||||
|
(
|
||||||
|
series,
|
||||||
|
dataX,
|
||||||
|
dataY,
|
||||||
|
scaleX,
|
||||||
|
scaleY,
|
||||||
|
valToPosX,
|
||||||
|
valToPosY,
|
||||||
|
xOff,
|
||||||
|
yOff,
|
||||||
|
xDim,
|
||||||
|
yDim,
|
||||||
|
moveTo,
|
||||||
|
lineTo,
|
||||||
|
rect,
|
||||||
|
arc,
|
||||||
|
) => {
|
||||||
let d = u.data[seriesIdx];
|
let d = u.data[seriesIdx];
|
||||||
let deg360 = 2 * Math.PI;
|
let deg360 = 2 * Math.PI;
|
||||||
for (let i = 0; i < d[0].length; i++) {
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
let p = new Path2D();
|
let p = new Path2D();
|
||||||
let xVal = d[0][i];
|
let xVal = d[0][i];
|
||||||
let yVal = d[1][i];
|
let yVal = d[1][i];
|
||||||
u.ctx.strokeStyle = getRGB(u.data[2][i])
|
u.ctx.strokeStyle = getRGB(u.data[2][i]);
|
||||||
u.ctx.fillStyle = getRGB(u.data[2][i])
|
u.ctx.fillStyle = getRGB(u.data[2][i]);
|
||||||
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
if (
|
||||||
|
xVal >= scaleX.min &&
|
||||||
|
xVal <= scaleX.max &&
|
||||||
|
yVal >= scaleY.min &&
|
||||||
|
yVal <= scaleY.max
|
||||||
|
) {
|
||||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
|
|
||||||
p.moveTo(cx + size/2, cy);
|
p.moveTo(cx + size / 2, cy);
|
||||||
arc(p, cx, cy, size/2, 0, deg360);
|
arc(p, cx, cy, size / 2, 0, deg360);
|
||||||
}
|
}
|
||||||
u.ctx.fill(p);
|
u.ctx.fill(p);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||||
const size = 5 * devicePixelRatio;
|
const size = 5 * devicePixelRatio;
|
||||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
uPlot.orient(
|
||||||
|
u,
|
||||||
|
seriesIdx,
|
||||||
|
(
|
||||||
|
series,
|
||||||
|
dataX,
|
||||||
|
dataY,
|
||||||
|
scaleX,
|
||||||
|
scaleY,
|
||||||
|
valToPosX,
|
||||||
|
valToPosY,
|
||||||
|
xOff,
|
||||||
|
yOff,
|
||||||
|
xDim,
|
||||||
|
yDim,
|
||||||
|
moveTo,
|
||||||
|
lineTo,
|
||||||
|
rect,
|
||||||
|
arc,
|
||||||
|
) => {
|
||||||
let d = u.data[seriesIdx];
|
let d = u.data[seriesIdx];
|
||||||
u.ctx.strokeStyle = getRGB(0);
|
u.ctx.strokeStyle = getRGB(0);
|
||||||
u.ctx.fillStyle = getRGB(0);
|
u.ctx.fillStyle = getRGB(0);
|
||||||
@ -95,15 +139,21 @@
|
|||||||
for (let i = 0; i < d[0].length; i++) {
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
let xVal = d[0][i];
|
let xVal = d[0][i];
|
||||||
let yVal = d[1][i];
|
let yVal = d[1][i];
|
||||||
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
if (
|
||||||
|
xVal >= scaleX.min &&
|
||||||
|
xVal <= scaleX.max &&
|
||||||
|
yVal >= scaleY.min &&
|
||||||
|
yVal <= scaleY.max
|
||||||
|
) {
|
||||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
p.moveTo(cx + size/2, cy);
|
p.moveTo(cx + size / 2, cy);
|
||||||
arc(p, cx, cy, size/2, 0, deg360);
|
arc(p, cx, cy, size / 2, 0, deg360);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u.ctx.fill(p);
|
u.ctx.fill(p);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -116,18 +166,18 @@
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
legend: {
|
legend: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
cursor: { drag: { x: false, y: false } },
|
cursor: { drag: { x: false, y: false } },
|
||||||
axes: [
|
axes: [
|
||||||
{
|
{
|
||||||
label: 'Intensity [FLOPS/Byte]',
|
label: "Intensity [FLOPS/Byte]",
|
||||||
values: (u, vals) => vals.map(v => formatNumber(v))
|
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Performace [GFLOPS]',
|
label: "Performace [GFLOPS]",
|
||||||
values: (u, vals) => vals.map(v => formatNumber(v))
|
values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
@ -137,118 +187,153 @@
|
|||||||
log: 10, // log exp
|
log: 10, // log exp
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
range: [1.0, cluster?.flopRateSimd?.value ? nearestThousand(cluster.flopRateSimd.value) : 10000],
|
range: [
|
||||||
|
1.0,
|
||||||
|
cluster?.flopRateSimd?.value
|
||||||
|
? nearestThousand(cluster.flopRateSimd.value)
|
||||||
|
: 10000,
|
||||||
|
],
|
||||||
distr: 3, // Render as log
|
distr: 3, // Render as log
|
||||||
log: 10, // log exp
|
log: 10, // log exp
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [{}, { paths: renderTime ? drawColorPoints : drawPoints }],
|
||||||
{},
|
|
||||||
{ paths: renderTime ? drawColorPoints : drawPoints }
|
|
||||||
],
|
|
||||||
hooks: {
|
hooks: {
|
||||||
drawClear: [
|
drawClear: [
|
||||||
u => {
|
(u) => {
|
||||||
u.series.forEach((s, i) => {
|
u.series.forEach((s, i) => {
|
||||||
if (i > 0)
|
if (i > 0) s._paths = null;
|
||||||
s._paths = null;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
draw: [
|
draw: [
|
||||||
u => { // draw roofs when cluster set
|
(u) => {
|
||||||
|
// draw roofs when cluster set
|
||||||
// console.log(u)
|
// console.log(u)
|
||||||
if (cluster != null) {
|
if (cluster != null) {
|
||||||
const padding = u._padding // [top, right, bottom, left]
|
const padding = u._padding; // [top, right, bottom, left]
|
||||||
|
|
||||||
u.ctx.strokeStyle = 'black'
|
u.ctx.strokeStyle = "black";
|
||||||
u.ctx.lineWidth = lineWidth
|
u.ctx.lineWidth = lineWidth;
|
||||||
u.ctx.beginPath()
|
u.ctx.beginPath();
|
||||||
|
|
||||||
const ycut = 0.01 * cluster.memoryBandwidth.value
|
const ycut = 0.01 * cluster.memoryBandwidth.value;
|
||||||
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
|
const scalarKnee =
|
||||||
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
|
(cluster.flopRateScalar.value - ycut) /
|
||||||
const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
|
cluster.memoryBandwidth.value;
|
||||||
simdKneeX = u.valToPos(simdKnee, 'x', true),
|
const simdKnee =
|
||||||
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
|
(cluster.flopRateSimd.value - ycut) /
|
||||||
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
|
cluster.memoryBandwidth.value;
|
||||||
|
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
|
||||||
|
simdKneeX = u.valToPos(simdKnee, "x", true),
|
||||||
|
flopRateScalarY = u.valToPos(
|
||||||
|
cluster.flopRateScalar.value,
|
||||||
|
"y",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
flopRateSimdY = u.valToPos(
|
||||||
|
cluster.flopRateSimd.value,
|
||||||
|
"y",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
// Debug get zoomLevel from browser
|
// Debug get zoomLevel from browser
|
||||||
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
|
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
|
||||||
|
|
||||||
if (scalarKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Lower horizontal roofline
|
if (
|
||||||
u.ctx.moveTo(scalarKneeX, flopRateScalarY)
|
scalarKneeX <
|
||||||
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateScalarY)
|
width * window.devicePixelRatio -
|
||||||
|
padding[1] * window.devicePixelRatio
|
||||||
|
) {
|
||||||
|
// Lower horizontal roofline
|
||||||
|
u.ctx.moveTo(scalarKneeX, flopRateScalarY);
|
||||||
|
u.ctx.lineTo(
|
||||||
|
width * window.devicePixelRatio -
|
||||||
|
padding[1] * window.devicePixelRatio,
|
||||||
|
flopRateScalarY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (simdKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Top horitontal roofline
|
if (
|
||||||
u.ctx.moveTo(simdKneeX, flopRateSimdY)
|
simdKneeX <
|
||||||
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateSimdY)
|
width * window.devicePixelRatio -
|
||||||
|
padding[1] * window.devicePixelRatio
|
||||||
|
) {
|
||||||
|
// Top horitontal roofline
|
||||||
|
u.ctx.moveTo(simdKneeX, flopRateSimdY);
|
||||||
|
u.ctx.lineTo(
|
||||||
|
width * window.devicePixelRatio -
|
||||||
|
padding[1] * window.devicePixelRatio,
|
||||||
|
flopRateSimdY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let x1 = u.valToPos(0.01, 'x', true),
|
let x1 = u.valToPos(0.01, "x", true),
|
||||||
y1 = u.valToPos(ycut, 'y', true)
|
y1 = u.valToPos(ycut, "y", true);
|
||||||
|
|
||||||
let x2 = u.valToPos(simdKnee, 'x', true),
|
let x2 = u.valToPos(simdKnee, "x", true),
|
||||||
y2 = flopRateSimdY
|
y2 = flopRateSimdY;
|
||||||
|
|
||||||
let xAxisIntersect = lineIntersect(
|
let xAxisIntersect = lineIntersect(
|
||||||
x1, y1, x2, y2,
|
x1,
|
||||||
u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
|
y1,
|
||||||
u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
|
x2,
|
||||||
)
|
y2,
|
||||||
|
u.valToPos(0.01, "x", true),
|
||||||
|
u.valToPos(1.0, "y", true), // X-Axis Start Coords
|
||||||
|
u.valToPos(1000, "x", true),
|
||||||
|
u.valToPos(1.0, "y", true), // X-Axis End Coords
|
||||||
|
);
|
||||||
|
|
||||||
if (xAxisIntersect.x > x1) {
|
if (xAxisIntersect.x > x1) {
|
||||||
x1 = xAxisIntersect.x
|
x1 = xAxisIntersect.x;
|
||||||
y1 = xAxisIntersect.y
|
y1 = xAxisIntersect.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagonal
|
// Diagonal
|
||||||
u.ctx.moveTo(x1, y1)
|
u.ctx.moveTo(x1, y1);
|
||||||
u.ctx.lineTo(x2, y2)
|
u.ctx.lineTo(x2, y2);
|
||||||
|
|
||||||
u.ctx.stroke()
|
u.ctx.stroke();
|
||||||
// Reset grid lineWidth
|
// Reset grid lineWidth
|
||||||
u.ctx.lineWidth = 0.15
|
u.ctx.lineWidth = 0.15;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
// cursor: { drag: { x: true, y: true } } // Activate zoom
|
// cursor: { drag: { x: true, y: true } } // Activate zoom
|
||||||
};
|
};
|
||||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||||
} else {
|
} else {
|
||||||
console.log('No data for roofline!')
|
console.log("No data for roofline!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Svelte and Sizechange
|
// Svelte and Sizechange
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
render(data)
|
render(data);
|
||||||
})
|
});
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (uplot)
|
if (uplot) uplot.destroy();
|
||||||
uplot.destroy()
|
|
||||||
|
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
});
|
||||||
})
|
|
||||||
function sizeChanged() {
|
function sizeChanged() {
|
||||||
if (timeoutId != null)
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
timeoutId = null
|
timeoutId = null;
|
||||||
if (uplot)
|
if (uplot) uplot.destroy();
|
||||||
uplot.destroy()
|
render(data);
|
||||||
render(data)
|
}, 200);
|
||||||
}, 200)
|
|
||||||
}
|
}
|
||||||
$: if (allowSizeChange) sizeChanged(width, height)
|
$: if (allowSizeChange) sizeChanged(width, height);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data != null}
|
{#if data != null}
|
||||||
<div bind:this={plotWrapper}/>
|
<div bind:this={plotWrapper} />
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
|
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user