mirror of
https://github.com/vale981/ray
synced 2025-03-06 02:21: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",
|
||||
"version": "0.1.0",
|
||||
"name": "ray-dashboard-client",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.1.0",
|
||||
"name": "ray-dashboard-client",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"@material-ui/pickers": "^3.2.10",
|
||||
"@reduxjs/toolkit": "^1.3.1",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/jest": "25.1.4",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/lowlight": "^0.0.1",
|
||||
"@types/node": "13.9.5",
|
||||
"@types/numeral": "^0.0.26",
|
||||
"@types/react": "16.9.26",
|
||||
"@types/react-dom": "16.9.5",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"axios": "^0.21.1",
|
||||
"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-dom": "^16.13.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "^3.4.3",
|
||||
"react-window": "^1.8.5",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"typescript": "3.8.3",
|
||||
"use-debounce": "^3.4.3"
|
||||
|
@ -1320,6 +1332,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
||||
"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": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
|
@ -1859,6 +1876,26 @@
|
|||
"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": {
|
||||
"version": "4.10.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
|
@ -2285,11 +2337,27 @@
|
|||
"@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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
"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": {
|
||||
"version": "13.0.11",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||
|
@ -5158,6 +5234,11 @@
|
|||
"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": {
|
||||
"version": "4.3.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
||||
|
@ -7318,6 +7411,14 @@
|
|||
"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": {
|
||||
"version": "0.1.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
|
@ -8191,12 +8300,9 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "7.0.4",
|
||||
|
@ -11001,6 +11107,19 @@
|
|||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
@ -11097,6 +11216,11 @@
|
|||
"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": {
|
||||
"version": "0.4.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
||||
|
@ -14371,6 +14503,22 @@
|
|||
"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": {
|
||||
"version": "3.0.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.6.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
|
@ -19715,6 +19879,19 @@
|
|||
"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": {
|
||||
"version": "4.10.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
|
@ -20084,11 +20276,27 @@
|
|||
"@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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
"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": {
|
||||
"version": "13.0.11",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"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": {
|
||||
"version": "4.3.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
||||
|
@ -24312,6 +24541,11 @@
|
|||
"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": {
|
||||
"version": "0.1.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
|
@ -25045,9 +25284,9 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "7.0.4",
|
||||
|
@ -27299,6 +27538,15 @@
|
|||
"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": {
|
||||
"version": "5.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"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",
|
||||
"integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4="
|
||||
},
|
||||
"numeral": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
|
||||
"integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY="
|
||||
},
|
||||
"nwsapi": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
||||
|
@ -30091,6 +30349,15 @@
|
|||
"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": {
|
||||
"version": "3.0.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"name": "ray-dashboard-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"@material-ui/pickers": "^3.2.10",
|
||||
"@reduxjs/toolkit": "^1.3.1",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/jest": "25.1.4",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/lowlight": "^0.0.1",
|
||||
"@types/node": "13.9.5",
|
||||
"@types/numeral": "^0.0.26",
|
||||
"@types/react": "16.9.26",
|
||||
"@types/react-dom": "16.9.5",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"axios": "^0.21.1",
|
||||
"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-dom": "^16.13.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "^3.4.3",
|
||||
"react-window": "^1.8.5",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"typescript": "3.8.3",
|
||||
"use-debounce": "^3.4.3"
|
||||
|
@ -40,6 +51,7 @@
|
|||
"eslint": "./node_modules/.bin/eslint \"src/**\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"ignorePatterns": ["*.svg", "*.css"],
|
||||
"extends": [
|
||||
"plugin:import/warnings",
|
||||
"react-app"
|
||||
|
@ -110,5 +122,6 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"proxy": "http://localhost:8265"
|
||||
}
|
||||
|
|
|
@ -1,21 +1,112 @@
|
|||
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 { BrowserRouter, Route } from "react-router-dom";
|
||||
import { HashRouter, Route, Switch } from "react-router-dom";
|
||||
import Dashboard from "./pages/dashboard/Dashboard";
|
||||
import Loading from "./pages/exception/Loading";
|
||||
import { getNodeList } from "./service/node";
|
||||
import { store } from "./store";
|
||||
import { darkTheme, lightTheme } from "./theme";
|
||||
import { getLocalStorage, setLocalStorage } from "./util/localData";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<CssBaseline />
|
||||
<Route component={Dashboard} exact path="/" />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
// lazy loading fro prevent loading too much code at once
|
||||
const Actors = React.lazy(() => import("./pages/actor"));
|
||||
const CMDResult = React.lazy(() => import("./pages/cmd/CMDResult"));
|
||||
const Index = React.lazy(() => import("./pages/index/Index"));
|
||||
const Job = React.lazy(() => import("./pages/job"));
|
||||
const JobDetail = React.lazy(() => import("./pages/job/JobDetail"));
|
||||
const BasicLayout = React.lazy(() => import("./pages/layout"));
|
||||
const Logs = React.lazy(() => import("./pages/log/Logs"));
|
||||
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;
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
const base =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:8265"
|
||||
: window.location.origin;
|
||||
const base = window.location.origin;
|
||||
|
||||
type APIResponse<T> = {
|
||||
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 {
|
||||
Button,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Tab,
|
||||
|
@ -8,6 +9,7 @@ import {
|
|||
} from "@material-ui/core";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { getActorGroups, getNodeInfo, getTuneAvailability } from "../../api";
|
||||
import { StoreState } from "../../store";
|
||||
import LastUpdated from "./LastUpdated";
|
||||
|
@ -59,6 +61,7 @@ const Dashboard: React.FC = () => {
|
|||
const tuneAvailability = useSelector(tuneAvailabilitySelector);
|
||||
const tab = useSelector(tabSelector);
|
||||
const classes = useDashboardStyles();
|
||||
const history = useHistory();
|
||||
|
||||
// Polling Function
|
||||
const refreshInfo = useCallback(async () => {
|
||||
|
@ -103,6 +106,9 @@ const Dashboard: React.FC = () => {
|
|||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h5">Ray Dashboard</Typography>
|
||||
<Button onClick={() => history.push("/summary")}>
|
||||
Try New Dashboard
|
||||
</Button>
|
||||
<Tabs
|
||||
className={classes.tabs}
|
||||
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