mirror of
https://github.com/vale981/ray
synced 2025-03-06 10:31:39 -05:00
[Dashboard] Add the new dashboard code and prompt users to try it (#11667)
This commit is contained in:
parent
42d501d747
commit
752da83bb7
52 changed files with 4650 additions and 33 deletions
299
dashboard/client/package-lock.json
generated
299
dashboard/client/package-lock.json
generated
|
@ -1,29 +1,41 @@
|
||||||
{
|
{
|
||||||
"name": "client",
|
"name": "ray-dashboard-client",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "0.1.0",
|
"name": "ray-dashboard-client",
|
||||||
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "4.11.0",
|
"@material-ui/core": "4.11.0",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||||
|
"@material-ui/pickers": "^3.2.10",
|
||||||
"@reduxjs/toolkit": "^1.3.1",
|
"@reduxjs/toolkit": "^1.3.1",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/jest": "25.1.4",
|
"@types/jest": "25.1.4",
|
||||||
|
"@types/lodash": "^4.14.161",
|
||||||
|
"@types/lowlight": "^0.0.1",
|
||||||
"@types/node": "13.9.5",
|
"@types/node": "13.9.5",
|
||||||
|
"@types/numeral": "^0.0.26",
|
||||||
"@types/react": "16.9.26",
|
"@types/react": "16.9.26",
|
||||||
"@types/react-dom": "16.9.5",
|
"@types/react-dom": "16.9.5",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "^7.1.7",
|
||||||
"@types/react-router-dom": "^5.1.3",
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"@types/react-window": "^1.8.2",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"dayjs": "^1.9.4",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"lowlight": "^1.14.0",
|
||||||
|
"numeral": "^2.0.6",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "^3.4.3",
|
"react-scripts": "^3.4.3",
|
||||||
|
"react-window": "^1.8.5",
|
||||||
"typeface-roboto": "0.0.75",
|
"typeface-roboto": "0.0.75",
|
||||||
"typescript": "3.8.3",
|
"typescript": "3.8.3",
|
||||||
"use-debounce": "^3.4.3"
|
"use-debounce": "^3.4.3"
|
||||||
|
@ -1320,6 +1332,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
||||||
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@date-io/core": {
|
||||||
|
"version": "1.3.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz",
|
||||||
|
"integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA=="
|
||||||
|
},
|
||||||
"node_modules/@emotion/hash": {
|
"node_modules/@emotion/hash": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||||
|
@ -1859,6 +1876,26 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@material-ui/pickers": {
|
||||||
|
"version": "3.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz",
|
||||||
|
"integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.6.0",
|
||||||
|
"@date-io/core": "1.x",
|
||||||
|
"@types/styled-jsx": "^2.2.8",
|
||||||
|
"clsx": "^1.0.2",
|
||||||
|
"react-transition-group": "^4.0.0",
|
||||||
|
"rifm": "^0.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@date-io/core": "^1.3.6",
|
||||||
|
"@material-ui/core": "^4.0.0",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"react": "^16.8.4",
|
||||||
|
"react-dom": "^16.8.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@material-ui/styles": {
|
"node_modules/@material-ui/styles": {
|
||||||
"version": "4.10.0",
|
"version": "4.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz",
|
||||||
|
@ -2205,6 +2242,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
||||||
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ=="
|
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.14.168",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
|
||||||
|
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/lowlight": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lowlight/-/lowlight-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-yPpbpV1KfpFOZ0ZZbsgwWumraiAKoX7/Ng75Ah//w+ZBt4j0xwrQ2aHSlk2kPzQVK4LiPbNFE1LjC00IL4nl/A=="
|
||||||
|
},
|
||||||
"node_modules/@types/minimatch": {
|
"node_modules/@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||||
|
@ -2215,6 +2262,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
|
||||||
"integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw=="
|
"integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/numeral": {
|
||||||
|
"version": "0.0.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-0.0.26.tgz",
|
||||||
|
"integrity": "sha512-DwCsRqeOWopdEsm5KLTxKVKDSDoj+pzZD1vlwu1GQJ6IF3RhjuleYlRwyRH6MJLGaf3v8wFTnC6wo3yYfz0bnA=="
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
@ -2285,11 +2337,27 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-window": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||||
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
|
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/styled-jsx": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "13.0.11",
|
"version": "13.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz",
|
||||||
|
@ -3007,6 +3075,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
|
||||||
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA=="
|
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "0.21.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||||
|
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
|
@ -5158,6 +5234,11 @@
|
||||||
"webidl-conversions": "^4.0.2"
|
"webidl-conversions": "^4.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
|
||||||
|
"integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||||
|
@ -6985,6 +7066,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
|
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
|
||||||
},
|
},
|
||||||
|
"node_modules/fault": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||||
|
"dependencies": {
|
||||||
|
"format": "^0.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/faye-websocket": {
|
"node_modules/faye-websocket": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
||||||
|
@ -7318,6 +7411,14 @@
|
||||||
"node": ">= 0.12"
|
"node": ">= 0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/format": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||||
|
"integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
|
@ -7804,6 +7905,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
||||||
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/history": {
|
"node_modules/history": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
|
@ -8191,12 +8300,9 @@
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
"node_modules/ini": {
|
"node_modules/ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/inquirer": {
|
"node_modules/inquirer": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.4",
|
||||||
|
@ -11001,6 +11107,19 @@
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lowlight": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-Zlc3GqclU71HRw5fTOy00zz5EOlqAdKMYhOFIO8ay4SQEDQgFuhR8JNwDIzAGMLoqTsWxe0elUNmq5o2USRAzw==",
|
||||||
|
"dependencies": {
|
||||||
|
"fault": "^1.0.0",
|
||||||
|
"highlight.js": "~10.5.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
@ -11097,6 +11216,11 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
|
||||||
|
},
|
||||||
"node_modules/memory-fs": {
|
"node_modules/memory-fs": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||||
|
@ -11737,6 +11861,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
|
||||||
"integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4="
|
"integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4="
|
||||||
},
|
},
|
||||||
|
"node_modules/numeral": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
|
||||||
|
"integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nwsapi": {
|
"node_modules/nwsapi": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
||||||
|
@ -14371,6 +14503,22 @@
|
||||||
"prop-types": "^15.6.2"
|
"prop-types": "^15.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-window": {
|
||||||
|
"version": "1.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||||
|
"integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"memoize-one": ">=3.1.1 <6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.0.0 || ^16.0.0 || ^17.0.0",
|
||||||
|
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-pkg": {
|
"node_modules/read-pkg": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
||||||
|
@ -14961,6 +15109,17 @@
|
||||||
"resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
|
||||||
"integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM="
|
"integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM="
|
||||||
},
|
},
|
||||||
|
"node_modules/rifm": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||||
|
@ -19268,6 +19427,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
||||||
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
||||||
},
|
},
|
||||||
|
"@date-io/core": {
|
||||||
|
"version": "1.3.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz",
|
||||||
|
"integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA=="
|
||||||
|
},
|
||||||
"@emotion/hash": {
|
"@emotion/hash": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||||
|
@ -19715,6 +19879,19 @@
|
||||||
"react-is": "^16.8.0"
|
"react-is": "^16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@material-ui/pickers": {
|
||||||
|
"version": "3.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz",
|
||||||
|
"integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.6.0",
|
||||||
|
"@date-io/core": "1.x",
|
||||||
|
"@types/styled-jsx": "^2.2.8",
|
||||||
|
"clsx": "^1.0.2",
|
||||||
|
"react-transition-group": "^4.0.0",
|
||||||
|
"rifm": "^0.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@material-ui/styles": {
|
"@material-ui/styles": {
|
||||||
"version": "4.10.0",
|
"version": "4.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz",
|
||||||
|
@ -20004,6 +20181,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
||||||
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ=="
|
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ=="
|
||||||
},
|
},
|
||||||
|
"@types/lodash": {
|
||||||
|
"version": "4.14.168",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
|
||||||
|
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
|
||||||
|
},
|
||||||
|
"@types/lowlight": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lowlight/-/lowlight-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-yPpbpV1KfpFOZ0ZZbsgwWumraiAKoX7/Ng75Ah//w+ZBt4j0xwrQ2aHSlk2kPzQVK4LiPbNFE1LjC00IL4nl/A=="
|
||||||
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||||
|
@ -20014,6 +20201,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
|
||||||
"integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw=="
|
"integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw=="
|
||||||
},
|
},
|
||||||
|
"@types/numeral": {
|
||||||
|
"version": "0.0.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-0.0.26.tgz",
|
||||||
|
"integrity": "sha512-DwCsRqeOWopdEsm5KLTxKVKDSDoj+pzZD1vlwu1GQJ6IF3RhjuleYlRwyRH6MJLGaf3v8wFTnC6wo3yYfz0bnA=="
|
||||||
|
},
|
||||||
"@types/parse-json": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
@ -20084,11 +20276,27 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-window": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/stack-utils": {
|
"@types/stack-utils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||||
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
|
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
|
||||||
},
|
},
|
||||||
|
"@types/styled-jsx": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/yargs": {
|
"@types/yargs": {
|
||||||
"version": "13.0.11",
|
"version": "13.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz",
|
||||||
|
@ -20693,6 +20901,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
|
||||||
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA=="
|
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA=="
|
||||||
},
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "0.21.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||||
|
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"axobject-query": {
|
"axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
|
@ -22520,6 +22736,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dayjs": {
|
||||||
|
"version": "1.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
|
||||||
|
"integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||||
|
@ -24038,6 +24259,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
|
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
|
||||||
},
|
},
|
||||||
|
"fault": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||||
|
"requires": {
|
||||||
|
"format": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"faye-websocket": {
|
"faye-websocket": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
||||||
|
@ -24312,6 +24541,11 @@
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"format": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||||
|
"integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs="
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
|
@ -24712,6 +24946,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
||||||
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
||||||
},
|
},
|
||||||
|
"highlight.js": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw=="
|
||||||
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
|
@ -25045,9 +25284,9 @@
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||||
},
|
},
|
||||||
"inquirer": {
|
"inquirer": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.4",
|
||||||
|
@ -27299,6 +27538,15 @@
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lowlight": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-Zlc3GqclU71HRw5fTOy00zz5EOlqAdKMYhOFIO8ay4SQEDQgFuhR8JNwDIzAGMLoqTsWxe0elUNmq5o2USRAzw==",
|
||||||
|
"requires": {
|
||||||
|
"fault": "^1.0.0",
|
||||||
|
"highlight.js": "~10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lru-cache": {
|
"lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
@ -27381,6 +27629,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||||
},
|
},
|
||||||
|
"memoize-one": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
|
||||||
|
},
|
||||||
"memory-fs": {
|
"memory-fs": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||||
|
@ -27933,6 +28186,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
|
||||||
"integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4="
|
"integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4="
|
||||||
},
|
},
|
||||||
|
"numeral": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
|
||||||
|
"integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY="
|
||||||
|
},
|
||||||
"nwsapi": {
|
"nwsapi": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
||||||
|
@ -30091,6 +30349,15 @@
|
||||||
"prop-types": "^15.6.2"
|
"prop-types": "^15.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-window": {
|
||||||
|
"version": "1.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||||
|
"integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"memoize-one": ">=3.1.1 <6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"read-pkg": {
|
"read-pkg": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
||||||
|
@ -30574,6 +30841,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
|
||||||
"integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM="
|
"integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM="
|
||||||
},
|
},
|
||||||
|
"rifm": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||||
|
|
|
@ -1,25 +1,36 @@
|
||||||
{
|
{
|
||||||
"name": "client",
|
"name": "ray-dashboard-client",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "4.11.0",
|
"@material-ui/core": "4.11.0",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||||
|
"@material-ui/pickers": "^3.2.10",
|
||||||
"@reduxjs/toolkit": "^1.3.1",
|
"@reduxjs/toolkit": "^1.3.1",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/jest": "25.1.4",
|
"@types/jest": "25.1.4",
|
||||||
|
"@types/lodash": "^4.14.161",
|
||||||
|
"@types/lowlight": "^0.0.1",
|
||||||
"@types/node": "13.9.5",
|
"@types/node": "13.9.5",
|
||||||
|
"@types/numeral": "^0.0.26",
|
||||||
"@types/react": "16.9.26",
|
"@types/react": "16.9.26",
|
||||||
"@types/react-dom": "16.9.5",
|
"@types/react-dom": "16.9.5",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "^7.1.7",
|
||||||
"@types/react-router-dom": "^5.1.3",
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"@types/react-window": "^1.8.2",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"dayjs": "^1.9.4",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"lowlight": "^1.14.0",
|
||||||
|
"numeral": "^2.0.6",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "^3.4.3",
|
"react-scripts": "^3.4.3",
|
||||||
|
"react-window": "^1.8.5",
|
||||||
"typeface-roboto": "0.0.75",
|
"typeface-roboto": "0.0.75",
|
||||||
"typescript": "3.8.3",
|
"typescript": "3.8.3",
|
||||||
"use-debounce": "^3.4.3"
|
"use-debounce": "^3.4.3"
|
||||||
|
@ -40,6 +51,7 @@
|
||||||
"eslint": "./node_modules/.bin/eslint \"src/**\""
|
"eslint": "./node_modules/.bin/eslint \"src/**\""
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
"ignorePatterns": ["*.svg", "*.css"],
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:import/warnings",
|
"plugin:import/warnings",
|
||||||
"react-app"
|
"react-app"
|
||||||
|
@ -110,5 +122,6 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"proxy": "http://localhost:8265"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,112 @@
|
||||||
import { CssBaseline } from "@material-ui/core";
|
import { CssBaseline } from "@material-ui/core";
|
||||||
import React from "react";
|
import { ThemeProvider } from "@material-ui/core/styles";
|
||||||
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { BrowserRouter, Route } from "react-router-dom";
|
import { HashRouter, Route, Switch } from "react-router-dom";
|
||||||
import Dashboard from "./pages/dashboard/Dashboard";
|
import Dashboard from "./pages/dashboard/Dashboard";
|
||||||
|
import Loading from "./pages/exception/Loading";
|
||||||
|
import { getNodeList } from "./service/node";
|
||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
import { darkTheme, lightTheme } from "./theme";
|
||||||
|
import { getLocalStorage, setLocalStorage } from "./util/localData";
|
||||||
|
|
||||||
class App extends React.Component {
|
// lazy loading fro prevent loading too much code at once
|
||||||
render() {
|
const Actors = React.lazy(() => import("./pages/actor"));
|
||||||
return (
|
const CMDResult = React.lazy(() => import("./pages/cmd/CMDResult"));
|
||||||
<Provider store={store}>
|
const Index = React.lazy(() => import("./pages/index/Index"));
|
||||||
<BrowserRouter>
|
const Job = React.lazy(() => import("./pages/job"));
|
||||||
<CssBaseline />
|
const JobDetail = React.lazy(() => import("./pages/job/JobDetail"));
|
||||||
<Route component={Dashboard} exact path="/" />
|
const BasicLayout = React.lazy(() => import("./pages/layout"));
|
||||||
</BrowserRouter>
|
const Logs = React.lazy(() => import("./pages/log/Logs"));
|
||||||
</Provider>
|
const Node = React.lazy(() => import("./pages/node"));
|
||||||
);
|
const NodeDetail = React.lazy(() => import("./pages/node/NodeDetail"));
|
||||||
|
|
||||||
|
// key to store theme in local storage
|
||||||
|
const RAY_DASHBOARD_THEME_KEY = "ray-dashboard-theme";
|
||||||
|
|
||||||
|
// a global map for relations
|
||||||
|
export const GlobalContext = React.createContext({
|
||||||
|
nodeMap: {} as { [key: string]: string },
|
||||||
|
ipLogMap: {} as { [key: string]: string },
|
||||||
|
namespaceMap: {} as { [key: string]: string[] },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDefaultTheme = () =>
|
||||||
|
getLocalStorage<string>(RAY_DASHBOARD_THEME_KEY) || "light";
|
||||||
|
export const setLocalTheme = (theme: string) =>
|
||||||
|
setLocalStorage(RAY_DASHBOARD_THEME_KEY, theme);
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [theme, _setTheme] = useState(getDefaultTheme());
|
||||||
|
const [context, setContext] = useState<{
|
||||||
|
nodeMap: { [key: string]: string };
|
||||||
|
ipLogMap: { [key: string]: string };
|
||||||
|
namespaceMap: { [key: string]: string[] };
|
||||||
|
}>({ nodeMap: {}, ipLogMap: {}, namespaceMap: {} });
|
||||||
|
const getTheme = (name: string) => {
|
||||||
|
switch (name) {
|
||||||
|
case "dark":
|
||||||
|
return darkTheme;
|
||||||
|
case "light":
|
||||||
|
default:
|
||||||
|
return lightTheme;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
const setTheme = (name: string) => {
|
||||||
|
setLocalTheme(name);
|
||||||
|
_setTheme(name);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
getNodeList().then((res) => {
|
||||||
|
if (res?.data?.data?.summary) {
|
||||||
|
const nodeMap = {} as { [key: string]: string };
|
||||||
|
const ipLogMap = {} as { [key: string]: string };
|
||||||
|
res.data.data.summary.forEach(({ hostname, raylet, ip, logUrl }) => {
|
||||||
|
nodeMap[hostname] = raylet.nodeId;
|
||||||
|
ipLogMap[ip] = logUrl;
|
||||||
|
});
|
||||||
|
setContext({ nodeMap, ipLogMap, namespaceMap: {} });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={getTheme(theme)}>
|
||||||
|
<Suspense fallback={Loading}>
|
||||||
|
<GlobalContext.Provider value={context}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<CssBaseline />
|
||||||
|
<HashRouter>
|
||||||
|
<Switch>
|
||||||
|
<Route component={Dashboard} exact path="/" />
|
||||||
|
<Route
|
||||||
|
render={(props) => (
|
||||||
|
<BasicLayout {...props} setTheme={setTheme} theme={theme}>
|
||||||
|
<Route component={Index} exact path="/summary" />
|
||||||
|
<Route component={Job} exact path="/job" />
|
||||||
|
<Route component={Node} exact path="/node" />
|
||||||
|
<Route component={Actors} exact path="/actors" />
|
||||||
|
<Route
|
||||||
|
render={(props) => (
|
||||||
|
<Logs {...props} theme={theme as "light" | "dark"} />
|
||||||
|
)}
|
||||||
|
exact
|
||||||
|
path="/log/:host?/:path?"
|
||||||
|
/>
|
||||||
|
<Route component={NodeDetail} path="/node/:id" />
|
||||||
|
<Route component={JobDetail} path="/job/:id" />
|
||||||
|
<Route component={CMDResult} path="/cmd/:cmd/:ip/:pid" />
|
||||||
|
<Route component={Loading} exact path="/loading" />
|
||||||
|
</BasicLayout>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</HashRouter>
|
||||||
|
</Provider>
|
||||||
|
</GlobalContext.Provider>
|
||||||
|
</Suspense>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
const base =
|
const base = window.location.origin;
|
||||||
process.env.NODE_ENV === "development"
|
|
||||||
? "http://localhost:8265"
|
|
||||||
: window.location.origin;
|
|
||||||
|
|
||||||
type APIResponse<T> = {
|
type APIResponse<T> = {
|
||||||
result: boolean;
|
result: boolean;
|
||||||
|
|
253
dashboard/client/src/components/ActorTable.tsx
Normal file
253
dashboard/client/src/components/ActorTable.tsx
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import {
|
||||||
|
InputAdornment,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TextField,
|
||||||
|
TextFieldProps,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { orange } from "@material-ui/core/colors";
|
||||||
|
import { SearchOutlined } from "@material-ui/icons";
|
||||||
|
import Autocomplete from "@material-ui/lab/Autocomplete";
|
||||||
|
import Pagination from "@material-ui/lab/Pagination";
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { GlobalContext } from "../App";
|
||||||
|
import { Actor } from "../type/actor";
|
||||||
|
import { Worker } from "../type/worker";
|
||||||
|
import { longTextCut } from "../util/func";
|
||||||
|
import { useFilter } from "../util/hook";
|
||||||
|
import StateCounter from "./StatesCounter";
|
||||||
|
import { StatusChip } from "./StatusChip";
|
||||||
|
import RayletWorkerTable, { ExpandableTableRow } from "./WorkerTable";
|
||||||
|
|
||||||
|
const ActorTable = ({
|
||||||
|
actors = {},
|
||||||
|
workers = [],
|
||||||
|
}: {
|
||||||
|
actors: { [actorId: string]: Actor };
|
||||||
|
workers?: Worker[];
|
||||||
|
}) => {
|
||||||
|
const [pageNo, setPageNo] = useState(1);
|
||||||
|
const { changeFilter, filterFunc } = useFilter();
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const { ipLogMap } = useContext(GlobalContext);
|
||||||
|
const actorList = Object.values(actors || {})
|
||||||
|
.map((e) => ({
|
||||||
|
...e,
|
||||||
|
functionDesc: Object.values(
|
||||||
|
e.taskSpec?.functionDescriptor?.javaFunctionDescriptor ||
|
||||||
|
e.taskSpec?.functionDescriptor?.pythonFunctionDescriptor ||
|
||||||
|
{},
|
||||||
|
).join(" "),
|
||||||
|
}))
|
||||||
|
.filter(filterFunc);
|
||||||
|
const list = actorList.slice((pageNo - 1) * pageSize, pageNo * pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div style={{ flex: 1, display: "flex", alignItems: "center" }}>
|
||||||
|
<Autocomplete
|
||||||
|
style={{ margin: 8, width: 120 }}
|
||||||
|
options={Array.from(
|
||||||
|
new Set(Object.values(actors).map((e) => e.state)),
|
||||||
|
)}
|
||||||
|
onInputChange={(_: any, value: string) => {
|
||||||
|
changeFilter("state", value.trim());
|
||||||
|
}}
|
||||||
|
renderInput={(params: TextFieldProps) => (
|
||||||
|
<TextField {...params} label="State" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Autocomplete
|
||||||
|
style={{ margin: 8, width: 150 }}
|
||||||
|
options={Array.from(
|
||||||
|
new Set(Object.values(actors).map((e) => e.address?.ipAddress)),
|
||||||
|
)}
|
||||||
|
onInputChange={(_: any, value: string) => {
|
||||||
|
changeFilter("address.ipAddress", value.trim());
|
||||||
|
}}
|
||||||
|
renderInput={(params: TextFieldProps) => (
|
||||||
|
<TextField {...params} label="IP" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
style={{ margin: 8, width: 120 }}
|
||||||
|
label="PID"
|
||||||
|
size="small"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
changeFilter("pid", value.trim());
|
||||||
|
},
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
style={{ margin: 8, width: 200 }}
|
||||||
|
label="Task Func Desc"
|
||||||
|
size="small"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
changeFilter("functionDesc", value.trim());
|
||||||
|
},
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
style={{ margin: 8, width: 120 }}
|
||||||
|
label="Name"
|
||||||
|
size="small"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
changeFilter("name", value.trim());
|
||||||
|
},
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
style={{ margin: 8, width: 120 }}
|
||||||
|
label="Actor ID"
|
||||||
|
size="small"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
changeFilter("actorId", value.trim());
|
||||||
|
},
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
style={{ margin: 8, width: 120 }}
|
||||||
|
label="Page Size"
|
||||||
|
size="small"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
setPageSize(Math.min(Number(value), 500) || 10);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
page={pageNo}
|
||||||
|
onChange={(e, num) => setPageNo(num)}
|
||||||
|
count={Math.ceil(actorList.length / pageSize)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StateCounter type="actor" list={actorList} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{[
|
||||||
|
"",
|
||||||
|
"ID(Num Restarts)",
|
||||||
|
"Name",
|
||||||
|
"Task Func Desc",
|
||||||
|
"Job Id",
|
||||||
|
"Pid",
|
||||||
|
"IP",
|
||||||
|
"Port",
|
||||||
|
"State",
|
||||||
|
"Log",
|
||||||
|
].map((col) => (
|
||||||
|
<TableCell align="center" key={col}>
|
||||||
|
{col}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{list.map(
|
||||||
|
({
|
||||||
|
actorId,
|
||||||
|
functionDesc,
|
||||||
|
jobId,
|
||||||
|
pid,
|
||||||
|
address,
|
||||||
|
state,
|
||||||
|
name,
|
||||||
|
numRestarts,
|
||||||
|
}) => (
|
||||||
|
<ExpandableTableRow
|
||||||
|
length={
|
||||||
|
workers.filter(
|
||||||
|
(e) =>
|
||||||
|
e.pid === pid &&
|
||||||
|
address.ipAddress === e.coreWorkerStats[0].ipAddress,
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
expandComponent={
|
||||||
|
<RayletWorkerTable
|
||||||
|
actorMap={{}}
|
||||||
|
workers={workers.filter(
|
||||||
|
(e) =>
|
||||||
|
e.pid === pid &&
|
||||||
|
address.ipAddress === e.coreWorkerStats[0].ipAddress,
|
||||||
|
)}
|
||||||
|
mini
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
key={actorId}
|
||||||
|
>
|
||||||
|
<TableCell
|
||||||
|
align="center"
|
||||||
|
style={{
|
||||||
|
color: Number(numRestarts) > 0 ? orange[500] : "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actorId}({numRestarts})
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{name}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{longTextCut(functionDesc, 60)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{jobId}</TableCell>
|
||||||
|
<TableCell align="center">{pid}</TableCell>
|
||||||
|
<TableCell align="center">{address?.ipAddress}</TableCell>
|
||||||
|
<TableCell align="center">{address?.port}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<StatusChip type="actor" status={state} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{ipLogMap[address?.ipAddress] && (
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
to={`/log/${encodeURIComponent(
|
||||||
|
ipLogMap[address?.ipAddress],
|
||||||
|
)}?fileName=${jobId}-${pid}`}
|
||||||
|
>
|
||||||
|
Log
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</ExpandableTableRow>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActorTable;
|
10
dashboard/client/src/components/Loading.tsx
Normal file
10
dashboard/client/src/components/Loading.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Backdrop, CircularProgress } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Loading = ({ loading }: { loading: boolean }) => (
|
||||||
|
<Backdrop open={loading} style={{ zIndex: 100 }}>
|
||||||
|
<CircularProgress color="primary" />
|
||||||
|
</Backdrop>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Loading;
|
221
dashboard/client/src/components/LogView/LogVirtualView.tsx
Normal file
221
dashboard/client/src/components/LogView/LogVirtualView.tsx
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import low from "lowlight";
|
||||||
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
MutableRefObject,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { FixedSizeList as List } from "react-window";
|
||||||
|
import "./darcula.css";
|
||||||
|
import "./github.css";
|
||||||
|
import "./index.css";
|
||||||
|
import { getDefaultTheme } from "../../App";
|
||||||
|
|
||||||
|
const uniqueKeySelector = () => Math.random().toString(16).slice(-8);
|
||||||
|
|
||||||
|
const timeReg = /(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)\s+([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]/;
|
||||||
|
|
||||||
|
const value2react = (
|
||||||
|
{ type, tagName, properties, children, value = "" }: any,
|
||||||
|
key: string,
|
||||||
|
keywords: string = "",
|
||||||
|
) => {
|
||||||
|
switch (type) {
|
||||||
|
case "element":
|
||||||
|
return React.createElement(
|
||||||
|
tagName,
|
||||||
|
{
|
||||||
|
className: properties.className[0],
|
||||||
|
key: `${key}line${uniqueKeySelector()}`,
|
||||||
|
},
|
||||||
|
children.map((e: any, i: number) =>
|
||||||
|
value2react(e, `${key}-${i}`, keywords),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case "text":
|
||||||
|
if (keywords && value.includes(keywords)) {
|
||||||
|
const afterChildren = [];
|
||||||
|
const vals = value.split(keywords);
|
||||||
|
let tmp = vals.shift();
|
||||||
|
if (!tmp) {
|
||||||
|
return React.createElement(
|
||||||
|
"span",
|
||||||
|
{ className: "find-kws" },
|
||||||
|
keywords,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while (typeof tmp === "string") {
|
||||||
|
if (tmp !== "") {
|
||||||
|
afterChildren.push(tmp);
|
||||||
|
} else {
|
||||||
|
afterChildren.push(
|
||||||
|
React.createElement("span", { className: "find-kws" }, keywords),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = vals.shift();
|
||||||
|
if (tmp) {
|
||||||
|
afterChildren.push(
|
||||||
|
React.createElement("span", { className: "find-kws" }, keywords),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return afterChildren;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogVirtualViewProps = {
|
||||||
|
content: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
theme?: "light" | "dark";
|
||||||
|
language?: string;
|
||||||
|
focusLine?: number;
|
||||||
|
keywords?: string;
|
||||||
|
style?: { [key: string]: string | number };
|
||||||
|
listRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
onScrollBottom?: (event: Event) => void;
|
||||||
|
revert?: boolean;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LogVirtualView: React.FC<LogVirtualViewProps> = ({
|
||||||
|
content,
|
||||||
|
width = "100%",
|
||||||
|
height,
|
||||||
|
fontSize = 12,
|
||||||
|
theme = getDefaultTheme(),
|
||||||
|
keywords = "",
|
||||||
|
language = "dos",
|
||||||
|
focusLine = 1,
|
||||||
|
style = {},
|
||||||
|
listRef,
|
||||||
|
onScrollBottom,
|
||||||
|
revert = false,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
}) => {
|
||||||
|
const [logs, setLogs] = useState<{ i: number; origin: string }[]>([]);
|
||||||
|
const total = logs.length;
|
||||||
|
const timmer = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const el = useRef<List>(null);
|
||||||
|
const outter = useRef<HTMLDivElement>(null);
|
||||||
|
if (listRef) {
|
||||||
|
listRef.current = outter.current;
|
||||||
|
}
|
||||||
|
const itemRenderer = ({
|
||||||
|
index,
|
||||||
|
style: s,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
style: CSSProperties;
|
||||||
|
}) => {
|
||||||
|
const { i, origin } = logs[revert ? logs.length - 1 - index : index];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${index}list`}
|
||||||
|
style={{ ...s, overflowX: "visible", whiteSpace: "pre" }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginRight: 4,
|
||||||
|
width: `${logs.length}`.length * 6 + 4,
|
||||||
|
color: "#999",
|
||||||
|
display: "inline-block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
{low
|
||||||
|
.highlight(language, origin)
|
||||||
|
.value.map((v) => value2react(v, index.toString(), keywords))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const originContent = content.split("\n");
|
||||||
|
if (timmer.current) {
|
||||||
|
clearTimeout(timmer.current);
|
||||||
|
}
|
||||||
|
timmer.current = setTimeout(() => {
|
||||||
|
setLogs(
|
||||||
|
originContent
|
||||||
|
.map((e, i) => ({
|
||||||
|
i,
|
||||||
|
origin: e,
|
||||||
|
time: (e?.match(timeReg) || [""])[0],
|
||||||
|
}))
|
||||||
|
.filter((e) => {
|
||||||
|
let bool = e.origin.includes(keywords);
|
||||||
|
if (
|
||||||
|
e.time &&
|
||||||
|
startTime &&
|
||||||
|
!dayjs(e.time).isAfter(dayjs(startTime))
|
||||||
|
) {
|
||||||
|
bool = false;
|
||||||
|
}
|
||||||
|
if (e.time && endTime && !dayjs(e.time).isBefore(dayjs(endTime))) {
|
||||||
|
bool = false;
|
||||||
|
}
|
||||||
|
return bool;
|
||||||
|
})
|
||||||
|
.map((e) => ({
|
||||||
|
...e,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
}, [content, keywords, language, startTime, endTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (el.current) {
|
||||||
|
el.current?.scrollTo((focusLine - 1) * (fontSize + 6));
|
||||||
|
}
|
||||||
|
}, [focusLine, fontSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (outter.current) {
|
||||||
|
const scrollFunc = (event: any) => {
|
||||||
|
const { target } = event;
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
target.scrollTop + target.clientHeight === target.scrollHeight
|
||||||
|
) {
|
||||||
|
if (onScrollBottom) {
|
||||||
|
onScrollBottom(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
outter.current.addEventListener("scroll", scrollFunc);
|
||||||
|
return () => outter?.current?.removeEventListener("scroll", scrollFunc);
|
||||||
|
}
|
||||||
|
}, [onScrollBottom]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
height={height || (content.split("\n").length + 1) * 18}
|
||||||
|
width={width}
|
||||||
|
ref={el}
|
||||||
|
outerRef={outter}
|
||||||
|
className={`hljs-${theme}`}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
itemSize={fontSize + 6}
|
||||||
|
itemCount={total}
|
||||||
|
>
|
||||||
|
{itemRenderer}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogVirtualView;
|
59
dashboard/client/src/components/LogView/darcula.css
Normal file
59
dashboard/client/src/components/LogView/darcula.css
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
Dracula Theme v1.2.0
|
||||||
|
https://github.com/zenorocha/dracula-theme
|
||||||
|
Copyright 2015, All rights reserved
|
||||||
|
Code licensed under the MIT license
|
||||||
|
http://zenorocha.mit-license.org
|
||||||
|
@author Éverton Ribeiro <nuxlli@gmail.com>
|
||||||
|
@author Zeno Rocha <hi@zenorocha.com>
|
||||||
|
*/
|
||||||
|
.hljs-dark {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
.hljs-dark .hljs-number,
|
||||||
|
.hljs-dark .hljs-keyword,
|
||||||
|
.hljs-dark .hljs-selector-tag,
|
||||||
|
.hljs-dark .hljs-literal,
|
||||||
|
.hljs-dark .hljs-section,
|
||||||
|
.hljs-dark .hljs-link {
|
||||||
|
color: #8be9fd;
|
||||||
|
}
|
||||||
|
.hljs-dark .hljs-function .hljs-keyword {
|
||||||
|
color: #ff79c6;
|
||||||
|
}
|
||||||
|
.hljs-dark .hljs-string,
|
||||||
|
.hljs-dark .hljs-title,
|
||||||
|
.hljs-dark .hljs-name,
|
||||||
|
.hljs-dark .hljs-type,
|
||||||
|
.hljs-dark .hljs-attribute,
|
||||||
|
.hljs-dark .hljs-symbol,
|
||||||
|
.hljs-dark .hljs-bullet,
|
||||||
|
.hljs-dark .hljs-addition,
|
||||||
|
.hljs-dark .hljs-variable,
|
||||||
|
.hljs-dark .hljs-template-tag,
|
||||||
|
.hljs-dark .hljs-template-variable {
|
||||||
|
color: #f1fa8c;
|
||||||
|
}
|
||||||
|
.hljs-dark .hljs-comment,
|
||||||
|
.hljs-dark .hljs-quote,
|
||||||
|
.hljs-dark .hljs-deletion,
|
||||||
|
.hljs-dark .hljs-meta {
|
||||||
|
color: #6272a4;
|
||||||
|
}
|
||||||
|
.hljs-dark .hljs-keyword,
|
||||||
|
.hljs-dark .hljs-selector-tag,
|
||||||
|
.hljs-dark .hljs-literal,
|
||||||
|
.hljs-dark .hljs-title,
|
||||||
|
.hljs-dark .hljs-section,
|
||||||
|
.hljs-dark .hljs-doctag,
|
||||||
|
.hljs-dark .hljs-type,
|
||||||
|
.hljs-dark .hljs-name,
|
||||||
|
.hljs-dark .hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.hljs-dark .hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
96
dashboard/client/src/components/LogView/github.css
Normal file
96
dashboard/client/src/components/LogView/github.css
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hljs-light {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-comment,
|
||||||
|
.hljs-light .hljs-quote {
|
||||||
|
color: #998;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-keyword,
|
||||||
|
.hljs-light .hljs-selector-tag,
|
||||||
|
.hljs-light .hljs-subst {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-number,
|
||||||
|
.hljs-light .hljs-literal,
|
||||||
|
.hljs-light .hljs-variable,
|
||||||
|
.hljs-light .hljs-template-variable,
|
||||||
|
.hljs-light .hljs-tag .hljs-attr {
|
||||||
|
color: #008080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-string,
|
||||||
|
.hljs-light .hljs-doctag {
|
||||||
|
color: #d14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-title,
|
||||||
|
.hljs-light .hljs-section,
|
||||||
|
.hljs-light .hljs-selector-id {
|
||||||
|
color: #900;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-subst {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-type,
|
||||||
|
.hljs-light .hljs-class .hljs-title {
|
||||||
|
color: #458;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-tag,
|
||||||
|
.hljs-light .hljs-name,
|
||||||
|
.hljs-light .hljs-attribute {
|
||||||
|
color: #000080;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-regexp,
|
||||||
|
.hljs-light .hljs-link {
|
||||||
|
color: #009926;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-symbol,
|
||||||
|
.hljs-light .hljs-bullet {
|
||||||
|
color: #990073;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-built_in,
|
||||||
|
.hljs-light .hljs-builtin-name {
|
||||||
|
color: #0086b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-meta {
|
||||||
|
color: #999;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-deletion {
|
||||||
|
background: #fdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-addition {
|
||||||
|
background: #dfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-light .hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
3
dashboard/client/src/components/LogView/index.css
Normal file
3
dashboard/client/src/components/LogView/index.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
span.find-kws {
|
||||||
|
background-color: #ffd800;
|
||||||
|
}
|
57
dashboard/client/src/components/PercentageBar.tsx
Normal file
57
dashboard/client/src/components/PercentageBar.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { makeStyles } from "@material-ui/core";
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
const useStyle = makeStyles((theme) => ({
|
||||||
|
container: {
|
||||||
|
background: "linear-gradient(45deg, #21CBF3ee 30%, #2196F3ee 90%)",
|
||||||
|
border: `1px solid #ffffffbb`,
|
||||||
|
padding: "0 12px",
|
||||||
|
height: 18,
|
||||||
|
lineHeight: "18px",
|
||||||
|
position: "relative",
|
||||||
|
boxSizing: "content-box",
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
displayBar: {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
height: 18,
|
||||||
|
transition: "0.5s width",
|
||||||
|
borderRadius: 2,
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
border: "2px solid transparent",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 12,
|
||||||
|
zIndex: 2,
|
||||||
|
position: "relative",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const PercentageBar = (
|
||||||
|
props: PropsWithChildren<{ num: number; total: number }>,
|
||||||
|
) => {
|
||||||
|
const { num, total } = props;
|
||||||
|
const classes = useStyle();
|
||||||
|
const per = Math.round((num / total) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.container}>
|
||||||
|
<div
|
||||||
|
className={classes.displayBar}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(Math.max(0, 100 - per), 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={classes.text}>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PercentageBar;
|
87
dashboard/client/src/components/SearchComponent.tsx
Normal file
87
dashboard/client/src/components/SearchComponent.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import {
|
||||||
|
InputAdornment,
|
||||||
|
makeStyles,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { SearchOutlined } from "@material-ui/icons";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
search: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SearchInput = ({
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
defaultValue,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
className={classes.search}
|
||||||
|
size="small"
|
||||||
|
label={label}
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultValue,
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchSelect = ({
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
options: (string | [string, string])[];
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
className={classes.search}
|
||||||
|
size="small"
|
||||||
|
label={label}
|
||||||
|
select
|
||||||
|
SelectProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(value as string);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="">All</MenuItem>
|
||||||
|
{options.map((e) =>
|
||||||
|
typeof e === "string" ? (
|
||||||
|
<MenuItem value={e}>{e}</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem value={e[0]}>{e[1]}</MenuItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
|
);
|
||||||
|
};
|
156
dashboard/client/src/components/SpeedTools.tsx
Normal file
156
dashboard/client/src/components/SpeedTools.tsx
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import {
|
||||||
|
Grow,
|
||||||
|
makeStyles,
|
||||||
|
Paper,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
TextField,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { red } from "@material-ui/core/colors";
|
||||||
|
import { Build, Close } from "@material-ui/icons";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { StatusChip } from "./StatusChip";
|
||||||
|
|
||||||
|
const chunkArray = (myArray: string[], chunk_size: number) => {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
while (myArray.length) {
|
||||||
|
results.push(myArray.splice(0, chunk_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const revertBit = (str: string) => {
|
||||||
|
return chunkArray(str.split(""), 2)
|
||||||
|
.reverse()
|
||||||
|
.map((e) => e.join(""))
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectFlag = (str: string, offset: number) => {
|
||||||
|
const flag = parseInt(str, 16);
|
||||||
|
const mask = 1 << offset;
|
||||||
|
|
||||||
|
return Number(!!(flag & mask));
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyle = makeStyles((theme) => ({
|
||||||
|
toolContainer: {
|
||||||
|
background: theme.palette.primary.main,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 48,
|
||||||
|
position: "fixed",
|
||||||
|
bottom: 100,
|
||||||
|
left: 50,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
top: 12,
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 50,
|
||||||
|
bottom: 48,
|
||||||
|
width: 500,
|
||||||
|
height: 300,
|
||||||
|
padding: 6,
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: theme.palette.text.disabled,
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
float: "right",
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ObjectIdReader = () => {
|
||||||
|
const [id, setId] = useState("");
|
||||||
|
const tagList = [
|
||||||
|
["Create From Task", 15, 1],
|
||||||
|
["Put Object", 14, 0],
|
||||||
|
["Return Object", 14, 1],
|
||||||
|
] as [string, number, number][];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 8 }}>
|
||||||
|
<TextField
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
id="standard-basic"
|
||||||
|
label="Object Id"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
setId(value);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{id.length === 40 ? (
|
||||||
|
<div style={{ padding: 8 }}>
|
||||||
|
Job ID: {id.slice(24, 28)} <br />
|
||||||
|
Actor ID: {id.slice(16, 28)} <br />
|
||||||
|
Task ID: {id.slice(0, 28)} <br />
|
||||||
|
Index: {parseInt(revertBit(id.slice(32)), 16)} <br />
|
||||||
|
Flag: {revertBit(id.slice(28, 32))}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{tagList
|
||||||
|
.filter(
|
||||||
|
([a, b, c]) => detectFlag(revertBit(id.slice(28, 32)), b) === c,
|
||||||
|
)
|
||||||
|
.map(([name]) => (
|
||||||
|
<StatusChip key={name} type="tag" status={name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: red[500] }}>
|
||||||
|
Object ID should be 40 letters long
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tools = () => {
|
||||||
|
const [sel, setSel] = useState("oid_converter");
|
||||||
|
const toolMap = {
|
||||||
|
oid_converter: <ObjectIdReader />,
|
||||||
|
} as { [key: string]: JSX.Element };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tabs value={sel} onChange={(e, val) => setSel(val)}>
|
||||||
|
<Tab
|
||||||
|
value="oid_converter"
|
||||||
|
label={<span style={{ fontSize: 12 }}>Object ID Reader</span>}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
{toolMap[sel]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpeedTools = () => {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const classes = useStyle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.toolContainer}>
|
||||||
|
<Build className={classes.icon} onClick={() => setShow(!show)} />
|
||||||
|
<Grow in={show} style={{ transformOrigin: "300 500 0" }}>
|
||||||
|
<Paper className={classes.popover}>
|
||||||
|
<Close className={classes.close} onClick={() => setShow(false)} />
|
||||||
|
<Tools />
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpeedTools;
|
31
dashboard/client/src/components/StatesCounter.tsx
Normal file
31
dashboard/client/src/components/StatesCounter.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Grid } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
import { StatusChip } from "./StatusChip";
|
||||||
|
|
||||||
|
const StateCounter = ({
|
||||||
|
type,
|
||||||
|
list,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
list: { state: string }[];
|
||||||
|
}) => {
|
||||||
|
const stateMap = {} as { [state: string]: number };
|
||||||
|
list.forEach(({ state }) => {
|
||||||
|
stateMap[state] = stateMap[state] + 1 || 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2} alignItems="center">
|
||||||
|
<Grid item>
|
||||||
|
<StatusChip status="TOTAL" type={type} suffix={`x ${list.length}`} />
|
||||||
|
</Grid>
|
||||||
|
{Object.entries(stateMap).map(([s, num]) => (
|
||||||
|
<Grid item>
|
||||||
|
<StatusChip status={s} type={type} suffix={` x ${num}`} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StateCounter;
|
90
dashboard/client/src/components/StatusChip.tsx
Normal file
90
dashboard/client/src/components/StatusChip.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { Color } from "@material-ui/core";
|
||||||
|
import {
|
||||||
|
blue,
|
||||||
|
blueGrey,
|
||||||
|
cyan,
|
||||||
|
green,
|
||||||
|
grey,
|
||||||
|
lightBlue,
|
||||||
|
red,
|
||||||
|
} from "@material-ui/core/colors";
|
||||||
|
import { CSSProperties } from "@material-ui/core/styles/withStyles";
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { ActorEnum } from "../type/actor";
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
node: {
|
||||||
|
ALIVE: green,
|
||||||
|
DEAD: red,
|
||||||
|
},
|
||||||
|
actor: {
|
||||||
|
[ActorEnum.ALIVE]: green,
|
||||||
|
[ActorEnum.DEAD]: red,
|
||||||
|
[ActorEnum.PENDING]: blue,
|
||||||
|
[ActorEnum.RECONSTRUCTING]: lightBlue,
|
||||||
|
},
|
||||||
|
job: {
|
||||||
|
INIT: grey,
|
||||||
|
SUBMITTED: blue,
|
||||||
|
DISPATCHED: lightBlue,
|
||||||
|
RUNNING: green,
|
||||||
|
COMPLETED: cyan,
|
||||||
|
FINISHED: cyan,
|
||||||
|
FAILED: red,
|
||||||
|
},
|
||||||
|
} as {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: Color;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMap = {
|
||||||
|
deps: blue,
|
||||||
|
INFO: cyan,
|
||||||
|
ERROR: red,
|
||||||
|
} as {
|
||||||
|
[key: string]: Color;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusChip = ({
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
suffix,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
status: string | ActorEnum | ReactNode;
|
||||||
|
suffix?: string;
|
||||||
|
}) => {
|
||||||
|
const style = {
|
||||||
|
padding: "2px 8px",
|
||||||
|
border: "solid 1px",
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
margin: 2,
|
||||||
|
} as CSSProperties;
|
||||||
|
|
||||||
|
let color = blueGrey as Color;
|
||||||
|
|
||||||
|
if (typeMap[type]) {
|
||||||
|
color = typeMap[type];
|
||||||
|
} else if (
|
||||||
|
typeof status === "string" &&
|
||||||
|
colorMap[type] &&
|
||||||
|
colorMap[type][status]
|
||||||
|
) {
|
||||||
|
color = colorMap[type][status];
|
||||||
|
}
|
||||||
|
|
||||||
|
style.color = color[500];
|
||||||
|
style.borderColor = color[500];
|
||||||
|
if (color !== blueGrey) {
|
||||||
|
style.backgroundColor = `${color[500]}20`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={style}>
|
||||||
|
{status}
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
34
dashboard/client/src/components/TitleCard.tsx
Normal file
34
dashboard/client/src/components/TitleCard.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { makeStyles, Paper } from "@material-ui/core";
|
||||||
|
import React, { PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
card: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(1.5),
|
||||||
|
margin: [theme.spacing(2), theme.spacing(1)].map((e) => `${e}px`).join(" "),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.typography.fontSize + 2,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TitleCard = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<{ title: ReactNode | string }>) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<Paper className={classes.card}>
|
||||||
|
<div className={classes.title}>{title}</div>
|
||||||
|
<div className={classes.body}>{children}</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TitleCard;
|
299
dashboard/client/src/components/WorkerTable.tsx
Normal file
299
dashboard/client/src/components/WorkerTable.tsx
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { KeyboardArrowDown, KeyboardArrowRight } from "@material-ui/icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import React, {
|
||||||
|
PropsWithChildren,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { GlobalContext } from "../App";
|
||||||
|
import { Actor } from "../type/actor";
|
||||||
|
import { CoreWorkerStats, Worker } from "../type/worker";
|
||||||
|
import { memoryConverter } from "../util/converter";
|
||||||
|
import { longTextCut } from "../util/func";
|
||||||
|
|
||||||
|
import { useFilter } from "../util/hook";
|
||||||
|
import ActorTable from "./ActorTable";
|
||||||
|
import PercentageBar from "./PercentageBar";
|
||||||
|
import { SearchInput } from "./SearchComponent";
|
||||||
|
|
||||||
|
export const ExpandableTableRow = ({
|
||||||
|
children,
|
||||||
|
expandComponent,
|
||||||
|
length,
|
||||||
|
stateKey = "",
|
||||||
|
...otherProps
|
||||||
|
}: PropsWithChildren<{
|
||||||
|
expandComponent: ReactNode;
|
||||||
|
length: number;
|
||||||
|
stateKey?: string;
|
||||||
|
}>) => {
|
||||||
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (stateKey.startsWith("ON")) {
|
||||||
|
setIsExpanded(true);
|
||||||
|
} else if (stateKey.startsWith("OFF")) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
}
|
||||||
|
}, [stateKey]);
|
||||||
|
|
||||||
|
if (length < 1) {
|
||||||
|
return (
|
||||||
|
<TableRow {...otherProps}>
|
||||||
|
<TableCell padding="checkbox" />
|
||||||
|
{children}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableRow {...otherProps}>
|
||||||
|
<TableCell padding="checkbox">
|
||||||
|
<IconButton
|
||||||
|
style={{ color: "inherit" }}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
|
{length}
|
||||||
|
{isExpanded ? <KeyboardArrowDown /> : <KeyboardArrowRight />}
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
{children}
|
||||||
|
</TableRow>
|
||||||
|
{isExpanded && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={24}>{expandComponent}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkerDetailTable = ({
|
||||||
|
actorMap,
|
||||||
|
coreWorkerStats,
|
||||||
|
}: {
|
||||||
|
actorMap: { [actorId: string]: Actor };
|
||||||
|
coreWorkerStats: CoreWorkerStats[];
|
||||||
|
}) => {
|
||||||
|
const actors = {} as { [actorId: string]: Actor };
|
||||||
|
(coreWorkerStats || [])
|
||||||
|
.filter((e) => actorMap[e.actorId])
|
||||||
|
.forEach((e) => (actors[e.actorId] = actorMap[e.actorId]));
|
||||||
|
|
||||||
|
if (!Object.values(actors).length) {
|
||||||
|
return <p>The Worker Haven't Had Related Actor Yet.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<ActorTable actors={actors} />
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RayletWorkerTable = ({
|
||||||
|
workers = [],
|
||||||
|
actorMap,
|
||||||
|
mini,
|
||||||
|
}: {
|
||||||
|
workers: Worker[];
|
||||||
|
actorMap: { [actorId: string]: Actor };
|
||||||
|
mini?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { changeFilter, filterFunc } = useFilter();
|
||||||
|
const [key, setKey] = useState("");
|
||||||
|
const { nodeMap, ipLogMap } = useContext(GlobalContext);
|
||||||
|
const open = () => setKey(`ON${Math.random()}`);
|
||||||
|
const close = () => setKey(`OFF${Math.random()}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{!mini && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<SearchInput
|
||||||
|
label="Pid"
|
||||||
|
onChange={(value) => changeFilter("pid", value)}
|
||||||
|
/>
|
||||||
|
<Button onClick={open}>Expand All</Button>
|
||||||
|
<Button onClick={close}>Collapse All</Button>
|
||||||
|
</div>
|
||||||
|
)}{" "}
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{[
|
||||||
|
"",
|
||||||
|
"Pid",
|
||||||
|
"CPU",
|
||||||
|
"CPU Times",
|
||||||
|
"Memory",
|
||||||
|
"CMD Line",
|
||||||
|
"Create Time",
|
||||||
|
"Log",
|
||||||
|
"Ops",
|
||||||
|
"IP/Hostname",
|
||||||
|
].map((col) => (
|
||||||
|
<TableCell align="center" key={col}>
|
||||||
|
{col}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{workers
|
||||||
|
.filter(filterFunc)
|
||||||
|
.sort((aWorker, bWorker) => {
|
||||||
|
const a =
|
||||||
|
(aWorker.coreWorkerStats || []).filter(
|
||||||
|
(e) => actorMap[e.actorId],
|
||||||
|
).length || 0;
|
||||||
|
const b =
|
||||||
|
(bWorker.coreWorkerStats || []).filter(
|
||||||
|
(e) => actorMap[e.actorId],
|
||||||
|
).length || 0;
|
||||||
|
return b - a;
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
pid,
|
||||||
|
cpuPercent,
|
||||||
|
cpuTimes,
|
||||||
|
memoryInfo,
|
||||||
|
cmdline,
|
||||||
|
createTime,
|
||||||
|
coreWorkerStats = [],
|
||||||
|
language,
|
||||||
|
ip,
|
||||||
|
hostname,
|
||||||
|
}) => (
|
||||||
|
<ExpandableTableRow
|
||||||
|
expandComponent={
|
||||||
|
<WorkerDetailTable
|
||||||
|
actorMap={actorMap}
|
||||||
|
coreWorkerStats={coreWorkerStats}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
length={
|
||||||
|
(coreWorkerStats || []).filter((e) => actorMap[e.actorId])
|
||||||
|
.length
|
||||||
|
}
|
||||||
|
key={pid}
|
||||||
|
stateKey={key}
|
||||||
|
>
|
||||||
|
<TableCell align="center">{pid}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<PercentageBar num={Number(cpuPercent)} total={100}>
|
||||||
|
{cpuPercent}%
|
||||||
|
</PercentageBar>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<div style={{ maxHeight: 55, overflow: "auto" }}>
|
||||||
|
{Object.entries(cpuTimes || {}).map(([key, val]) => (
|
||||||
|
<div style={{ margin: 4 }}>
|
||||||
|
{key}:{val}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<div style={{ maxHeight: 55, overflow: "auto" }}>
|
||||||
|
{Object.entries(memoryInfo || {}).map(([key, val]) => (
|
||||||
|
<div style={{ margin: 4 }}>
|
||||||
|
{key}:{memoryConverter(val)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" style={{ lineBreak: "anywhere" }}>
|
||||||
|
{cmdline && longTextCut(cmdline.filter((e) => e).join(" "))}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{dayjs(createTime * 1000).format("YYYY/MM/DD HH:mm:ss")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{ipLogMap[ip] && (
|
||||||
|
<Grid item>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
to={`/log/${encodeURIComponent(
|
||||||
|
ipLogMap[ip],
|
||||||
|
)}?fileName=${
|
||||||
|
coreWorkerStats[0].jobId || ""
|
||||||
|
}-${pid}`}
|
||||||
|
>
|
||||||
|
Log
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{language === "JAVA" && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.open(
|
||||||
|
`#/cmd/jstack/${coreWorkerStats[0]?.ipAddress}/${pid}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
jstack
|
||||||
|
</Button>{" "}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.open(
|
||||||
|
`#/cmd/jmap/${coreWorkerStats[0]?.ipAddress}/${pid}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
jmap
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.open(
|
||||||
|
`#/cmd/jstat/${coreWorkerStats[0]?.ipAddress}/${pid}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
jstat
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{ip}
|
||||||
|
<br />
|
||||||
|
{nodeMap[hostname] ? (
|
||||||
|
<Link target="_blank" to={`/node/${nodeMap[hostname]}`}>
|
||||||
|
{hostname}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
hostname
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</ExpandableTableRow>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RayletWorkerTable;
|
34
dashboard/client/src/logo.svg
Normal file
34
dashboard/client/src/logo.svg
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="ray" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 144.5 144.6" style="enable-background:new 0 0 144.5 144.6;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:url(#SVGID_1_);}
|
||||||
|
</style>
|
||||||
|
<title>Ray Logo</title>
|
||||||
|
<g>
|
||||||
|
<g id="layer-1">
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="31.9659" y1="112.5396" x2="112.4544" y2="32.0512">
|
||||||
|
<stop offset="0.3" style="stop-color:#1976D2"/>
|
||||||
|
<stop offset="0.9" style="stop-color:#0091EA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path class="st0" d="M97.3,77.2c-3.8-1.1-6.2,0.9-8.3,5.1c-3.5,6.8-9.9,9.9-17.4,9.6S58,88.1,54.8,81.2c-1.4-3-3-4-6.3-4.1
|
||||||
|
c-5.6-0.1-9.9,0.1-13.1,6.4c-3.8,7.6-13.6,10.2-21.8,7.6C5.2,88.4-0.4,80.5,0,71.7c0.1-8.4,5.7-15.8,13.8-18.2
|
||||||
|
c8.4-2.6,17.5,0.7,22.3,8c1.3,1.9,1.3,5.2,3.6,5.6c3.9,0.6,8,0.2,12,0.2c1.8,0,1.9-1.6,2.4-2.8c3.5-7.8,9.7-11.8,18-11.9
|
||||||
|
c8.2-0.1,14.4,3.9,17.8,11.4c1.3,2.8,2.9,3.6,5.7,3.3c1-0.1,2,0.1,3,0c2.8-0.5,6.4,1.7,8.1-2.7s-2.3-5.5-4.1-7.5
|
||||||
|
c-5.1-5.7-10.9-10.8-16.1-16.3C84,38,81.9,37.1,78,38.3C66.7,42,56.2,35.7,53,24.1C50.3,14,57.3,2.8,67.7,0.5
|
||||||
|
C78.4-2,89,4.7,91.5,15.3c0.1,0.3,0.1,0.5,0.2,0.8c0.7,3.4,0.7,6.9-0.8,9.8c-1.7,3.2-0.8,5,1.5,7.2c6.7,6.5,13.3,13,19.8,19.7
|
||||||
|
c1.8,1.8,3,2.1,5.5,1.2c9.1-3.4,17.9-0.6,23.4,7c4.8,6.9,4.6,16.1-0.4,22.9c-5.4,7.2-14.2,9.9-23.1,6.5c-2.3-0.9-3.5-0.6-5.1,1.1
|
||||||
|
c-6.7,6.9-13.6,13.7-20.5,20.4c-1.8,1.8-2.5,3.2-1.4,5.9c3.5,8.7,0.3,18.6-7.7,23.6c-7.9,5-18.2,3.8-24.8-2.9
|
||||||
|
c-6.4-6.4-7.4-16.2-2.5-24.3c4.9-7.8,14.5-11,23.1-7.8c3,1.1,4.7,0.5,6.9-1.7C91.7,98.4,98,92.3,104.2,86c1.6-1.6,4.1-2.7,2.6-6.2
|
||||||
|
c-1.4-3.3-3.8-2.5-6.2-2.6C99.8,77.2,98.9,77.2,97.3,77.2z M72.1,29.7c5.5,0.1,9.9-4.3,10-9.8c0-0.1,0-0.2,0-0.3
|
||||||
|
C81.8,14,77,9.8,71.5,10.2c-5,0.3-9,4.2-9.3,9.2c-0.2,5.5,4,10.1,9.5,10.3C71.8,29.7,72,29.7,72.1,29.7z M72.3,62.3
|
||||||
|
c-5.4-0.1-9.9,4.2-10.1,9.7c0,0.2,0,0.3,0,0.5c0.2,5.4,4.5,9.7,9.9,10c5.1,0.1,9.9-4.7,10.1-9.8c0.2-5.5-4-10-9.5-10.3
|
||||||
|
C72.6,62.3,72.4,62.3,72.3,62.3z M115,72.5c0.1,5.4,4.5,9.7,9.8,9.9c5.6-0.2,10-4.8,10-10.4c-0.2-5.4-4.6-9.7-10-9.7
|
||||||
|
c-5.3-0.1-9.8,4.2-9.9,9.5C115,72.1,115,72.3,115,72.5z M19.5,62.3c-5.4,0.1-9.8,4.4-10,9.8c-0.1,5.1,5.2,10.4,10.2,10.3
|
||||||
|
c5.6-0.2,10-4.9,9.8-10.5c-0.1-5.4-4.5-9.7-9.9-9.6C19.6,62.3,19.5,62.3,19.5,62.3z M71.8,134.6c5.9,0.2,10.3-3.9,10.4-9.6
|
||||||
|
c0.5-5.5-3.6-10.4-9.1-10.8c-5.5-0.5-10.4,3.6-10.8,9.1c0,0.5,0,0.9,0,1.4c-0.2,5.3,4,9.8,9.3,10
|
||||||
|
C71.6,134.6,71.7,134.6,71.8,134.6z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
36
dashboard/client/src/pages/actor/index.tsx
Normal file
36
dashboard/client/src/pages/actor/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { makeStyles } from "@material-ui/core";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import ActorTable from "../../components/ActorTable";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import { getActors } from "../../service/actor";
|
||||||
|
import { Actor } from "../../type/actor";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Actors = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [actors, setActors] = useState<{ [actorId: string]: Actor }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getActors().then((res) => {
|
||||||
|
if (res?.data?.data?.actors) {
|
||||||
|
setActors(res.data.data.actors);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<TitleCard title="ACTORS">
|
||||||
|
<ActorTable actors={actors} />
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Actors;
|
137
dashboard/client/src/pages/cmd/CMDResult.tsx
Normal file
137
dashboard/client/src/pages/cmd/CMDResult.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
makeStyles,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
import LogVirtualView from "../../components/LogView/LogVirtualView";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import { getJmap, getJstack, getJstat } from "../../service/util";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
marginTop: theme.spacing(4),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
pageMeta: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CMDResult = (
|
||||||
|
props: RouteComponentProps<{ cmd: string; ip: string; pid: string }>,
|
||||||
|
) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const {
|
||||||
|
match: { params },
|
||||||
|
} = props;
|
||||||
|
const { cmd, ip, pid } = params;
|
||||||
|
const [result, setResult] = useState<string>();
|
||||||
|
const [option, setOption] = useState("gcutil");
|
||||||
|
const executeJstat = useCallback(
|
||||||
|
() =>
|
||||||
|
getJstat(ip, pid, option)
|
||||||
|
.then((rsp) => {
|
||||||
|
if (rsp.data.result) {
|
||||||
|
setResult(rsp.data.data.output);
|
||||||
|
} else {
|
||||||
|
setResult(rsp.data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => setResult(err.toString())),
|
||||||
|
[ip, pid, option],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (cmd) {
|
||||||
|
case "jstack":
|
||||||
|
getJstack(ip, pid)
|
||||||
|
.then((rsp) => {
|
||||||
|
if (rsp.data.result) {
|
||||||
|
setResult(rsp.data.data.output);
|
||||||
|
} else {
|
||||||
|
setResult(rsp.data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => setResult(err.toString()));
|
||||||
|
break;
|
||||||
|
case "jmap":
|
||||||
|
getJmap(ip, pid)
|
||||||
|
.then((rsp) => {
|
||||||
|
if (rsp.data.result) {
|
||||||
|
setResult(rsp.data.data.output);
|
||||||
|
} else {
|
||||||
|
setResult(rsp.data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => setResult(err.toString()));
|
||||||
|
break;
|
||||||
|
case "jstat":
|
||||||
|
executeJstat();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setResult(`Command ${cmd} is not supported.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [cmd, executeJstat, ip, pid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<TitleCard title={cmd}>
|
||||||
|
{cmd === "jstat" && (
|
||||||
|
<Paper className={classes.pageMeta}>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item>
|
||||||
|
<Select
|
||||||
|
value={option}
|
||||||
|
onChange={(e) => setOption(e.target.value as string)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"class",
|
||||||
|
"compiler",
|
||||||
|
"gc",
|
||||||
|
"gccapacity",
|
||||||
|
"gcmetacapacity",
|
||||||
|
"gcnew",
|
||||||
|
"gcnewcapacity",
|
||||||
|
"gcold",
|
||||||
|
"gcoldcapacity",
|
||||||
|
"gcutil",
|
||||||
|
"gccause",
|
||||||
|
"printcompilation",
|
||||||
|
].map((e) => (
|
||||||
|
<MenuItem value={e}>{e}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button onClick={executeJstat}>Execute</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</TitleCard>
|
||||||
|
<TitleCard title={`IP: ${ip} / Pid: ${pid}`}>
|
||||||
|
<LogVirtualView
|
||||||
|
content={result || "loading"}
|
||||||
|
language="prolog"
|
||||||
|
height={800}
|
||||||
|
/>
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CMDResult;
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
createStyles,
|
createStyles,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Tab,
|
Tab,
|
||||||
|
@ -8,6 +9,7 @@ import {
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import React, { useCallback, useEffect, useRef } from "react";
|
import React, { useCallback, useEffect, useRef } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { getActorGroups, getNodeInfo, getTuneAvailability } from "../../api";
|
import { getActorGroups, getNodeInfo, getTuneAvailability } from "../../api";
|
||||||
import { StoreState } from "../../store";
|
import { StoreState } from "../../store";
|
||||||
import LastUpdated from "./LastUpdated";
|
import LastUpdated from "./LastUpdated";
|
||||||
|
@ -59,6 +61,7 @@ const Dashboard: React.FC = () => {
|
||||||
const tuneAvailability = useSelector(tuneAvailabilitySelector);
|
const tuneAvailability = useSelector(tuneAvailabilitySelector);
|
||||||
const tab = useSelector(tabSelector);
|
const tab = useSelector(tabSelector);
|
||||||
const classes = useDashboardStyles();
|
const classes = useDashboardStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
// Polling Function
|
// Polling Function
|
||||||
const refreshInfo = useCallback(async () => {
|
const refreshInfo = useCallback(async () => {
|
||||||
|
@ -103,6 +106,9 @@ const Dashboard: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<Typography variant="h5">Ray Dashboard</Typography>
|
<Typography variant="h5">Ray Dashboard</Typography>
|
||||||
|
<Button onClick={() => history.push("/summary")}>
|
||||||
|
Try New Dashboard
|
||||||
|
</Button>
|
||||||
<Tabs
|
<Tabs
|
||||||
className={classes.tabs}
|
className={classes.tabs}
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
|
|
32
dashboard/client/src/pages/error/404.tsx
Normal file
32
dashboard/client/src/pages/error/404.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import { HelpOutlineOutlined } from "@material-ui/icons";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Error404 = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
position: "fixed",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ height: 400 }}>
|
||||||
|
<Typography variant="h2">
|
||||||
|
<HelpOutlineOutlined fontSize="large" />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">404 NOT FOUND</Typography>
|
||||||
|
<p>
|
||||||
|
We can't provide the page you wanted yet, better try with another path
|
||||||
|
next time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error404;
|
21
dashboard/client/src/pages/exception/Loading.tsx
Normal file
21
dashboard/client/src/pages/exception/Loading.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react";
|
||||||
|
import Logo from "../../logo.svg";
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<div style={{ height: "100vh", width: "100vw" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "250px auto 0 auto",
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={Logo} alt="Loading" width={100} />
|
||||||
|
<br />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
110
dashboard/client/src/pages/index/Index.tsx
Normal file
110
dashboard/client/src/pages/index/Index.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import {
|
||||||
|
makeStyles,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { version } from "../../../package.json";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import { getRayConfig } from "../../service/cluster";
|
||||||
|
import { getNodeList } from "../../service/node";
|
||||||
|
import { RayConfig } from "../../type/config";
|
||||||
|
import { NodeDetail } from "../../type/node";
|
||||||
|
import { memoryConverter } from "../../util/converter";
|
||||||
|
|
||||||
|
const useStyle = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getVal = (key: string, value: any) => {
|
||||||
|
if (key === "containerMemory") {
|
||||||
|
return memoryConverter(value * 1024 * 1024);
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useIndex = () => {
|
||||||
|
const [rayConfig, setConfig] = useState<RayConfig>();
|
||||||
|
const [nodes, setNodes] = useState<NodeDetail[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
getRayConfig().then((res) => {
|
||||||
|
if (res?.data?.data?.config) {
|
||||||
|
setConfig(res.data.data.config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
getNodeList().then((res) => {
|
||||||
|
if (res?.data?.data?.summary) {
|
||||||
|
setNodes(res.data.data.summary);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { rayConfig, nodes };
|
||||||
|
};
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
const { rayConfig } = useIndex();
|
||||||
|
const classes = useStyle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<TitleCard title={rayConfig?.clusterName || "SUMMARY"}>
|
||||||
|
<p>Dashboard Frontend Version: {version}</p>
|
||||||
|
{rayConfig?.imageUrl && (
|
||||||
|
<p>
|
||||||
|
Image Url:{" "}
|
||||||
|
<a
|
||||||
|
href={rayConfig.imageUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{rayConfig.imageUrl}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{rayConfig?.sourceCodeLink && (
|
||||||
|
<p>
|
||||||
|
Source Code:{" "}
|
||||||
|
<a
|
||||||
|
href={rayConfig.sourceCodeLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{rayConfig.sourceCodeLink}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</TitleCard>
|
||||||
|
{rayConfig && (
|
||||||
|
<TitleCard title="Config">
|
||||||
|
<TableContainer>
|
||||||
|
<TableHead>
|
||||||
|
<TableCell>Key</TableCell>
|
||||||
|
<TableCell>Value</TableCell>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{Object.entries(rayConfig).map(([key, value]) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className={classes.label}>{key}</TableCell>
|
||||||
|
<TableCell>{getVal(key, value)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</TableContainer>
|
||||||
|
</TitleCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
246
dashboard/client/src/pages/job/JobDetail.tsx
Normal file
246
dashboard/client/src/pages/job/JobDetail.tsx
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
makeStyles,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tabs,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
import { Link, RouteComponentProps } from "react-router-dom";
|
||||||
|
import ActorTable from "../../components/ActorTable";
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
|
import { StatusChip } from "../../components/StatusChip";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import RayletWorkerTable from "../../components/WorkerTable";
|
||||||
|
import { longTextCut } from "../../util/func";
|
||||||
|
import { useJobDetail } from "./hook/useJobDetail";
|
||||||
|
|
||||||
|
const useStyle = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
pageMeta: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
dependenciesChip: {
|
||||||
|
margin: theme.spacing(0.5),
|
||||||
|
wordBreak: "break-all",
|
||||||
|
},
|
||||||
|
alert: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const JobDetailPage = (props: RouteComponentProps<{ id: string }>) => {
|
||||||
|
const classes = useStyle();
|
||||||
|
const {
|
||||||
|
actorMap,
|
||||||
|
jobInfo,
|
||||||
|
job,
|
||||||
|
msg,
|
||||||
|
selectedTab,
|
||||||
|
handleChange,
|
||||||
|
handleSwitchChange,
|
||||||
|
params,
|
||||||
|
refreshing,
|
||||||
|
ipLogMap,
|
||||||
|
} = useJobDetail(props);
|
||||||
|
|
||||||
|
if (!job || !jobInfo) {
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Loading loading={msg.startsWith("Loading")} />
|
||||||
|
<TitleCard title={`JOB - ${params.id}`}>
|
||||||
|
<StatusChip type="job" status="LOADING" />
|
||||||
|
<br />
|
||||||
|
Auto Refresh:
|
||||||
|
<Switch
|
||||||
|
checked={refreshing}
|
||||||
|
onChange={handleSwitchChange}
|
||||||
|
name="refresh"
|
||||||
|
inputProps={{ "aria-label": "secondary checkbox" }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Request Status: {msg} <br />
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<TitleCard title={`JOB - ${params.id}`}>
|
||||||
|
<StatusChip type="job" status={jobInfo.isDead ? "DEAD" : "ALIVE"} />
|
||||||
|
<br />
|
||||||
|
Auto Refresh:
|
||||||
|
<Switch
|
||||||
|
checked={refreshing}
|
||||||
|
onChange={handleSwitchChange}
|
||||||
|
name="refresh"
|
||||||
|
inputProps={{ "aria-label": "secondary checkbox" }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Request Status: {msg} <br />
|
||||||
|
</TitleCard>
|
||||||
|
<TitleCard title="Job Detail">
|
||||||
|
<Tabs
|
||||||
|
value={selectedTab}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={classes.tab}
|
||||||
|
>
|
||||||
|
<Tab value="info" label="Info" />
|
||||||
|
<Tab value="dep" label="Dependencies" />
|
||||||
|
<Tab
|
||||||
|
value="worker"
|
||||||
|
label={`Worker(${job?.jobWorkers?.length || 0})`}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="actor"
|
||||||
|
label={`Actor(${Object.entries(job?.jobActors || {}).length || 0})`}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
{selectedTab === "info" && (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<span className={classes.label}>Driver IP</span>:{" "}
|
||||||
|
{jobInfo.driverIpAddress}
|
||||||
|
</Grid>
|
||||||
|
{ipLogMap[jobInfo.driverIpAddress] && (
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<span className={classes.label}>Driver Log</span>:{" "}
|
||||||
|
<Link
|
||||||
|
to={`/log/${encodeURIComponent(
|
||||||
|
ipLogMap[jobInfo.driverIpAddress],
|
||||||
|
)}?fileName=driver-${jobInfo.jobId}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Log
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<span className={classes.label}>Driver Pid</span>:{" "}
|
||||||
|
{jobInfo.driverPid}
|
||||||
|
</Grid>
|
||||||
|
{jobInfo.eventUrl && (
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<span className={classes.label}>Event Link</span>:{" "}
|
||||||
|
<a
|
||||||
|
href={jobInfo.eventUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Event Log
|
||||||
|
</a>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{jobInfo.failErrorMessage && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<span className={classes.label}>Fail Error</span>:{" "}
|
||||||
|
<span className={classes.alert}>
|
||||||
|
{jobInfo.failErrorMessage}
|
||||||
|
</span>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{jobInfo?.dependencies && selectedTab === "dep" && (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
{jobInfo?.dependencies?.python && (
|
||||||
|
<TitleCard title="Python Dependencies">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyItems: "space-around",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{jobInfo.dependencies.python.map((e) => (
|
||||||
|
<StatusChip
|
||||||
|
type="deps"
|
||||||
|
status={e.startsWith("http") ? longTextCut(e, 30) : e}
|
||||||
|
key={e}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TitleCard>
|
||||||
|
)}
|
||||||
|
{jobInfo?.dependencies?.java && (
|
||||||
|
<TitleCard title="Java Dependencies">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{["Name", "Version", "URL"].map((col) => (
|
||||||
|
<TableCell align="center" key={col}>
|
||||||
|
{col}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{jobInfo.dependencies.java.map(
|
||||||
|
({ name, version, url }) => (
|
||||||
|
<TableRow key={url}>
|
||||||
|
<TableCell align="center">{name}</TableCell>
|
||||||
|
<TableCell align="center">{version}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</TitleCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedTab === "worker" && (
|
||||||
|
<div>
|
||||||
|
<TableContainer className={classes.paper}>
|
||||||
|
<RayletWorkerTable
|
||||||
|
workers={job.jobWorkers}
|
||||||
|
actorMap={actorMap || {}}
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedTab === "actor" && (
|
||||||
|
<div>
|
||||||
|
<TableContainer className={classes.paper}>
|
||||||
|
<ActorTable actors={actorMap || {}} workers={job.jobWorkers} />
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JobDetailPage;
|
73
dashboard/client/src/pages/job/hook/useJobDetail.ts
Normal file
73
dashboard/client/src/pages/job/hook/useJobDetail.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
import { GlobalContext } from "../../../App";
|
||||||
|
import { getJobDetail } from "../../../service/job";
|
||||||
|
import { JobDetail } from "../../../type/job";
|
||||||
|
|
||||||
|
export const useJobDetail = (props: RouteComponentProps<{ id: string }>) => {
|
||||||
|
const {
|
||||||
|
match: { params },
|
||||||
|
} = props;
|
||||||
|
const [job, setJob] = useState<JobDetail>();
|
||||||
|
const [msg, setMsg] = useState("Loading the job detail");
|
||||||
|
const [refreshing, setRefresh] = useState(true);
|
||||||
|
const [selectedTab, setTab] = useState("info");
|
||||||
|
const { ipLogMap } = useContext(GlobalContext);
|
||||||
|
const tot = useRef<NodeJS.Timeout>();
|
||||||
|
const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => {
|
||||||
|
setTab(newValue);
|
||||||
|
};
|
||||||
|
const handleSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRefresh(event.target.checked);
|
||||||
|
};
|
||||||
|
const getJob = useCallback(async () => {
|
||||||
|
if (!refreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rsp = await getJobDetail(params.id);
|
||||||
|
|
||||||
|
if (rsp.data?.data?.detail) {
|
||||||
|
setJob(rsp.data.data.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rsp.data?.msg) {
|
||||||
|
setMsg(rsp.data.msg || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rsp.data.result === false) {
|
||||||
|
setMsg("Job Query Error Please Check JobId");
|
||||||
|
setJob(undefined);
|
||||||
|
setRefresh(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
tot.current = setTimeout(getJob, 4000);
|
||||||
|
}, [refreshing, params.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tot.current) {
|
||||||
|
clearTimeout(tot.current);
|
||||||
|
}
|
||||||
|
getJob();
|
||||||
|
return () => {
|
||||||
|
if (tot.current) {
|
||||||
|
clearTimeout(tot.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [getJob]);
|
||||||
|
|
||||||
|
const { jobInfo } = job || {};
|
||||||
|
const actorMap = job?.jobActors;
|
||||||
|
|
||||||
|
return {
|
||||||
|
actorMap,
|
||||||
|
jobInfo,
|
||||||
|
job,
|
||||||
|
msg,
|
||||||
|
selectedTab,
|
||||||
|
handleChange,
|
||||||
|
handleSwitchChange,
|
||||||
|
params,
|
||||||
|
refreshing,
|
||||||
|
ipLogMap,
|
||||||
|
};
|
||||||
|
};
|
68
dashboard/client/src/pages/job/hook/useJobList.ts
Normal file
68
dashboard/client/src/pages/job/hook/useJobList.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { getJobList } from "../../../service/job";
|
||||||
|
import { Job } from "../../../type/job";
|
||||||
|
|
||||||
|
export const useJobList = () => {
|
||||||
|
const [jobList, setList] = useState<Job[]>([]);
|
||||||
|
const [page, setPage] = useState({ pageSize: 10, pageNo: 1 });
|
||||||
|
const [msg, setMsg] = useState("Loading the job list...");
|
||||||
|
const [isRefreshing, setRefresh] = useState(true);
|
||||||
|
const [filter, setFilter] = useState<
|
||||||
|
{
|
||||||
|
key: "jobId" | "name" | "language" | "state" | "namespaceId";
|
||||||
|
val: string;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
const refreshRef = useRef(isRefreshing);
|
||||||
|
const tot = useRef<NodeJS.Timeout>();
|
||||||
|
const changeFilter = (
|
||||||
|
key: "jobId" | "name" | "language" | "state" | "namespaceId",
|
||||||
|
val: string,
|
||||||
|
) => {
|
||||||
|
const f = filter.find((e) => e.key === key);
|
||||||
|
if (f) {
|
||||||
|
f.val = val;
|
||||||
|
} else {
|
||||||
|
filter.push({ key, val });
|
||||||
|
}
|
||||||
|
setFilter([...filter]);
|
||||||
|
};
|
||||||
|
const onSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRefresh(event.target.checked);
|
||||||
|
};
|
||||||
|
refreshRef.current = isRefreshing;
|
||||||
|
const getJob = useCallback(async () => {
|
||||||
|
if (!refreshRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rsp = await getJobList();
|
||||||
|
|
||||||
|
if (rsp?.data?.data?.summary) {
|
||||||
|
setList(rsp.data.data.summary.sort((a, b) => b.timestamp - a.timestamp));
|
||||||
|
setMsg(rsp.data.msg || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
tot.current = setTimeout(getJob, 4000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getJob();
|
||||||
|
return () => {
|
||||||
|
if (tot.current) {
|
||||||
|
clearTimeout(tot.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [getJob]);
|
||||||
|
return {
|
||||||
|
jobList: jobList.filter((node) =>
|
||||||
|
filter.every((f) => node[f.key] && node[f.key].includes(f.val)),
|
||||||
|
),
|
||||||
|
msg,
|
||||||
|
isRefreshing,
|
||||||
|
onSwitchChange,
|
||||||
|
changeFilter,
|
||||||
|
page,
|
||||||
|
originalJobs: jobList,
|
||||||
|
setPage: (key: string, val: number) => setPage({ ...page, [key]: val }),
|
||||||
|
};
|
||||||
|
};
|
129
dashboard/client/src/pages/job/index.tsx
Normal file
129
dashboard/client/src/pages/job/index.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import {
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Pagination from "@material-ui/lab/Pagination";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
|
import { SearchInput, SearchSelect } from "../../components/SearchComponent";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import { useJobList } from "./hook/useJobList";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns = ["ID", "DriverIpAddress", "DriverPid", "IsDead", "Timestamp"];
|
||||||
|
|
||||||
|
const JobList = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
isRefreshing,
|
||||||
|
onSwitchChange,
|
||||||
|
jobList,
|
||||||
|
changeFilter,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
} = useJobList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Loading loading={msg.startsWith("Loading")} />
|
||||||
|
<TitleCard title="JOBS">
|
||||||
|
Auto Refresh:
|
||||||
|
<Switch
|
||||||
|
checked={isRefreshing}
|
||||||
|
onChange={onSwitchChange}
|
||||||
|
name="refresh"
|
||||||
|
inputProps={{ "aria-label": "secondary checkbox" }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Request Status: {msg}
|
||||||
|
</TitleCard>
|
||||||
|
<TitleCard title="Job List">
|
||||||
|
<TableContainer>
|
||||||
|
<SearchInput
|
||||||
|
label="ID"
|
||||||
|
onChange={(value) => changeFilter("jobId", value)}
|
||||||
|
/>
|
||||||
|
<SearchSelect
|
||||||
|
label="Language"
|
||||||
|
onChange={(value) => changeFilter("language", value)}
|
||||||
|
options={["JAVA", "PYTHON"]}
|
||||||
|
/>
|
||||||
|
<SearchInput
|
||||||
|
label="Page Size"
|
||||||
|
onChange={(value) =>
|
||||||
|
setPage("pageSize", Math.min(Number(value), 500) || 10)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
count={Math.ceil(jobList.length / page.pageSize)}
|
||||||
|
page={page.pageNo}
|
||||||
|
onChange={(e, pageNo) => setPage("pageNo", pageNo)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<TableCell align="center" key={col}>
|
||||||
|
{col}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{jobList
|
||||||
|
.slice(
|
||||||
|
(page.pageNo - 1) * page.pageSize,
|
||||||
|
page.pageNo * page.pageSize,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
jobId = "",
|
||||||
|
driverIpAddress,
|
||||||
|
isDead,
|
||||||
|
driverPid,
|
||||||
|
state,
|
||||||
|
timestamp,
|
||||||
|
namespaceId,
|
||||||
|
}) => (
|
||||||
|
<TableRow key={jobId}>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Link to={`/job/${jobId}`}>{jobId}</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{driverIpAddress}</TableCell>
|
||||||
|
<TableCell align="center">{driverPid}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{isDead ? "true" : "false"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{dayjs(timestamp * 1000).format("YYYY/MM/DD HH:mm:ss")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{namespaceId}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JobList;
|
167
dashboard/client/src/pages/layout/index.tsx
Normal file
167
dashboard/client/src/pages/layout/index.tsx
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
import { IconButton, Tooltip } from "@material-ui/core";
|
||||||
|
import Drawer from "@material-ui/core/Drawer";
|
||||||
|
import List from "@material-ui/core/List";
|
||||||
|
import ListItem from "@material-ui/core/ListItem";
|
||||||
|
import ListItemText from "@material-ui/core/ListItemText";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import { NightsStay, VerticalAlignTop, WbSunny } from "@material-ui/icons";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
|
||||||
|
import SpeedTools from "../../components/SpeedTools";
|
||||||
|
import Logo from "../../logo.svg";
|
||||||
|
|
||||||
|
const drawerWidth = 200;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
"& a": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
drawerPaper: {
|
||||||
|
width: drawerWidth,
|
||||||
|
border: "none",
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
boxShadow: theme.shadows[1],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
textAlign: "center",
|
||||||
|
lineHeight: "36px",
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
background: "rgba(255, 255, 255, .12)",
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
background: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
background: `linear-gradient(45deg, ${theme.palette.primary.main} 30%, ${theme.palette.secondary.main} 90%)`,
|
||||||
|
},
|
||||||
|
child: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BasicLayout = (
|
||||||
|
props: PropsWithChildren<
|
||||||
|
{ setTheme: (theme: string) => void; theme: string } & RouteComponentProps
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { location, history, children, setTheme, theme } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
anchor="left"
|
||||||
|
className={classes.drawer}
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" className={classes.title}>
|
||||||
|
<img width={48} src={Logo} alt="Ray" /> <br /> Ray Dashboard
|
||||||
|
</Typography>
|
||||||
|
<List>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
className={classnames(
|
||||||
|
classes.menuItem,
|
||||||
|
location.pathname === "/summary" && classes.selected,
|
||||||
|
)}
|
||||||
|
onClick={() => history.push("/summary")}
|
||||||
|
>
|
||||||
|
<ListItemText>SUMMARY</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
className={classnames(
|
||||||
|
classes.menuItem,
|
||||||
|
location.pathname.includes("node") && classes.selected,
|
||||||
|
)}
|
||||||
|
onClick={() => history.push("/node")}
|
||||||
|
>
|
||||||
|
<ListItemText>NODES</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
className={classnames(
|
||||||
|
classes.menuItem,
|
||||||
|
location.pathname.includes("job") && classes.selected,
|
||||||
|
)}
|
||||||
|
onClick={() => history.push("/job")}
|
||||||
|
>
|
||||||
|
<ListItemText>JOBS</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
className={classnames(
|
||||||
|
classes.menuItem,
|
||||||
|
location.pathname.includes("actor") && classes.selected,
|
||||||
|
)}
|
||||||
|
onClick={() => history.push("/actors")}
|
||||||
|
>
|
||||||
|
<ListItemText>ACTORS</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
className={classnames(
|
||||||
|
classes.menuItem,
|
||||||
|
location.pathname.includes("log") && classes.selected,
|
||||||
|
)}
|
||||||
|
onClick={() => history.push("/log")}
|
||||||
|
>
|
||||||
|
<ListItemText>LOGS</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
button
|
||||||
|
className={classnames(classes.menuItem)}
|
||||||
|
onClick={() => history.push("/")}
|
||||||
|
>
|
||||||
|
<ListItemText>BACK TO LEGACY</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title="Back To Top">
|
||||||
|
<VerticalAlignTop />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setTheme(theme === "dark" ? "light" : "dark");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title={`Theme - ${theme}`}>
|
||||||
|
{theme === "dark" ? <NightsStay /> : <WbSunny />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
<SpeedTools />
|
||||||
|
</List>
|
||||||
|
</Drawer>
|
||||||
|
<div className={classes.child}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicLayout;
|
306
dashboard/client/src/pages/log/Logs.tsx
Normal file
306
dashboard/client/src/pages/log/Logs.tsx
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
InputAdornment,
|
||||||
|
LinearProgress,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
makeStyles,
|
||||||
|
Paper,
|
||||||
|
Switch,
|
||||||
|
TextField,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { SearchOutlined } from "@material-ui/icons";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
import LogVirtualView from "../../components/LogView/LogVirtualView";
|
||||||
|
import { SearchInput } from "../../components/SearchComponent";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import { getLogDetail } from "../../service/log";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
marginTop: theme.spacing(4),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
pageMeta: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
type LogsProps = RouteComponentProps<{ host?: string; path?: string }> & {
|
||||||
|
theme?: "dark" | "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLogs = (props: LogsProps) => {
|
||||||
|
const {
|
||||||
|
match: { params },
|
||||||
|
location: { search: urlSearch },
|
||||||
|
theme,
|
||||||
|
} = props;
|
||||||
|
const { host, path } = params;
|
||||||
|
const searchMap = new URLSearchParams(urlSearch);
|
||||||
|
const urlFileName = searchMap.get("fileName");
|
||||||
|
const el = useRef<HTMLDivElement>(null);
|
||||||
|
const [origin, setOrigin] = useState<string>();
|
||||||
|
const [search, setSearch] = useState<{
|
||||||
|
keywords?: string;
|
||||||
|
lineNumber?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
revert?: boolean;
|
||||||
|
}>();
|
||||||
|
const [fileName, setFileName] = useState(searchMap.get("fileName") || "");
|
||||||
|
const [log, setLogs] = useState<
|
||||||
|
undefined | string | { [key: string]: string }[]
|
||||||
|
>();
|
||||||
|
const [startTime, setStart] = useState<string>();
|
||||||
|
const [endTime, setEnd] = useState<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFileName(urlFileName || "");
|
||||||
|
}, [urlFileName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let url = "log_index";
|
||||||
|
setLogs("Loading...");
|
||||||
|
if (host) {
|
||||||
|
url = decodeURIComponent(host);
|
||||||
|
setOrigin(new URL(url).origin);
|
||||||
|
if (path) {
|
||||||
|
url += decodeURIComponent(path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOrigin(undefined);
|
||||||
|
}
|
||||||
|
getLogDetail(url)
|
||||||
|
.then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setLogs(res);
|
||||||
|
} else {
|
||||||
|
setLogs("(null)");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLogs("Failed to load");
|
||||||
|
});
|
||||||
|
}, [host, path]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
log,
|
||||||
|
origin,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
el,
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
theme,
|
||||||
|
fileName,
|
||||||
|
setFileName,
|
||||||
|
startTime,
|
||||||
|
setStart,
|
||||||
|
endTime,
|
||||||
|
setEnd,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Logs = (props: LogsProps) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const {
|
||||||
|
log,
|
||||||
|
origin,
|
||||||
|
path,
|
||||||
|
el,
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
theme,
|
||||||
|
fileName,
|
||||||
|
setFileName,
|
||||||
|
startTime,
|
||||||
|
setStart,
|
||||||
|
endTime,
|
||||||
|
setEnd,
|
||||||
|
} = useLogs(props);
|
||||||
|
let href = "#/log/";
|
||||||
|
|
||||||
|
if (origin) {
|
||||||
|
if (path) {
|
||||||
|
const after = decodeURIComponent(path).split("/");
|
||||||
|
after.pop();
|
||||||
|
if (after.length > 1) {
|
||||||
|
href += encodeURIComponent(origin);
|
||||||
|
href += "/";
|
||||||
|
href += encodeURIComponent(after.join("/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root} ref={el}>
|
||||||
|
<TitleCard title="Logs Viewer">
|
||||||
|
<Paper>
|
||||||
|
{!origin && <p>Please choose an url to get log path</p>}
|
||||||
|
{origin && (
|
||||||
|
<p>
|
||||||
|
Now Path: {origin}
|
||||||
|
{decodeURIComponent(path || "")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{origin && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
href={href}
|
||||||
|
className={classes.search}
|
||||||
|
>
|
||||||
|
Back To ../
|
||||||
|
</Button>
|
||||||
|
{typeof log === "object" && (
|
||||||
|
<SearchInput
|
||||||
|
defaultValue={fileName}
|
||||||
|
label="File Name"
|
||||||
|
onChange={(val) => {
|
||||||
|
setFileName(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
<Paper>
|
||||||
|
{typeof log === "object" && (
|
||||||
|
<List>
|
||||||
|
{log
|
||||||
|
.filter((e) => !fileName || e?.name?.includes(fileName))
|
||||||
|
.map((e: { [key: string]: string }) => (
|
||||||
|
<ListItem key={e.name}>
|
||||||
|
<a
|
||||||
|
href={`#/log/${
|
||||||
|
origin ? `${encodeURIComponent(origin)}/` : ""
|
||||||
|
}${encodeURIComponent(e.href)}`}
|
||||||
|
>
|
||||||
|
{e.name}
|
||||||
|
</a>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
{typeof log === "string" && log !== "Loading..." && (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
className={classes.search}
|
||||||
|
label="Keyword"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
setSearch({ ...search, keywords: value });
|
||||||
|
},
|
||||||
|
type: "",
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className={classes.search}
|
||||||
|
label="Line Number"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
setSearch({ ...search, lineNumber: value });
|
||||||
|
},
|
||||||
|
type: "",
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<SearchOutlined />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
className={classes.search}
|
||||||
|
label="Font Size"
|
||||||
|
InputProps={{
|
||||||
|
onChange: ({ target: { value } }) => {
|
||||||
|
setSearch({ ...search, fontSize: Number(value) });
|
||||||
|
},
|
||||||
|
type: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="datetime-local"
|
||||||
|
label="Start Time"
|
||||||
|
type="datetime-local"
|
||||||
|
value={startTime}
|
||||||
|
className={classes.search}
|
||||||
|
onChange={(val) => {
|
||||||
|
setStart(val.target.value);
|
||||||
|
}}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="End Time"
|
||||||
|
type="datetime-local"
|
||||||
|
value={endTime}
|
||||||
|
className={classes.search}
|
||||||
|
onChange={(val) => {
|
||||||
|
setEnd(val.target.value);
|
||||||
|
}}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={classes.search}>
|
||||||
|
Reverse:{" "}
|
||||||
|
<Switch
|
||||||
|
checked={search?.revert}
|
||||||
|
onChange={(e, v) => setSearch({ ...search, revert: v })}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={classes.search}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setStart("");
|
||||||
|
setEnd("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Time
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<LogVirtualView
|
||||||
|
height={600}
|
||||||
|
theme={theme}
|
||||||
|
revert={search?.revert}
|
||||||
|
keywords={search?.keywords}
|
||||||
|
focusLine={Number(search?.lineNumber) || undefined}
|
||||||
|
fontSize={search?.fontSize || 12}
|
||||||
|
content={log}
|
||||||
|
language="prolog"
|
||||||
|
startTime={startTime}
|
||||||
|
endTime={endTime}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{log === "Loading..." && (
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
<LinearProgress />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Logs;
|
287
dashboard/client/src/pages/node/NodeDetail.tsx
Normal file
287
dashboard/client/src/pages/node/NodeDetail.tsx
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
makeStyles,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
TableContainer,
|
||||||
|
Tabs,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import React from "react";
|
||||||
|
import { Link, RouteComponentProps } from "react-router-dom";
|
||||||
|
import ActorTable from "../../components/ActorTable";
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
|
import PercentageBar from "../../components/PercentageBar";
|
||||||
|
import { StatusChip } from "../../components/StatusChip";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import RayletWorkerTable from "../../components/WorkerTable";
|
||||||
|
import { ViewMeasures } from "../../type/raylet";
|
||||||
|
import { memoryConverter } from "../../util/converter";
|
||||||
|
import { useNodeDetail } from "./hook/useNodeDetail";
|
||||||
|
|
||||||
|
const useStyle = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const showMeasureKeys = [
|
||||||
|
"local_total_resource",
|
||||||
|
"local_available_resource",
|
||||||
|
"actor_stats",
|
||||||
|
"task_dependency_manager_stats",
|
||||||
|
"reconstruction_policy_stats",
|
||||||
|
"scheduling_queue_stats",
|
||||||
|
"object_manager_stats",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ViewDataDisplayer = ({ view }: { view?: ViewMeasures }) => {
|
||||||
|
if (!view) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { tags = "", ...otherProps } = view;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<span>{tags.split(",").pop()?.split(":").slice(1).join(":")}</span>=
|
||||||
|
{Object.keys(otherProps).length > 0 ? (
|
||||||
|
JSON.stringify(Object.values(otherProps).pop())
|
||||||
|
) : (
|
||||||
|
<span style={{ color: "gray" }}>null</span>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeDetailPage = (props: RouteComponentProps<{ id: string }>) => {
|
||||||
|
const classes = useStyle();
|
||||||
|
const {
|
||||||
|
params,
|
||||||
|
selectedTab,
|
||||||
|
nodeDetail,
|
||||||
|
msg,
|
||||||
|
isRefreshing,
|
||||||
|
onRefreshChange,
|
||||||
|
raylet,
|
||||||
|
handleChange,
|
||||||
|
} = useNodeDetail(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Loading loading={msg.startsWith("Loading")} />
|
||||||
|
<TitleCard title={`NODE - ${params.id}`}>
|
||||||
|
<StatusChip
|
||||||
|
type="node"
|
||||||
|
status={nodeDetail?.raylet?.state || "LOADING"}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Auto Refresh:
|
||||||
|
<Switch
|
||||||
|
checked={isRefreshing}
|
||||||
|
onChange={onRefreshChange}
|
||||||
|
name="refresh"
|
||||||
|
inputProps={{ "aria-label": "secondary checkbox" }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Request Status: {msg}
|
||||||
|
</TitleCard>
|
||||||
|
<TitleCard title="Node Detail">
|
||||||
|
<Tabs
|
||||||
|
value={selectedTab}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={classes.tab}
|
||||||
|
>
|
||||||
|
<Tab value="info" label="Info" />
|
||||||
|
<Tab value="raylet" label="Raylet" />
|
||||||
|
<Tab
|
||||||
|
value="worker"
|
||||||
|
label={`Worker (${nodeDetail?.workers.length || 0})`}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="actor"
|
||||||
|
label={`Actor (${
|
||||||
|
Object.values(nodeDetail?.actors || {}).length || 0
|
||||||
|
})`}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
{nodeDetail && selectedTab === "info" && (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Hostname</div>{" "}
|
||||||
|
{nodeDetail.hostname}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>IP</div> {nodeDetail.ip}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>CPU (Logic/Physic)</div>{" "}
|
||||||
|
{nodeDetail.cpus[0]}/ {nodeDetail.cpus[1]}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Load (1/5/15min)</div>{" "}
|
||||||
|
{nodeDetail?.loadAvg[0] &&
|
||||||
|
nodeDetail.loadAvg[0]
|
||||||
|
.map((e) => Number(e).toFixed(2))
|
||||||
|
.join("/")}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Load per CPU (1/5/15min)</div>{" "}
|
||||||
|
{nodeDetail?.loadAvg[1] &&
|
||||||
|
nodeDetail.loadAvg[1]
|
||||||
|
.map((e) => Number(e).toFixed(2))
|
||||||
|
.join("/")}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Boot Time</div>{" "}
|
||||||
|
{dayjs(nodeDetail.bootTime * 1000).format(
|
||||||
|
"YYYY/MM/DD HH:mm:ss",
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Sent Tps</div>{" "}
|
||||||
|
{memoryConverter(nodeDetail?.net[0])}/s
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Recieved Tps</div>{" "}
|
||||||
|
{memoryConverter(nodeDetail?.net[1])}/s
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Memory</div>{" "}
|
||||||
|
{nodeDetail?.mem && (
|
||||||
|
<PercentageBar
|
||||||
|
num={Number(nodeDetail?.mem[0] - nodeDetail?.mem[1])}
|
||||||
|
total={nodeDetail?.mem[0]}
|
||||||
|
>
|
||||||
|
{memoryConverter(nodeDetail?.mem[0] - nodeDetail?.mem[1])}/
|
||||||
|
{memoryConverter(nodeDetail?.mem[0])}({nodeDetail?.mem[2]}%)
|
||||||
|
</PercentageBar>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>CPU</div>{" "}
|
||||||
|
<PercentageBar num={Number(nodeDetail.cpu)} total={100}>
|
||||||
|
{nodeDetail.cpu}%
|
||||||
|
</PercentageBar>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{nodeDetail?.disk &&
|
||||||
|
Object.entries(nodeDetail?.disk).map(([path, obj]) => (
|
||||||
|
<Grid item xs={6} key={path}>
|
||||||
|
<div className={classes.label}>Disk ({path})</div>{" "}
|
||||||
|
{obj && (
|
||||||
|
<PercentageBar num={Number(obj.used)} total={obj.total}>
|
||||||
|
{memoryConverter(obj.used)}/{memoryConverter(obj.total)}
|
||||||
|
({obj.percent}%, {memoryConverter(obj.free)} free)
|
||||||
|
</PercentageBar>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Logs</div>{" "}
|
||||||
|
<Link to={`/log/${encodeURIComponent(nodeDetail.logUrl)}`}>
|
||||||
|
log
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{raylet && Object.keys(raylet).length > 0 && selectedTab === "raylet" && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Command</div>
|
||||||
|
<br />
|
||||||
|
<div style={{ height: 200, overflow: "auto" }}>
|
||||||
|
{nodeDetail?.cmdline.join(" ")}
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Pid</div> {raylet?.pid}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Workers Num</div>{" "}
|
||||||
|
{raylet?.numWorkers}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
<div className={classes.label}>Node Manager Port</div>{" "}
|
||||||
|
{raylet?.nodeManagerPort}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{showMeasureKeys
|
||||||
|
.map((e) => raylet.viewData.find((view) => view.viewName === e))
|
||||||
|
.map((e) =>
|
||||||
|
e ? (
|
||||||
|
<React.Fragment key={e.viewName}>
|
||||||
|
<p className={classes.label}>
|
||||||
|
{e.viewName
|
||||||
|
.split("_")
|
||||||
|
.map((e) => e[0].toUpperCase() + e.slice(1))
|
||||||
|
.join(" ")}
|
||||||
|
</p>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
style={{ maxHeight: 177, overflow: "auto" }}
|
||||||
|
>
|
||||||
|
{e.measures.map((e) => (
|
||||||
|
<ViewDataDisplayer key={e.tags} view={e} />
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{nodeDetail?.workers && selectedTab === "worker" && (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableContainer className={classes.paper}>
|
||||||
|
<RayletWorkerTable
|
||||||
|
workers={nodeDetail?.workers}
|
||||||
|
actorMap={nodeDetail?.actors}
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{nodeDetail?.actors && selectedTab === "actor" && (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableContainer className={classes.paper}>
|
||||||
|
<ActorTable
|
||||||
|
actors={nodeDetail.actors}
|
||||||
|
workers={nodeDetail?.workers}
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodeDetailPage;
|
66
dashboard/client/src/pages/node/hook/useNodeDetail.ts
Normal file
66
dashboard/client/src/pages/node/hook/useNodeDetail.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
import { GlobalContext } from "../../../App";
|
||||||
|
import { getNodeDetail } from "../../../service/node";
|
||||||
|
import { NodeDetailExtend } from "../../../type/node";
|
||||||
|
|
||||||
|
export const useNodeDetail = (props: RouteComponentProps<{ id: string }>) => {
|
||||||
|
const {
|
||||||
|
match: { params },
|
||||||
|
} = props;
|
||||||
|
const [selectedTab, setTab] = useState("info");
|
||||||
|
const [nodeDetail, setNode] = useState<NodeDetailExtend | undefined>();
|
||||||
|
const [msg, setMsg] = useState("Loading the node infos...");
|
||||||
|
const { namespaceMap } = useContext(GlobalContext);
|
||||||
|
const [isRefreshing, setRefresh] = useState(true);
|
||||||
|
const tot = useRef<NodeJS.Timeout>();
|
||||||
|
const onRefreshChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRefresh(event.target.checked);
|
||||||
|
};
|
||||||
|
const getDetail = useCallback(async () => {
|
||||||
|
if (!isRefreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await getNodeDetail(params.id);
|
||||||
|
const { data: rspData, msg, result } = data;
|
||||||
|
if (rspData?.detail) {
|
||||||
|
setNode(rspData.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
|
setMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === false) {
|
||||||
|
setMsg("Node Query Error Please Check Node Name");
|
||||||
|
setRefresh(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
tot.current = setTimeout(getDetail, 4000);
|
||||||
|
}, [isRefreshing, params.id]);
|
||||||
|
const raylet = nodeDetail?.raylet;
|
||||||
|
const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => {
|
||||||
|
setTab(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDetail();
|
||||||
|
return () => {
|
||||||
|
if (tot.current) {
|
||||||
|
clearTimeout(tot.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [getDetail]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
params,
|
||||||
|
selectedTab,
|
||||||
|
nodeDetail,
|
||||||
|
msg,
|
||||||
|
isRefreshing,
|
||||||
|
onRefreshChange,
|
||||||
|
raylet,
|
||||||
|
handleChange,
|
||||||
|
namespaceMap,
|
||||||
|
};
|
||||||
|
};
|
74
dashboard/client/src/pages/node/hook/useNodeList.ts
Normal file
74
dashboard/client/src/pages/node/hook/useNodeList.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { getNodeList } from "../../../service/node";
|
||||||
|
import { NodeDetail } from "../../../type/node";
|
||||||
|
import { useSorter } from "../../../util/hook";
|
||||||
|
|
||||||
|
export const useNodeList = () => {
|
||||||
|
const [nodeList, setList] = useState<NodeDetail[]>([]);
|
||||||
|
const [msg, setMsg] = useState("Loading the nodes infos...");
|
||||||
|
const [isRefreshing, setRefresh] = useState(true);
|
||||||
|
const [mode, setMode] = useState("table");
|
||||||
|
const [filter, setFilter] = useState<
|
||||||
|
{ key: "hostname" | "ip" | "state"; val: string }[]
|
||||||
|
>([]);
|
||||||
|
const [page, setPage] = useState({ pageSize: 10, pageNo: 1 });
|
||||||
|
const { sorterFunc, setOrderDesc, setSortKey, sorterKey } = useSorter("cpu");
|
||||||
|
const tot = useRef<NodeJS.Timeout>();
|
||||||
|
const changeFilter = (key: "hostname" | "ip" | "state", val: string) => {
|
||||||
|
const f = filter.find((e) => e.key === key);
|
||||||
|
if (f) {
|
||||||
|
f.val = val;
|
||||||
|
} else {
|
||||||
|
filter.push({ key, val });
|
||||||
|
}
|
||||||
|
setFilter([...filter]);
|
||||||
|
};
|
||||||
|
const onSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRefresh(event.target.checked);
|
||||||
|
};
|
||||||
|
const getList = useCallback(async () => {
|
||||||
|
if (!isRefreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await getNodeList();
|
||||||
|
const { data: rspData, msg } = data;
|
||||||
|
setList(rspData.summary || []);
|
||||||
|
if (msg) {
|
||||||
|
setMsg(msg);
|
||||||
|
} else {
|
||||||
|
setMsg("");
|
||||||
|
}
|
||||||
|
tot.current = setTimeout(getList, 4000);
|
||||||
|
}, [isRefreshing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getList();
|
||||||
|
return () => {
|
||||||
|
if (tot.current) {
|
||||||
|
clearTimeout(tot.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [getList]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodeList: nodeList
|
||||||
|
.map((e) => ({ ...e, state: e.raylet.state }))
|
||||||
|
.sort((a, b) => (a.raylet.nodeId > b.raylet.nodeId ? 1 : -1))
|
||||||
|
.sort(sorterFunc)
|
||||||
|
.filter((node) =>
|
||||||
|
filter.every((f) => node[f.key] && node[f.key].includes(f.val)),
|
||||||
|
),
|
||||||
|
msg,
|
||||||
|
isRefreshing,
|
||||||
|
onSwitchChange,
|
||||||
|
changeFilter,
|
||||||
|
page,
|
||||||
|
originalNodes: nodeList,
|
||||||
|
setPage: (key: string, val: number) => setPage({ ...page, [key]: val }),
|
||||||
|
sorterKey,
|
||||||
|
setSortKey,
|
||||||
|
setOrderDesc,
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
};
|
||||||
|
};
|
392
dashboard/client/src/pages/node/index.tsx
Normal file
392
dashboard/client/src/pages/node/index.tsx
Normal file
|
@ -0,0 +1,392 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Pagination from "@material-ui/lab/Pagination";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
|
import PercentageBar from "../../components/PercentageBar";
|
||||||
|
import { SearchInput, SearchSelect } from "../../components/SearchComponent";
|
||||||
|
import StateCounter from "../../components/StatesCounter";
|
||||||
|
import { StatusChip } from "../../components/StatusChip";
|
||||||
|
import TitleCard from "../../components/TitleCard";
|
||||||
|
import { NodeDetail } from "../../type/node";
|
||||||
|
import { memoryConverter } from "../../util/converter";
|
||||||
|
import { useNodeList } from "./hook/useNodeList";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
"State",
|
||||||
|
"ID",
|
||||||
|
"Host",
|
||||||
|
"IP",
|
||||||
|
"CPU Usage",
|
||||||
|
"Memory",
|
||||||
|
"Disk(root)",
|
||||||
|
"Sent",
|
||||||
|
"Received",
|
||||||
|
"BRPC Port",
|
||||||
|
"Time Info",
|
||||||
|
"Log",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const brpcLinkChanger = (href: string) => {
|
||||||
|
const { location } = window;
|
||||||
|
const { pathname } = location;
|
||||||
|
const pathArr = pathname.split("/");
|
||||||
|
if (pathArr.some((e) => e.split(".").length > 1)) {
|
||||||
|
const index = pathArr.findIndex((e) => e.includes("."));
|
||||||
|
const resultArr = pathArr.slice(0, index);
|
||||||
|
resultArr.push(href);
|
||||||
|
return `${location.protocol}//${location.host}${resultArr.join("/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `http://${href}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeCard = (props: { node: NodeDetail }) => {
|
||||||
|
const { node } = props;
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { raylet, hostname, ip, cpu, mem, net, disk, logUrl } = node;
|
||||||
|
const { nodeId, state, brpcPort } = raylet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" style={{ padding: "12px 12px", margin: 12 }}>
|
||||||
|
<p style={{ fontWeight: "bold", fontSize: 12, textDecoration: "none" }}>
|
||||||
|
<Link to={`node/${nodeId}`}>{nodeId}</Link>{" "}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item>
|
||||||
|
<StatusChip type="node" status={state} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{hostname}({ip})
|
||||||
|
</Grid>
|
||||||
|
{net && net[0] >= 0 && (
|
||||||
|
<Grid item>
|
||||||
|
<span style={{ fontWeight: "bold" }}>Sent</span>{" "}
|
||||||
|
{memoryConverter(net[0])}/s{" "}
|
||||||
|
<span style={{ fontWeight: "bold" }}>Received</span>{" "}
|
||||||
|
{memoryConverter(net[1])}/s
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</p>
|
||||||
|
<Grid container spacing={1} alignItems="baseline">
|
||||||
|
{cpu >= 0 && (
|
||||||
|
<Grid item xs>
|
||||||
|
CPU
|
||||||
|
<PercentageBar num={Number(cpu)} total={100}>
|
||||||
|
{cpu}%
|
||||||
|
</PercentageBar>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{mem && (
|
||||||
|
<Grid item xs>
|
||||||
|
Memory
|
||||||
|
<PercentageBar num={Number(mem[0] - mem[1])} total={mem[0]}>
|
||||||
|
{memoryConverter(mem[0] - mem[1])}/{memoryConverter(mem[0])}(
|
||||||
|
{mem[2]}%)
|
||||||
|
</PercentageBar>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{disk && disk["/"] && (
|
||||||
|
<Grid item xs>
|
||||||
|
Disk('/')
|
||||||
|
<PercentageBar num={Number(disk["/"].used)} total={disk["/"].total}>
|
||||||
|
{memoryConverter(disk["/"].used)}/
|
||||||
|
{memoryConverter(disk["/"].total)}({disk["/"].percent}%)
|
||||||
|
</PercentageBar>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid container justify="flex-end" spacing={1} style={{ margin: 8 }}>
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={brpcLinkChanger(`${ip}:${raylet.brpcPort}`)}
|
||||||
|
>
|
||||||
|
BRPC {brpcPort}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button>
|
||||||
|
<Link to={`/log/${encodeURIComponent(logUrl)}`}>log</Link>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Nodes = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
isRefreshing,
|
||||||
|
onSwitchChange,
|
||||||
|
nodeList,
|
||||||
|
changeFilter,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
setSortKey,
|
||||||
|
setOrderDesc,
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
} = useNodeList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Loading loading={msg.startsWith("Loading")} />
|
||||||
|
<TitleCard title="NODES">
|
||||||
|
Auto Refresh:
|
||||||
|
<Switch
|
||||||
|
checked={isRefreshing}
|
||||||
|
onChange={onSwitchChange}
|
||||||
|
name="refresh"
|
||||||
|
inputProps={{ "aria-label": "secondary checkbox" }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
Request Status: {msg}
|
||||||
|
</TitleCard>
|
||||||
|
<TitleCard title="Statistics">
|
||||||
|
<StateCounter type="node" list={nodeList} />
|
||||||
|
</TitleCard>
|
||||||
|
<TitleCard title="Node List">
|
||||||
|
<Grid container alignItems="center">
|
||||||
|
<Grid item>
|
||||||
|
<SearchInput
|
||||||
|
label="Host"
|
||||||
|
onChange={(value) => changeFilter("hostname", value.trim())}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<SearchInput
|
||||||
|
label="IP"
|
||||||
|
onChange={(value) => changeFilter("ip", value.trim())}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<SearchSelect
|
||||||
|
label="State"
|
||||||
|
onChange={(value) => changeFilter("state", value.trim())}
|
||||||
|
options={["ALIVE", "DEAD"]}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<SearchInput
|
||||||
|
label="Page Size"
|
||||||
|
onChange={(value) =>
|
||||||
|
setPage("pageSize", Math.min(Number(value), 500) || 10)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<SearchSelect
|
||||||
|
label="Sort By"
|
||||||
|
options={[
|
||||||
|
["state", "State"],
|
||||||
|
["mem[2]", "Used Memory"],
|
||||||
|
["mem[0]", "Total Memory"],
|
||||||
|
["cpu", "CPU"],
|
||||||
|
["net[0]", "Sent"],
|
||||||
|
["net[1]", "Received"],
|
||||||
|
["disk./.used", "Used Disk"],
|
||||||
|
]}
|
||||||
|
onChange={(val) => setSortKey(val)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<span style={{ margin: 8, marginTop: 0 }}>
|
||||||
|
Reverse:
|
||||||
|
<Switch onChange={(_, checked) => setOrderDesc(checked)} />
|
||||||
|
</span>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<ButtonGroup size="small">
|
||||||
|
<Button
|
||||||
|
onClick={() => setMode("table")}
|
||||||
|
color={mode === "table" ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
Table
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setMode("card")}
|
||||||
|
color={mode === "card" ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
Card
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
count={Math.ceil(nodeList.length / page.pageSize)}
|
||||||
|
page={page.pageNo}
|
||||||
|
onChange={(e, pageNo) => setPage("pageNo", pageNo)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{mode === "table" && (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<TableCell align="center" key={col}>
|
||||||
|
{col}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{nodeList
|
||||||
|
.slice(
|
||||||
|
(page.pageNo - 1) * page.pageSize,
|
||||||
|
page.pageNo * page.pageSize,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
hostname = "",
|
||||||
|
ip = "",
|
||||||
|
cpu = 0,
|
||||||
|
mem = [],
|
||||||
|
disk,
|
||||||
|
net = [0, 0],
|
||||||
|
raylet,
|
||||||
|
logUrl,
|
||||||
|
}: NodeDetail,
|
||||||
|
i,
|
||||||
|
) => (
|
||||||
|
<TableRow key={hostname + i}>
|
||||||
|
<TableCell>
|
||||||
|
<StatusChip type="node" status={raylet.state} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Tooltip title={raylet.nodeId} arrow interactive>
|
||||||
|
<Link to={`/node/${raylet.nodeId}`}>
|
||||||
|
{raylet.nodeId.slice(0, 5)}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{hostname}</TableCell>
|
||||||
|
<TableCell align="center">{ip}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PercentageBar num={Number(cpu)} total={100}>
|
||||||
|
{cpu}%
|
||||||
|
</PercentageBar>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PercentageBar
|
||||||
|
num={Number(mem[0] - mem[1])}
|
||||||
|
total={mem[0]}
|
||||||
|
>
|
||||||
|
{memoryConverter(mem[0] - mem[1])}/
|
||||||
|
{memoryConverter(mem[0])}({mem[2]}%)
|
||||||
|
</PercentageBar>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{disk && disk["/"] && (
|
||||||
|
<PercentageBar
|
||||||
|
num={Number(disk["/"].used)}
|
||||||
|
total={disk["/"].total}
|
||||||
|
>
|
||||||
|
{memoryConverter(disk["/"].used)}/
|
||||||
|
{memoryConverter(disk["/"].total)}(
|
||||||
|
{disk["/"].percent}%)
|
||||||
|
</PercentageBar>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{memoryConverter(net[0])}/s
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{memoryConverter(net[1])}/s
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{raylet.brpcPort && (
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={brpcLinkChanger(`${ip}:${raylet.brpcPort}`)}
|
||||||
|
>
|
||||||
|
{raylet.brpcPort}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{!!raylet.startTime && (
|
||||||
|
<p>
|
||||||
|
Start Time:{" "}
|
||||||
|
{dayjs(raylet.startTime * 1000).format(
|
||||||
|
"YYYY/MM/DD HH:mm:ss",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!!raylet.terminateTime && (
|
||||||
|
<p>
|
||||||
|
End Time:{" "}
|
||||||
|
{dayjs(raylet.terminateTime * 1000).format(
|
||||||
|
"YYYY/MM/DD HH:mm:ss",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link to={`/log/${encodeURIComponent(logUrl)}`}>
|
||||||
|
Log
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
{mode === "card" && (
|
||||||
|
<Grid container>
|
||||||
|
{nodeList
|
||||||
|
.slice(
|
||||||
|
(page.pageNo - 1) * page.pageSize,
|
||||||
|
page.pageNo * page.pageSize,
|
||||||
|
)
|
||||||
|
.map((e) => (
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<NodeCard node={e} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</TitleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Nodes;
|
14
dashboard/client/src/service/actor.ts
Normal file
14
dashboard/client/src/service/actor.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { Actor } from "../type/actor";
|
||||||
|
|
||||||
|
export const getActors = () => {
|
||||||
|
return axios.get<{
|
||||||
|
result: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
actors: {
|
||||||
|
[actorId: string]: Actor;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>("logical/actors");
|
||||||
|
};
|
6
dashboard/client/src/service/cluster.ts
Normal file
6
dashboard/client/src/service/cluster.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { RayConfigRsp } from "../type/config";
|
||||||
|
|
||||||
|
export const getRayConfig = () => {
|
||||||
|
return axios.get<RayConfigRsp>("api/ray_config");
|
||||||
|
};
|
10
dashboard/client/src/service/job.ts
Normal file
10
dashboard/client/src/service/job.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { JobDetailRsp, JobListRsp } from "../type/job";
|
||||||
|
|
||||||
|
export const getJobList = () => {
|
||||||
|
return axios.get<JobListRsp>("jobs?view=summary");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getJobDetail = (id: string) => {
|
||||||
|
return axios.get<JobDetailRsp>(`jobs/${id}`);
|
||||||
|
};
|
35
dashboard/client/src/service/log.ts
Normal file
35
dashboard/client/src/service/log.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const getLogDetail = async (url: string) => {
|
||||||
|
if (window.location.pathname !== "/" && url !== "log_index") {
|
||||||
|
const pathArr = window.location.pathname.split("/");
|
||||||
|
if (pathArr.length > 1) {
|
||||||
|
const idx = pathArr.findIndex((e) => e.includes(":"));
|
||||||
|
if (idx > -1) {
|
||||||
|
const afterArr = pathArr.slice(0, idx);
|
||||||
|
afterArr.push(url.replace(/https?:\/\//, ""));
|
||||||
|
url = afterArr.join("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rsp = await axios.get(
|
||||||
|
url === "log_index" ? url : `log_proxy?url=${encodeURIComponent(url)}`,
|
||||||
|
);
|
||||||
|
if (rsp.headers["content-type"]?.includes("html")) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.innerHTML = rsp.data;
|
||||||
|
const arr = [].map.call(
|
||||||
|
el.getElementsByTagName("li"),
|
||||||
|
(li: HTMLLIElement) => {
|
||||||
|
const a = li.children[0] as HTMLAnchorElement;
|
||||||
|
return {
|
||||||
|
name: li.innerText,
|
||||||
|
href: li.innerText.includes("http") ? a.href : a.pathname,
|
||||||
|
} as { [key: string]: string };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return arr as { [key: string]: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsp.data as string;
|
||||||
|
};
|
10
dashboard/client/src/service/node.ts
Normal file
10
dashboard/client/src/service/node.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { NodeDetailRsp, NodeListRsp } from "../type/node";
|
||||||
|
|
||||||
|
export const getNodeList = async () => {
|
||||||
|
return await axios.get<NodeListRsp>("nodes?view=summary");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNodeDetail = async (id: string) => {
|
||||||
|
return await axios.get<NodeDetailRsp>(`nodes/${id}`);
|
||||||
|
};
|
52
dashboard/client/src/service/util.ts
Normal file
52
dashboard/client/src/service/util.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
type CMDRsp = {
|
||||||
|
result: boolean;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
output: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getJstack = (ip: string, pid: string) => {
|
||||||
|
return axios.get<CMDRsp>("utils/jstack", {
|
||||||
|
params: {
|
||||||
|
ip,
|
||||||
|
pid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getJmap = (ip: string, pid: string) => {
|
||||||
|
return axios.get<CMDRsp>("utils/jmap", {
|
||||||
|
params: {
|
||||||
|
ip,
|
||||||
|
pid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getJstat = (ip: string, pid: string, options: string) => {
|
||||||
|
return axios.get<CMDRsp>("utils/jstat", {
|
||||||
|
params: {
|
||||||
|
ip,
|
||||||
|
pid,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type NamespacesRsp = {
|
||||||
|
result: boolean;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
namespaces: {
|
||||||
|
namespaceId: string;
|
||||||
|
hostNameList: string[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNamespaces = () => {
|
||||||
|
return axios.get<NamespacesRsp>("namespaces");
|
||||||
|
};
|
61
dashboard/client/src/theme.ts
Normal file
61
dashboard/client/src/theme.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { blue, blueGrey, grey, lightBlue } from "@material-ui/core/colors";
|
||||||
|
import { createMuiTheme } from "@material-ui/core/styles";
|
||||||
|
|
||||||
|
const basicTheme = {
|
||||||
|
typography: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: [
|
||||||
|
"-apple-system",
|
||||||
|
"BlinkMacSystemFont",
|
||||||
|
'"Segoe UI"',
|
||||||
|
"Roboto",
|
||||||
|
'"Helvetica Neue"',
|
||||||
|
"Arial",
|
||||||
|
"sans-serif",
|
||||||
|
'"Apple Color Emoji"',
|
||||||
|
'"Segoe UI Emoji"',
|
||||||
|
'"Segoe UI Symbol"',
|
||||||
|
].join(","),
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
MuiPaper: {
|
||||||
|
elevation: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lightTheme = createMuiTheme({
|
||||||
|
...basicTheme,
|
||||||
|
palette: {
|
||||||
|
primary: blue,
|
||||||
|
secondary: lightBlue,
|
||||||
|
text: {
|
||||||
|
primary: grey[900],
|
||||||
|
secondary: grey[800],
|
||||||
|
disabled: grey[400],
|
||||||
|
hint: grey[300],
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
paper: "#fff",
|
||||||
|
default: blueGrey[50],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const darkTheme = createMuiTheme({
|
||||||
|
...basicTheme,
|
||||||
|
palette: {
|
||||||
|
primary: blue,
|
||||||
|
secondary: lightBlue,
|
||||||
|
text: {
|
||||||
|
primary: blueGrey[50],
|
||||||
|
secondary: blueGrey[100],
|
||||||
|
disabled: blueGrey[200],
|
||||||
|
hint: blueGrey[300],
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
paper: grey[800],
|
||||||
|
default: grey[900],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
94
dashboard/client/src/type/actor.ts
Normal file
94
dashboard/client/src/type/actor.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
export enum ActorEnum {
|
||||||
|
ALIVE = "ALIVE",
|
||||||
|
PENDING = "PENDING",
|
||||||
|
RECONSTRUCTING = "RECONSTRUCTING",
|
||||||
|
DEAD = "DEAD",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Address = {
|
||||||
|
rayletId: string;
|
||||||
|
ipAddress: string;
|
||||||
|
port: number;
|
||||||
|
workerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaskSpec = {
|
||||||
|
actorCreationTaskSpec: {
|
||||||
|
actorId: string;
|
||||||
|
dynamicWorkerOptions: string[];
|
||||||
|
extensionData: string;
|
||||||
|
isAsyncio: boolean;
|
||||||
|
isDetached: boolean;
|
||||||
|
maxActorRestarts: boolean;
|
||||||
|
maxConcurrency: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
args: {
|
||||||
|
data: string;
|
||||||
|
metadata: string;
|
||||||
|
nestedInlinedIds: string[];
|
||||||
|
objectIds: string[];
|
||||||
|
}[];
|
||||||
|
callerAddress: {
|
||||||
|
ipAddress: string;
|
||||||
|
port: number;
|
||||||
|
rayletId: string;
|
||||||
|
workerId: string;
|
||||||
|
};
|
||||||
|
callerId: string;
|
||||||
|
functionDescriptor: {
|
||||||
|
javaFunctionDescriptor: {
|
||||||
|
className: string;
|
||||||
|
functionName: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
pythonFunctionDescriptor: {
|
||||||
|
className: string;
|
||||||
|
functionName: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
jobId: string;
|
||||||
|
language: string;
|
||||||
|
maxRetries: number;
|
||||||
|
numReturns: string;
|
||||||
|
parentCounter: string;
|
||||||
|
parentTaskId: string;
|
||||||
|
requiredPlacementResources: {
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
requiredResources: {
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
sourceActorId: string;
|
||||||
|
taskId: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Actor = {
|
||||||
|
actorId: string;
|
||||||
|
children: { [key: string]: Actor };
|
||||||
|
taskSpec: TaskSpec;
|
||||||
|
ipAddress: string;
|
||||||
|
isDirectCall: boolean;
|
||||||
|
jobId: string;
|
||||||
|
numExecutedTasks: number;
|
||||||
|
numLocalObjects: number;
|
||||||
|
numObjectIdsInScope: number;
|
||||||
|
state: ActorEnum | string; // PENDING, ALIVE, RECONSTRUCTING, DEAD
|
||||||
|
taskQueueLength: number;
|
||||||
|
usedObjectStoreMemory: number;
|
||||||
|
usedResources: { [key: string]: string | number };
|
||||||
|
timestamp: number;
|
||||||
|
actorTitle: string;
|
||||||
|
averageTaskExecutionSpeed: number;
|
||||||
|
nodeId: string;
|
||||||
|
pid: number;
|
||||||
|
ownerAddress: Address;
|
||||||
|
address: Address;
|
||||||
|
maxReconstructions: string;
|
||||||
|
remainingReconstructions: string;
|
||||||
|
isDetached: false;
|
||||||
|
name: string;
|
||||||
|
numRestarts: string;
|
||||||
|
};
|
22
dashboard/client/src/type/config.d.ts
vendored
Normal file
22
dashboard/client/src/type/config.d.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export type RayConfig = {
|
||||||
|
userName: string;
|
||||||
|
workNodeNumber: number;
|
||||||
|
headNodeNumber: number;
|
||||||
|
containerVcores: number;
|
||||||
|
containerMemory: number;
|
||||||
|
clusterName: string;
|
||||||
|
supremeFo: boolean;
|
||||||
|
jobManagerPort: number;
|
||||||
|
externalRedisAddresses: string;
|
||||||
|
envParams: string;
|
||||||
|
sourceCodeLink: string;
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RayConfigRsp = {
|
||||||
|
result: boolean;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
config: RayConfig;
|
||||||
|
};
|
||||||
|
};
|
31
dashboard/client/src/type/event.d.ts
vendored
Normal file
31
dashboard/client/src/type/event.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export type Event = {
|
||||||
|
eventId: string;
|
||||||
|
jobId: string;
|
||||||
|
nodeId: string;
|
||||||
|
sourceType: string;
|
||||||
|
sourceHostname: string;
|
||||||
|
sourcePid: number;
|
||||||
|
label: string;
|
||||||
|
message: string;
|
||||||
|
timestamp: number;
|
||||||
|
severity: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventRsp = {
|
||||||
|
result: boolean;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
jobId: string;
|
||||||
|
events: Event[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventGlobalRsp = {
|
||||||
|
result: boolean;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
events: {
|
||||||
|
global: Event[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
70
dashboard/client/src/type/job.d.ts
vendored
Normal file
70
dashboard/client/src/type/job.d.ts
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { Actor } from "./actor";
|
||||||
|
import { Worker } from "./worker";
|
||||||
|
|
||||||
|
export type Job = {
|
||||||
|
jobId: string;
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
language: string;
|
||||||
|
driverEntry: string;
|
||||||
|
state: string;
|
||||||
|
timestamp: number;
|
||||||
|
namespaceId: string;
|
||||||
|
driverPid: number;
|
||||||
|
driverIpAddress: string;
|
||||||
|
isDead: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PythonDependenciey = string;
|
||||||
|
|
||||||
|
export type JavaDependency = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
md5: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobInfo = {
|
||||||
|
url: string;
|
||||||
|
driverArgs: string;
|
||||||
|
customConfig: {
|
||||||
|
[k: string]: string;
|
||||||
|
};
|
||||||
|
jvmOptions: string;
|
||||||
|
dependencies: {
|
||||||
|
python: PythonDependenciey[];
|
||||||
|
java: JavaDependency[];
|
||||||
|
};
|
||||||
|
driverStarted: boolean;
|
||||||
|
submitTime: string;
|
||||||
|
startTime: null | string | number;
|
||||||
|
endTime: null | string | number;
|
||||||
|
driverIpAddress: string;
|
||||||
|
driverHostname: string;
|
||||||
|
driverPid: number;
|
||||||
|
eventUrl: string;
|
||||||
|
failErrorMessage: string;
|
||||||
|
driverCmdline: string;
|
||||||
|
} & Job;
|
||||||
|
|
||||||
|
export type JobDetail = {
|
||||||
|
jobInfo: JobInfo;
|
||||||
|
jobActors: { [id: string]: Actor };
|
||||||
|
jobWorkers: Worker[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobDetailRsp = {
|
||||||
|
data: {
|
||||||
|
detail: JobDetail;
|
||||||
|
};
|
||||||
|
msg: string;
|
||||||
|
result: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JobListRsp = {
|
||||||
|
data: {
|
||||||
|
summary: Job[];
|
||||||
|
};
|
||||||
|
msg: string;
|
||||||
|
result: boolean;
|
||||||
|
};
|
62
dashboard/client/src/type/node.d.ts
vendored
Normal file
62
dashboard/client/src/type/node.d.ts
vendored
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { Actor } from "./actor";
|
||||||
|
import { Raylet } from "./raylet";
|
||||||
|
import { Worker } from "./worker";
|
||||||
|
|
||||||
|
export type NodeDetail = {
|
||||||
|
now: number;
|
||||||
|
hostname: string;
|
||||||
|
ip: string;
|
||||||
|
cpu: number; // cpu usage
|
||||||
|
cpus: number[]; // Logic CPU Count, Physical CPU Count
|
||||||
|
mem: number[]; // total memory, free memory, memory used ratio
|
||||||
|
bootTime: number; // start time
|
||||||
|
loadAvg: number[][]; // recent 1,5,15 minitues system load,load per cpu http://man7.org/linux/man-pages/man3/getloadavg.3.html
|
||||||
|
disk: {
|
||||||
|
// disk used on root
|
||||||
|
"/": {
|
||||||
|
total: number;
|
||||||
|
used: number;
|
||||||
|
free: number;
|
||||||
|
percent: number;
|
||||||
|
};
|
||||||
|
// disk used on tmp
|
||||||
|
"/tmp": {
|
||||||
|
total: number;
|
||||||
|
used: number;
|
||||||
|
free: number;
|
||||||
|
percent: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
net: number[]; // sent tps, received tps
|
||||||
|
raylet: Raylet;
|
||||||
|
logCounts: number;
|
||||||
|
errorCounts: number;
|
||||||
|
actors: { [id: string]: Actor };
|
||||||
|
cmdline: string[];
|
||||||
|
state: string;
|
||||||
|
logUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeListRsp = {
|
||||||
|
data: {
|
||||||
|
summary: NodeDetail[];
|
||||||
|
};
|
||||||
|
result: boolean;
|
||||||
|
msg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeDetailExtend = {
|
||||||
|
workers: Worker[];
|
||||||
|
raylet: Raylet;
|
||||||
|
actors: {
|
||||||
|
[actorId: string]: Actor;
|
||||||
|
};
|
||||||
|
} & NodeDetail;
|
||||||
|
|
||||||
|
export type NodeDetailRsp = {
|
||||||
|
data: {
|
||||||
|
detail: NodeDetailExtend;
|
||||||
|
};
|
||||||
|
msg: string;
|
||||||
|
result: boolean;
|
||||||
|
};
|
28
dashboard/client/src/type/raylet.d.ts
vendored
Normal file
28
dashboard/client/src/type/raylet.d.ts
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
export type ViewMeasures = {
|
||||||
|
tags: string;
|
||||||
|
int_value?: number;
|
||||||
|
double_value?: number;
|
||||||
|
distribution_min?: number;
|
||||||
|
distribution_mean?: number;
|
||||||
|
distribution_max?: number;
|
||||||
|
distribution_count?: number;
|
||||||
|
distribution_bucket_boundaries?: number[];
|
||||||
|
distribution_bucket_counts?: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ViewData = {
|
||||||
|
viewName: string;
|
||||||
|
measures: ViewMeasures[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Raylet = {
|
||||||
|
viewData: ViewData[];
|
||||||
|
numWorkers: number;
|
||||||
|
pid: number;
|
||||||
|
nodeId: string;
|
||||||
|
nodeManagerPort: number;
|
||||||
|
brpcPort: pid;
|
||||||
|
state: string;
|
||||||
|
startTime: number;
|
||||||
|
terminateTime: number;
|
||||||
|
};
|
36
dashboard/client/src/type/worker.d.ts
vendored
Normal file
36
dashboard/client/src/type/worker.d.ts
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
export type CoreWorkerStats = {
|
||||||
|
currentTaskFuncDesc: string;
|
||||||
|
ipAddress: string;
|
||||||
|
port: string;
|
||||||
|
actorId: string;
|
||||||
|
usedResources: { [key: string]: number };
|
||||||
|
numExecutedTasks: number;
|
||||||
|
workerId: string;
|
||||||
|
actorTitle: string;
|
||||||
|
jobId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Worker = {
|
||||||
|
createTime: number;
|
||||||
|
cpuPercent: number;
|
||||||
|
cmdline: string[];
|
||||||
|
memoryInfo: {
|
||||||
|
rss: number; // aka “Resident Set Size”, this is the non-swapped physical memory a process has used. On UNIX it matches “top“‘s RES column). On Windows this is an alias for wset field and it matches “Mem Usage” column of taskmgr.exe.
|
||||||
|
vms: number; // aka “Virtual Memory Size”, this is the total amount of virtual memory used by the process. On UNIX it matches “top“‘s VIRT column. On Windows this is an alias for pagefile field and it matches “Mem Usage” “VM Size” column of taskmgr.exe.
|
||||||
|
pfaults: number; // number of page faults.
|
||||||
|
pageins: number; // number of actual pageins.
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
cpuTimes: {
|
||||||
|
user: number;
|
||||||
|
system: number;
|
||||||
|
childrenUser: number;
|
||||||
|
childrenUystem: number;
|
||||||
|
iowait?: number;
|
||||||
|
};
|
||||||
|
pid: number;
|
||||||
|
coreWorkerStats: CoreWorkerStats[];
|
||||||
|
language: string;
|
||||||
|
hostname: string;
|
||||||
|
ip: hostname;
|
||||||
|
};
|
27
dashboard/client/src/util/converter.ts
Normal file
27
dashboard/client/src/util/converter.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export const memoryConverter = (bytes: number) => {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return `${bytes}KB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1024 ** 2) {
|
||||||
|
return `${(bytes / 1024 ** 1).toFixed(2)}KB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1024 ** 3) {
|
||||||
|
return `${(bytes / 1024 ** 2).toFixed(2)}MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1024 ** 4) {
|
||||||
|
return `${(bytes / 1024 ** 3).toFixed(2)}GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1024 ** 5) {
|
||||||
|
return `${(bytes / 1024 ** 4).toFixed(2)}TB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1024 ** 6) {
|
||||||
|
return `${(bytes / 1024 ** 5).toFixed(2)}TB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
28
dashboard/client/src/util/func.tsx
Normal file
28
dashboard/client/src/util/func.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Tooltip } from "@material-ui/core";
|
||||||
|
import React, { CSSProperties } from "react";
|
||||||
|
|
||||||
|
export const longTextCut = (text: string = "", len: number = 28) => (
|
||||||
|
<Tooltip title={text} interactive>
|
||||||
|
<span>{text.length > len ? text.slice(0, len) + "..." : text}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const jsonFormat = (str: string | object) => {
|
||||||
|
const preStyle = {
|
||||||
|
textAlign: "left",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
} as CSSProperties;
|
||||||
|
if (typeof str === "object") {
|
||||||
|
return <pre style={preStyle}>{JSON.stringify(str, null, 2)}</pre>;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(str);
|
||||||
|
if (typeof j !== "object") {
|
||||||
|
return JSON.stringify(j);
|
||||||
|
}
|
||||||
|
return <pre style={preStyle}>{JSON.stringify(j, null, 2)}</pre>;
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
63
dashboard/client/src/util/hook.ts
Normal file
63
dashboard/client/src/util/hook.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { get } from "lodash";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const useFilter = <KeyType extends string>() => {
|
||||||
|
const [filters, setFilters] = useState<{ key: KeyType; val: string }[]>([]);
|
||||||
|
const changeFilter = (key: KeyType, val: string) => {
|
||||||
|
const f = filters.find((e) => e.key === key);
|
||||||
|
if (f) {
|
||||||
|
f.val = val;
|
||||||
|
} else {
|
||||||
|
filters.push({ key, val });
|
||||||
|
}
|
||||||
|
setFilters([...filters]);
|
||||||
|
};
|
||||||
|
const filterFunc = (instance: { [key: string]: any }) => {
|
||||||
|
return filters.every(
|
||||||
|
(f) => !f.val || get(instance, f.key, "").toString().includes(f.val),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeFilter,
|
||||||
|
filterFunc,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSorter = (initialSortKey?: string) => {
|
||||||
|
const [sorter, setSorter] = useState({
|
||||||
|
key: initialSortKey || "",
|
||||||
|
desc: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorterFunc = (
|
||||||
|
instanceA: { [key: string]: any },
|
||||||
|
instanceB: { [key: string]: any },
|
||||||
|
) => {
|
||||||
|
if (!sorter.key) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [b, a] = [instanceA, instanceB];
|
||||||
|
if (sorter.desc) {
|
||||||
|
[a, b] = [instanceA, instanceB];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!get(a, sorter.key)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!get(b, sorter.key)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(a, sorter.key) > get(b, sorter.key) ? 1 : -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sorterFunc,
|
||||||
|
setSortKey: (key: string) => setSorter({ ...sorter, key }),
|
||||||
|
setOrderDesc: (desc: boolean) => setSorter({ ...sorter, desc }),
|
||||||
|
sorterKey: sorter.key,
|
||||||
|
};
|
||||||
|
};
|
12
dashboard/client/src/util/localData.ts
Normal file
12
dashboard/client/src/util/localData.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export const getLocalStorage = <T>(key: string) => {
|
||||||
|
const data = window.localStorage.getItem(key);
|
||||||
|
try {
|
||||||
|
return JSON.parse(data || "") as T;
|
||||||
|
} catch {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLocalStorage = (key: string, value: any) => {
|
||||||
|
return window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue