Merge pull request #115 from ClusterCockpit/update-web-frontend-env

Update web frontend env
This commit is contained in:
Jan Eitzinger 2023-05-25 14:40:53 +02:00 committed by GitHub
commit f2fa3ebf77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1169 additions and 1031 deletions

33
docs/dev-frontend.md Normal file
View File

@ -0,0 +1,33 @@
## Tips for frontend development
The frontend assets including the Svelte js files are per default embedded in
the bgo binary. To enable a quick turnaround cycle for web development of the
frontend disable embedding of static assets in `config.json`:
```
"embed-static-files": false,
"static-files": "./web/frontend/public/",
```
Start the node build process (in directory `./web/frontend`) in development mode:
```
$ npm run dev
```
This will start the build process in listen mode. Whenever you change a source
files the depending javascript targets will be automatically rebuild.
In case the javascript files are minified you may need to set the production
flag by hand to false in `./web/frontend/rollup.config.mjs`:
```
const production = false
```
Usually this should work automatically.
Because the files are still served by ./cc-backend you have to reload the view
explicitly in your browser.
A common setup is to have three terminals open:
* One running cc-backend (working directory repository root): `./cc-backend -server -dev`
* Another running npm in developer mode (working directory `./web/frontend`): `npm run dev`
* And the last one editing the frontend source files

34
docs/dev-testing.md Normal file
View File

@ -0,0 +1,34 @@
## Overview
We use the standard golang testing environment.
The following conventions are used:
* *White box unit tests*: Tests for internal functionality are placed in files
* *Black box unit tests*: Tests for public interfaces are placed in files
with `<package name>_test.go` and belong to the package `<package_name>_test`.
There only exists one package test file per package.
* *Integration tests*: Tests that use multiple componenents are placed in a
package test file. These are named `<package name>_test.go` and belong to the
package `<package_name>_test`.
* *Test assets*: Any required files are placed in a directory `./testdata`
within each package directory.
## Executing tests
Visual Studio Code has a very good golang test integration.
For debugging a test this is the recommended solution.
The Makefile provided by us has a `test` target that executes:
```
$ go clean -testcache
$ go build ./...
$ go vet ./...
$ go test ./...
```
Of course the commands can also be used on the command line.
For details about golang testing refer to the standard documentation:
* [Testing package](https://pkg.go.dev/testing)
* [go test command](https://pkg.go.dev/cmd/go#hdr-Test_packages)

View File

@ -9,70 +9,40 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-replace": "^5.0.2",
"@urql/svelte": "^1.3.0", "@urql/svelte": "^4.0.1",
"graphql": "^15.6.0", "graphql": "^16.6.0",
"sveltestrap": "^5.6.1", "sveltestrap": "^5.10.0",
"uplot": "^1.6.7", "uplot": "^1.6.24",
"wonka": "^4.0.15" "wonka": "^6.3.2"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^11.0.0", "@rollup/plugin-node-resolve": "^15.0.2",
"rollup": "^2.3.4", "@rollup/plugin-terser": "^0.4.1",
"rollup-plugin-css-only": "^3.1.0", "rollup": "^3.21.0",
"rollup-plugin-svelte": "^7.0.0", "rollup-plugin-css-only": "^4.3.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-svelte": "^7.1.4",
"svelte": "^3.49.0" "svelte": "^3.58.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@0no-co/graphql.web": {
"version": "7.21.4", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.1.tgz",
"integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", "integrity": "sha512-6Yaxyv6rOwRkLIvFaL0NrLDgfNqC/Ng9QOPmTmlqW4mORXMEKmh5NYGkIvvt5Yw8fZesnMAqkj8cIqTj8f40cQ==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.18.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"peerDependencies": { "peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
},
"peerDependenciesMeta": {
"graphql": {
"optional": true
}
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.2",
"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.2.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.0.1",
@ -102,9 +72,9 @@
} }
}, },
"node_modules/@jridgewell/source-map": { "node_modules/@jridgewell/source-map": {
"version": "0.3.3", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/gen-mapping": "^0.3.0",
@ -112,159 +82,187 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.18",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
"integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14", "version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
"dev": true },
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
}, },
"node_modules/@popperjs/core": { "node_modules/@popperjs/core": {
"version": "2.11.7", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@rollup/plugin-commonjs": { "node_modules/@rollup/plugin-commonjs": {
"version": "17.1.0", "version": "24.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz",
"integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^3.1.0", "@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1", "commondir": "^1.0.1",
"estree-walker": "^2.0.1", "estree-walker": "^2.0.2",
"glob": "^7.1.6", "glob": "^8.0.3",
"is-reference": "^1.2.1", "is-reference": "1.2.1",
"magic-string": "^0.25.7", "magic-string": "^0.27.0"
"resolve": "^1.17.0"
}, },
"engines": { "engines": {
"node": ">= 8.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.30.0" "rollup": "^2.68.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
} }
}, },
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "11.2.1", "version": "15.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz",
"integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", "integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^3.1.0", "@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.17.1", "@types/resolve": "1.20.2",
"builtin-modules": "^3.1.0",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"is-builtin-module": "^3.2.1",
"is-module": "^1.0.0", "is-module": "^1.0.0",
"resolve": "^1.19.0" "resolve": "^1.22.1"
}, },
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^1.20.0||^2.0.0" "rollup": "^2.78.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
} }
}, },
"node_modules/@rollup/plugin-replace": { "node_modules/@rollup/plugin-replace": {
"version": "2.4.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz",
"integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", "integrity": "sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==",
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^3.1.0", "@rollup/pluginutils": "^5.0.1",
"magic-string": "^0.25.7" "magic-string": "^0.27.0"
},
"engines": {
"node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^1.20.0 || ^2.0.0" "rollup": "^1.20.0||^2.0.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-terser": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.1.tgz",
"integrity": "sha512-aKS32sw5a7hy+fEXVy+5T95aDIwjpGHCTv833HXVtyKMDoVS7pBr5K3L9hEQoNqbJFjfANPrNpIXlTQ7is00eA==",
"dev": true,
"dependencies": {
"serialize-javascript": "^6.0.0",
"smob": "^0.0.6",
"terser": "^5.15.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.x || ^3.x"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
} }
}, },
"node_modules/@rollup/pluginutils": { "node_modules/@rollup/pluginutils": {
"version": "3.1.0", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
"dependencies": { "dependencies": {
"@types/estree": "0.0.39", "@types/estree": "^1.0.0",
"estree-walker": "^1.0.1", "estree-walker": "^2.0.2",
"picomatch": "^2.2.2" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
"node": ">= 8.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^1.20.0||^2.0.0" "rollup": "^1.20.0||^2.0.0||^3.0.0"
}
}, },
"node_modules/@rollup/pluginutils/node_modules/estree-walker": { "peerDependenciesMeta": {
"version": "1.0.1", "rollup": {
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", "optional": true
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" }
}
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "0.0.39", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
},
"node_modules/@types/node": {
"version": "18.16.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz",
"integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==",
"dev": true
}, },
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.17.1", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true, "dev": true
"dependencies": {
"@types/node": "*"
}
}, },
"node_modules/@urql/core": { "node_modules/@urql/core": {
"version": "2.6.1", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@urql/core/-/core-4.0.7.tgz",
"integrity": "sha512-gYrEHy3tViJhwIhauK6MIf2Qp09QTsgNHZRd0n71rS+hF6gdwjspf1oKljl4m25+272cJF7fPjBUGmjaiEr7Kg==", "integrity": "sha512-UtZ9oSbSFODXzFydgLCXpAQz26KGT1d6uEfcylKphiRWNXSWZi8k7vhJXNceNm/Dn0MiZ+kaaJHKcnGY1jvHRQ==",
"dependencies": { "dependencies": {
"@graphql-typed-document-node/core": "^3.1.1", "@0no-co/graphql.web": "^1.0.1",
"wonka": "^4.0.14" "wonka": "^6.3.2"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
} }
}, },
"node_modules/@urql/svelte": { "node_modules/@urql/svelte": {
"version": "1.3.3", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@urql/svelte/-/svelte-4.0.1.tgz",
"integrity": "sha512-5XbKcEfvMBYrQfuKjCzLgcT46XW4RoVTeOM5VPmAwk7mD709kgL8LXOSF/9A+fTLOByncMxKTp/9lkI8HKCBkA==", "integrity": "sha512-WbsVjuK7IUNlJlvXAgevjQunoso0T+AngFlb0zafDvay6HN47Zc3CSVbAlP8KjETjERUMJLuiqknmPFFm2GEFQ==",
"dependencies": { "dependencies": {
"@urql/core": "^2.3.6", "@urql/core": "^4.0.0",
"wonka": "^4.0.14" "wonka": "^6.3.2"
}, },
"peerDependencies": { "peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
"svelte": "^3.0.0" "svelte": "^3.0.0"
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.8.2", "version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -273,18 +271,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"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",
@ -292,13 +278,12 @@
"dev": true "dev": true
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0"
"concat-map": "0.0.1"
} }
}, },
"node_modules/buffer-from": { "node_modules/buffer-from": {
@ -319,35 +304,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/commander": { "node_modules/commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -357,38 +313,22 @@
"node_modules/commondir": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "dev": true
}, },
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
"dev": true
}, },
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
@ -400,6 +340,7 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": [
@ -416,31 +357,30 @@
"dev": true "dev": true
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
"inherits": "2", "inherits": "2",
"minimatch": "^3.1.1", "minimatch": "^5.0.1",
"once": "^1.3.0", "once": "^1.3.0"
"path-is-absolute": "^1.0.0"
}, },
"engines": { "engines": {
"node": "*" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/graphql": { "node_modules/graphql": {
"version": "15.8.0", "version": "16.6.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
"integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==",
"engines": { "engines": {
"node": ">= 10.x" "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
} }
}, },
"node_modules/has": { "node_modules/has": {
@ -455,15 +395,6 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -480,6 +411,21 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.12.0", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
@ -495,7 +441,7 @@
"node_modules/is-module": { "node_modules/is-module": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true "dev": true
}, },
"node_modules/is-reference": { "node_modules/is-reference": {
@ -507,71 +453,27 @@
"@types/estree": "*" "@types/estree": "*"
} }
}, },
"node_modules/jest-worker": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
"integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
"supports-color": "^7.0.0"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/jest-worker/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/jest-worker/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.25.9", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dependencies": { "dependencies": {
"sourcemap-codec": "^1.4.8" "@jridgewell/sourcemap-codec": "^1.4.13"
}
}, },
"node_modules/merge-stream": { "engines": {
"version": "2.0.0", "node": ">=12"
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", }
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^2.0.1"
}, },
"engines": { "engines": {
"node": "*" "node": ">=10"
} }
}, },
"node_modules/once": { "node_modules/once": {
@ -583,15 +485,6 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@ -645,45 +538,34 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "2.79.1", "version": "3.21.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==",
"devOptional": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=14.18.0",
"npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rollup-plugin-css-only": { "node_modules/rollup-plugin-css-only": {
"version": "3.1.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.3.0.tgz",
"integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", "integrity": "sha512-BsiCqJJQzZh2lQiHY5irejRoJ3I1EUFHEi5PjVqsr+EmOh54YrWVwd3YZEXnQJ2+fzlhif0YM/Kf0GuH90GAdQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "4" "@rollup/pluginutils": "5"
}, },
"engines": { "engines": {
"node": ">=10.12.0" "node": ">=14"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "1 || 2" "rollup": "<4"
}
},
"node_modules/rollup-plugin-css-only/node_modules/@rollup/pluginutils": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
"integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
"dev": true,
"dependencies": {
"estree-walker": "^2.0.1",
"picomatch": "^2.2.2"
},
"engines": {
"node": ">= 8.0.0"
} }
}, },
"node_modules/rollup-plugin-svelte": { "node_modules/rollup-plugin-svelte": {
@ -716,22 +598,6 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
"integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
"deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"jest-worker": "^26.2.1",
"serialize-javascript": "^4.0.0",
"terser": "^5.0.0"
},
"peerDependencies": {
"rollup": "^2.0.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -752,14 +618,11 @@
} }
] ]
}, },
"node_modules/serialize-javascript": { "node_modules/smob": {
"version": "4.0.0", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-0.0.6.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "integrity": "sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==",
"dev": true, "dev": true
"dependencies": {
"randombytes": "^2.1.0"
}
}, },
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
@ -780,24 +643,6 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead"
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@ -853,9 +698,9 @@
"integrity": "sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg==" "integrity": "sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg=="
}, },
"node_modules/wonka": { "node_modules/wonka": {
"version": "4.0.15", "version": "6.3.2",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz", "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.2.tgz",
"integrity": "sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==" "integrity": "sha512-2xXbQ1LnwNS7egVm1HPhW2FyKrekolzhpM3mCwXdQr55gO+tAiY76rhb32OL9kKsW8taj++iP7C6hxlVzbnvrw=="
}, },
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",

View File

@ -7,20 +7,20 @@
"dev": "rollup -c -w" "dev": "rollup -c -w"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^11.0.0", "@rollup/plugin-node-resolve": "^15.0.2",
"rollup": "^2.3.4", "@rollup/plugin-terser": "^0.4.1",
"rollup-plugin-css-only": "^3.1.0", "rollup": "^3.21.0",
"rollup-plugin-svelte": "^7.0.0", "rollup-plugin-css-only": "^4.3.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-svelte": "^7.1.4",
"svelte": "^3.49.0" "svelte": "^3.58.0"
}, },
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-replace": "^5.0.2",
"@urql/svelte": "^1.3.0", "@urql/svelte": "^4.0.1",
"graphql": "^15.6.0", "graphql": "^16.6.0",
"sveltestrap": "^5.6.1", "sveltestrap": "^5.10.0",
"uplot": "^1.6.7", "uplot": "^1.6.24",
"wonka": "^4.0.15" "wonka": "^6.3.2"
} }
} }

View File

@ -1,71 +0,0 @@
import svelte from 'rollup-plugin-svelte';
import replace from "@rollup/plugin-replace";
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
// const production = !process.env.ROLLUP_WATCH;
const production = true
const plugins = [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production
}
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
replace({
"process.env.NODE_ENV": JSON.stringify("development"),
preventAssignment: true
})
];
const entrypoint = (name, path) => ({
input: path,
output: {
sourcemap: false,
format: 'iife',
name: 'app',
file: `public/build/${name}.js`
},
plugins: [
...plugins,
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: `${name}.css` }),
],
watch: {
clearScreen: false
}
});
export default [
entrypoint('header', 'src/header.entrypoint.js'),
entrypoint('jobs', 'src/jobs.entrypoint.js'),
entrypoint('user', 'src/user.entrypoint.js'),
entrypoint('list', 'src/list.entrypoint.js'),
entrypoint('job', 'src/job.entrypoint.js'),
entrypoint('systems', 'src/systems.entrypoint.js'),
entrypoint('node', 'src/node.entrypoint.js'),
entrypoint('analysis', 'src/analysis.entrypoint.js'),
entrypoint('status', 'src/status.entrypoint.js'),
entrypoint('config', 'src/config.entrypoint.js')
];

View File

@ -0,0 +1,71 @@
import svelte from 'rollup-plugin-svelte';
import replace from "@rollup/plugin-replace";
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
// const production = false
const plugins = [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production
}
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
replace({
"process.env.NODE_ENV": JSON.stringify("development"),
preventAssignment: true
})
];
const entrypoint = (name, path) => ({
input: path,
output: {
sourcemap: false,
format: 'iife',
name: 'app',
file: `public/build/${name}.js`
},
plugins: [
...plugins,
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: `${name}.css` }),
],
watch: {
clearScreen: false
}
});
export default [
entrypoint('header', 'src/header.entrypoint.js'),
entrypoint('jobs', 'src/jobs.entrypoint.js'),
entrypoint('user', 'src/user.entrypoint.js'),
entrypoint('list', 'src/list.entrypoint.js'),
entrypoint('job', 'src/job.entrypoint.js'),
entrypoint('systems', 'src/systems.entrypoint.js'),
entrypoint('node', 'src/node.entrypoint.js'),
entrypoint('analysis', 'src/analysis.entrypoint.js'),
entrypoint('status', 'src/status.entrypoint.js'),
entrypoint('config', 'src/config.entrypoint.js')
];

View File

@ -1,7 +1,7 @@
<script> <script>
import { init } from './utils.js' import { init } from './utils.js'
import { getContext, onMount } from 'svelte' import { getContext, onMount } from 'svelte'
import { operationStore, query } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import { Row, Col, Spinner, Card, Table } from 'sveltestrap' import { Row, Col, Spinner, Card, Table } from 'sveltestrap'
import Filters from './filters/Filters.svelte' import Filters from './filters/Filters.svelte'
import PlotSelection from './PlotSelection.svelte' import PlotSelection from './PlotSelection.svelte'
@ -30,6 +30,7 @@
let rooflineMaxY let rooflineMaxY
let colWidth let colWidth
let numBins = 50 let numBins = 50
let maxY = -1
const ccconfig = getContext('cc-config') const ccconfig = getContext('cc-config')
const metricConfig = getContext('metrics') const metricConfig = getContext('metrics')
@ -44,15 +45,17 @@
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((max, part) => Math.max(max, part.flopRateSimd.value), 0)
$rooflineQuery.variables.maxY = rooflineMaxY maxY = rooflineMaxY
$rooflineQuery.context.pause = false
$rooflineQuery.reexecute()
} }
}) })
const statsQuery = operationStore(` const client = getContextClient();
query($filter: [JobFilter!]!) {
stats: jobsStatistics(filter: $filter) { $: statsQuery = queryStore({
client: client,
query: gql`
query($filters: [JobFilter!]!) {
stats: jobsStatistics(filter: $filters) {
totalJobs totalJobs
shortJobs shortJobs
totalWalltime totalWalltime
@ -61,35 +64,36 @@
histNumNodes { count, value } histNumNodes { count, value }
} }
topUsers: jobsCount(filter: $filter, groupBy: USER, weight: NODE_HOURS, limit: 5) { name, count } topUsers: jobsCount(filter: $filters, groupBy: USER, weight: NODE_HOURS, limit: 5) { name, count }
} }
`, { filter: [] }, { pause: true }) `,
variables: { filters }
})
const footprintsQuery = operationStore(` $: footprintsQuery = queryStore({
query($filter: [JobFilter!]!, $metrics: [String!]!) { client: client,
footprints: jobsFootprints(filter: $filter, metrics: $metrics) { query: gql`
query($filters: [JobFilter!]!, $metrics: [String!]!) {
footprints: jobsFootprints(filter: $filters, metrics: $metrics) {
nodehours, nodehours,
metrics { metric, data } metrics { metric, data }
} }
} }`,
`, { filter: [], metrics }, { pause: true }) variables: { filters, metrics }
$: $footprintsQuery.variables = { ...$footprintsQuery.variables, metrics } })
const rooflineQuery = operationStore(` $: rooflineQuery = queryStore({
query($filter: [JobFilter!]!, $rows: Int!, $cols: Int!, client: client,
query: gql`
query($filters: [JobFilter!]!, $rows: Int!, $cols: Int!,
$minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) { $minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) {
rooflineHeatmap(filter: $filter, rows: $rows, cols: $cols, rooflineHeatmap(filter: $filters, rows: $rows, cols: $cols,
minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY) minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY)
} }
`, { `,
filter: [], variables: { filters, rows: 50, cols: 50, minX: 0.01, minY: 1., maxX: 1000., maxY }
rows: 50, cols: 50, })
minX: 0.01, minY: 1., maxX: 1000., maxY: -1.
}, { pause: true });
query(statsQuery)
query(footprintsQuery)
query(rooflineQuery)
onMount(() => filters.update()) onMount(() => filters.update())
</script> </script>
@ -116,11 +120,7 @@
disableClusterSelection={true} disableClusterSelection={true}
startTimeQuickSelect={true} startTimeQuickSelect={true}
on:update={({ detail }) => { on:update={({ detail }) => {
$statsQuery.context.pause = false filters = detail.filters;
$statsQuery.variables = { filter: detail.filters }
$footprintsQuery.context.pause = false
$footprintsQuery.variables = { metrics, filter: detail.filters }
$rooflineQuery.variables = { ...$rooflineQuery.variables, filter: detail.filters }
}} /> }} />
</Col> </Col>
</Row> </Row>

View File

@ -114,8 +114,8 @@
cluster={clusters cluster={clusters
.find(c => c.name == $initq.data.job.cluster).subClusters .find(c => c.name == $initq.data.job.cluster).subClusters
.find(sc => sc.name == $initq.data.job.subCluster)} .find(sc => sc.name == $initq.data.job.subCluster)}
flopsAny={$jobMetrics.data.jobMetrics.find(m => m.name == 'flops_any' && m.scope == 'node').metric} flopsAny={$jobMetrics.data.jobMetrics.find(m => m.name == 'flops_any' && m.scope == 'node')}
memBw={$jobMetrics.data.jobMetrics.find(m => m.name == 'mem_bw' && m.scope == 'node').metric} /> memBw={$jobMetrics.data.jobMetrics.find(m => m.name == 'mem_bw' && m.scope == 'node')} />
</Col> </Col>
{:else} {:else}
<Col></Col> <Col></Col>

View File

@ -72,7 +72,7 @@
<UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filters.update(detail)}/> <UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filters.update(detail)}/>
</Col> </Col>
<Col xs="2"> <Col xs="2">
<Refresher on:reload={() => jobList.update()} /> <Refresher on:reload={() => jobList.refresh()} />
</Col> </Col>
</Row> </Row>
<br/> <br/>

View File

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

View File

@ -10,7 +10,7 @@
<script> <script>
import { Modal, ModalBody, ModalHeader, ModalFooter, Button, ListGroup } from 'sveltestrap' import { Modal, ModalBody, ModalHeader, ModalFooter, Button, ListGroup } from 'sveltestrap'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { mutation } from '@urql/svelte' import { gql, getContextClient , mutationStore } from '@urql/svelte'
export let metrics export let metrics
export let isOpen export let isOpen
@ -53,11 +53,17 @@
} }
} }
const updateConfiguration = mutation({ const client = getContextClient();
query: `mutation($name: String!, $value: String!) { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value) updateConfiguration(name: $name, value: $value)
}` }
}) `,
variables: { name, value }
})}
let columnHovering = null let columnHovering = null
@ -84,13 +90,14 @@
metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m)) metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m))
isOpen = false isOpen = false
updateConfiguration({ updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`, name: cluster == null ? configName : `${configName}:${cluster}`,
value: JSON.stringify(metrics) value: JSON.stringify(metrics)
}) }).subscribe(res => {
.then(res => { if (res.fetching === false && res.error) {
if (res.error) throw res.error
console.error(res.error) // console.log('Error on subscription: ' + res.error)
}
}) })
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<script> <script>
import { init } from './utils.js' import { init } from './utils.js'
import { Row, Col, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap' import { Row, Col, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
import { operationStore, query } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import TimeSelection from './filters/TimeSelection.svelte' import TimeSelection from './filters/TimeSelection.svelte'
import PlotTable from './PlotTable.svelte' import PlotTable from './PlotTable.svelte'
import MetricPlot from './plots/MetricPlot.svelte' import MetricPlot from './plots/MetricPlot.svelte'
@ -22,8 +22,8 @@
const ccconfig = getContext('cc-config') const ccconfig = getContext('cc-config')
const clusters = getContext('clusters') const clusters = getContext('clusters')
const client = getContextClient();
const nodesQuery = operationStore(`query($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) { const query = gql`query($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) { nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
host host
subCluster subCluster
@ -40,14 +40,18 @@
} }
} }
} }
}`, { }`;
$: nodesQuery = queryStore({
client: client,
query: query,
variables: {
cluster: cluster, cluster: cluster,
nodes: [hostname], nodes: [hostname],
from: from.toISOString(), from: from.toISOString(),
to: to.toISOString() to: to.toISOString(),
}) }
});
$: $nodesQuery.variables = { cluster, nodes: [hostname], from: from.toISOString(), to: to.toISOString() }
let metricUnits = {} let metricUnits = {}
$: if ($nodesQuery.data) { $: if ($nodesQuery.data) {
@ -59,9 +63,6 @@
} }
} }
} }
query(nodesQuery)
// $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics) // $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
</script> </script>

View File

@ -1,17 +1,22 @@
<script> <script>
import { Modal, ModalBody, ModalHeader, ModalFooter, InputGroup, import { Modal, ModalBody, ModalHeader, ModalFooter, InputGroup,
Button, ListGroup, ListGroupItem, Icon } from 'sveltestrap' Button, ListGroup, ListGroupItem, Icon } from 'sveltestrap'
import { mutation } from '@urql/svelte' 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 updateConfigurationMutation = mutation({ const client = getContextClient();
query: `mutation($name: String!, $value: String!) { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`mutation($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value) updateConfiguration(name: $name, value: $value)
}` }`,
variables: { name, value }
}) })
}
let isHistogramConfigOpen = false, isScatterPlotConfigOpen = false let isHistogramConfigOpen = false, isScatterPlotConfigOpen = false
let selectedMetric1 = null, selectedMetric2 = null let selectedMetric1 = null, selectedMetric2 = null
@ -20,11 +25,12 @@
updateConfigurationMutation({ updateConfigurationMutation({
name: data.name, name: data.name,
value: JSON.stringify(data.value) value: JSON.stringify(data.value)
}).subscribe(res => {
if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
}) })
.then(res => {
if (res.error)
console.error(res.error)
});
} }
</script> </script>

View File

@ -4,16 +4,19 @@
import Histogram from './plots/Histogram.svelte' import Histogram from './plots/Histogram.svelte'
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap' import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
import { init } from './utils.js' import { init } from './utils.js'
import { operationStore, query } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
const { query: initq } = init() const { query: initq } = init()
export let cluster export let cluster
let plotWidths = [], colWidth1 = 0, colWidth2 let plotWidths = [], colWidth1 = 0, colWidth2
let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now()) let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now())
const mainQuery = operationStore(`query($cluster: String!, $filter: [JobFilter!]!, $metrics: [String!], $from: Time!, $to: Time!) {
const client = getContextClient();
$: mainQuery = queryStore({
client: client,
query: gql`query($cluster: String!, $filter: [JobFilter!]!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) { nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
host host
subCluster subCluster
@ -36,12 +39,11 @@
allocatedNodes(cluster: $cluster) { name, count } allocatedNodes(cluster: $cluster) { name, count }
topUsers: jobsCount(filter: $filter, groupBy: USER, weight: NODE_COUNT, limit: 10) { name, count } topUsers: jobsCount(filter: $filter, groupBy: USER, weight: NODE_COUNT, limit: 10) { name, count }
topProjects: jobsCount(filter: $filter, groupBy: PROJECT, weight: NODE_COUNT, limit: 10) { name, count } topProjects: jobsCount(filter: $filter, groupBy: PROJECT, weight: NODE_COUNT, limit: 10) { name, count }
}`, { }`,
cluster: cluster, variables: {
metrics: ['flops_any', 'mem_bw'], cluster: cluster, metrics: ['flops_any', 'mem_bw'], from: from.toISOString(), to: to.toISOString(),
from: from.toISOString(),
to: to.toISOString(),
filter: [{ state: ['running'] }, { cluster: { eq: cluster } }] filter: [{ state: ['running'] }, { cluster: { eq: cluster } }]
}
}) })
const sumUp = (data, subcluster, metric) => data.reduce((sum, node) => node.subCluster == subcluster const sumUp = (data, subcluster, metric) => data.reduce((sum, node) => node.subCluster == subcluster
@ -60,7 +62,6 @@
} }
} }
query(mainQuery)
</script> </script>
<!-- Loading indicator & Refresh --> <!-- Loading indicator & Refresh -->
@ -80,13 +81,8 @@
</Col> </Col>
<Col xs="auto" style="margin-left: auto;"> <Col xs="auto" style="margin-left: auto;">
<Refresher initially={120} on:reload={() => { <Refresher initially={120} on:reload={() => {
console.log('reload...')
from = new Date(Date.now() - 5 * 60 * 1000) from = new Date(Date.now() - 5 * 60 * 1000)
to = new Date(Date.now()) to = new Date(Date.now())
$mainQuery.variables = { ...$mainQuery.variables, from: from, to: to }
$mainQuery.reexecute({ requestPolicy: 'network-only' })
}} /> }} />
</Col> </Col>
</Row> </Row>

View File

@ -1,7 +1,7 @@
<script> <script>
import { init } from './utils.js' import { init } from './utils.js'
import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap' import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
import { operationStore, query } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import TimeSelection from './filters/TimeSelection.svelte' import TimeSelection from './filters/TimeSelection.svelte'
import PlotTable from './PlotTable.svelte' import PlotTable from './PlotTable.svelte'
import MetricPlot from './plots/MetricPlot.svelte' import MetricPlot from './plots/MetricPlot.svelte'
@ -27,7 +27,10 @@
let hostnameFilter = '' let hostnameFilter = ''
let selectedMetric = ccconfig.system_view_selectedMetric let selectedMetric = ccconfig.system_view_selectedMetric
const nodesQuery = operationStore(`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) { const client = getContextClient();
$: nodesQuery = queryStore({
client: client,
query: gql`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) { nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
host host
subCluster subCluster
@ -44,11 +47,13 @@
} }
} }
} }
}`, { }`,
variables: {
cluster: cluster, cluster: cluster,
metrics: [], metrics: [selectedMetric],
from: from.toISOString(), from: from.toISOString(),
to: to.toISOString() to: to.toISOString()
}
}) })
let metricUnits = {} let metricUnits = {}
@ -63,9 +68,6 @@
} }
} }
$: $nodesQuery.variables = { cluster, metrics: [selectedMetric], from: from.toISOString(), to: to.toISOString() }
query(nodesQuery)
</script> </script>
<Row> <Row>

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { mutation } from '@urql/svelte' import { gql, getContextClient , mutationStore } from '@urql/svelte'
import { Icon, Button, ListGroupItem, Spinner, Modal, Input, import { Icon, Button, ListGroupItem, Spinner, Modal, Input,
ModalBody, ModalHeader, ModalFooter, Alert } from 'sveltestrap' ModalBody, ModalHeader, ModalFooter, Alert } from 'sveltestrap'
import { fuzzySearchTags } from './utils.js' import { fuzzySearchTags } from './utils.js'
@ -15,23 +15,37 @@
let pendingChange = false let pendingChange = false
let isOpen = false let isOpen = false
const createTagMutation = mutation({ const client = getContextClient();
query: `mutation($type: String!, $name: String!) {
const createTagMutation = ({ type, name }) => {
return mutationStore({
client: client,
query: gql`mutation($type: String!, $name: String!) {
createTag(type: $type, name: $name) { id, type, name } createTag(type: $type, name: $name) { id, type, name }
}` }`,
variables: { type, name}
}) })
}
const addTagsToJobMutation = mutation({ const addTagsToJobMutation = ({ job, tagIds }) => {
query: `mutation($job: ID!, $tagIds: [ID!]!) { return mutationStore({
client: client,
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
addTagsToJob(job: $job, tagIds: $tagIds) { id, type, name } addTagsToJob(job: $job, tagIds: $tagIds) { id, type, name }
}` }`,
variables: {job, tagIds}
}) })
}
const removeTagsFromJobMutation = mutation({ const removeTagsFromJobMutation = ({ job, tagIds }) => {
query: `mutation($job: ID!, $tagIds: [ID!]!) { return mutationStore({
client: client,
query: gql`mutation($job: ID!, $tagIds: [ID!]!) {
removeTagsFromJob(job: $job, tagIds: $tagIds) { id, type, name } removeTagsFromJob(job: $job, tagIds: $tagIds) { id, type, name }
}` }`,
variables: {job, tagIds}
}) })
}
let allTagsFiltered // $initialized is in there because when it becomes true, allTags is initailzed. let allTagsFiltered // $initialized is in there because when it becomes true, allTags is initailzed.
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags)) $: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags))
@ -55,43 +69,47 @@
function createTag(type, name) { function createTag(type, name) {
pendingChange = true pendingChange = true
return createTagMutation({ type: type, name: name }) createTagMutation({ type: type, name: name })
.then(res => { .subscribe(res => {
if (res.error) if (res.fetching === false && !res.error) {
throw res.error
pendingChange = false pendingChange = false
allTags = [...allTags, res.data.createTag] allTags = [...allTags, res.data.createTag]
newTagType = '' newTagType = ''
newTagName = '' newTagName = ''
return res.data.createTag addTagToJob(res.data.createTag)
}, err => console.error(err)) } else if (res.fetching === false && res.error) {
throw 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] })
.then(res => { .subscribe(res => {
if (res.error) if (res.fetching === false && !res.error) {
throw 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) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
}) })
.catch(err => console.error(err))
} }
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] })
.then(res => { .subscribe(res => {
if (res.error) if (res.fetching === false && !res.error) {
throw 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) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
}) })
.catch(err => console.error(err))
} }
</script> </script>
@ -154,8 +172,7 @@
<br/> <br/>
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)} {#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<Button outline color="success" <Button outline color="success"
on:click={e => (e.preventDefault(), createTag(newTagType, newTagName)) on:click={e => (e.preventDefault(), createTag(newTagType, newTagName))}>
.then(tag => addTagToJob(tag))}>
Create & Add Tag: Create & Add Tag:
<Tag tag={({ type: newTagType, name: newTagName })} clickable={false}/> <Tag tag={({ type: newTagType, name: newTagName })} clickable={false}/>
</Button> </Button>

View File

@ -2,7 +2,7 @@
import { onMount, getContext } from 'svelte' import { onMount, getContext } from 'svelte'
import { init } from './utils.js' import { init } from './utils.js'
import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap' import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
import { operationStore, query } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte' import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte' import JobList from './joblist/JobList.svelte'
import Sorting from './joblist/SortSelection.svelte' import Sorting from './joblist/SortSelection.svelte'
@ -25,31 +25,24 @@
let w1, w2, histogramHeight = 250 let w1, w2, histogramHeight = 250
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
const stats = operationStore(` const client = getContextClient();
query($filter: [JobFilter!]!) { $: stats = queryStore({
jobsStatistics(filter: $filter) { client: client,
query: gql`
query($filters: [JobFilter!]!) {
jobsStatistics(filter: $filters) {
totalJobs totalJobs
shortJobs shortJobs
totalWalltime totalWalltime
totalCoreHours totalCoreHours
histDuration { count, value } histDuration { count, value }
histNumNodes { count, value } histNumNodes { count, value }
} }}`,
} variables: { filters }
`, {
filter: []
}, {
pause: true
}) })
// filters[filters.findIndex(filter => filter.cluster != null)] ?
// filters[filters.findIndex(filter => filter.cluster != null)].cluster.eq :
// null
// Cluster filter has to be alwas @ first index, above will throw error
$: selectedCluster = filters[0]?.cluster ? filters[0].cluster.eq : null $: selectedCluster = filters[0]?.cluster ? filters[0].cluster.eq : null
query(stats)
onMount(() => filters.update()) onMount(() => filters.update())
</script> </script>
@ -84,15 +77,12 @@
bind:this={filters} bind:this={filters}
on:update={({ detail }) => { on:update={({ detail }) => {
let jobFilters = [...detail.filters, { user: { eq: user.username } }] let jobFilters = [...detail.filters, { user: { eq: user.username } }]
$stats.variables = { filter: jobFilters }
$stats.context.pause = false
$stats.reexecute()
filters = jobFilters filters = jobFilters
jobList.update(jobFilters) jobList.update(jobFilters)
}} /> }} />
</Col> </Col>
<Col xs="auto" style="margin-left: auto;"> <Col xs="auto" style="margin-left: auto;">
<Refresher on:reload={() => jobList.update()} /> <Refresher on:reload={() => jobList.refresh()} />
</Col> </Col>
</Row> </Row>
<br/> <br/>

View File

@ -9,83 +9,144 @@
- update(filters?: [JobFilter]) - update(filters?: [JobFilter])
--> -->
<script> <script>
import { operationStore, query, mutation } from '@urql/svelte' import {
import { getContext } from 'svelte'; queryStore,
import { Row, Table, Card, Spinner } from 'sveltestrap' gql,
import Pagination from './Pagination.svelte' getContextClient,
import JobListRow from './Row.svelte' mutationStore,
import { stickyHeader } from '../utils.js' } from "@urql/svelte";
import { getContext } from "svelte";
import { Row, Table, Card, Spinner } from "sveltestrap";
import Pagination from "./Pagination.svelte";
import JobListRow from "./Row.svelte";
import { stickyHeader } from "../utils.js";
const ccconfig = getContext('cc-config'), const ccconfig = getContext("cc-config"),
clusters = getContext('clusters'), clusters = getContext("clusters"),
initialized = getContext('initialized') initialized = getContext("initialized");
export let sorting = { field: "startTime", order: "DESC" } export let sorting = { field: "startTime", order: "DESC" };
export let matchedJobs = 0 export let matchedJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics export let metrics = ccconfig.plot_list_selectedMetrics;
let itemsPerPage = ccconfig.plot_list_jobsPerPage let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1 let page = 1;
let paging = { itemsPerPage, page } let paging = { itemsPerPage, page };
let filter = [] let filter = [];
const jobs = operationStore(` const client = getContextClient();
query($filter: [JobFilter!]!, $sorting: OrderByInput!, $paging: PageRequest! ){ const query = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) { jobs(filter: $filter, order: $sorting, page: $paging) {
items { items {
id, jobId, user, project, jobName, cluster, subCluster, startTime, id
duration, numNodes, numHWThreads, numAcc, walltime, resources { hostname }, jobId
SMT, exclusive, partition, arrayJobId, user
monitoringStatus, state, project
tags { id, type, name } jobName
userData { name } cluster
subCluster
startTime
duration
numNodes
numHWThreads
numAcc
walltime
resources {
hostname
}
SMT
exclusive
partition
arrayJobId
monitoringStatus
state
tags {
id
type
name
}
userData {
name
}
metaData metaData
} }
count count
} }
}`, { }
paging, `;
sorting,
filter,
}, {
pause: true
})
const updateConfiguration = mutation({ $: jobs = queryStore({
query: `mutation($name: String!, $value: String!) { client: client,
updateConfiguration(name: $name, value: $value) query: query,
}` variables: { paging, sorting, filter }
}) });
$: $jobs.variables = { ...$jobs.variables, sorting, paging } $: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refresh() {
queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
requestPolicy: 'network-only'
});
}
// (Re-)query and optionally set new filters. // (Re-)query and optionally set new filters.
export function update(filters) { export function update(filters) {
if (filters != null) { if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) { if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor }) filters.push({ minRunningFor });
}
filter = filters;
}
page = 1;
paging = paging = { page, itemsPerPage };
} }
$jobs.variables.filter = filters const updateConfigurationMutation = ({ name, value }) => {
// console.log('filters:', ...filters.map(f => Object.entries(f)).flat(2)) return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
});
} }
page = 1 function updateConfiguration(value, page) {
$jobs.variables.paging = paging = { page, itemsPerPage }; updateConfigurationMutation({ name: 'plot_list_jobsPerPage', value: value })
$jobs.context.pause = false .subscribe(res => {
$jobs.reexecute({ requestPolicy: 'network-only' }) if (res.fetching === false && !res.error) {
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
} }
})
};
query(jobs) let tableWidth = null;
let jobInfoColumnWidth = 250;
let tableWidth = null $: plotWidth = Math.floor(
let jobInfoColumnWidth = 250 (tableWidth - jobInfoColumnWidth) / metrics.length - 10
$: plotWidth = Math.floor((tableWidth - jobInfoColumnWidth) / metrics.length - 10) );
let headerPaddingTop = 0 let headerPaddingTop = 0;
stickyHeader('.cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)', (x) => (headerPaddingTop = x)) stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x)
);
</script> </script>
<Row> <Row>
@ -93,20 +154,43 @@
<Table cellspacing="0px" cellpadding="0px"> <Table cellspacing="0px" cellpadding="0px">
<thead> <thead>
<tr> <tr>
<th class="position-sticky top-0" scope="col" style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"> <th
class="position-sticky top-0"
scope="col"
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"
>
Job Info Job Info
</th> </th>
{#each metrics as metric (metric)} {#each metrics as metric (metric)}
<th class="position-sticky top-0 text-center" scope="col" style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"> <th
class="position-sticky top-0 text-center"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
{metric} {metric}
{#if $initialized} {#if $initialized}
({clusters ({clusters
.map(cluster => cluster.metricConfig.find(m => m.name == metric)) .map((cluster) =>
.filter(m => m != null) cluster.metricConfig.find(
.map(m => (m.unit?.prefix?m.unit?.prefix:'') + (m.unit?.base?m.unit?.base:'')) // Build unitStr (m) => m.name == metric
.reduce((arr, unitStr) => arr.includes(unitStr) ? arr : [...arr, unitStr], []) // w/o this, output would be [unitStr, unitStr] )
.join(', ') )
}) .filter((m) => m != null)
.map(
(m) =>
(m.unit?.prefix
? m.unit?.prefix
: "") +
(m.unit?.base ? m.unit?.base : "")
) // Build unitStr
.reduce(
(arr, unitStr) =>
arr.includes(unitStr)
? arr
: [...arr, unitStr],
[]
) // w/o this, output would be [unitStr, unitStr]
.join(", ")})
{/if} {/if}
</th> </th>
{/each} {/each}
@ -115,25 +199,24 @@
<tbody> <tbody>
{#if $jobs.error} {#if $jobs.error}
<tr> <tr>
<td colspan="{metrics.length + 1}"> <td colspan={metrics.length + 1}>
<Card body color="danger" class="mb-3"><h2>{$jobs.error.message}</h2></Card> <Card body color="danger" class="mb-3"
><h2>{$jobs.error.message}</h2></Card
>
</td> </td>
</tr> </tr>
{:else if $jobs.fetching || !$jobs.data} {:else if $jobs.fetching || !$jobs.data}
<tr> <tr>
<td colspan="{metrics.length + 1}"> <td colspan={metrics.length + 1}>
<Spinner secondary /> <Spinner secondary />
</td> </td>
</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 <JobListRow {job} {metrics} {plotWidth} />
job={job}
metrics={metrics}
plotWidth={plotWidth} />
{:else} {:else}
<tr> <tr>
<td colspan="{metrics.length + 1}"> <td colspan={metrics.length + 1}>
No jobs found No jobs found
</td> </td>
</tr> </tr>
@ -145,24 +228,21 @@
</Row> </Row>
<Pagination <Pagination
bind:page={page} bind:page
{itemsPerPage} {itemsPerPage}
itemText="Jobs" itemText="Jobs"
totalItems={matchedJobs} totalItems={matchedJobs}
on:update={({ detail }) => { on:update={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) { if (detail.itemsPerPage != itemsPerPage) {
itemsPerPage = detail.itemsPerPage updateConfiguration(
updateConfiguration({ detail.itemsPerPage.toString(),
name: "plot_list_jobsPerPage", detail.page
value: itemsPerPage.toString() )
}).then(res => { } else {
if (res.error)
console.error(res.error);
})
}
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page } paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
}} /> }
}}
/>
<style> <style>
.cc-table-wrapper { .cc-table-wrapper {

View File

@ -9,108 +9,153 @@
--> -->
<script> <script>
import { operationStore, query } 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";
import MetricPlot from '../plots/MetricPlot.svelte' import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from './JobInfo.svelte' import JobInfo from "./JobInfo.svelte";
import { maxScope } from '../utils.js' import { maxScope } from "../utils.js";
export let job export let job;
export let metrics export let metrics;
export let plotWidth export let plotWidth;
export let plotHeight = 275 export let plotHeight = 275;
let scopes = [job.numNodes == 1 ? 'core' : 'node'] let { id } = job;
let scopes = [job.numNodes == 1 ? "core" : "node"];
const cluster = getContext('clusters').find(c => c.name == job.cluster) const cluster = getContext("clusters").find((c) => c.name == job.cluster);
// Get all MetricConfs which include subCluster-specific settings for this job const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
const metricConfig = getContext('metrics') const client = getContextClient();
const metricsQuery = operationStore(`query($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) { const query = gql`
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) {
name name
scope scope
metric { metric {
unit { prefix, base }, timestep unit {
statisticsSeries { min, mean, max } prefix
base
}
timestep
statisticsSeries {
min
mean
max
}
series { series {
hostname, id, data hostname
statistics { min, avg, max } id
data
statistics {
min
avg
max
} }
} }
} }
}`, { }
id: job.id, }
metrics, `;
scopes
})
const selectScope = (jobMetrics) => jobMetrics.reduce( $: metricsQuery = queryStore({
(a, b) => maxScope([a.scope, b.scope]) == a.scope client: client,
? (job.numNodes > 1 ? a : b) query: query,
: (job.numNodes > 1 ? b : a), jobMetrics[0]) variables: { id, metrics, scopes }
});
const sortAndSelectScope = (jobMetrics) => metrics function refresh() {
.map(function(name) { queryStore({
client: client,
query: query,
variables: { id, metrics, scopes }
});
}
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope
? job.numNodes > 1
? a
: b
: job.numNodes > 1
? b
: a,
jobMetrics[0]
);
const sortAndSelectScope = (jobMetrics) =>
metrics
.map(function (name) {
// Get MetricConf for this selected/requested metric // Get MetricConf for this selected/requested metric
let thisConfig = metricConfig(cluster, name) let thisConfig = metricConfig(cluster, name);
let thisSCIndex = thisConfig.subClusters.findIndex(sc => sc.name == job.subCluster) let thisSCIndex = -1
if (thisConfig) {
thisSCIndex = thisConfig.subClusters.findIndex(
(sc) => sc.name == job.subCluster
);
};
// Check if Subcluster has MetricConf: If not found (index == -1), no further remove flag check required // Check if Subcluster has MetricConf: If not found (index == -1), no further remove flag check required
if (thisSCIndex >= 0) { if (thisSCIndex >= 0) {
// SubCluster Config present: Check if remove flag is set // SubCluster Config present: Check if remove flag is set
if (thisConfig.subClusters[thisSCIndex].remove == true) { if (thisConfig.subClusters[thisSCIndex].remove == true) {
// Return null data and informational flag // Return null data and informational flag
return {removed: true, data: null} return { removed: true, data: null };
} else { } else {
// load and return metric, if data available // load and return metric, if data available
let thisMetric = jobMetrics.filter(jobMetric => jobMetric.name == name) // Returns Array let thisMetric = jobMetrics.filter(
(jobMetric) => jobMetric.name == name
); // Returns Array
if (thisMetric.length > 0) { if (thisMetric.length > 0) {
return {removed: false, data: thisMetric} return { removed: false, data: thisMetric };
} else { } else {
return {removed: false, data: null} return { removed: false, data: null };
} }
} }
} else { } else {
// No specific subCluster config: 'remove' flag not set, deemed false -> load and return metric, if data available // No specific subCluster config: 'remove' flag not set, deemed false -> load and return metric, if data available
let thisMetric = jobMetrics.filter(jobMetric => jobMetric.name == name) // Returns Array let thisMetric = jobMetrics.filter(
(jobMetric) => jobMetric.name == name
); // Returns Array
if (thisMetric.length > 0) { if (thisMetric.length > 0) {
return {removed: false, data: thisMetric} return { removed: false, data: thisMetric };
} else { } else {
return {removed: false, data: null} return { removed: false, data: null };
} }
} }
}) })
.map(function(jobMetrics) { .map(function (jobMetrics) {
if (jobMetrics.data != null && jobMetrics.data.length > 0) { if (jobMetrics.data != null && jobMetrics.data.length > 0) {
return {removed: jobMetrics.removed, data: selectScope(jobMetrics.data)} return {
removed: jobMetrics.removed,
data: selectScope(jobMetrics.data),
};
} else { } else {
return jobMetrics return jobMetrics;
} }
}) });
$: metricsQuery.variables = { id: job.id, metrics, scopes } if (job.monitoringStatus) refresh();
if (job.monitoringStatus)
query(metricsQuery)
</script> </script>
<tr> <tr>
<td> <td>
<JobInfo job={job}/> <JobInfo {job} />
</td> </td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2} {#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan="{metrics.length}"> <td colspan={metrics.length}>
<Card body color="warning">Not monitored or archiving failed</Card> <Card body color="warning">Not monitored or archiving failed</Card>
</td> </td>
{:else if $metricsQuery.fetching} {:else if $metricsQuery.fetching}
<td colspan="{metrics.length}" style="text-align: center;"> <td colspan={metrics.length} style="text-align: center;">
<Spinner secondary /> <Spinner secondary />
</td> </td>
{:else if $metricsQuery.error} {:else if $metricsQuery.error}
<td colspan="{metrics.length}"> <td colspan={metrics.length}>
<Card body color="danger" class="mb-3"> <Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500 {$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499)+'...' ? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message} : $metricsQuery.error.message}
</Card> </Card>
</td> </td>
@ -127,10 +172,13 @@
series={metric.data.metric.series} series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries} statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name} metric={metric.data.name}
cluster={cluster} {cluster}
subCluster={job.subCluster} /> subCluster={job.subCluster}
/>
{:else if metric.removed == true && metric.data == null} {:else if metric.removed == true && metric.data == null}
<Card body color="info">Metric disabled for subcluster '{ job.subCluster }'</Card> <Card body color="info"
>Metric disabled for subcluster '{job.subCluster}'</Card
>
{:else} {:else}
<Card body color="warning">Missing Data</Card> <Card body color="warning">Missing Data</Card>
{/if} {/if}

View File

@ -323,7 +323,7 @@
let ctx, canvasElement, prevWidth = width, prevHeight = height let ctx, canvasElement, prevWidth = width, prevHeight = height
data = data != null ? data : (flopsAny && memBw data = data != null ? data : (flopsAny && memBw
? transformData(flopsAny, memBw, colorDots) // Use Metric Object from Parent ? transformData(flopsAny.metric, memBw.metric, colorDots) // Use Metric Object from Parent
: { : {
tiles: tiles, tiles: tiles,
xLabel: 'Intensity [FLOPS/byte]', xLabel: 'Intensity [FLOPS/byte]',

View File

@ -1,8 +1,11 @@
import { expiringCacheExchange } from './cache-exchange.js' import { expiringCacheExchange } from "./cache-exchange.js";
import { initClient } from '@urql/svelte' import {
import { setContext, getContext, hasContext, onDestroy, tick } from 'svelte' Client,
import { dedupExchange, fetchExchange } from '@urql/core' setContextClient,
import { readable } from 'svelte/store' fetchExchange,
} from "@urql/svelte";
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
import { readable } from "svelte/store";
/* /*
* Call this function only at component initialization time! * Call this function only at component initialization time!
@ -14,26 +17,29 @@ import { readable } from 'svelte/store'
* - Adds 'clusters' to the context (object with cluster names as keys) * - Adds 'clusters' to the context (object with cluster names as keys)
* - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined) * - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined)
*/ */
export function init(extraInitQuery = '') { export function init(extraInitQuery = "") {
const jwt = hasContext('jwt') const jwt = hasContext("jwt")
? getContext('jwt') ? getContext("jwt")
: getContext('cc-config')['jwt'] : getContext("cc-config")["jwt"];
const client = initClient({ const client = new Client({
url: `${window.location.origin}/query`, url: `${window.location.origin}/query`,
fetchOptions: jwt != null fetchOptions:
? { headers: { 'Authorization': `Bearer ${jwt}` } } : {}, jwt != null ? { headers: { Authorization: `Bearer ${jwt}` } } : {},
exchanges: [ exchanges: [
dedupExchange,
expiringCacheExchange({ expiringCacheExchange({
ttl: 5 * 60 * 1000, ttl: 5 * 60 * 1000,
maxSize: 150, maxSize: 150,
}), }),
fetchExchange fetchExchange,
] ],
}) });
const query = client.query(`query { setContextClient(client);
const query = client
.query(
`query {
clusters { clusters {
name, name,
metricConfig { metricConfig {
@ -61,227 +67,247 @@ export function init(extraInitQuery = '') {
} }
tags { id, name, type } tags { id, name, type }
${extraInitQuery} ${extraInitQuery}
}`).toPromise() }`
)
.toPromise();
let state = { fetching: true, error: null, data: null } let state = { fetching: true, error: null, data: null };
let subscribers = [] let subscribers = [];
const subscribe = (callback) => { const subscribe = (callback) => {
callback(state) callback(state);
subscribers.push(callback) subscribers.push(callback);
return () => { return () => {
subscribers = subscribers.filter(cb => cb != callback) subscribers = subscribers.filter((cb) => cb != callback);
} };
}; };
const tags = [], clusters = [] const tags = [],
setContext('tags', tags) clusters = [];
setContext('clusters', clusters) setContext("tags", tags);
setContext('metrics', (cluster, metric) => { setContext("clusters", clusters);
if (typeof cluster !== 'object') setContext("metrics", (cluster, metric) => {
cluster = clusters.find(c => c.name == cluster) if (typeof cluster !== "object")
cluster = clusters.find((c) => c.name == cluster);
return cluster.metricConfig.find(m => m.name == metric) return cluster.metricConfig.find((m) => m.name == metric);
}) });
setContext('on-init', callback => state.fetching setContext("on-init", (callback) =>
? subscribers.push(callback) state.fetching ? subscribers.push(callback) : callback(state)
: callback(state)) );
setContext('initialized', readable(false, (set) => setContext(
subscribers.push(() => set(true)))) "initialized",
readable(false, (set) => subscribers.push(() => set(true)))
);
query.then(({ error, data }) => { query.then(({ error, data }) => {
state.fetching = false state.fetching = false;
if (error != null) { if (error != null) {
console.error(error) console.error(error);
state.error = error state.error = error;
tick().then(() => subscribers.forEach(cb => cb(state))) tick().then(() => subscribers.forEach((cb) => cb(state)));
return return;
} }
for (let tag of data.tags) for (let tag of data.tags) tags.push(tag);
tags.push(tag)
for (let cluster of data.clusters) for (let cluster of data.clusters) clusters.push(cluster);
clusters.push(cluster)
state.data = data state.data = data;
tick().then(() => subscribers.forEach(cb => cb(state))) tick().then(() => subscribers.forEach((cb) => cb(state)));
}) });
return { return {
query: { subscribe }, query: { subscribe },
tags, tags,
clusters, clusters,
} };
} }
export function formatNumber(x) { export function formatNumber(x) {
let suffix = '' let suffix = "";
if (x >= 1000000000) { if (x >= 1000000000) {
x /= 1000000 x /= 1000000;
suffix = 'G' suffix = "G";
} else if (x >= 1000000) { } else if (x >= 1000000) {
x /= 1000000 x /= 1000000;
suffix = 'M' suffix = "M";
} else if (x >= 1000) { } else if (x >= 1000) {
x /= 1000 x /= 1000;
suffix = 'k' suffix = "k";
} }
return `${(Math.round(x * 100) / 100)} ${suffix}` return `${Math.round(x * 100) / 100} ${suffix}`;
} }
// Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead? // Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead?
export function deepCopy(x) { export function deepCopy(x) {
return JSON.parse(JSON.stringify(x)) return JSON.parse(JSON.stringify(x));
} }
function fuzzyMatch(term, string) { function fuzzyMatch(term, string) {
return string.toLowerCase().includes(term) return string.toLowerCase().includes(term);
} }
export function fuzzySearchTags(term, tags) { export function fuzzySearchTags(term, tags) {
if (!tags) if (!tags) return [];
return []
let results = [] let results = [];
let termparts = term.split(':').map(s => s.trim()).filter(s => s.length > 0) let termparts = term
.split(":")
.map((s) => s.trim())
.filter((s) => s.length > 0);
if (termparts.length == 0) { if (termparts.length == 0) {
results = tags.slice() results = tags.slice();
} else if (termparts.length == 1) { } else if (termparts.length == 1) {
for (let tag of tags) for (let tag of tags)
if (fuzzyMatch(termparts[0], tag.type) if (
|| fuzzyMatch(termparts[0], tag.name)) fuzzyMatch(termparts[0], tag.type) ||
results.push(tag) fuzzyMatch(termparts[0], tag.name)
)
results.push(tag);
} else if (termparts.length == 2) { } else if (termparts.length == 2) {
for (let tag of tags) for (let tag of tags)
if (fuzzyMatch(termparts[0], tag.type) if (
&& fuzzyMatch(termparts[1], tag.name)) fuzzyMatch(termparts[0], tag.type) &&
results.push(tag) fuzzyMatch(termparts[1], tag.name)
)
results.push(tag);
} }
return results.sort((a, b) => { return results.sort((a, b) => {
if (a.type < b.type) return -1 if (a.type < b.type) return -1;
if (a.type > b.type) return 1 if (a.type > b.type) return 1;
if (a.name < b.name) return -1 if (a.name < b.name) return -1;
if (a.name > b.name) return 1 if (a.name > b.name) return 1;
return 0 return 0;
}) });
} }
export function groupByScope(jobMetrics) { export function groupByScope(jobMetrics) {
let metrics = new Map() let metrics = new Map();
for (let metric of jobMetrics) { for (let metric of jobMetrics) {
if (metrics.has(metric.name)) if (metrics.has(metric.name)) metrics.get(metric.name).push(metric);
metrics.get(metric.name).push(metric) else metrics.set(metric.name, [metric]);
else
metrics.set(metric.name, [metric])
} }
return [...metrics.values()].sort((a, b) => a[0].name.localeCompare(b[0].name)) return [...metrics.values()].sort((a, b) =>
a[0].name.localeCompare(b[0].name)
);
} }
const scopeGranularity = { const scopeGranularity = {
"node": 10, node: 10,
"socket": 5, socket: 5,
"accelerator": 5, accelerator: 5,
"core": 2, core: 2,
"hwthread": 1 hwthread: 1,
}; };
export function maxScope(scopes) { export function maxScope(scopes) {
console.assert(scopes.length > 0 && scopes.every(x => scopeGranularity[x] != null)) console.assert(
let sm = scopes[0], gran = scopeGranularity[scopes[0]] scopes.length > 0 && scopes.every((x) => scopeGranularity[x] != null)
);
let sm = scopes[0],
gran = scopeGranularity[scopes[0]];
for (let scope of scopes) { for (let scope of scopes) {
let otherGran = scopeGranularity[scope] let otherGran = scopeGranularity[scope];
if (otherGran > gran) { if (otherGran > gran) {
sm = scope sm = scope;
gran = otherGran gran = otherGran;
} }
} }
return sm return sm;
} }
export function minScope(scopes) { export function minScope(scopes) {
console.assert(scopes.length > 0 && scopes.every(x => scopeGranularity[x] != null)) console.assert(
let sm = scopes[0], gran = scopeGranularity[scopes[0]] scopes.length > 0 && scopes.every((x) => scopeGranularity[x] != null)
);
let sm = scopes[0],
gran = scopeGranularity[scopes[0]];
for (let scope of scopes) { for (let scope of scopes) {
let otherGran = scopeGranularity[scope] let otherGran = scopeGranularity[scope];
if (otherGran < gran) { if (otherGran < gran) {
sm = scope sm = scope;
gran = otherGran gran = otherGran;
} }
} }
return sm return sm;
} }
export async function fetchMetrics(job, metrics, scopes) { export async function fetchMetrics(job, metrics, scopes) {
if (job.monitoringStatus == 0) if (job.monitoringStatus == 0) return null;
return null
let query = [] let query = [];
if (metrics != null) { if (metrics != null) {
for (let metric of metrics) { for (let metric of metrics) {
query.push(`metric=${metric}`) query.push(`metric=${metric}`);
} }
} }
if (scopes != null) { if (scopes != null) {
for (let scope of scopes) { for (let scope of scopes) {
query.push(`scope=${scope}`) query.push(`scope=${scope}`);
} }
} }
try { try {
let res = await fetch(`/api/jobs/metrics/${job.id}${(query.length > 0) ? '?' : ''}${query.join('&')}`) let res = await fetch(
`/api/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
"&"
)}`
);
if (res.status != 200) { if (res.status != 200) {
return { error: { status: res.status, message: await res.text() } } return { error: { status: res.status, message: await res.text() } };
} }
return await res.json() return await res.json();
} catch (e) { } catch (e) {
return { error: e } return { error: e };
} }
} }
export function fetchMetricsStore() { export function fetchMetricsStore() {
let set = null let set = null;
let prev = { fetching: true, error: null, data: null } let prev = { fetching: true, error: null, data: null };
return [ return [
readable(prev, (_set) => { set = _set }), readable(prev, (_set) => {
(job, metrics, scopes) => fetchMetrics(job, metrics, scopes).then(res => { set = _set;
let next = { fetching: false, error: res.error, data: res.data } }),
(job, metrics, scopes) =>
fetchMetrics(job, metrics, scopes).then((res) => {
let next = { fetching: false, error: res.error, data: res.data };
if (prev.data && next.data) if (prev.data && next.data)
next.data.jobMetrics.push(...prev.data.jobMetrics) next.data.jobMetrics.push(...prev.data.jobMetrics);
prev = next prev = next;
set(next) set(next);
}) }),
] ];
} }
export function stickyHeader(datatableHeaderSelector, updatePading) { export function stickyHeader(datatableHeaderSelector, updatePading) {
const header = document.querySelector('header > nav.navbar') const header = document.querySelector("header > nav.navbar");
if (!header) if (!header) return;
return
let ticking = false, datatableHeader = null let ticking = false,
const onscroll = event => { datatableHeader = null;
if (ticking) const onscroll = (event) => {
return if (ticking) return;
ticking = true ticking = true;
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
ticking = false ticking = false;
if (!datatableHeader) if (!datatableHeader)
datatableHeader = document.querySelector(datatableHeaderSelector) datatableHeader = document.querySelector(datatableHeaderSelector);
const top = datatableHeader.getBoundingClientRect().top const top = datatableHeader.getBoundingClientRect().top;
updatePading(top < header.clientHeight updatePading(
? (header.clientHeight - top) + 10 top < header.clientHeight ? header.clientHeight - top + 10 : 10
: 10) );
}) });
} };
document.addEventListener('scroll', onscroll) document.addEventListener("scroll", onscroll);
onDestroy(() => document.removeEventListener('scroll', onscroll)) onDestroy(() => document.removeEventListener("scroll", onscroll));
} }