mirror of
https://github.com/wassname/ray.git
synced 2026-07-05 23:12:30 +08:00
[Dashboard] Add remaining features from old dashboard (#6489)
* [Dashboard] Add remaining features from old dashboard * Fix linting errors * Set cluster uptime statistic to N/A * Use proper singular or plural words for workers column * Ignore .js, .jsx, .ts, .tsx files in check-git-clang-format-output.sh * Fix bash quote issue
This commit is contained in:
@@ -3,8 +3,8 @@ import React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { BrowserRouter, Route } from "react-router-dom";
|
||||
import Dashboard from "./pages/dashboard/Dashboard";
|
||||
import Errors from "./pages/errors/Errors";
|
||||
import Logs from "./pages/logs/Logs";
|
||||
import Errors from "./pages/dashboard/dialogs/errors/Errors";
|
||||
import Logs from "./pages/dashboard/dialogs/logs/Logs";
|
||||
import { store } from "./store";
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
@@ -22,6 +22,18 @@ const get = async <T>(path: string, params: { [key: string]: any }) => {
|
||||
return result as T;
|
||||
};
|
||||
|
||||
export interface RayConfigResponse {
|
||||
min_workers: number;
|
||||
max_workers: number;
|
||||
initial_workers: number;
|
||||
autoscaling_mode: string;
|
||||
idle_timeout_minutes: number;
|
||||
head_type: string;
|
||||
worker_type: string;
|
||||
}
|
||||
|
||||
export const getRayConfig = () => get<RayConfigResponse>("/api/ray_config", {});
|
||||
|
||||
export interface NodeInfoResponse {
|
||||
clients: Array<{
|
||||
now: number;
|
||||
@@ -44,7 +56,6 @@ export interface NodeInfoResponse {
|
||||
workers: Array<{
|
||||
pid: number;
|
||||
create_time: number;
|
||||
name: string;
|
||||
cmdline: string[];
|
||||
cpu_percent: number;
|
||||
cpu_times: {
|
||||
|
||||
@@ -43,7 +43,7 @@ interface Props {
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
class Component extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
class NumberedLines extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
render() {
|
||||
const { classes, lines } = this.props;
|
||||
return (
|
||||
@@ -66,4 +66,4 @@ class Component extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
export default withStyles(styles)(NumberedLines);
|
||||
|
||||
@@ -19,6 +19,10 @@ const styles = (theme: Theme) =>
|
||||
borderColor: theme.palette.divider,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1
|
||||
},
|
||||
inner: {
|
||||
paddingLeft: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,7 +31,7 @@ interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
class Component extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
class UsageBar extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
render() {
|
||||
const { classes, text } = this.props;
|
||||
|
||||
@@ -55,10 +59,12 @@ class Component extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
// gradient background otherwise.
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div style={{ background: gradient }}>{text}</div>
|
||||
<div className={classes.inner} style={{ background: gradient }}>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Component);
|
||||
export default withStyles(styles)(UsageBar);
|
||||
|
||||
@@ -17,17 +17,16 @@ export const formatUsage = (
|
||||
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);
|
||||
export const formatDuration = (durationInSeconds: number) => {
|
||||
const durationSeconds = Math.floor(durationInSeconds) % 60;
|
||||
const durationMinutes = Math.floor(durationInSeconds / 60) % 60;
|
||||
const durationHours = Math.floor(durationInSeconds / 60 / 60) % 24;
|
||||
const durationDays = Math.floor(durationInSeconds / 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`
|
||||
durationDays ? `${durationDays}d` : "",
|
||||
`${pad(durationHours)}h`,
|
||||
`${pad(durationMinutes)}m`,
|
||||
`${pad(durationSeconds)}s`
|
||||
].join(" ");
|
||||
};
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
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";
|
||||
import NodeInfo from "./node-info/NodeInfo";
|
||||
import RayConfig from "./ray-config/RayConfig";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -20,151 +12,22 @@ const styles = (theme: Theme) =>
|
||||
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)
|
||||
marginTop: theme.spacing(4)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
class Dashboard extends React.Component<WithStyles<typeof styles>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { classes } = this.props;
|
||||
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>
|
||||
)}
|
||||
<NodeInfo />
|
||||
<RayConfig />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Dashboard));
|
||||
export default withStyles(styles)(Dashboard);
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { ErrorsResponse, getErrors } from "../../api";
|
||||
import DialogWithTitle from "../../common/DialogWithTitle";
|
||||
import NumberedLines from "../../common/NumberedLines";
|
||||
import { ErrorsResponse, getErrors } from "../../../../api";
|
||||
import DialogWithTitle from "../../../../common/DialogWithTitle";
|
||||
import NumberedLines from "../../../../common/NumberedLines";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
+3
-3
@@ -5,9 +5,9 @@ import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { getLogs, LogsResponse } from "../../api";
|
||||
import DialogWithTitle from "../../common/DialogWithTitle";
|
||||
import NumberedLines from "../../common/NumberedLines";
|
||||
import { getLogs, LogsResponse } from "../../../../api";
|
||||
import DialogWithTitle from "../../../../common/DialogWithTitle";
|
||||
import NumberedLines from "../../../../common/NumberedLines";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -1,18 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
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")}
|
||||
/>
|
||||
);
|
||||
@@ -1,11 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -1,13 +0,0 @@
|
||||
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,51 @@
|
||||
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 React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { StoreState } from "../../../store";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
marginTop: theme.spacing(2)
|
||||
},
|
||||
lastUpdated: {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: "0.8125rem",
|
||||
textAlign: "center"
|
||||
},
|
||||
error: {
|
||||
color: theme.palette.error.main,
|
||||
fontSize: "0.8125rem",
|
||||
textAlign: "center"
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
lastUpdatedAt: state.dashboard.lastUpdatedAt,
|
||||
error: state.dashboard.error
|
||||
});
|
||||
|
||||
class LastUpdated extends React.Component<
|
||||
WithStyles<typeof styles> & ReturnType<typeof mapStateToProps>
|
||||
> {
|
||||
render() {
|
||||
const { classes, lastUpdatedAt, error } = this.props;
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{lastUpdatedAt !== null && (
|
||||
<Typography className={classes.lastUpdated}>
|
||||
Last updated: {new Date(lastUpdatedAt).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
{error !== null && (
|
||||
<Typography className={classes.error}>{error}</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(LastUpdated));
|
||||
@@ -0,0 +1,167 @@
|
||||
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 { dashboardActions } from "../state";
|
||||
import LastUpdated from "./LastUpdated";
|
||||
import NodeRowGroup from "./NodeRowGroup";
|
||||
import TotalRow from "./TotalRow";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(2),
|
||||
"& > :not(:first-child)": {
|
||||
marginTop: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
table: {
|
||||
marginTop: theme.spacing(1)
|
||||
},
|
||||
cell: {
|
||||
padding: theme.spacing(1),
|
||||
textAlign: "center",
|
||||
"&:last-child": {
|
||||
paddingRight: theme.spacing(1)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
nodeInfo: state.dashboard.nodeInfo
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dashboardActions;
|
||||
|
||||
class NodeInfo extends React.Component<
|
||||
WithStyles<typeof styles> &
|
||||
ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps
|
||||
> {
|
||||
refreshNodeInfo = async () => {
|
||||
try {
|
||||
const nodeInfo = await getNodeInfo();
|
||||
this.props.setNodeInfo(nodeInfo);
|
||||
this.props.setError(null);
|
||||
} catch (error) {
|
||||
this.props.setError(error.toString());
|
||||
} finally {
|
||||
setTimeout(this.refreshNodeInfo, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await this.refreshNodeInfo();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, nodeInfo } = this.props;
|
||||
|
||||
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>
|
||||
<Typography>Node information:</Typography>
|
||||
<Table className={classes.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]}
|
||||
/>
|
||||
))}
|
||||
<TotalRow
|
||||
nodes={nodeInfo.clients}
|
||||
logCounts={logCounts}
|
||||
errorCounts={errorCounts}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<LastUpdated />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(NodeInfo));
|
||||
+9
-5
@@ -7,13 +7,15 @@ 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 { 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 { NodeReceived, WorkerReceived } from "./features/Received";
|
||||
import { NodeSent, WorkerSent } from "./features/Sent";
|
||||
import { NodeUptime, WorkerUptime } from "./features/Uptime";
|
||||
import { NodeWorkers, WorkerWorkers } from "./features/Workers";
|
||||
|
||||
@@ -80,6 +82,8 @@ class NodeRowGroup extends React.Component<
|
||||
{ NodeFeature: NodeCPU, WorkerFeature: WorkerCPU },
|
||||
{ NodeFeature: NodeRAM, WorkerFeature: WorkerRAM },
|
||||
{ NodeFeature: NodeDisk, WorkerFeature: WorkerDisk },
|
||||
{ NodeFeature: NodeSent, WorkerFeature: WorkerSent },
|
||||
{ NodeFeature: NodeReceived, WorkerFeature: WorkerReceived },
|
||||
{
|
||||
NodeFeature: makeNodeLogs(logCounts),
|
||||
WorkerFeature: makeWorkerLogs(logCounts)
|
||||
@@ -103,8 +107,8 @@ class NodeRowGroup extends React.Component<
|
||||
<RemoveIcon className={classes.expandCollapseIcon} />
|
||||
)}
|
||||
</TableCell>
|
||||
{features.map(({ NodeFeature }) => (
|
||||
<TableCell className={classes.cell}>
|
||||
{features.map(({ NodeFeature }, index) => (
|
||||
<TableCell className={classes.cell} key={index}>
|
||||
<NodeFeature node={node} />
|
||||
</TableCell>
|
||||
))}
|
||||
@@ -113,8 +117,8 @@ class NodeRowGroup extends React.Component<
|
||||
node.workers.map((worker, index: number) => (
|
||||
<TableRow hover key={index}>
|
||||
<TableCell className={classes.cell} />
|
||||
{features.map(({ WorkerFeature }) => (
|
||||
<TableCell className={classes.cell}>
|
||||
{features.map(({ WorkerFeature }, index) => (
|
||||
<TableCell className={classes.cell} key={index}>
|
||||
<WorkerFeature node={node} worker={worker} />
|
||||
</TableCell>
|
||||
))}
|
||||
@@ -0,0 +1,87 @@
|
||||
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 LayersIcon from "@material-ui/icons/Layers";
|
||||
import React from "react";
|
||||
import { NodeInfoResponse } from "../../../api";
|
||||
import { ClusterCPU } from "./features/CPU";
|
||||
import { ClusterDisk } from "./features/Disk";
|
||||
import { makeClusterErrors } from "./features/Errors";
|
||||
import { ClusterHost } from "./features/Host";
|
||||
import { makeClusterLogs } from "./features/Logs";
|
||||
import { ClusterRAM } from "./features/RAM";
|
||||
import { ClusterReceived } from "./features/Received";
|
||||
import { ClusterSent } from "./features/Sent";
|
||||
import { ClusterUptime } from "./features/Uptime";
|
||||
import { ClusterWorkers } from "./features/Workers";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
cell: {
|
||||
borderTopColor: theme.palette.divider,
|
||||
borderTopStyle: "solid",
|
||||
borderTopWidth: 2,
|
||||
padding: theme.spacing(1),
|
||||
textAlign: "center",
|
||||
"&:last-child": {
|
||||
paddingRight: theme.spacing(1)
|
||||
}
|
||||
},
|
||||
totalIcon: {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: "1.5em",
|
||||
verticalAlign: "middle"
|
||||
}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
nodes: NodeInfoResponse["clients"];
|
||||
logCounts: {
|
||||
[ip: string]: {
|
||||
perWorker: { [pid: string]: number };
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
errorCounts: {
|
||||
[ip: string]: {
|
||||
perWorker: { [pid: string]: number };
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class TotalRow extends React.Component<Props & WithStyles<typeof styles>> {
|
||||
render() {
|
||||
const { classes, nodes, logCounts, errorCounts } = this.props;
|
||||
|
||||
const features = [
|
||||
{ ClusterFeature: ClusterHost },
|
||||
{ ClusterFeature: ClusterWorkers },
|
||||
{ ClusterFeature: ClusterUptime },
|
||||
{ ClusterFeature: ClusterCPU },
|
||||
{ ClusterFeature: ClusterRAM },
|
||||
{ ClusterFeature: ClusterDisk },
|
||||
{ ClusterFeature: ClusterSent },
|
||||
{ ClusterFeature: ClusterReceived },
|
||||
{ ClusterFeature: makeClusterLogs(logCounts) },
|
||||
{ ClusterFeature: makeClusterErrors(errorCounts) }
|
||||
];
|
||||
|
||||
return (
|
||||
<TableRow hover>
|
||||
<TableCell className={classes.cell}>
|
||||
<LayersIcon className={classes.totalIcon} />
|
||||
</TableCell>
|
||||
{features.map(({ ClusterFeature }, index) => (
|
||||
<TableCell className={classes.cell} key={index}>
|
||||
<ClusterFeature nodes={nodes} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(TotalRow);
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import UsageBar from "../../../../common/UsageBar";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
const getWeightedAverage = (
|
||||
input: {
|
||||
weight: number;
|
||||
value: number;
|
||||
}[]
|
||||
) => {
|
||||
if (input.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let totalWeightTimesValue = 0;
|
||||
let totalWeight = 0;
|
||||
for (const { weight, value } of input) {
|
||||
totalWeightTimesValue += weight * value;
|
||||
totalWeight += weight;
|
||||
}
|
||||
return totalWeightTimesValue / totalWeight;
|
||||
};
|
||||
|
||||
export const ClusterCPU: ClusterFeatureComponent = ({ nodes }) => {
|
||||
const cpuWeightedAverage = getWeightedAverage(
|
||||
nodes.map(node => ({ weight: node.cpus[0], value: node.cpu }))
|
||||
);
|
||||
return (
|
||||
<div style={{ minWidth: 60 }}>
|
||||
<UsageBar
|
||||
percent={cpuWeightedAverage}
|
||||
text={`${cpuWeightedAverage.toFixed(1)}%`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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,37 @@
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { formatUsage } from "../../../../common/formatUtils";
|
||||
import UsageBar from "../../../../common/UsageBar";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const ClusterDisk: ClusterFeatureComponent = ({ nodes }) => {
|
||||
let used = 0;
|
||||
let total = 0;
|
||||
for (const node of nodes) {
|
||||
used += node.disk["/"].used;
|
||||
total += node.disk["/"].total;
|
||||
}
|
||||
return (
|
||||
<UsageBar
|
||||
percent={(100 * used) / total}
|
||||
text={formatUsage(used, total, "gibibyte")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeDisk: NodeFeatureComponent = ({ node }) => (
|
||||
<UsageBar
|
||||
percent={(100 * node.disk["/"].used) / node.disk["/"].total}
|
||||
text={formatUsage(node.disk["/"].used, node.disk["/"].total, "gibibyte")}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WorkerDisk: WorkerFeatureComponent = () => (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
N/A
|
||||
</Typography>
|
||||
);
|
||||
+31
-1
@@ -2,7 +2,37 @@ 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";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const makeClusterErrors = (errorCounts: {
|
||||
[ip: string]: {
|
||||
perWorker: {
|
||||
[pid: string]: number;
|
||||
};
|
||||
total: number;
|
||||
};
|
||||
}): ClusterFeatureComponent => ({ nodes }) => {
|
||||
let totalErrorCount = 0;
|
||||
for (const node of nodes) {
|
||||
if (node.ip in errorCounts) {
|
||||
totalErrorCount += errorCounts[node.ip].total;
|
||||
}
|
||||
}
|
||||
return totalErrorCount === 0 ? (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
No errors
|
||||
</Typography>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{totalErrorCount.toLocaleString()}{" "}
|
||||
{totalErrorCount === 1 ? "error" : "errors"}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const makeNodeErrors = (errorCounts: {
|
||||
perWorker: { [pid: string]: number };
|
||||
+12
-1
@@ -1,5 +1,16 @@
|
||||
import React from "react";
|
||||
import { NodeFeatureComponent, WorkerFeatureComponent } from "./types";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const ClusterHost: ClusterFeatureComponent = ({ nodes }) => (
|
||||
<React.Fragment>
|
||||
Totals ({nodes.length.toLocaleString()}{" "}
|
||||
{nodes.length === 1 ? "host" : "hosts"})
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
export const NodeHost: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>
|
||||
+30
-1
@@ -2,7 +2,36 @@ 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";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const makeClusterLogs = (logCounts: {
|
||||
[ip: string]: {
|
||||
perWorker: {
|
||||
[pid: string]: number;
|
||||
};
|
||||
total: number;
|
||||
};
|
||||
}): ClusterFeatureComponent => ({ nodes }) => {
|
||||
let totalLogCount = 0;
|
||||
for (const node of nodes) {
|
||||
if (node.ip in logCounts) {
|
||||
totalLogCount += logCounts[node.ip].total;
|
||||
}
|
||||
}
|
||||
return totalLogCount === 0 ? (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
No logs
|
||||
</Typography>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{totalLogCount.toLocaleString()} {totalLogCount === 1 ? "line" : "lines"}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const makeNodeLogs = (logCounts: {
|
||||
perWorker: { [pid: string]: number };
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { formatByteAmount, formatUsage } from "../../../../common/formatUtils";
|
||||
import UsageBar from "../../../../common/UsageBar";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const ClusterRAM: ClusterFeatureComponent = ({ nodes }) => {
|
||||
let used = 0;
|
||||
let total = 0;
|
||||
for (const node of nodes) {
|
||||
used += node.mem[0] - node.mem[1];
|
||||
total += node.mem[0];
|
||||
}
|
||||
return (
|
||||
<UsageBar
|
||||
percent={(100 * used) / total}
|
||||
text={formatUsage(used, total, "gibibyte")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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,30 @@
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { formatByteAmount } from "../../../../common/formatUtils";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const ClusterReceived: ClusterFeatureComponent = ({ nodes }) => {
|
||||
let totalReceived = 0;
|
||||
for (const node of nodes) {
|
||||
totalReceived += node.net[1];
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{formatByteAmount(totalReceived, "mebibyte")}/s
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeReceived: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>{formatByteAmount(node.net[1], "mebibyte")}/s</React.Fragment>
|
||||
);
|
||||
|
||||
export const WorkerReceived: WorkerFeatureComponent = () => (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
N/A
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { formatByteAmount } from "../../../../common/formatUtils";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const ClusterSent: ClusterFeatureComponent = ({ nodes }) => {
|
||||
let totalSent = 0;
|
||||
for (const node of nodes) {
|
||||
totalSent += node.net[0];
|
||||
}
|
||||
return (
|
||||
<React.Fragment>{formatByteAmount(totalSent, "mebibyte")}/s</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeSent: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>{formatByteAmount(node.net[0], "mebibyte")}/s</React.Fragment>
|
||||
);
|
||||
|
||||
export const WorkerSent: WorkerFeatureComponent = () => (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
N/A
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { formatDuration } from "../../../../common/formatUtils";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
const getUptime = (bootTime: number) => Date.now() / 1000 - bootTime;
|
||||
|
||||
export const ClusterUptime: ClusterFeatureComponent = ({ nodes }) => (
|
||||
<Typography color="textSecondary" component="span" variant="inherit">
|
||||
N/A
|
||||
</Typography>
|
||||
);
|
||||
|
||||
export const NodeUptime: NodeFeatureComponent = ({ node }) => (
|
||||
<React.Fragment>{formatDuration(getUptime(node.boot_time))}</React.Fragment>
|
||||
);
|
||||
|
||||
export const WorkerUptime: WorkerFeatureComponent = ({ worker }) => (
|
||||
<React.Fragment>
|
||||
{formatDuration(getUptime(worker.create_time))}
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ClusterFeatureComponent,
|
||||
NodeFeatureComponent,
|
||||
WorkerFeatureComponent
|
||||
} from "./types";
|
||||
|
||||
export const ClusterWorkers: ClusterFeatureComponent = ({ nodes }) => {
|
||||
let totalWorkers = 0;
|
||||
let totalCpus = 0;
|
||||
for (const node of nodes) {
|
||||
totalWorkers += node.workers.length;
|
||||
totalCpus += node.cpus[0];
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{totalWorkers.toLocaleString()}{" "}
|
||||
{totalWorkers === 1 ? "worker" : "workers"} / {totalCpus.toLocaleString()}{" "}
|
||||
{totalCpus === 1 ? "core" : "cores"}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeWorkers: NodeFeatureComponent = ({ node }) => {
|
||||
const workers = node.workers.length;
|
||||
const cpus = node.cpus[0];
|
||||
return (
|
||||
<React.Fragment>
|
||||
{workers.toLocaleString()} {workers === 1 ? "worker" : "workers"} /{" "}
|
||||
{cpus.toLocaleString()} {cpus === 1 ? "core" : "cores"}
|
||||
</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>
|
||||
);
|
||||
+5
-1
@@ -1,13 +1,17 @@
|
||||
import React from "react";
|
||||
import { NodeInfoResponse } from "../../../api";
|
||||
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 ClusterFeatureData = { nodes: Node[] };
|
||||
type NodeFeatureData = { node: Node };
|
||||
type WorkerFeatureData = { node: Node; worker: Worker };
|
||||
|
||||
export type ClusterFeatureComponent = (
|
||||
data: ClusterFeatureData
|
||||
) => React.ReactElement;
|
||||
export type NodeFeatureComponent = (
|
||||
data: NodeFeatureData
|
||||
) => React.ReactElement;
|
||||
@@ -0,0 +1,132 @@
|
||||
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 classNames from "classnames";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { getRayConfig } from "../../../api";
|
||||
import { StoreState } from "../../../store";
|
||||
import { dashboardActions } from "../state";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
table: {
|
||||
marginTop: theme.spacing(1),
|
||||
width: "auto"
|
||||
},
|
||||
cell: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3),
|
||||
textAlign: "center",
|
||||
"&:last-child": {
|
||||
paddingRight: theme.spacing(3)
|
||||
}
|
||||
},
|
||||
key: {
|
||||
color: theme.palette.text.secondary
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
rayConfig: state.dashboard.rayConfig
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dashboardActions;
|
||||
|
||||
class RayConfig extends React.Component<
|
||||
WithStyles<typeof styles> &
|
||||
ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps
|
||||
> {
|
||||
refreshRayConfig = async () => {
|
||||
try {
|
||||
const rayConfig = await getRayConfig();
|
||||
this.props.setRayConfig(rayConfig);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setTimeout(this.refreshRayConfig, 10 * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await this.refreshRayConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, rayConfig } = this.props;
|
||||
|
||||
if (rayConfig === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formattedRayConfig = [
|
||||
{
|
||||
key: "Autoscaling mode",
|
||||
value: rayConfig.autoscaling_mode
|
||||
},
|
||||
{
|
||||
key: "Head node type",
|
||||
value: rayConfig.head_type
|
||||
},
|
||||
{
|
||||
key: "Worker node type",
|
||||
value: rayConfig.worker_type
|
||||
},
|
||||
{
|
||||
key: "Min worker nodes",
|
||||
value: rayConfig.min_workers
|
||||
},
|
||||
{
|
||||
key: "Initial worker nodes",
|
||||
value: rayConfig.initial_workers
|
||||
},
|
||||
{
|
||||
key: "Max worker nodes",
|
||||
value: rayConfig.max_workers
|
||||
},
|
||||
{
|
||||
key: "Idle timeout",
|
||||
value: `${rayConfig.idle_timeout_minutes} ${
|
||||
rayConfig.idle_timeout_minutes === 1 ? "minute" : "minutes"
|
||||
}`
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography>Ray cluster configuration:</Typography>
|
||||
<Table className={classes.table}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cell}>Setting</TableCell>
|
||||
<TableCell className={classes.cell}>Value</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{formattedRayConfig.map(({ key, value }, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className={classNames(classes.cell, classes.key)}>
|
||||
{key}
|
||||
</TableCell>
|
||||
<TableCell className={classes.cell}>{value}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(RayConfig));
|
||||
@@ -1,15 +1,17 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { NodeInfoResponse } from "../../api";
|
||||
import { NodeInfoResponse, RayConfigResponse } from "../../api";
|
||||
|
||||
const name = "dashboard";
|
||||
|
||||
interface State {
|
||||
rayConfig: RayConfigResponse | null;
|
||||
nodeInfo: NodeInfoResponse | null;
|
||||
lastUpdatedAt: number | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
rayConfig: null,
|
||||
nodeInfo: null,
|
||||
lastUpdatedAt: null,
|
||||
error: null
|
||||
@@ -19,11 +21,14 @@ const slice = createSlice({
|
||||
name,
|
||||
initialState,
|
||||
reducers: {
|
||||
setRayConfig: (state, action: PayloadAction<RayConfigResponse>) => {
|
||||
state.rayConfig = action.payload;
|
||||
},
|
||||
setNodeInfo: (state, action: PayloadAction<NodeInfoResponse>) => {
|
||||
state.nodeInfo = action.payload;
|
||||
state.lastUpdatedAt = Date.now();
|
||||
},
|
||||
setError: (state, action: PayloadAction<string>) => {
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import time
|
||||
import traceback
|
||||
import yaml
|
||||
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from typing import Dict
|
||||
@@ -113,8 +111,8 @@ class Dashboard(object):
|
||||
|
||||
async def ray_config(_) -> aiohttp.web.Response:
|
||||
try:
|
||||
with open(
|
||||
Path("~/ray_bootstrap_config.yaml").expanduser()) as f:
|
||||
config_path = os.path.expanduser("~/ray_bootstrap_config.yaml")
|
||||
with open(config_path) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
except Exception:
|
||||
return await json_response(error="No config")
|
||||
@@ -213,51 +211,6 @@ class NodeStats(threading.Thread):
|
||||
|
||||
super().__init__()
|
||||
|
||||
def calculate_totals(self) -> Dict:
|
||||
total_boot_time = 0
|
||||
total_cpus = 0
|
||||
total_workers = 0
|
||||
total_load = [0.0, 0.0, 0.0]
|
||||
total_storage_avail = 0
|
||||
total_storage_total = 0
|
||||
total_ram_avail = 0
|
||||
total_ram_total = 0
|
||||
total_sent = 0
|
||||
total_recv = 0
|
||||
|
||||
for v in self._node_stats.values():
|
||||
total_boot_time += v["boot_time"]
|
||||
total_cpus += v["cpus"][0]
|
||||
total_workers += len(v["workers"])
|
||||
total_load[0] += v["load_avg"][0][0]
|
||||
total_load[1] += v["load_avg"][0][1]
|
||||
total_load[2] += v["load_avg"][0][2]
|
||||
total_storage_avail += v["disk"]["/"]["free"]
|
||||
total_storage_total += v["disk"]["/"]["total"]
|
||||
total_ram_avail += v["mem"][1]
|
||||
total_ram_total += v["mem"][0]
|
||||
total_sent += v["net"][0]
|
||||
total_recv += v["net"][1]
|
||||
|
||||
return {
|
||||
"boot_time": total_boot_time,
|
||||
"n_workers": total_workers,
|
||||
"n_cores": total_cpus,
|
||||
"m_avail": total_ram_avail,
|
||||
"m_total": total_ram_total,
|
||||
"d_avail": total_storage_avail,
|
||||
"d_total": total_storage_total,
|
||||
"load": total_load,
|
||||
"n_sent": total_sent,
|
||||
"n_recv": total_recv,
|
||||
}
|
||||
|
||||
def calculate_tasks(self) -> Counter:
|
||||
return Counter(
|
||||
(x["name"]
|
||||
for y in (v["workers"] for v in self._node_stats.values())
|
||||
for x in y))
|
||||
|
||||
def calculate_log_counts(self):
|
||||
return {
|
||||
ip: {
|
||||
@@ -296,8 +249,6 @@ class NodeStats(threading.Thread):
|
||||
(v for v in self._node_stats.values()),
|
||||
key=itemgetter("boot_time"))
|
||||
return {
|
||||
"totals": self.calculate_totals(),
|
||||
"tasks": self.calculate_tasks(),
|
||||
"clients": node_stats,
|
||||
"log_counts": self.calculate_log_counts(),
|
||||
"error_counts": self.calculate_error_counts(),
|
||||
|
||||
@@ -112,8 +112,8 @@ class Reporter(object):
|
||||
def get_workers():
|
||||
return [
|
||||
x.as_dict(attrs=[
|
||||
"pid", "create_time", "cpu_percent", "cpu_times", "name",
|
||||
"cmdline", "memory_info", "memory_full_info"
|
||||
"pid", "create_time", "cpu_percent", "cpu_times", "cmdline",
|
||||
"memory_info", "memory_full_info"
|
||||
]) for x in psutil.process_iter(attrs=["cmdline"])
|
||||
if is_worker(x.info["cmdline"])
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user