From 43d20fff62fa754b34cf7fafe0bf2e320f7a3fe9 Mon Sep 17 00:00:00 2001 From: Mitchell Stern Date: Mon, 2 Dec 2019 14:05:40 -0500 Subject: [PATCH] Refactor dashboard codebase to improve modularity (#6330) * Refactor dashboard codebase to improve modularity * Simplify feature interface * Use arrow notation in makeFeature argument types * Use separate components for node and worker features rather than a single conditionally-rendered component * Add comments about Ray worker process titles * Add comments to non-obvious fields in node info API response --- python/ray/dashboard/client/package-lock.json | 97 +++- python/ray/dashboard/client/package.json | 3 + python/ray/dashboard/client/src/App.tsx | 22 +- python/ray/dashboard/client/src/Dashboard.tsx | 448 ------------------ python/ray/dashboard/client/src/api.ts | 95 ++++ .../client/src/common/DialogWithTitle.tsx | 61 +++ .../client/src/{ => common}/NumberedLines.tsx | 0 .../client/src/{ => common}/UsageBar.tsx | 0 .../client/src/common/formatUtils.ts | 33 ++ .../client/src/pages/dashboard/Dashboard.tsx | 170 +++++++ .../src/pages/dashboard/NodeRowGroup.tsx | 128 +++++ .../src/pages/dashboard/features/CPU.tsx | 18 + .../src/pages/dashboard/features/Disk.tsx | 20 + .../src/pages/dashboard/features/Errors.tsx | 33 ++ .../src/pages/dashboard/features/Host.tsx | 18 + .../src/pages/dashboard/features/Logs.tsx | 35 ++ .../src/pages/dashboard/features/RAM.tsx | 18 + .../src/pages/dashboard/features/Uptime.tsx | 11 + .../src/pages/dashboard/features/Workers.tsx | 13 + .../src/pages/dashboard/features/types.tsx | 16 + .../client/src/pages/dashboard/state.ts | 33 ++ .../client/src/{ => pages/errors}/Errors.tsx | 69 +-- .../client/src/{ => pages/logs}/Logs.tsx | 71 +-- python/ray/dashboard/client/src/store.ts | 11 + python/ray/reporter.py | 2 +- 25 files changed, 857 insertions(+), 568 deletions(-) delete mode 100644 python/ray/dashboard/client/src/Dashboard.tsx create mode 100644 python/ray/dashboard/client/src/api.ts create mode 100644 python/ray/dashboard/client/src/common/DialogWithTitle.tsx rename python/ray/dashboard/client/src/{ => common}/NumberedLines.tsx (100%) rename python/ray/dashboard/client/src/{ => common}/UsageBar.tsx (100%) create mode 100644 python/ray/dashboard/client/src/common/formatUtils.ts create mode 100644 python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/NodeRowGroup.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/CPU.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/Disk.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/Errors.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/Host.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/Logs.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/RAM.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/Uptime.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/Workers.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/features/types.tsx create mode 100644 python/ray/dashboard/client/src/pages/dashboard/state.ts rename python/ray/dashboard/client/src/{ => pages/errors}/Errors.tsx (59%) rename python/ray/dashboard/client/src/{ => pages/logs}/Logs.tsx (52%) create mode 100644 python/ray/dashboard/client/src/store.ts diff --git a/python/ray/dashboard/client/package-lock.json b/python/ray/dashboard/client/package-lock.json index 8c1df6684..9486480c8 100644 --- a/python/ray/dashboard/client/package-lock.json +++ b/python/ray/dashboard/client/package-lock.json @@ -1289,6 +1289,26 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@reduxjs/toolkit": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.0.4.tgz", + "integrity": "sha512-nyCZ9/CpnMXFZ//0wm1mNPSEl0J0bCghY2qeHM8zuubaBBMBr6KsIaLLms1jThbOJ1O+Ej0Tl11z5naE9czfzA==", + "requires": { + "immer": "^4.0.1", + "redux": "^4.0.0", + "redux-devtools-extension": "^2.13.8", + "redux-immutable-state-invariant": "^2.1.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "immer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/immer/-/immer-4.0.2.tgz", + "integrity": "sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w==" + } + } + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -1450,6 +1470,15 @@ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.3.tgz", "integrity": "sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw==" }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1522,6 +1551,17 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.5.tgz", + "integrity": "sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/react-router": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.3.tgz", @@ -5955,9 +5995,9 @@ "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==" }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -10634,6 +10674,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" }, + "react-redux": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz", + "integrity": "sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, "react-router": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz", @@ -10831,6 +10884,34 @@ "minimatch": "3.0.4" } }, + "redux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", + "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-devtools-extension": { + "version": "2.13.8", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", + "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==" + }, + "redux-immutable-state-invariant": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz", + "integrity": "sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==", + "requires": { + "invariant": "^2.1.0", + "json-stringify-safe": "^5.0.1" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -11057,6 +11138,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", @@ -12189,6 +12275,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/python/ray/dashboard/client/package.json b/python/ray/dashboard/client/package.json index a81a6a21b..29646a6a0 100644 --- a/python/ray/dashboard/client/package.json +++ b/python/ray/dashboard/client/package.json @@ -5,15 +5,18 @@ "dependencies": { "@material-ui/core": "^4.3.3", "@material-ui/icons": "^4.2.1", + "@reduxjs/toolkit": "^1.0.4", "@types/classnames": "^2.2.9", "@types/jest": "24.0.18", "@types/node": "12.7.2", "@types/react": "16.9.2", "@types/react-dom": "16.9.0", + "@types/react-redux": "^7.1.5", "@types/react-router-dom": "^4.3.5", "classnames": "^2.2.6", "react": "^16.9.0", "react-dom": "^16.9.0", + "react-redux": "^7.1.3", "react-router-dom": "^5.0.1", "react-scripts": "3.1.1", "typeface-roboto": "0.0.75", diff --git a/python/ray/dashboard/client/src/App.tsx b/python/ray/dashboard/client/src/App.tsx index 5ffaa1ba7..ccd6c3c99 100644 --- a/python/ray/dashboard/client/src/App.tsx +++ b/python/ray/dashboard/client/src/App.tsx @@ -1,19 +1,23 @@ import CssBaseline from "@material-ui/core/CssBaseline"; import React from "react"; +import { Provider } from "react-redux"; import { BrowserRouter, Route } from "react-router-dom"; -import Dashboard from "./Dashboard"; -import Errors from "./Errors"; -import Logs from "./Logs"; +import Dashboard from "./pages/dashboard/Dashboard"; +import Errors from "./pages/errors/Errors"; +import Logs from "./pages/logs/Logs"; +import { store } from "./store"; class App extends React.Component { render() { return ( - - - - - - + + + + + + + + ); } } diff --git a/python/ray/dashboard/client/src/Dashboard.tsx b/python/ray/dashboard/client/src/Dashboard.tsx deleted file mode 100644 index d4690f20d..000000000 --- a/python/ray/dashboard/client/src/Dashboard.tsx +++ /dev/null @@ -1,448 +0,0 @@ -import Link from "@material-ui/core/Link"; -import { Theme } from "@material-ui/core/styles/createMuiTheme"; -import createStyles from "@material-ui/core/styles/createStyles"; -import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import Typography from "@material-ui/core/Typography"; -import AddIcon from "@material-ui/icons/Add"; -import RemoveIcon from "@material-ui/icons/Remove"; -import classNames from "classnames"; -import React from "react"; -import { Link as RouterLink } from "react-router-dom"; -import UsageBar from "./UsageBar"; - -const formatByteAmount = (amount: number, unit: "mebibyte" | "gibibyte") => - `${( - amount / (unit === "mebibyte" ? Math.pow(1024, 2) : Math.pow(1024, 3)) - ).toFixed(1)} ${unit === "mebibyte" ? "MiB" : "GiB"}`; - -const formatUsage = ( - used: number, - total: number, - unit: "mebibyte" | "gibibyte" -) => { - const usedFormatted = formatByteAmount(used, unit); - const totalFormatted = formatByteAmount(total, unit); - const percent = (100 * used) / total; - return `${usedFormatted} / ${totalFormatted} (${percent.toFixed(0)}%)`; -}; - -const formatUptime = (bootTime: number) => { - const uptimeSecondsTotal = Date.now() / 1000 - bootTime; - const uptimeSeconds = Math.floor(uptimeSecondsTotal) % 60; - const uptimeMinutes = Math.floor(uptimeSecondsTotal / 60) % 60; - const uptimeHours = Math.floor(uptimeSecondsTotal / 60 / 60) % 24; - const uptimeDays = Math.floor(uptimeSecondsTotal / 60 / 60 / 24); - const pad = (value: number) => value.toString().padStart(2, "0"); - return [ - uptimeDays ? `${uptimeDays}d` : "", - `${pad(uptimeHours)}h`, - `${pad(uptimeMinutes)}m`, - `${pad(uptimeSeconds)}s` - ].join(" "); -}; - -const styles = (theme: Theme) => - createStyles({ - root: { - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(2), - "& > :not(:first-child)": { - marginTop: theme.spacing(2) - } - }, - cell: { - padding: theme.spacing(1), - textAlign: "center", - "&:last-child": { - paddingRight: theme.spacing(1) - } - }, - expandCollapseCell: { - cursor: "pointer" - }, - expandCollapseIcon: { - color: theme.palette.text.secondary, - fontSize: "1.5em", - verticalAlign: "middle" - }, - cpuUsage: { - minWidth: 60 - }, - secondary: { - color: theme.palette.text.secondary - } - }); - -// TODO(mitchellstern): Add JSON schema validation for the node info. -interface NodeInfo { - clients: Array<{ - now: number; - hostname: string; - ip: string; - boot_time: number; - cpu: number; - cpus: [number, number]; - mem: [number, number, number]; - disk: { - [path: string]: { - total: number; - free: number; - used: number; - percent: number; - }; - }; - load_avg: [[number, number, number], [number, number, number]]; - net: [number, number]; - workers: Array<{ - pid: number; - create_time: number; - name: string; - cmdline: string[]; - cpu_percent: number; - cpu_times: { - system: number; - children_system: number; - user: number; - children_user: number; - }; - memory_info: { - pageins: number; - pfaults: number; - vms: number; - rss: number; - }; - memory_full_info: null; - }>; - }>; - log_counts: { - [ip: string]: { - [pid: string]: number; - }; - }; - error_counts: { - [ip: string]: { - [pid: string]: number; - }; - }; -} - -interface State { - response: { - result: NodeInfo; - timestamp: number; - } | null; - error: string | null; - expanded: { - [ip: string]: boolean; - }; -} - -class Component extends React.Component, State> { - state: State = { - response: null, - error: null, - expanded: {} - }; - - fetchNodeInfo = async () => { - try { - const url = new URL( - "/api/node_info", - process.env.NODE_ENV === "development" - ? "http://localhost:8080" - : window.location.origin - ); - const response = await fetch(url.toString()); - const json = await response.json(); - this.setState({ response: json, error: null }); - } catch (error) { - this.setState({ response: null, error: error.toString() }); - } finally { - setTimeout(this.fetchNodeInfo, 1000); - } - }; - - toggleExpand = (ip: string) => () => { - this.setState(state => ({ - expanded: { - ...state.expanded, - [ip]: !state.expanded[ip] - } - })); - }; - - async componentDidMount() { - await this.fetchNodeInfo(); - } - - render() { - const { classes } = this.props; - const { response, error, expanded } = this.state; - - if (error !== null) { - return ( - - {error} - - ); - } - - if (response === null) { - return ( - - Loading... - - ); - } - - const { result, timestamp } = response; - - const logCounts: { - [ip: string]: { - perWorker: { - [pid: string]: number; - }; - total: number; - }; - } = {}; - - const errorCounts: { - [ip: string]: { - perWorker: { - [pid: string]: number; - }; - total: number; - }; - } = {}; - - for (const client of result.clients) { - logCounts[client.ip] = { perWorker: {}, total: 0 }; - errorCounts[client.ip] = { perWorker: {}, total: 0 }; - for (const worker of client.workers) { - logCounts[client.ip].perWorker[worker.pid] = 0; - errorCounts[client.ip].perWorker[worker.pid] = 0; - } - } - - for (const ip of Object.keys(result.log_counts)) { - if (ip in logCounts) { - for (const [pid, count] of Object.entries(result.log_counts[ip])) { - logCounts[ip].perWorker[pid] = count; - logCounts[ip].total += count; - } - } - } - - for (const ip of Object.keys(result.error_counts)) { - if (ip in errorCounts) { - for (const [pid, count] of Object.entries(result.error_counts[ip])) { - errorCounts[ip].perWorker[pid] = count; - errorCounts[ip].total += count; - } - } - } - - return ( -
- Ray Dashboard - - - - - Host - Workers - Uptime - CPU - RAM - Disk - {/*Sent*/} - {/*Received*/} - Logs - Errors - - - - {result.clients.map(client => { - return ( - - - - {!expanded[client.ip] ? ( - - ) : ( - - )} - - - {client.hostname} ({client.ip}) - - - {client.workers.length} - - - {formatUptime(client.boot_time)} - - -
- -
-
- - - - - - - {/*{(client.net[0] / Math.pow(1024, 2)).toFixed(3)} MiB/s*/} - {/*{(client.net[1] / Math.pow(1024, 2)).toFixed(3)} MiB/s*/} - - {logCounts[client.ip].total === 0 ? ( - No logs - ) : ( - - View all logs ( - {logCounts[client.ip].total.toLocaleString()}{" "} - {logCounts[client.ip].total === 1 ? "line" : "lines"}) - - )} - - - {errorCounts[client.ip].total === 0 ? ( - No errors - ) : ( - - View all errors ( - {errorCounts[client.ip].total.toLocaleString()}) - - )} - -
- {expanded[client.ip] && - client.workers.map((worker, index: number) => ( - - - - {worker.cmdline[0].split(":", 2)[0]} (PID:{" "} - {worker.pid}) - - - {worker.cmdline[0].split(":", 2)[1] || ( - Idle - )} - - - {formatUptime(worker.create_time)} - - - - - - - - - - Not available - - - - {logCounts[client.ip].perWorker[worker.pid] === 0 ? ( - No logs - ) : ( - - View log ( - {logCounts[client.ip].perWorker[ - worker.pid - ].toLocaleString()}{" "} - {logCounts[client.ip].perWorker[worker.pid] === 1 - ? "line" - : "lines"} - ) - - )} - - - {errorCounts[client.ip].perWorker[worker.pid] === - 0 ? ( - No errors - ) : ( - - View errors ( - {errorCounts[client.ip].perWorker[ - worker.pid - ].toLocaleString()} - ) - - )} - - - ))} -
- ); - })} -
-
- - Last updated: {new Date(timestamp * 1000).toLocaleString()} - -
- ); - } -} - -export default withStyles(styles)(Component); diff --git a/python/ray/dashboard/client/src/api.ts b/python/ray/dashboard/client/src/api.ts new file mode 100644 index 000000000..ad40549c2 --- /dev/null +++ b/python/ray/dashboard/client/src/api.ts @@ -0,0 +1,95 @@ +const base = + process.env.NODE_ENV === "development" + ? "http://localhost:8080" + : window.location.origin; + +// TODO(mitchellstern): Add JSON schema validation for the responses. +const get = async (path: string, params: { [key: string]: any }) => { + const url = new URL(path, base); + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } + + const response = await fetch(url.toString()); + const json = await response.json(); + + const { result, error } = json; + + if (error !== null) { + throw Error(error); + } + + return result as T; +}; + +export interface NodeInfoResponse { + clients: Array<{ + now: number; + hostname: string; + ip: string; + boot_time: number; // System boot time expressed in seconds since epoch + cpu: number; // System-wide CPU utilization expressed as a percentage + cpus: [number, number]; // Number of logical CPUs and physical CPUs + mem: [number, number, number]; // Total, available, and used percentage of memory + disk: { + [path: string]: { + total: number; + free: number; + used: number; + percent: number; + }; + }; + load_avg: [[number, number, number], [number, number, number]]; + net: [number, number]; // Sent and received network traffic in bytes / second + workers: Array<{ + pid: number; + create_time: number; + name: string; + cmdline: string[]; + cpu_percent: number; + cpu_times: { + system: number; + children_system: number; + user: number; + children_user: number; + }; + memory_info: { + pageins: number; + pfaults: number; + vms: number; + rss: number; + }; + memory_full_info: null; // Currently unused as it requires superuser permission on some systems + }>; + }>; + log_counts: { + [ip: string]: { + [pid: string]: number; + }; + }; + error_counts: { + [ip: string]: { + [pid: string]: number; + }; + }; +} + +export const getNodeInfo = () => get("/api/node_info", {}); + +export interface ErrorsResponse { + [pid: string]: Array<{ + message: string; + timestamp: number; + type: string; + }>; +} + +export const getErrors = (hostname: string, pid: string | undefined) => + get("/api/errors", { hostname, pid: pid || "" }); + +export interface LogsResponse { + [pid: string]: string[]; +} + +export const getLogs = (hostname: string, pid: string | undefined) => + get("/api/logs", { hostname, pid: pid || "" }); diff --git a/python/ray/dashboard/client/src/common/DialogWithTitle.tsx b/python/ray/dashboard/client/src/common/DialogWithTitle.tsx new file mode 100644 index 000000000..780399056 --- /dev/null +++ b/python/ray/dashboard/client/src/common/DialogWithTitle.tsx @@ -0,0 +1,61 @@ +import Dialog from "@material-ui/core/Dialog"; +import IconButton from "@material-ui/core/IconButton"; +import { Theme } from "@material-ui/core/styles/createMuiTheme"; +import createStyles from "@material-ui/core/styles/createStyles"; +import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; +import Typography from "@material-ui/core/Typography"; +import CloseIcon from "@material-ui/icons/Close"; +import React from "react"; + +const styles = (theme: Theme) => + createStyles({ + paper: { + padding: theme.spacing(3) + }, + closeButton: { + position: "absolute", + right: theme.spacing(1.5), + top: theme.spacing(1.5), + zIndex: 1 + }, + title: { + borderBottomColor: theme.palette.divider, + borderBottomStyle: "solid", + borderBottomWidth: 1, + fontSize: "1.5rem", + lineHeight: 1, + marginBottom: theme.spacing(3), + paddingBottom: theme.spacing(3) + } + }); + +interface Props { + handleClose: () => void; + title: string; +} + +class DialogWithTitle extends React.Component< + Props & WithStyles +> { + render() { + const { classes, handleClose, title } = this.props; + return ( + + + + + {title} + {this.props.children} + + ); + } +} + +export default withStyles(styles)(DialogWithTitle); diff --git a/python/ray/dashboard/client/src/NumberedLines.tsx b/python/ray/dashboard/client/src/common/NumberedLines.tsx similarity index 100% rename from python/ray/dashboard/client/src/NumberedLines.tsx rename to python/ray/dashboard/client/src/common/NumberedLines.tsx diff --git a/python/ray/dashboard/client/src/UsageBar.tsx b/python/ray/dashboard/client/src/common/UsageBar.tsx similarity index 100% rename from python/ray/dashboard/client/src/UsageBar.tsx rename to python/ray/dashboard/client/src/common/UsageBar.tsx diff --git a/python/ray/dashboard/client/src/common/formatUtils.ts b/python/ray/dashboard/client/src/common/formatUtils.ts new file mode 100644 index 000000000..71bcb913d --- /dev/null +++ b/python/ray/dashboard/client/src/common/formatUtils.ts @@ -0,0 +1,33 @@ +export const formatByteAmount = ( + amount: number, + unit: "mebibyte" | "gibibyte" +) => + `${( + amount / (unit === "mebibyte" ? Math.pow(1024, 2) : Math.pow(1024, 3)) + ).toFixed(1)} ${unit === "mebibyte" ? "MiB" : "GiB"}`; + +export const formatUsage = ( + used: number, + total: number, + unit: "mebibyte" | "gibibyte" +) => { + const usedFormatted = formatByteAmount(used, unit); + const totalFormatted = formatByteAmount(total, unit); + const percent = (100 * used) / total; + return `${usedFormatted} / ${totalFormatted} (${percent.toFixed(0)}%)`; +}; + +export const formatUptime = (bootTime: number) => { + const uptimeSecondsTotal = Date.now() / 1000 - bootTime; + const uptimeSeconds = Math.floor(uptimeSecondsTotal) % 60; + const uptimeMinutes = Math.floor(uptimeSecondsTotal / 60) % 60; + const uptimeHours = Math.floor(uptimeSecondsTotal / 60 / 60) % 24; + const uptimeDays = Math.floor(uptimeSecondsTotal / 60 / 60 / 24); + const pad = (value: number) => value.toString().padStart(2, "0"); + return [ + uptimeDays ? `${uptimeDays}d` : "", + `${pad(uptimeHours)}h`, + `${pad(uptimeMinutes)}m`, + `${pad(uptimeSeconds)}s` + ].join(" "); +}; diff --git a/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx b/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx new file mode 100644 index 000000000..ba0d316bf --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/Dashboard.tsx @@ -0,0 +1,170 @@ +import { Theme } from "@material-ui/core/styles/createMuiTheme"; +import createStyles from "@material-ui/core/styles/createStyles"; +import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import Typography from "@material-ui/core/Typography"; +import React from "react"; +import { connect } from "react-redux"; +import { getNodeInfo } from "../../api"; +import { StoreState } from "../../store"; +import NodeRowGroup from "./NodeRowGroup"; +import { dashboardActions } from "./state"; + +const styles = (theme: Theme) => + createStyles({ + root: { + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(2), + "& > :not(:first-child)": { + marginTop: theme.spacing(2) + } + }, + cell: { + padding: theme.spacing(1), + textAlign: "center", + "&:last-child": { + paddingRight: theme.spacing(1) + } + } + }); + +const mapStateToProps = (state: StoreState) => ({ + nodeInfo: state.dashboard.nodeInfo, + lastUpdatedAt: state.dashboard.lastUpdatedAt, + error: state.dashboard.error +}); + +const mapDispatchToProps = dashboardActions; + +class Dashboard extends React.Component< + WithStyles & + ReturnType & + typeof mapDispatchToProps +> { + refresh = async () => { + try { + const nodeInfo = await getNodeInfo(); + this.props.setNodeInfo(nodeInfo); + } catch (error) { + this.props.setError(error.toString()); + } finally { + setTimeout(this.refresh, 1000); + } + }; + + async componentDidMount() { + await this.refresh(); + } + + render() { + const { classes, nodeInfo, lastUpdatedAt, error } = this.props; + + if (error !== null) { + return ( + + {error} + + ); + } + + if (nodeInfo === null) { + return ( + + Loading... + + ); + } + + const logCounts: { + [ip: string]: { + perWorker: { + [pid: string]: number; + }; + total: number; + }; + } = {}; + + const errorCounts: { + [ip: string]: { + perWorker: { + [pid: string]: number; + }; + total: number; + }; + } = {}; + + for (const client of nodeInfo.clients) { + logCounts[client.ip] = { perWorker: {}, total: 0 }; + errorCounts[client.ip] = { perWorker: {}, total: 0 }; + for (const worker of client.workers) { + logCounts[client.ip].perWorker[worker.pid] = 0; + errorCounts[client.ip].perWorker[worker.pid] = 0; + } + } + + for (const ip of Object.keys(nodeInfo.log_counts)) { + if (ip in logCounts) { + for (const [pid, count] of Object.entries(nodeInfo.log_counts[ip])) { + logCounts[ip].perWorker[pid] = count; + logCounts[ip].total += count; + } + } + } + + for (const ip of Object.keys(nodeInfo.error_counts)) { + if (ip in errorCounts) { + for (const [pid, count] of Object.entries(nodeInfo.error_counts[ip])) { + errorCounts[ip].perWorker[pid] = count; + errorCounts[ip].total += count; + } + } + } + + return ( +
+ Ray Dashboard + + + + + Host + Workers + Uptime + CPU + RAM + Disk + {/*Sent*/} + {/*Received*/} + Logs + Errors + + + + {nodeInfo.clients.map(client => ( + + ))} + +
+ {lastUpdatedAt !== null && ( + + Last updated: {new Date(lastUpdatedAt).toLocaleString()} + + )} +
+ ); + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(Dashboard)); diff --git a/python/ray/dashboard/client/src/pages/dashboard/NodeRowGroup.tsx b/python/ray/dashboard/client/src/pages/dashboard/NodeRowGroup.tsx new file mode 100644 index 000000000..eb5f907ac --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/NodeRowGroup.tsx @@ -0,0 +1,128 @@ +import { Theme } from "@material-ui/core/styles/createMuiTheme"; +import createStyles from "@material-ui/core/styles/createStyles"; +import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; +import TableCell from "@material-ui/core/TableCell"; +import TableRow from "@material-ui/core/TableRow"; +import AddIcon from "@material-ui/icons/Add"; +import RemoveIcon from "@material-ui/icons/Remove"; +import classNames from "classnames"; +import React from "react"; +import { NodeInfoResponse } from "../../api"; +import { NodeCPU, WorkerCPU } from "./features/CPU"; +import { NodeDisk, WorkerDisk } from "./features/Disk"; +import { makeNodeErrors, makeWorkerErrors } from "./features/Errors"; +import { NodeHost, WorkerHost } from "./features/Host"; +import { makeNodeLogs, makeWorkerLogs } from "./features/Logs"; +import { NodeRAM, WorkerRAM } from "./features/RAM"; +import { NodeUptime, WorkerUptime } from "./features/Uptime"; +import { NodeWorkers, WorkerWorkers } from "./features/Workers"; + +const styles = (theme: Theme) => + createStyles({ + cell: { + padding: theme.spacing(1), + textAlign: "center", + "&:last-child": { + paddingRight: theme.spacing(1) + } + }, + expandCollapseCell: { + cursor: "pointer" + }, + expandCollapseIcon: { + color: theme.palette.text.secondary, + fontSize: "1.5em", + verticalAlign: "middle" + } + }); + +type ArrayType = T extends Array ? U : never; +type Node = ArrayType; + +interface Props { + node: Node; + logCounts: { + perWorker: { [pid: string]: number }; + total: number; + }; + errorCounts: { + perWorker: { [pid: string]: number }; + total: number; + }; +} + +interface State { + expanded: boolean; +} + +class NodeRowGroup extends React.Component< + Props & WithStyles, + State +> { + state: State = { + expanded: false + }; + + toggleExpand = () => { + this.setState(state => ({ + expanded: !state.expanded + })); + }; + + render() { + const { classes, node, logCounts, errorCounts } = this.props; + const { expanded } = this.state; + + const features = [ + { NodeFeature: NodeHost, WorkerFeature: WorkerHost }, + { NodeFeature: NodeWorkers, WorkerFeature: WorkerWorkers }, + { NodeFeature: NodeUptime, WorkerFeature: WorkerUptime }, + { NodeFeature: NodeCPU, WorkerFeature: WorkerCPU }, + { NodeFeature: NodeRAM, WorkerFeature: WorkerRAM }, + { NodeFeature: NodeDisk, WorkerFeature: WorkerDisk }, + { + NodeFeature: makeNodeLogs(logCounts), + WorkerFeature: makeWorkerLogs(logCounts) + }, + { + NodeFeature: makeNodeErrors(errorCounts), + WorkerFeature: makeWorkerErrors(errorCounts) + } + ]; + + return ( + + + + {!expanded ? ( + + ) : ( + + )} + + {features.map(({ NodeFeature }) => ( + + + + ))} + + {expanded && + node.workers.map((worker, index: number) => ( + + + {features.map(({ WorkerFeature }) => ( + + + + ))} + + ))} + + ); + } +} + +export default withStyles(styles)(NodeRowGroup); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/CPU.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/CPU.tsx new file mode 100644 index 000000000..1ccd4179b --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/CPU.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import UsageBar from "../../../common/UsageBar"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const NodeCPU: NodeFeatureComponent = ({ node }) => ( +
+ +
+); + +export const WorkerCPU: WorkerFeatureComponent = ({ worker }) => ( +
+ +
+); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/Disk.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/Disk.tsx new file mode 100644 index 000000000..f26c9b5c5 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/Disk.tsx @@ -0,0 +1,20 @@ +import Typography from "@material-ui/core/Typography"; +import React from "react"; +import { formatUsage } from "../../../common/formatUtils"; +import UsageBar from "../../../common/UsageBar"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const NodeDisk: NodeFeatureComponent = ({ node }) => ( + + + +); + +export const WorkerDisk: WorkerFeatureComponent = () => ( + + Not available + +); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/Errors.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/Errors.tsx new file mode 100644 index 000000000..2ec7095e5 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/Errors.tsx @@ -0,0 +1,33 @@ +import Link from "@material-ui/core/Link"; +import Typography from "@material-ui/core/Typography"; +import React from "react"; +import { Link as RouterLink } from "react-router-dom"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const makeNodeErrors = (errorCounts: { + perWorker: { [pid: string]: number }; + total: number; +}): NodeFeatureComponent => ({ node }) => + errorCounts.total === 0 ? ( + + No errors + + ) : ( + + View all errors ({errorCounts.total.toLocaleString()}) + + ); + +export const makeWorkerErrors = (errorCounts: { + perWorker: { [pid: string]: number }; + total: number; +}): WorkerFeatureComponent => ({ node, worker }) => + errorCounts.perWorker[worker.pid] === 0 ? ( + + No errors + + ) : ( + + View errors ({errorCounts.perWorker[worker.pid].toLocaleString()}) + + ); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/Host.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/Host.tsx new file mode 100644 index 000000000..71f87a97d --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/Host.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const NodeHost: NodeFeatureComponent = ({ node }) => ( + + {node.hostname} ({node.ip}) + +); + +// Ray worker process titles have one of the following forms: `ray::IDLE`, +// `ray::function()`, `ray::Class`, or `ray::Class.method()`. We extract the +// first portion here for display in the "Host" column. Note that this will +// always be `ray` under the current setup, but it may vary in the future. +export const WorkerHost: WorkerFeatureComponent = ({ worker }) => ( + + {worker.cmdline[0].split("::", 2)[0]} (PID: {worker.pid}) + +); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/Logs.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/Logs.tsx new file mode 100644 index 000000000..25039da17 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/Logs.tsx @@ -0,0 +1,35 @@ +import Link from "@material-ui/core/Link"; +import Typography from "@material-ui/core/Typography"; +import React from "react"; +import { Link as RouterLink } from "react-router-dom"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const makeNodeLogs = (logCounts: { + perWorker: { [pid: string]: number }; + total: number; +}): NodeFeatureComponent => ({ node }) => + logCounts.total === 0 ? ( + + No logs + + ) : ( + + View all logs ({logCounts.total.toLocaleString()}{" "} + {logCounts.total === 1 ? "line" : "lines"}) + + ); + +export const makeWorkerLogs = (logCounts: { + perWorker: { [pid: string]: number }; + total: number; +}): WorkerFeatureComponent => ({ node, worker }) => + logCounts.perWorker[worker.pid] === 0 ? ( + + No logs + + ) : ( + + View log ({logCounts.perWorker[worker.pid].toLocaleString()}{" "} + {logCounts.perWorker[worker.pid] === 1 ? "line" : "lines"}) + + ); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/RAM.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/RAM.tsx new file mode 100644 index 000000000..363f3ba66 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/RAM.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { formatByteAmount, formatUsage } from "../../../common/formatUtils"; +import UsageBar from "../../../common/UsageBar"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const NodeRAM: NodeFeatureComponent = ({ node }) => ( + +); + +export const WorkerRAM: WorkerFeatureComponent = ({ node, worker }) => ( + +); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/Uptime.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/Uptime.tsx new file mode 100644 index 000000000..c70988129 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/Uptime.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { formatUptime } from "../../../common/formatUtils"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const NodeUptime: NodeFeatureComponent = ({ node }) => ( + {formatUptime(node.boot_time)} +); + +export const WorkerUptime: WorkerFeatureComponent = ({ worker }) => ( + {formatUptime(worker.create_time)} +); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/Workers.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/Workers.tsx new file mode 100644 index 000000000..5fc064c5f --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/Workers.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { NodeFeatureComponent, WorkerFeatureComponent } from "./types"; + +export const NodeWorkers: NodeFeatureComponent = ({ node }) => ( + {node.workers.length} +); + +// Ray worker process titles have one of the following forms: `ray::IDLE`, +// `ray::function()`, `ray::Class`, or `ray::Class.method()`. We extract the +// second portion here for display in the "Workers" column. +export const WorkerWorkers: WorkerFeatureComponent = ({ worker }) => ( + {worker.cmdline[0].split("::", 2)[1]} +); diff --git a/python/ray/dashboard/client/src/pages/dashboard/features/types.tsx b/python/ray/dashboard/client/src/pages/dashboard/features/types.tsx new file mode 100644 index 000000000..f3320e0c2 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/features/types.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { NodeInfoResponse } from "../../../api"; + +type ArrayType = T extends Array ? U : never; +type Node = ArrayType; +type Worker = ArrayType; + +type NodeFeatureData = { node: Node }; +type WorkerFeatureData = { node: Node; worker: Worker }; + +export type NodeFeatureComponent = ( + data: NodeFeatureData +) => React.ReactElement; +export type WorkerFeatureComponent = ( + data: WorkerFeatureData +) => React.ReactElement; diff --git a/python/ray/dashboard/client/src/pages/dashboard/state.ts b/python/ray/dashboard/client/src/pages/dashboard/state.ts new file mode 100644 index 000000000..a31c327d6 --- /dev/null +++ b/python/ray/dashboard/client/src/pages/dashboard/state.ts @@ -0,0 +1,33 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { NodeInfoResponse } from "../../api"; + +const name = "dashboard"; + +interface State { + nodeInfo: NodeInfoResponse | null; + lastUpdatedAt: number | null; + error: string | null; +} + +const initialState: State = { + nodeInfo: null, + lastUpdatedAt: null, + error: null +}; + +const slice = createSlice({ + name, + initialState, + reducers: { + setNodeInfo: (state, action: PayloadAction) => { + state.nodeInfo = action.payload; + state.lastUpdatedAt = Date.now(); + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload; + } + } +}); + +export const dashboardActions = slice.actions; +export const dashboardReducer = slice.reducer; diff --git a/python/ray/dashboard/client/src/Errors.tsx b/python/ray/dashboard/client/src/pages/errors/Errors.tsx similarity index 59% rename from python/ray/dashboard/client/src/Errors.tsx rename to python/ray/dashboard/client/src/pages/errors/Errors.tsx index 189cdd853..728afe872 100644 --- a/python/ray/dashboard/client/src/Errors.tsx +++ b/python/ray/dashboard/client/src/pages/errors/Errors.tsx @@ -1,45 +1,26 @@ -import Dialog from "@material-ui/core/Dialog"; -import IconButton from "@material-ui/core/IconButton"; import { fade } from "@material-ui/core/styles/colorManipulator"; import { Theme } from "@material-ui/core/styles/createMuiTheme"; import createStyles from "@material-ui/core/styles/createStyles"; import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; import Typography from "@material-ui/core/Typography"; -import CloseIcon from "@material-ui/icons/Close"; import React from "react"; import { RouteComponentProps } from "react-router"; -import NumberedLines from "./NumberedLines"; +import { ErrorsResponse, getErrors } from "../../api"; +import DialogWithTitle from "../../common/DialogWithTitle"; +import NumberedLines from "../../common/NumberedLines"; const styles = (theme: Theme) => createStyles({ - paper: { - padding: theme.spacing(3) - }, - closeButton: { - position: "absolute", - right: theme.spacing(1.5), - top: theme.spacing(1.5), - zIndex: 1 - }, - title: { - borderBottomColor: theme.palette.divider, - borderBottomStyle: "solid", - borderBottomWidth: 1, - fontSize: "1.5rem", - lineHeight: 1, - marginBottom: theme.spacing(3), - paddingBottom: theme.spacing(3) - }, header: { lineHeight: 1, marginBottom: theme.spacing(3), marginTop: theme.spacing(3) }, error: { - backgroundColor: fade(theme.palette.error.main, 0.06), + backgroundColor: fade(theme.palette.error.main, 0.04), borderLeftColor: theme.palette.error.main, borderLeftStyle: "solid", - borderLeftWidth: 3, + borderLeftWidth: 2, marginTop: theme.spacing(3), padding: theme.spacing(2) }, @@ -50,17 +31,11 @@ const styles = (theme: Theme) => }); interface State { - result: { - [pid: string]: Array<{ - message: string; - timestamp: number; - type: string; - }>; - } | null; + result: ErrorsResponse | null; error: string | null; } -class Component extends React.Component< +class Errors extends React.Component< WithStyles & RouteComponentProps<{ hostname: string; pid: string | undefined }>, State @@ -78,17 +53,8 @@ class Component extends React.Component< try { const { match } = this.props; const { hostname, pid } = match.params; - const url = new URL( - "/api/errors", - process.env.NODE_ENV === "development" - ? "http://localhost:8080" - : window.location.origin - ); - url.searchParams.set("hostname", hostname); - url.searchParams.set("pid", pid || ""); - const response = await fetch(url.toString()); - const json = await response.json(); - this.setState({ result: json.result, error: null }); + const result = await getErrors(hostname, pid); + this.setState({ result, error: null }); } catch (error) { this.setState({ result: null, error: error.toString() }); } @@ -101,18 +67,7 @@ class Component extends React.Component< const { hostname } = match.params; return ( - - - - - Errors + {error !== null ? ( {error} ) : result === null ? ( @@ -138,9 +93,9 @@ class Component extends React.Component< )) )} - + ); } } -export default withStyles(styles)(Component); +export default withStyles(styles)(Errors); diff --git a/python/ray/dashboard/client/src/Logs.tsx b/python/ray/dashboard/client/src/pages/logs/Logs.tsx similarity index 52% rename from python/ray/dashboard/client/src/Logs.tsx rename to python/ray/dashboard/client/src/pages/logs/Logs.tsx index 5f5625f52..4059c42a7 100644 --- a/python/ray/dashboard/client/src/Logs.tsx +++ b/python/ray/dashboard/client/src/pages/logs/Logs.tsx @@ -1,47 +1,36 @@ -import Dialog from "@material-ui/core/Dialog"; -import IconButton from "@material-ui/core/IconButton"; +import { fade } from "@material-ui/core/styles/colorManipulator"; import { Theme } from "@material-ui/core/styles/createMuiTheme"; import createStyles from "@material-ui/core/styles/createStyles"; import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; import Typography from "@material-ui/core/Typography"; -import CloseIcon from "@material-ui/icons/Close"; import React from "react"; import { RouteComponentProps } from "react-router"; -import NumberedLines from "./NumberedLines"; +import { getLogs, LogsResponse } from "../../api"; +import DialogWithTitle from "../../common/DialogWithTitle"; +import NumberedLines from "../../common/NumberedLines"; const styles = (theme: Theme) => createStyles({ - paper: { - padding: theme.spacing(3) - }, - closeButton: { - position: "absolute", - right: theme.spacing(1.5), - top: theme.spacing(1.5), - zIndex: 1 - }, - title: { - borderBottomColor: theme.palette.divider, - borderBottomStyle: "solid", - borderBottomWidth: 1, - fontSize: "1.5rem", - lineHeight: 1, - marginBottom: theme.spacing(3), - paddingBottom: theme.spacing(3) - }, header: { lineHeight: 1, marginBottom: theme.spacing(3), marginTop: theme.spacing(3) + }, + log: { + backgroundColor: fade(theme.palette.primary.main, 0.04), + borderLeftColor: theme.palette.primary.main, + borderLeftStyle: "solid", + borderLeftWidth: 2, + padding: theme.spacing(2) } }); interface State { - result: { [pid: string]: string[] } | null; + result: LogsResponse | null; error: string | null; } -class Component extends React.Component< +class Logs extends React.Component< WithStyles & RouteComponentProps<{ hostname: string; pid: string | undefined }>, State @@ -59,17 +48,8 @@ class Component extends React.Component< try { const { match } = this.props; const { hostname, pid } = match.params; - const url = new URL( - "/api/logs", - process.env.NODE_ENV === "development" - ? "http://localhost:8080" - : window.location.origin - ); - url.searchParams.set("hostname", hostname); - url.searchParams.set("pid", pid || ""); - const response = await fetch(url.toString()); - const json = await response.json(); - this.setState({ result: json.result, error: null }); + const result = await getLogs(hostname, pid); + this.setState({ result, error: null }); } catch (error) { this.setState({ result: null, error: error.toString() }); } @@ -82,18 +62,7 @@ class Component extends React.Component< const { hostname } = match.params; return ( - - - - - Logs + {error !== null ? ( {error} ) : result === null ? ( @@ -105,16 +74,18 @@ class Component extends React.Component< {hostname} (PID: {pid}) {lines.length > 0 ? ( - +
+ +
) : ( No logs found. )} )) )} -
+ ); } } -export default withStyles(styles)(Component); +export default withStyles(styles)(Logs); diff --git a/python/ray/dashboard/client/src/store.ts b/python/ray/dashboard/client/src/store.ts new file mode 100644 index 000000000..f8bf3238c --- /dev/null +++ b/python/ray/dashboard/client/src/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from "@reduxjs/toolkit"; +import { dashboardReducer } from "./pages/dashboard/state"; + +export const store = configureStore({ + reducer: { + dashboard: dashboardReducer + }, + devTools: process.env.NODE_ENV === "development" +}); + +export type StoreState = ReturnType; diff --git a/python/ray/reporter.py b/python/ray/reporter.py index 220fc580e..3c3f5848f 100644 --- a/python/ray/reporter.py +++ b/python/ray/reporter.py @@ -49,7 +49,7 @@ def jsonify_asdict(o): def is_worker(cmdline): - return cmdline and cmdline[0].startswith("ray_") + return cmdline and cmdline[0].startswith("ray::") def to_posix_time(dt):