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:
Mitchell Stern
2019-12-02 14:05:40 -05:00
committed by Simon Mo
parent 69dd5c9319
commit 43d20fff62
25 changed files with 857 additions and 568 deletions
+94 -3
View File
@@ -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",
+3
View File
@@ -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",
+13 -9
View File
@@ -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);
+95
View File
@@ -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;
@@ -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);
@@ -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);
+11
View File
@@ -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>;
+1 -1
View File
@@ -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):