[Dashboard] Add the new dashboard code and prompt users to try it (#11667)

This commit is contained in:
Dominic Ming 2021-01-29 15:22:26 +08:00 committed by GitHub
parent 42d501d747
commit 752da83bb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 4650 additions and 33 deletions

View file

@ -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",

View file

@ -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"
}

View file

@ -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;

View file

@ -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;

View 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;

View 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;

View 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;

View 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;
}

View 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;
}

View file

@ -0,0 +1,3 @@
span.find-kws {
background-color: #ffd800;
}

View 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;

View 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>
);
};

View 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;

View 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;

View 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>
);
};

View 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;

View 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;

View 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

View 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;

View 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;

View file

@ -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"

View 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;

View 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>
);
};

View 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;

View 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;

View 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,
};
};

View 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 }),
};
};

View 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;

View 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;

View 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;

View 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;

View 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,
};
};

View 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,
};
};

View 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;

View 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");
};

View file

@ -0,0 +1,6 @@
import axios from "axios";
import { RayConfigRsp } from "../type/config";
export const getRayConfig = () => {
return axios.get<RayConfigRsp>("api/ray_config");
};

View 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}`);
};

View 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;
};

View 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}`);
};

View 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");
};

View 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],
},
},
});

View 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
View 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
View 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
View 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
View 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 1515 minitues system loadload 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
View 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
View 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;
};

View 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 "";
};

View 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;
}
};

View 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,
};
};

View 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));
};