mirror of
https://github.com/wassname/ray.git
synced 2026-06-28 00:29:38 +08:00
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
This commit is contained in:
+94
-3
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<BrowserRouter>
|
||||
<CssBaseline />
|
||||
<Dashboard />
|
||||
<Route component={Logs} path="/logs/:hostname/:pid?" />
|
||||
<Route component={Errors} path="/errors/:hostname/:pid?" />
|
||||
</BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<CssBaseline />
|
||||
<Dashboard />
|
||||
<Route component={Logs} path="/logs/:hostname/:pid?" />
|
||||
<Route component={Errors} path="/errors/:hostname/:pid?" />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WithStyles<typeof styles>, 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 (
|
||||
<Typography className={classes.root} color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
if (response === null) {
|
||||
return (
|
||||
<Typography className={classes.root} color="textSecondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h5">Ray Dashboard</Typography>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cell} />
|
||||
<TableCell className={classes.cell}>Host</TableCell>
|
||||
<TableCell className={classes.cell}>Workers</TableCell>
|
||||
<TableCell className={classes.cell}>Uptime</TableCell>
|
||||
<TableCell className={classes.cell}>CPU</TableCell>
|
||||
<TableCell className={classes.cell}>RAM</TableCell>
|
||||
<TableCell className={classes.cell}>Disk</TableCell>
|
||||
{/*<TableCell className={classes.cell}>Sent</TableCell>*/}
|
||||
{/*<TableCell className={classes.cell}>Received</TableCell>*/}
|
||||
<TableCell className={classes.cell}>Logs</TableCell>
|
||||
<TableCell className={classes.cell}>Errors</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{result.clients.map(client => {
|
||||
return (
|
||||
<React.Fragment key={client.ip}>
|
||||
<TableRow hover>
|
||||
<TableCell
|
||||
className={classNames(
|
||||
classes.cell,
|
||||
classes.expandCollapseCell
|
||||
)}
|
||||
onClick={this.toggleExpand(client.ip)}
|
||||
>
|
||||
{!expanded[client.ip] ? (
|
||||
<AddIcon className={classes.expandCollapseIcon} />
|
||||
) : (
|
||||
<RemoveIcon className={classes.expandCollapseIcon} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{client.hostname} ({client.ip})
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{client.workers.length}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{formatUptime(client.boot_time)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<div className={classes.cpuUsage}>
|
||||
<UsageBar
|
||||
percent={client.cpu}
|
||||
text={`${client.cpu.toFixed(1)}%`}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={
|
||||
(100 * (client.mem[0] - client.mem[1])) /
|
||||
client.mem[0]
|
||||
}
|
||||
text={formatUsage(
|
||||
client.mem[0] - client.mem[1],
|
||||
client.mem[0],
|
||||
"gibibyte"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={
|
||||
(100 * client.disk["/"].used) / client.disk["/"].total
|
||||
}
|
||||
text={formatUsage(
|
||||
client.disk["/"].used,
|
||||
client.disk["/"].total,
|
||||
"gibibyte"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
{/*<TableCell className={classes.cell}>{(client.net[0] / Math.pow(1024, 2)).toFixed(3)} MiB/s</TableCell>*/}
|
||||
{/*<TableCell className={classes.cell}>{(client.net[1] / Math.pow(1024, 2)).toFixed(3)} MiB/s</TableCell>*/}
|
||||
<TableCell className={classes.cell}>
|
||||
{logCounts[client.ip].total === 0 ? (
|
||||
<span className={classes.secondary}>No logs</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/logs/${client.hostname}`}
|
||||
>
|
||||
View all logs (
|
||||
{logCounts[client.ip].total.toLocaleString()}{" "}
|
||||
{logCounts[client.ip].total === 1 ? "line" : "lines"})
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{errorCounts[client.ip].total === 0 ? (
|
||||
<span className={classes.secondary}>No errors</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/errors/${client.hostname}`}
|
||||
>
|
||||
View all errors (
|
||||
{errorCounts[client.ip].total.toLocaleString()})
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{expanded[client.ip] &&
|
||||
client.workers.map((worker, index: number) => (
|
||||
<TableRow hover key={index}>
|
||||
<TableCell className={classes.cell} />
|
||||
<TableCell className={classes.cell}>
|
||||
{worker.cmdline[0].split(":", 2)[0]} (PID:{" "}
|
||||
{worker.pid})
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{worker.cmdline[0].split(":", 2)[1] || (
|
||||
<span className={classes.secondary}>Idle</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{formatUptime(worker.create_time)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={worker.cpu_percent}
|
||||
text={`${worker.cpu_percent.toFixed(1)}%`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<UsageBar
|
||||
percent={
|
||||
(100 * worker.memory_info.rss) / client.mem[0]
|
||||
}
|
||||
text={formatByteAmount(
|
||||
worker.memory_info.rss,
|
||||
"mebibyte"
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
<span className={classes.secondary}>
|
||||
Not available
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{logCounts[client.ip].perWorker[worker.pid] === 0 ? (
|
||||
<span className={classes.secondary}>No logs</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/logs/${client.hostname}/${worker.pid}`}
|
||||
>
|
||||
View log (
|
||||
{logCounts[client.ip].perWorker[
|
||||
worker.pid
|
||||
].toLocaleString()}{" "}
|
||||
{logCounts[client.ip].perWorker[worker.pid] === 1
|
||||
? "line"
|
||||
: "lines"}
|
||||
)
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>
|
||||
{errorCounts[client.ip].perWorker[worker.pid] ===
|
||||
0 ? (
|
||||
<span className={classes.secondary}>No errors</span>
|
||||
) : (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/errors/${client.hostname}/${worker.pid}`}
|
||||
>
|
||||
View errors (
|
||||
{errorCounts[client.ip].perWorker[
|
||||
worker.pid
|
||||
].toLocaleString()}
|
||||
)
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Typography align="center">
|
||||
Last updated: {new Date(timestamp * 1000).toLocaleString()}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
@@ -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 <T>(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<NodeInfoResponse>("/api/node_info", {});
|
||||
|
||||
export interface ErrorsResponse {
|
||||
[pid: string]: Array<{
|
||||
message: string;
|
||||
timestamp: number;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const getErrors = (hostname: string, pid: string | undefined) =>
|
||||
get<ErrorsResponse>("/api/errors", { hostname, pid: pid || "" });
|
||||
|
||||
export interface LogsResponse {
|
||||
[pid: string]: string[];
|
||||
}
|
||||
|
||||
export const getLogs = (hostname: string, pid: string | undefined) =>
|
||||
get<LogsResponse>("/api/logs", { hostname, pid: pid || "" });
|
||||
@@ -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<typeof styles>
|
||||
> {
|
||||
render() {
|
||||
const { classes, handleClose, title } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
classes={{ paper: classes.paper }}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
onClose={handleClose}
|
||||
open
|
||||
scroll="body"
|
||||
>
|
||||
<IconButton className={classes.closeButton} onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography className={classes.title}>{title}</Typography>
|
||||
{this.props.children}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(DialogWithTitle);
|
||||
@@ -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(" ");
|
||||
};
|
||||
@@ -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<typeof styles> &
|
||||
ReturnType<typeof mapStateToProps> &
|
||||
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 (
|
||||
<Typography className={classes.root} color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
if (nodeInfo === null) {
|
||||
return (
|
||||
<Typography className={classes.root} color="textSecondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h5">Ray Dashboard</Typography>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cell} />
|
||||
<TableCell className={classes.cell}>Host</TableCell>
|
||||
<TableCell className={classes.cell}>Workers</TableCell>
|
||||
<TableCell className={classes.cell}>Uptime</TableCell>
|
||||
<TableCell className={classes.cell}>CPU</TableCell>
|
||||
<TableCell className={classes.cell}>RAM</TableCell>
|
||||
<TableCell className={classes.cell}>Disk</TableCell>
|
||||
{/*<TableCell className={classes.cell}>Sent</TableCell>*/}
|
||||
{/*<TableCell className={classes.cell}>Received</TableCell>*/}
|
||||
<TableCell className={classes.cell}>Logs</TableCell>
|
||||
<TableCell className={classes.cell}>Errors</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{nodeInfo.clients.map(client => (
|
||||
<NodeRowGroup
|
||||
key={client.ip}
|
||||
node={client}
|
||||
logCounts={logCounts[client.ip]}
|
||||
errorCounts={errorCounts[client.ip]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{lastUpdatedAt !== null && (
|
||||
<Typography align="center">
|
||||
Last updated: {new Date(lastUpdatedAt).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Dashboard));
|
||||
@@ -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> = T extends Array<infer U> ? U : never;
|
||||
type Node = ArrayType<NodeInfoResponse["clients"]>;
|
||||
|
||||
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<typeof styles>,
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<TableRow hover>
|
||||
<TableCell
|
||||
className={classNames(classes.cell, classes.expandCollapseCell)}
|
||||
onClick={this.toggleExpand}
|
||||
>
|
||||
{!expanded ? (
|
||||
<AddIcon className={classes.expandCollapseIcon} />
|
||||
) : (
|
||||
<RemoveIcon className={classes.expandCollapseIcon} />
|
||||
)}
|
||||
</TableCell>
|
||||
{features.map(({ NodeFeature }) => (
|
||||
<TableCell className={classes.cell}>
|
||||
<NodeFeature node={node} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
{expanded &&
|
||||
node.workers.map((worker, index: number) => (
|
||||
<TableRow hover key={index}>
|
||||
<TableCell className={classes.cell} />
|
||||
{features.map(({ WorkerFeature }) => (
|
||||
<TableCell className={classes.cell}>
|
||||
<WorkerFeature node={node} worker={worker} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(NodeRowGroup);
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import UsageBar from "../../../common/UsageBar";
|
||||
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
|
||||
|
||||
export const NodeCPU: NodeFeatureComponent = ({ node }) => (
|
||||
<div style={{ minWidth: 60 }}>
|
||||
<UsageBar percent={node.cpu} text={`${node.cpu.toFixed(1)}%`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const WorkerCPU: WorkerFeatureComponent = ({ worker }) => (
|
||||
<div style={{ minWidth: 60 }}>
|
||||
<UsageBar
|
||||
percent={worker.cpu_percent}
|
||||
text={`${worker.cpu_percent.toFixed(1)}%`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -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 }) => (
|
||||
<React.Fragment>
|
||||
<UsageBar
|
||||
percent={(100 * node.disk["/"].used) / node.disk["/"].total}
|
||||
text={formatUsage(node.disk["/"].used, node.disk["/"].total, "gibibyte")}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
export const WorkerDisk: WorkerFeatureComponent = () => (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
Not available
|
||||
</Typography>
|
||||
);
|
||||
@@ -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 ? (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
No errors
|
||||
</Typography>
|
||||
) : (
|
||||
<Link component={RouterLink} to={`/errors/${node.hostname}`}>
|
||||
View all errors ({errorCounts.total.toLocaleString()})
|
||||
</Link>
|
||||
);
|
||||
|
||||
export const makeWorkerErrors = (errorCounts: {
|
||||
perWorker: { [pid: string]: number };
|
||||
total: number;
|
||||
}): WorkerFeatureComponent => ({ node, worker }) =>
|
||||
errorCounts.perWorker[worker.pid] === 0 ? (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
No errors
|
||||
</Typography>
|
||||
) : (
|
||||
<Link component={RouterLink} to={`/errors/${node.hostname}/${worker.pid}`}>
|
||||
View errors ({errorCounts.perWorker[worker.pid].toLocaleString()})
|
||||
</Link>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
|
||||
|
||||
export const NodeHost: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>
|
||||
{node.hostname} ({node.ip})
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
// 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 }) => (
|
||||
<React.Fragment>
|
||||
{worker.cmdline[0].split("::", 2)[0]} (PID: {worker.pid})
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -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 ? (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
No logs
|
||||
</Typography>
|
||||
) : (
|
||||
<Link component={RouterLink} to={`/logs/${node.hostname}`}>
|
||||
View all logs ({logCounts.total.toLocaleString()}{" "}
|
||||
{logCounts.total === 1 ? "line" : "lines"})
|
||||
</Link>
|
||||
);
|
||||
|
||||
export const makeWorkerLogs = (logCounts: {
|
||||
perWorker: { [pid: string]: number };
|
||||
total: number;
|
||||
}): WorkerFeatureComponent => ({ node, worker }) =>
|
||||
logCounts.perWorker[worker.pid] === 0 ? (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
No logs
|
||||
</Typography>
|
||||
) : (
|
||||
<Link component={RouterLink} to={`/logs/${node.hostname}/${worker.pid}`}>
|
||||
View log ({logCounts.perWorker[worker.pid].toLocaleString()}{" "}
|
||||
{logCounts.perWorker[worker.pid] === 1 ? "line" : "lines"})
|
||||
</Link>
|
||||
);
|
||||
@@ -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 }) => (
|
||||
<UsageBar
|
||||
percent={(100 * (node.mem[0] - node.mem[1])) / node.mem[0]}
|
||||
text={formatUsage(node.mem[0] - node.mem[1], node.mem[0], "gibibyte")}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WorkerRAM: WorkerFeatureComponent = ({ node, worker }) => (
|
||||
<UsageBar
|
||||
percent={(100 * worker.memory_info.rss) / node.mem[0]}
|
||||
text={formatByteAmount(worker.memory_info.rss, "mebibyte")}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { formatUptime } from "../../../common/formatUtils";
|
||||
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
|
||||
|
||||
export const NodeUptime: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>{formatUptime(node.boot_time)}</React.Fragment>
|
||||
);
|
||||
|
||||
export const WorkerUptime: WorkerFeatureComponent = ({ worker }) => (
|
||||
<React.Fragment>{formatUptime(worker.create_time)}</React.Fragment>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
|
||||
|
||||
export const NodeWorkers: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>{node.workers.length}</React.Fragment>
|
||||
);
|
||||
|
||||
// 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 }) => (
|
||||
<React.Fragment>{worker.cmdline[0].split("::", 2)[1]}</React.Fragment>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { NodeInfoResponse } from "../../../api";
|
||||
|
||||
type ArrayType<T> = T extends Array<infer U> ? U : never;
|
||||
type Node = ArrayType<NodeInfoResponse["clients"]>;
|
||||
type Worker = ArrayType<Node["workers"]>;
|
||||
|
||||
type NodeFeatureData = { node: Node };
|
||||
type WorkerFeatureData = { node: Node; worker: Worker };
|
||||
|
||||
export type NodeFeatureComponent = (
|
||||
data: NodeFeatureData
|
||||
) => React.ReactElement;
|
||||
export type WorkerFeatureComponent = (
|
||||
data: WorkerFeatureData
|
||||
) => React.ReactElement;
|
||||
@@ -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<NodeInfoResponse>) => {
|
||||
state.nodeInfo = action.payload;
|
||||
state.lastUpdatedAt = Date.now();
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
state.error = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const dashboardActions = slice.actions;
|
||||
export const dashboardReducer = slice.reducer;
|
||||
+12
-57
@@ -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<typeof styles> &
|
||||
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 (
|
||||
<Dialog
|
||||
classes={{ paper: classes.paper }}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
onClose={this.handleClose}
|
||||
open
|
||||
scroll="body"
|
||||
>
|
||||
<IconButton className={classes.closeButton} onClick={this.handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography className={classes.title}>Errors</Typography>
|
||||
<DialogWithTitle handleClose={this.handleClose} title="Errors">
|
||||
{error !== null ? (
|
||||
<Typography color="error">{error}</Typography>
|
||||
) : result === null ? (
|
||||
@@ -138,9 +93,9 @@ class Component extends React.Component<
|
||||
</React.Fragment>
|
||||
))
|
||||
)}
|
||||
</Dialog>
|
||||
</DialogWithTitle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
export default withStyles(styles)(Errors);
|
||||
+21
-50
@@ -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<typeof styles> &
|
||||
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 (
|
||||
<Dialog
|
||||
classes={{ paper: classes.paper }}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
onClose={this.handleClose}
|
||||
open
|
||||
scroll="body"
|
||||
>
|
||||
<IconButton className={classes.closeButton} onClick={this.handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography className={classes.title}>Logs</Typography>
|
||||
<DialogWithTitle handleClose={this.handleClose} title="Logs">
|
||||
{error !== null ? (
|
||||
<Typography color="error">{error}</Typography>
|
||||
) : result === null ? (
|
||||
@@ -105,16 +74,18 @@ class Component extends React.Component<
|
||||
{hostname} (PID: {pid})
|
||||
</Typography>
|
||||
{lines.length > 0 ? (
|
||||
<NumberedLines lines={lines} />
|
||||
<div className={classes.log}>
|
||||
<NumberedLines lines={lines} />
|
||||
</div>
|
||||
) : (
|
||||
<Typography color="textSecondary">No logs found.</Typography>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))
|
||||
)}
|
||||
</Dialog>
|
||||
</DialogWithTitle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
export default withStyles(styles)(Logs);
|
||||
@@ -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<typeof store.getState>;
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user