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