diff --git a/dashboard/client/package-lock.json b/dashboard/client/package-lock.json index 8b6612942..eccde1558 100644 --- a/dashboard/client/package-lock.json +++ b/dashboard/client/package-lock.json @@ -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", diff --git a/dashboard/client/package.json b/dashboard/client/package.json index 3ac262ef7..535d3b483 100644 --- a/dashboard/client/package.json +++ b/dashboard/client/package.json @@ -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" } diff --git a/dashboard/client/src/App.tsx b/dashboard/client/src/App.tsx index c0bdae6a1..be2a8fc0b 100644 --- a/dashboard/client/src/App.tsx +++ b/dashboard/client/src/App.tsx @@ -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 ( - - - - - - - ); - } -} +// 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(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 ( + + + + + + + + + ( + + + + + + ( + + )} + exact + path="/log/:host?/:path?" + /> + + + + + + )} + /> + + + + + + + ); +}; export default App; diff --git a/dashboard/client/src/api.ts b/dashboard/client/src/api.ts index e2ff52464..b7f4f5f41 100644 --- a/dashboard/client/src/api.ts +++ b/dashboard/client/src/api.ts @@ -1,7 +1,4 @@ -const base = - process.env.NODE_ENV === "development" - ? "http://localhost:8265" - : window.location.origin; +const base = window.location.origin; type APIResponse = { result: boolean; diff --git a/dashboard/client/src/components/ActorTable.tsx b/dashboard/client/src/components/ActorTable.tsx new file mode 100644 index 000000000..b90e5cf34 --- /dev/null +++ b/dashboard/client/src/components/ActorTable.tsx @@ -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 ( + +
+ e.state)), + )} + onInputChange={(_: any, value: string) => { + changeFilter("state", value.trim()); + }} + renderInput={(params: TextFieldProps) => ( + + )} + /> + e.address?.ipAddress)), + )} + onInputChange={(_: any, value: string) => { + changeFilter("address.ipAddress", value.trim()); + }} + renderInput={(params: TextFieldProps) => ( + + )} + /> + { + changeFilter("pid", value.trim()); + }, + endAdornment: ( + + + + ), + }} + /> + { + changeFilter("functionDesc", value.trim()); + }, + endAdornment: ( + + + + ), + }} + /> + { + changeFilter("name", value.trim()); + }, + endAdornment: ( + + + + ), + }} + /> + { + changeFilter("actorId", value.trim()); + }, + endAdornment: ( + + + + ), + }} + /> + { + setPageSize(Math.min(Number(value), 500) || 10); + }, + }} + /> +
+
+
+ setPageNo(num)} + count={Math.ceil(actorList.length / pageSize)} + /> +
+
+ +
+
+ + + + {[ + "", + "ID(Num Restarts)", + "Name", + "Task Func Desc", + "Job Id", + "Pid", + "IP", + "Port", + "State", + "Log", + ].map((col) => ( + + {col} + + ))} + + + + {list.map( + ({ + actorId, + functionDesc, + jobId, + pid, + address, + state, + name, + numRestarts, + }) => ( + + e.pid === pid && + address.ipAddress === e.coreWorkerStats[0].ipAddress, + ).length + } + expandComponent={ + + e.pid === pid && + address.ipAddress === e.coreWorkerStats[0].ipAddress, + )} + mini + /> + } + key={actorId} + > + 0 ? orange[500] : "inherit", + }} + > + {actorId}({numRestarts}) + + {name} + + {longTextCut(functionDesc, 60)} + + {jobId} + {pid} + {address?.ipAddress} + {address?.port} + + + + + {ipLogMap[address?.ipAddress] && ( + + Log + + )} + + + ), + )} + +
+
+ ); +}; + +export default ActorTable; diff --git a/dashboard/client/src/components/Loading.tsx b/dashboard/client/src/components/Loading.tsx new file mode 100644 index 000000000..6c1cb1e8f --- /dev/null +++ b/dashboard/client/src/components/Loading.tsx @@ -0,0 +1,10 @@ +import { Backdrop, CircularProgress } from "@material-ui/core"; +import React from "react"; + +const Loading = ({ loading }: { loading: boolean }) => ( + + + +); + +export default Loading; diff --git a/dashboard/client/src/components/LogView/LogVirtualView.tsx b/dashboard/client/src/components/LogView/LogVirtualView.tsx new file mode 100644 index 000000000..2046989c2 --- /dev/null +++ b/dashboard/client/src/components/LogView/LogVirtualView.tsx @@ -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; + onScrollBottom?: (event: Event) => void; + revert?: boolean; + startTime?: string; + endTime?: string; +}; + +const LogVirtualView: React.FC = ({ + 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>(); + const el = useRef(null); + const outter = useRef(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 ( +
+ + {i + 1} + + {low + .highlight(language, origin) + .value.map((v) => value2react(v, index.toString(), keywords))} +
+ ); + }; + + 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 ( + + {itemRenderer} + + ); +}; + +export default LogVirtualView; diff --git a/dashboard/client/src/components/LogView/darcula.css b/dashboard/client/src/components/LogView/darcula.css new file mode 100644 index 000000000..8564bf895 --- /dev/null +++ b/dashboard/client/src/components/LogView/darcula.css @@ -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 +@author Zeno Rocha +*/ +.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; +} diff --git a/dashboard/client/src/components/LogView/github.css b/dashboard/client/src/components/LogView/github.css new file mode 100644 index 000000000..ca16d3f73 --- /dev/null +++ b/dashboard/client/src/components/LogView/github.css @@ -0,0 +1,96 @@ +/* +github.com style (c) Vasily Polovnyov +*/ + +.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; +} diff --git a/dashboard/client/src/components/LogView/index.css b/dashboard/client/src/components/LogView/index.css new file mode 100644 index 000000000..32e5f884f --- /dev/null +++ b/dashboard/client/src/components/LogView/index.css @@ -0,0 +1,3 @@ +span.find-kws { + background-color: #ffd800; +} diff --git a/dashboard/client/src/components/PercentageBar.tsx b/dashboard/client/src/components/PercentageBar.tsx new file mode 100644 index 000000000..6b2cc48ad --- /dev/null +++ b/dashboard/client/src/components/PercentageBar.tsx @@ -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 ( +
+
+
{props.children}
+
+ ); +}; + +export default PercentageBar; diff --git a/dashboard/client/src/components/SearchComponent.tsx b/dashboard/client/src/components/SearchComponent.tsx new file mode 100644 index 000000000..02170b13c --- /dev/null +++ b/dashboard/client/src/components/SearchComponent.tsx @@ -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 ( + { + if (onChange) { + onChange(value); + } + }, + defaultValue, + endAdornment: ( + + + + ), + }} + /> + ); +}; + +export const SearchSelect = ({ + label, + onChange, + options, +}: { + label: string; + onChange?: (value: string) => void; + options: (string | [string, string])[]; +}) => { + const classes = useStyles(); + return ( + { + if (onChange) { + onChange(value as string); + } + }, + style: { + width: 100, + }, + }} + > + All + {options.map((e) => + typeof e === "string" ? ( + {e} + ) : ( + {e[1]} + ), + )} + + ); +}; diff --git a/dashboard/client/src/components/SpeedTools.tsx b/dashboard/client/src/components/SpeedTools.tsx new file mode 100644 index 000000000..7094a4117 --- /dev/null +++ b/dashboard/client/src/components/SpeedTools.tsx @@ -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 ( +
+ { + setId(value); + }, + }} + /> +
+ {id.length === 40 ? ( +
+ Job ID: {id.slice(24, 28)}
+ Actor ID: {id.slice(16, 28)}
+ Task ID: {id.slice(0, 28)}
+ Index: {parseInt(revertBit(id.slice(32)), 16)}
+ Flag: {revertBit(id.slice(28, 32))} +
+
+ {tagList + .filter( + ([a, b, c]) => detectFlag(revertBit(id.slice(28, 32)), b) === c, + ) + .map(([name]) => ( + + ))} +
+ ) : ( + + Object ID should be 40 letters long + + )} +
+
+ ); +}; + +const Tools = () => { + const [sel, setSel] = useState("oid_converter"); + const toolMap = { + oid_converter: , + } as { [key: string]: JSX.Element }; + + return ( +
+ setSel(val)}> + Object ID Reader} + /> + + {toolMap[sel]} +
+ ); +}; + +const SpeedTools = () => { + const [show, setShow] = useState(false); + const classes = useStyle(); + + return ( + + setShow(!show)} /> + + + setShow(false)} /> + + + + + ); +}; + +export default SpeedTools; diff --git a/dashboard/client/src/components/StatesCounter.tsx b/dashboard/client/src/components/StatesCounter.tsx new file mode 100644 index 000000000..b5fc987e5 --- /dev/null +++ b/dashboard/client/src/components/StatesCounter.tsx @@ -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 ( + + + + + {Object.entries(stateMap).map(([s, num]) => ( + + + + ))} + + ); +}; + +export default StateCounter; diff --git a/dashboard/client/src/components/StatusChip.tsx b/dashboard/client/src/components/StatusChip.tsx new file mode 100644 index 000000000..dc9fb11fa --- /dev/null +++ b/dashboard/client/src/components/StatusChip.tsx @@ -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 ( + + {status} + {suffix} + + ); +}; diff --git a/dashboard/client/src/components/TitleCard.tsx b/dashboard/client/src/components/TitleCard.tsx new file mode 100644 index 000000000..db088f775 --- /dev/null +++ b/dashboard/client/src/components/TitleCard.tsx @@ -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 ( + +
{title}
+
{children}
+
+ ); +}; + +export default TitleCard; diff --git a/dashboard/client/src/components/WorkerTable.tsx b/dashboard/client/src/components/WorkerTable.tsx new file mode 100644 index 000000000..aa6bba57b --- /dev/null +++ b/dashboard/client/src/components/WorkerTable.tsx @@ -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 ( + + + {children} + + ); + } + + return ( + + + + setIsExpanded(!isExpanded)} + > + {length} + {isExpanded ? : } + + + {children} + + {isExpanded && ( + + {expandComponent} + + )} + + ); +}; + +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

The Worker Haven't Had Related Actor Yet.

; + } + + return ( + + + + ); +}; + +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 ( + + {!mini && ( +
+ changeFilter("pid", value)} + /> + + +
+ )}{" "} + + + + {[ + "", + "Pid", + "CPU", + "CPU Times", + "Memory", + "CMD Line", + "Create Time", + "Log", + "Ops", + "IP/Hostname", + ].map((col) => ( + + {col} + + ))} + + + + {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, + }) => ( + + } + length={ + (coreWorkerStats || []).filter((e) => actorMap[e.actorId]) + .length + } + key={pid} + stateKey={key} + > + {pid} + + + {cpuPercent}% + + + +
+ {Object.entries(cpuTimes || {}).map(([key, val]) => ( +
+ {key}:{val} +
+ ))} +
+
+ +
+ {Object.entries(memoryInfo || {}).map(([key, val]) => ( +
+ {key}:{memoryConverter(val)} +
+ ))} +
+
+ + {cmdline && longTextCut(cmdline.filter((e) => e).join(" "))} + + + {dayjs(createTime * 1000).format("YYYY/MM/DD HH:mm:ss")} + + + + {ipLogMap[ip] && ( + + + Log + + + )} + + + + {language === "JAVA" && ( +
+ {" "} + + +
+ )} +
+ + {ip} +
+ {nodeMap[hostname] ? ( + + {hostname} + + ) : ( + hostname + )} +
+
+ ), + )} +
+
+
+ ); +}; + +export default RayletWorkerTable; diff --git a/dashboard/client/src/logo.svg b/dashboard/client/src/logo.svg new file mode 100644 index 000000000..70be9ee54 --- /dev/null +++ b/dashboard/client/src/logo.svg @@ -0,0 +1,34 @@ + + + + +Ray Logo + + + + + + + + + + diff --git a/dashboard/client/src/pages/actor/index.tsx b/dashboard/client/src/pages/actor/index.tsx new file mode 100644 index 000000000..cbcd264e2 --- /dev/null +++ b/dashboard/client/src/pages/actor/index.tsx @@ -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 ( +
+ + + +
+ ); +}; + +export default Actors; diff --git a/dashboard/client/src/pages/cmd/CMDResult.tsx b/dashboard/client/src/pages/cmd/CMDResult.tsx new file mode 100644 index 000000000..ed87c10d8 --- /dev/null +++ b/dashboard/client/src/pages/cmd/CMDResult.tsx @@ -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(); + 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 ( +
+ + {cmd === "jstat" && ( + + + + + + + + + + + )} + + + + +
+ ); +}; + +export default CMDResult; diff --git a/dashboard/client/src/pages/dashboard/Dashboard.tsx b/dashboard/client/src/pages/dashboard/Dashboard.tsx index 0ffbce7f5..d7eeaf936 100644 --- a/dashboard/client/src/pages/dashboard/Dashboard.tsx +++ b/dashboard/client/src/pages/dashboard/Dashboard.tsx @@ -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 (
Ray Dashboard + { + return ( +
+
+ + + + 404 NOT FOUND +

+ We can't provide the page you wanted yet, better try with another path + next time. +

+
+
+ ); +}; + +export default Error404; diff --git a/dashboard/client/src/pages/exception/Loading.tsx b/dashboard/client/src/pages/exception/Loading.tsx new file mode 100644 index 000000000..24140c4dc --- /dev/null +++ b/dashboard/client/src/pages/exception/Loading.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import Logo from "../../logo.svg"; + +export default () => { + return ( +
+
+ Loading +
+ Loading... +
+
+ ); +}; diff --git a/dashboard/client/src/pages/index/Index.tsx b/dashboard/client/src/pages/index/Index.tsx new file mode 100644 index 000000000..961216449 --- /dev/null +++ b/dashboard/client/src/pages/index/Index.tsx @@ -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(); + const [nodes, setNodes] = useState([]); + 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 ( +
+ +

Dashboard Frontend Version: {version}

+ {rayConfig?.imageUrl && ( +

+ Image Url:{" "} + + {rayConfig.imageUrl} + +

+ )} + {rayConfig?.sourceCodeLink && ( +

+ Source Code:{" "} + + {rayConfig.sourceCodeLink} + +

+ )} +
+ {rayConfig && ( + + + + Key + Value + + + {Object.entries(rayConfig).map(([key, value]) => ( + + {key} + {getVal(key, value)} + + ))} + + + + )} +
+ ); +}; + +export default Index; diff --git a/dashboard/client/src/pages/job/JobDetail.tsx b/dashboard/client/src/pages/job/JobDetail.tsx new file mode 100644 index 000000000..b720b9c05 --- /dev/null +++ b/dashboard/client/src/pages/job/JobDetail.tsx @@ -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 ( +
+ + + +
+ Auto Refresh: + +
+ Request Status: {msg}
+
+
+ ); + } + + return ( +
+ + +
+ Auto Refresh: + +
+ Request Status: {msg}
+
+ + + + + + + + {selectedTab === "info" && ( + + + Driver IP:{" "} + {jobInfo.driverIpAddress} + + {ipLogMap[jobInfo.driverIpAddress] && ( + + Driver Log:{" "} + + Log + + + )} + + Driver Pid:{" "} + {jobInfo.driverPid} + + {jobInfo.eventUrl && ( + + Event Link:{" "} + + Event Log + + + )} + {jobInfo.failErrorMessage && ( + + Fail Error:{" "} + + {jobInfo.failErrorMessage} + + + )} + + )} + {jobInfo?.dependencies && selectedTab === "dep" && ( +
+ {jobInfo?.dependencies?.python && ( + +
+ {jobInfo.dependencies.python.map((e) => ( + + ))} +
+
+ )} + {jobInfo?.dependencies?.java && ( + + + + + + {["Name", "Version", "URL"].map((col) => ( + + {col} + + ))} + + + + {jobInfo.dependencies.java.map( + ({ name, version, url }) => ( + + {name} + {version} + + + {url} + + + + ), + )} + +
+
+
+ )} +
+ )} + {selectedTab === "worker" && ( +
+ + + +
+ )} + {selectedTab === "actor" && ( +
+ + + +
+ )} +
+
+ ); +}; + +export default JobDetailPage; diff --git a/dashboard/client/src/pages/job/hook/useJobDetail.ts b/dashboard/client/src/pages/job/hook/useJobDetail.ts new file mode 100644 index 000000000..695fca760 --- /dev/null +++ b/dashboard/client/src/pages/job/hook/useJobDetail.ts @@ -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(); + 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(); + const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => { + setTab(newValue); + }; + const handleSwitchChange = (event: React.ChangeEvent) => { + 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, + }; +}; diff --git a/dashboard/client/src/pages/job/hook/useJobList.ts b/dashboard/client/src/pages/job/hook/useJobList.ts new file mode 100644 index 000000000..04f97532f --- /dev/null +++ b/dashboard/client/src/pages/job/hook/useJobList.ts @@ -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([]); + 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(); + 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) => { + 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 }), + }; +}; diff --git a/dashboard/client/src/pages/job/index.tsx b/dashboard/client/src/pages/job/index.tsx new file mode 100644 index 000000000..8d2a4aaa4 --- /dev/null +++ b/dashboard/client/src/pages/job/index.tsx @@ -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 ( +
+ + + Auto Refresh: + +
+ Request Status: {msg} +
+ + + changeFilter("jobId", value)} + /> + changeFilter("language", value)} + options={["JAVA", "PYTHON"]} + /> + + setPage("pageSize", Math.min(Number(value), 500) || 10) + } + /> +
+ setPage("pageNo", pageNo)} + /> +
+ + + + {columns.map((col) => ( + + {col} + + ))} + + + + {jobList + .slice( + (page.pageNo - 1) * page.pageSize, + page.pageNo * page.pageSize, + ) + .map( + ({ + jobId = "", + driverIpAddress, + isDead, + driverPid, + state, + timestamp, + namespaceId, + }) => ( + + + {jobId} + + {driverIpAddress} + {driverPid} + + {isDead ? "true" : "false"} + + + {dayjs(timestamp * 1000).format("YYYY/MM/DD HH:mm:ss")} + + {namespaceId} + + ), + )} + +
+
+
+
+ ); +}; + +export default JobList; diff --git a/dashboard/client/src/pages/layout/index.tsx b/dashboard/client/src/pages/layout/index.tsx new file mode 100644 index 000000000..b484a29db --- /dev/null +++ b/dashboard/client/src/pages/layout/index.tsx @@ -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 ( +
+ + + Ray
Ray Dashboard +
+ + history.push("/summary")} + > + SUMMARY + + history.push("/node")} + > + NODES + + history.push("/job")} + > + JOBS + + history.push("/actors")} + > + ACTORS + + history.push("/log")} + > + LOGS + + history.push("/")} + > + BACK TO LEGACY + + + { + window.scrollTo(0, 0); + }} + > + + + + + { + setTheme(theme === "dark" ? "light" : "dark"); + }} + > + + {theme === "dark" ? : } + + + + + +
+
{children}
+
+ ); +}; + +export default BasicLayout; diff --git a/dashboard/client/src/pages/log/Logs.tsx b/dashboard/client/src/pages/log/Logs.tsx new file mode 100644 index 000000000..12218d52a --- /dev/null +++ b/dashboard/client/src/pages/log/Logs.tsx @@ -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(null); + const [origin, setOrigin] = useState(); + 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(); + const [endTime, setEnd] = useState(); + + 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 ( +
+ + + {!origin &&

Please choose an url to get log path

} + {origin && ( +

+ Now Path: {origin} + {decodeURIComponent(path || "")} +

+ )} + {origin && ( +
+ + {typeof log === "object" && ( + { + setFileName(val); + }} + /> + )} +
+ )} +
+ + {typeof log === "object" && ( + + {log + .filter((e) => !fileName || e?.name?.includes(fileName)) + .map((e: { [key: string]: string }) => ( + + + {e.name} + + + ))} + + )} + {typeof log === "string" && log !== "Loading..." && ( +
+
+ { + setSearch({ ...search, keywords: value }); + }, + type: "", + endAdornment: ( + + + + ), + }} + /> + { + setSearch({ ...search, lineNumber: value }); + }, + type: "", + endAdornment: ( + + + + ), + }} + /> + { + setSearch({ ...search, fontSize: Number(value) }); + }, + type: "", + }} + /> + { + setStart(val.target.value); + }} + InputLabelProps={{ + shrink: true, + }} + /> + { + setEnd(val.target.value); + }} + InputLabelProps={{ + shrink: true, + }} + /> +
+ Reverse:{" "} + setSearch({ ...search, revert: v })} + /> + +
+
+ +
+ )} + {log === "Loading..." && ( +
+
+ +
+ )} +
+
+
+ ); +}; + +export default Logs; diff --git a/dashboard/client/src/pages/node/NodeDetail.tsx b/dashboard/client/src/pages/node/NodeDetail.tsx new file mode 100644 index 000000000..6f5187bdb --- /dev/null +++ b/dashboard/client/src/pages/node/NodeDetail.tsx @@ -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 ( + + {tags.split(",").pop()?.split(":").slice(1).join(":")}= + {Object.keys(otherProps).length > 0 ? ( + JSON.stringify(Object.values(otherProps).pop()) + ) : ( + null + )} + + ); +}; + +const NodeDetailPage = (props: RouteComponentProps<{ id: string }>) => { + const classes = useStyle(); + const { + params, + selectedTab, + nodeDetail, + msg, + isRefreshing, + onRefreshChange, + raylet, + handleChange, + } = useNodeDetail(props); + + return ( +
+ + + +
+ Auto Refresh: + +
+ Request Status: {msg} +
+ + + + + + + + {nodeDetail && selectedTab === "info" && ( +
+ + +
Hostname
{" "} + {nodeDetail.hostname} +
+ +
IP
{nodeDetail.ip} +
+
+ + +
CPU (Logic/Physic)
{" "} + {nodeDetail.cpus[0]}/ {nodeDetail.cpus[1]} +
+ +
Load (1/5/15min)
{" "} + {nodeDetail?.loadAvg[0] && + nodeDetail.loadAvg[0] + .map((e) => Number(e).toFixed(2)) + .join("/")} +
+
+ + +
Load per CPU (1/5/15min)
{" "} + {nodeDetail?.loadAvg[1] && + nodeDetail.loadAvg[1] + .map((e) => Number(e).toFixed(2)) + .join("/")} +
+ +
Boot Time
{" "} + {dayjs(nodeDetail.bootTime * 1000).format( + "YYYY/MM/DD HH:mm:ss", + )} +
+
+ + +
Sent Tps
{" "} + {memoryConverter(nodeDetail?.net[0])}/s +
+ +
Recieved Tps
{" "} + {memoryConverter(nodeDetail?.net[1])}/s +
+
+ + +
Memory
{" "} + {nodeDetail?.mem && ( + + {memoryConverter(nodeDetail?.mem[0] - nodeDetail?.mem[1])}/ + {memoryConverter(nodeDetail?.mem[0])}({nodeDetail?.mem[2]}%) + + )} +
+ +
CPU
{" "} + + {nodeDetail.cpu}% + +
+
+ + {nodeDetail?.disk && + Object.entries(nodeDetail?.disk).map(([path, obj]) => ( + +
Disk ({path})
{" "} + {obj && ( + + {memoryConverter(obj.used)}/{memoryConverter(obj.total)} + ({obj.percent}%, {memoryConverter(obj.free)} free) + + )} +
+ ))} +
+ + +
Logs
{" "} + + log + +
+
+
+ )} + {raylet && Object.keys(raylet).length > 0 && selectedTab === "raylet" && ( + +
+ + +
Command
+
+
+ {nodeDetail?.cmdline.join(" ")} +
+
+
+ + +
Pid
{raylet?.pid} +
+ +
Workers Num
{" "} + {raylet?.numWorkers} +
+ +
Node Manager Port
{" "} + {raylet?.nodeManagerPort} +
+
+ {showMeasureKeys + .map((e) => raylet.viewData.find((view) => view.viewName === e)) + .map((e) => + e ? ( + +

+ {e.viewName + .split("_") + .map((e) => e[0].toUpperCase() + e.slice(1)) + .join(" ")} +

+ + {e.measures.map((e) => ( + + ))} + +
+ ) : null, + )} +
+
+ )} + {nodeDetail?.workers && selectedTab === "worker" && ( + + + + + + )} + {nodeDetail?.actors && selectedTab === "actor" && ( + + + + + + )} +
+
+ ); +}; + +export default NodeDetailPage; diff --git a/dashboard/client/src/pages/node/hook/useNodeDetail.ts b/dashboard/client/src/pages/node/hook/useNodeDetail.ts new file mode 100644 index 000000000..1ca3570a2 --- /dev/null +++ b/dashboard/client/src/pages/node/hook/useNodeDetail.ts @@ -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(); + const [msg, setMsg] = useState("Loading the node infos..."); + const { namespaceMap } = useContext(GlobalContext); + const [isRefreshing, setRefresh] = useState(true); + const tot = useRef(); + const onRefreshChange = (event: React.ChangeEvent) => { + 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, + }; +}; diff --git a/dashboard/client/src/pages/node/hook/useNodeList.ts b/dashboard/client/src/pages/node/hook/useNodeList.ts new file mode 100644 index 000000000..96a3339ba --- /dev/null +++ b/dashboard/client/src/pages/node/hook/useNodeList.ts @@ -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([]); + 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(); + 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) => { + 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, + }; +}; diff --git a/dashboard/client/src/pages/node/index.tsx b/dashboard/client/src/pages/node/index.tsx new file mode 100644 index 000000000..3713fdc15 --- /dev/null +++ b/dashboard/client/src/pages/node/index.tsx @@ -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 ( + +

+ {nodeId}{" "} +

+

+ + + + + + {hostname}({ip}) + + {net && net[0] >= 0 && ( + + Sent{" "} + {memoryConverter(net[0])}/s{" "} + Received{" "} + {memoryConverter(net[1])}/s + + )} + +

+ + {cpu >= 0 && ( + + CPU + + {cpu}% + + + )} + {mem && ( + + Memory + + {memoryConverter(mem[0] - mem[1])}/{memoryConverter(mem[0])}( + {mem[2]}%) + + + )} + {disk && disk["/"] && ( + + Disk('/') + + {memoryConverter(disk["/"].used)}/ + {memoryConverter(disk["/"].total)}({disk["/"].percent}%) + + + )} + + + + + + + + + +
+ ); +}; + +const Nodes = () => { + const classes = useStyles(); + const { + msg, + isRefreshing, + onSwitchChange, + nodeList, + changeFilter, + page, + setPage, + setSortKey, + setOrderDesc, + mode, + setMode, + } = useNodeList(); + + return ( +
+ + + Auto Refresh: + +
+ Request Status: {msg} +
+ + + + + + + changeFilter("hostname", value.trim())} + /> + + + changeFilter("ip", value.trim())} + /> + + + changeFilter("state", value.trim())} + options={["ALIVE", "DEAD"]} + /> + + + + setPage("pageSize", Math.min(Number(value), 500) || 10) + } + /> + + + setSortKey(val)} + /> + + + + Reverse: + setOrderDesc(checked)} /> + + + + + + + + + +
+ setPage("pageNo", pageNo)} + /> +
+ {mode === "table" && ( + + + + + {columns.map((col) => ( + + {col} + + ))} + + + + {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, + ) => ( + + + + + + + + {raylet.nodeId.slice(0, 5)} + + + + {hostname} + {ip} + + + {cpu}% + + + + + {memoryConverter(mem[0] - mem[1])}/ + {memoryConverter(mem[0])}({mem[2]}%) + + + + {disk && disk["/"] && ( + + {memoryConverter(disk["/"].used)}/ + {memoryConverter(disk["/"].total)}( + {disk["/"].percent}%) + + )} + + + {memoryConverter(net[0])}/s + + + {memoryConverter(net[1])}/s + + + {raylet.brpcPort && ( + + {raylet.brpcPort} + + )} + + + {!!raylet.startTime && ( +

+ Start Time:{" "} + {dayjs(raylet.startTime * 1000).format( + "YYYY/MM/DD HH:mm:ss", + )} +

+ )} + {!!raylet.terminateTime && ( +

+ End Time:{" "} + {dayjs(raylet.terminateTime * 1000).format( + "YYYY/MM/DD HH:mm:ss", + )} +

+ )} +
+ + + Log + + +
+ ), + )} +
+
+
+ )} + {mode === "card" && ( + + {nodeList + .slice( + (page.pageNo - 1) * page.pageSize, + page.pageNo * page.pageSize, + ) + .map((e) => ( + + + + ))} + + )} +
+
+ ); +}; + +export default Nodes; diff --git a/dashboard/client/src/service/actor.ts b/dashboard/client/src/service/actor.ts new file mode 100644 index 000000000..425fd62a4 --- /dev/null +++ b/dashboard/client/src/service/actor.ts @@ -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"); +}; diff --git a/dashboard/client/src/service/cluster.ts b/dashboard/client/src/service/cluster.ts new file mode 100644 index 000000000..9bf53e76d --- /dev/null +++ b/dashboard/client/src/service/cluster.ts @@ -0,0 +1,6 @@ +import axios from "axios"; +import { RayConfigRsp } from "../type/config"; + +export const getRayConfig = () => { + return axios.get("api/ray_config"); +}; diff --git a/dashboard/client/src/service/job.ts b/dashboard/client/src/service/job.ts new file mode 100644 index 000000000..fc5d5452d --- /dev/null +++ b/dashboard/client/src/service/job.ts @@ -0,0 +1,10 @@ +import axios from "axios"; +import { JobDetailRsp, JobListRsp } from "../type/job"; + +export const getJobList = () => { + return axios.get("jobs?view=summary"); +}; + +export const getJobDetail = (id: string) => { + return axios.get(`jobs/${id}`); +}; diff --git a/dashboard/client/src/service/log.ts b/dashboard/client/src/service/log.ts new file mode 100644 index 000000000..b485b12f1 --- /dev/null +++ b/dashboard/client/src/service/log.ts @@ -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; +}; diff --git a/dashboard/client/src/service/node.ts b/dashboard/client/src/service/node.ts new file mode 100644 index 000000000..5eac1dc9c --- /dev/null +++ b/dashboard/client/src/service/node.ts @@ -0,0 +1,10 @@ +import axios from "axios"; +import { NodeDetailRsp, NodeListRsp } from "../type/node"; + +export const getNodeList = async () => { + return await axios.get("nodes?view=summary"); +}; + +export const getNodeDetail = async (id: string) => { + return await axios.get(`nodes/${id}`); +}; diff --git a/dashboard/client/src/service/util.ts b/dashboard/client/src/service/util.ts new file mode 100644 index 000000000..966c82db2 --- /dev/null +++ b/dashboard/client/src/service/util.ts @@ -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("utils/jstack", { + params: { + ip, + pid, + }, + }); +}; + +export const getJmap = (ip: string, pid: string) => { + return axios.get("utils/jmap", { + params: { + ip, + pid, + }, + }); +}; + +export const getJstat = (ip: string, pid: string, options: string) => { + return axios.get("utils/jstat", { + params: { + ip, + pid, + options, + }, + }); +}; + +type NamespacesRsp = { + result: boolean; + msg: string; + data: { + namespaces: { + namespaceId: string; + hostNameList: string[]; + }[]; + }; +}; + +export const getNamespaces = () => { + return axios.get("namespaces"); +}; diff --git a/dashboard/client/src/theme.ts b/dashboard/client/src/theme.ts new file mode 100644 index 000000000..f83d58b5a --- /dev/null +++ b/dashboard/client/src/theme.ts @@ -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], + }, + }, +}); diff --git a/dashboard/client/src/type/actor.ts b/dashboard/client/src/type/actor.ts new file mode 100644 index 000000000..8a00c0e41 --- /dev/null +++ b/dashboard/client/src/type/actor.ts @@ -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; +}; diff --git a/dashboard/client/src/type/config.d.ts b/dashboard/client/src/type/config.d.ts new file mode 100644 index 000000000..40a34a25f --- /dev/null +++ b/dashboard/client/src/type/config.d.ts @@ -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; + }; +}; diff --git a/dashboard/client/src/type/event.d.ts b/dashboard/client/src/type/event.d.ts new file mode 100644 index 000000000..4f586f9a0 --- /dev/null +++ b/dashboard/client/src/type/event.d.ts @@ -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[]; + }; + }; +}; diff --git a/dashboard/client/src/type/job.d.ts b/dashboard/client/src/type/job.d.ts new file mode 100644 index 000000000..c5ca4dce8 --- /dev/null +++ b/dashboard/client/src/type/job.d.ts @@ -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; +}; diff --git a/dashboard/client/src/type/node.d.ts b/dashboard/client/src/type/node.d.ts new file mode 100644 index 000000000..12106d9ad --- /dev/null +++ b/dashboard/client/src/type/node.d.ts @@ -0,0 +1,62 @@ +import { Actor } from "./actor"; +import { Raylet } from "./raylet"; +import { Worker } from "./worker"; + +export type NodeDetail = { + now: number; + hostname: string; + ip: string; + cpu: number; // cpu usage + cpus: number[]; // Logic CPU Count, Physical CPU Count + mem: number[]; // total memory, free memory, memory used ratio + bootTime: number; // start time + loadAvg: number[][]; // recent 1,5,15 minitues system load,load per cpu http://man7.org/linux/man-pages/man3/getloadavg.3.html + disk: { + // disk used on root + "/": { + total: number; + used: number; + free: number; + percent: number; + }; + // disk used on tmp + "/tmp": { + total: number; + used: number; + free: number; + percent: number; + }; + }; + net: number[]; // sent tps, received tps + raylet: Raylet; + logCounts: number; + errorCounts: number; + actors: { [id: string]: Actor }; + cmdline: string[]; + state: string; + logUrl: string; +}; + +export type NodeListRsp = { + data: { + summary: NodeDetail[]; + }; + result: boolean; + msg: string; +}; + +export type NodeDetailExtend = { + workers: Worker[]; + raylet: Raylet; + actors: { + [actorId: string]: Actor; + }; +} & NodeDetail; + +export type NodeDetailRsp = { + data: { + detail: NodeDetailExtend; + }; + msg: string; + result: boolean; +}; diff --git a/dashboard/client/src/type/raylet.d.ts b/dashboard/client/src/type/raylet.d.ts new file mode 100644 index 000000000..459b4c2b9 --- /dev/null +++ b/dashboard/client/src/type/raylet.d.ts @@ -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; +}; diff --git a/dashboard/client/src/type/worker.d.ts b/dashboard/client/src/type/worker.d.ts new file mode 100644 index 000000000..cf35bfa01 --- /dev/null +++ b/dashboard/client/src/type/worker.d.ts @@ -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; +}; diff --git a/dashboard/client/src/util/converter.ts b/dashboard/client/src/util/converter.ts new file mode 100644 index 000000000..427ae86b7 --- /dev/null +++ b/dashboard/client/src/util/converter.ts @@ -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 ""; +}; diff --git a/dashboard/client/src/util/func.tsx b/dashboard/client/src/util/func.tsx new file mode 100644 index 000000000..c07ef70fe --- /dev/null +++ b/dashboard/client/src/util/func.tsx @@ -0,0 +1,28 @@ +import { Tooltip } from "@material-ui/core"; +import React, { CSSProperties } from "react"; + +export const longTextCut = (text: string = "", len: number = 28) => ( + + {text.length > len ? text.slice(0, len) + "..." : text} + +); + +export const jsonFormat = (str: string | object) => { + const preStyle = { + textAlign: "left", + wordBreak: "break-all", + whiteSpace: "pre-wrap", + } as CSSProperties; + if (typeof str === "object") { + return
{JSON.stringify(str, null, 2)}
; + } + try { + const j = JSON.parse(str); + if (typeof j !== "object") { + return JSON.stringify(j); + } + return
{JSON.stringify(j, null, 2)}
; + } catch (e) { + return str; + } +}; diff --git a/dashboard/client/src/util/hook.ts b/dashboard/client/src/util/hook.ts new file mode 100644 index 000000000..3c6f61b06 --- /dev/null +++ b/dashboard/client/src/util/hook.ts @@ -0,0 +1,63 @@ +import { get } from "lodash"; +import { useState } from "react"; + +export const useFilter = () => { + 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, + }; +}; diff --git a/dashboard/client/src/util/localData.ts b/dashboard/client/src/util/localData.ts new file mode 100644 index 000000000..0066c4788 --- /dev/null +++ b/dashboard/client/src/util/localData.ts @@ -0,0 +1,12 @@ +export const getLocalStorage = (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)); +};