Node Info Functional Components (#9073)

This commit is contained in:
Max Fitton
2020-06-23 13:11:32 -07:00
committed by GitHub
parent f77c638d6d
commit 7904235517
3 changed files with 299 additions and 345 deletions
@@ -1,5 +1,6 @@
import {
createStyles,
makeStyles,
Table,
TableBody,
TableCell,
@@ -7,15 +8,12 @@ import {
TableRow,
Theme,
Typography,
withStyles,
WithStyles,
} from "@material-ui/core";
import React from "react";
import { connect } from "react-redux";
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { RayletInfoResponse } from "../../../api";
import { sum } from "../../../common/util";
import { StoreState } from "../../../store";
import {} from "../../../common/tableUtils";
import Errors from "./dialogs/errors/Errors";
import Logs from "./dialogs/logs/Logs";
import NodeRowGroup from "./NodeRowGroup";
@@ -39,7 +37,7 @@ const clusterWorkerPids = (
return nodeMap;
};
const styles = (theme: Theme) =>
const useNodeInfoStyles = makeStyles((theme: Theme) =>
createStyles({
table: {
marginTop: theme.spacing(1),
@@ -51,187 +49,165 @@ const styles = (theme: Theme) =>
paddingRight: theme.spacing(1),
},
},
});
}),
);
const mapStateToProps = (state: StoreState) => ({
const nodeInfoSelector = (state: StoreState) => ({
nodeInfo: state.dashboard.nodeInfo,
rayletInfo: state.dashboard.rayletInfo,
});
type State = {
logDialog: { hostname: string; pid: number | null } | null;
errorDialog: { hostname: string; pid: number | null } | null;
type dialogState = {
hostname: string;
pid: number | null;
} | null;
const NodeInfo: React.FC<{}> = () => {
const [logDialog, setLogDialog] = useState<dialogState>(null);
const [errorDialog, setErrorDialog] = useState<dialogState>(null);
const classes = useNodeInfoStyles();
const { nodeInfo, rayletInfo } = useSelector(nodeInfoSelector);
if (nodeInfo === null || rayletInfo === null) {
return <Typography color="textSecondary">Loading...</Typography>;
}
const logCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
const errorCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
// We fetch data about which process IDs are registered with
// the cluster's raylet for each node. We use this to filter
// the worker data contained in the node info data because
// the node info can contain data from more than one cluster
// if more than one cluster is running on a machine.
const clusterWorkerPidsByIp = clusterWorkerPids(rayletInfo);
const clusterTotalWorkers = sum(
Array.from(clusterWorkerPidsByIp.values()).map(
(workerSet) => workerSet.size,
),
);
// Initialize inner structure of the count objects
for (const client of nodeInfo.clients) {
const clusterWorkerPids = clusterWorkerPidsByIp.get(client.ip);
if (!clusterWorkerPids) {
continue;
}
const filteredLogEntries = Object.entries(
nodeInfo.log_counts[client.ip] || {},
).filter(([pid, _]) => clusterWorkerPids.has(pid));
const totalLogEntries = sum(filteredLogEntries.map(([_, count]) => count));
logCounts[client.ip] = {
perWorker: Object.fromEntries(filteredLogEntries),
total: totalLogEntries,
};
const filteredErrEntries = Object.entries(
nodeInfo.error_counts[client.ip] || {},
).filter(([pid, _]) => clusterWorkerPids.has(pid));
const totalErrEntries = sum(filteredErrEntries.map(([_, count]) => count));
errorCounts[client.ip] = {
perWorker: Object.fromEntries(filteredErrEntries),
total: totalErrEntries,
};
}
return (
<React.Fragment>
<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}>GPU</TableCell>
<TableCell className={classes.cell}>GRAM</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) => {
const clusterWorkerPids =
clusterWorkerPidsByIp.get(client.ip) || new Set();
return (
<NodeRowGroup
key={client.ip}
clusterWorkers={client.workers
.filter((worker) =>
clusterWorkerPids.has(worker.pid.toString()),
)
.sort((w1, w2) => {
if (w2.cmdline[0] === "ray::IDLE") {
return -1;
}
if (w1.cmdline[0] === "ray::IDLE") {
return 1;
}
return w1.pid < w2.pid ? -1 : 1;
})}
node={client}
raylet={
client.ip in rayletInfo.nodes
? rayletInfo.nodes[client.ip]
: null
}
logCounts={logCounts[client.ip]}
errorCounts={errorCounts[client.ip]}
setLogDialog={(hostname, pid) =>
setLogDialog({ hostname, pid })
}
setErrorDialog={(hostname, pid) =>
setErrorDialog({ hostname, pid })
}
initialExpanded={nodeInfo.clients.length <= 1}
/>
);
})}
<TotalRow
clusterTotalWorkers={clusterTotalWorkers}
nodes={nodeInfo.clients}
logCounts={logCounts}
errorCounts={errorCounts}
/>
</TableBody>
</Table>
{logDialog !== null && (
<Logs
clearLogDialog={() => setLogDialog(null)}
hostname={logDialog.hostname}
pid={logDialog.pid}
/>
)}
{errorDialog !== null && (
<Errors
clearErrorDialog={() => setErrorDialog(null)}
hostname={errorDialog.hostname}
pid={errorDialog.pid}
/>
)}
</React.Fragment>
);
};
class NodeInfo extends React.Component<
WithStyles<typeof styles> & ReturnType<typeof mapStateToProps>
> {
state: State = {
logDialog: null,
errorDialog: null,
};
setLogDialog = (hostname: string, pid: number | null) => {
this.setState({ logDialog: { hostname, pid } });
};
clearLogDialog = () => {
this.setState({ logDialog: null });
};
setErrorDialog = (hostname: string, pid: number | null) => {
this.setState({ errorDialog: { hostname, pid } });
};
clearErrorDialog = () => {
this.setState({ errorDialog: null });
};
render() {
const { classes, nodeInfo, rayletInfo } = this.props;
const { logDialog, errorDialog } = this.state;
if (nodeInfo === null || rayletInfo === null) {
return <Typography color="textSecondary">Loading...</Typography>;
}
const logCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
const errorCounts: {
[ip: string]: {
perWorker: {
[pid: string]: number;
};
total: number;
};
} = {};
// We fetch data about which process IDs are registered with
// the cluster's raylet for each node. We use this to filter
// the worker data contained in the node info data because
// the node info can contain data from more than one cluster
// if more than one cluster is running on a machine.
const clusterWorkerPidsByIp = clusterWorkerPids(rayletInfo);
const clusterTotalWorkers = sum(
Array.from(clusterWorkerPidsByIp.values()).map(
(workerSet) => workerSet.size,
),
);
// Initialize inner structure of the count objects
for (const client of nodeInfo.clients) {
const clusterWorkerPids = clusterWorkerPidsByIp.get(client.ip);
if (!clusterWorkerPids) {
continue;
}
const filteredLogEntries = Object.entries(
nodeInfo.log_counts[client.ip] || {},
).filter(([pid, _]) => clusterWorkerPids.has(pid));
const totalLogEntries = sum(
filteredLogEntries.map(([_, count]) => count),
);
logCounts[client.ip] = {
perWorker: Object.fromEntries(filteredLogEntries),
total: totalLogEntries,
};
const filteredErrEntries = Object.entries(
nodeInfo.error_counts[client.ip] || {},
).filter(([pid, _]) => clusterWorkerPids.has(pid));
const totalErrEntries = sum(
filteredErrEntries.map(([_, count]) => count),
);
errorCounts[client.ip] = {
perWorker: Object.fromEntries(filteredErrEntries),
total: totalErrEntries,
};
}
return (
<React.Fragment>
<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}>GPU</TableCell>
<TableCell className={classes.cell}>GRAM</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) => {
const clusterWorkerPids =
clusterWorkerPidsByIp.get(client.ip) || new Set();
return (
<NodeRowGroup
key={client.ip}
clusterWorkers={client.workers
.filter((worker) =>
clusterWorkerPids.has(worker.pid.toString()),
)
.sort((w1, w2) => {
if (w2.cmdline[0] === "ray::IDLE") {
return -1;
}
if (w1.cmdline[0] === "ray::IDLE") {
return 1;
}
return w1.pid < w2.pid ? -1 : 1;
})}
node={client}
raylet={
client.ip in rayletInfo.nodes
? rayletInfo.nodes[client.ip]
: null
}
logCounts={logCounts[client.ip]}
errorCounts={errorCounts[client.ip]}
setLogDialog={this.setLogDialog}
setErrorDialog={this.setErrorDialog}
initialExpanded={nodeInfo.clients.length <= 1}
/>
);
})}
<TotalRow
clusterTotalWorkers={clusterTotalWorkers}
nodes={nodeInfo.clients}
logCounts={logCounts}
errorCounts={errorCounts}
/>
</TableBody>
</Table>
{logDialog !== null && (
<Logs
clearLogDialog={this.clearLogDialog}
hostname={logDialog.hostname}
pid={logDialog.pid}
/>
)}
{errorDialog !== null && (
<Errors
clearErrorDialog={this.clearErrorDialog}
hostname={errorDialog.hostname}
pid={errorDialog.pid}
/>
)}
</React.Fragment>
);
}
}
export default connect(mapStateToProps)(withStyles(styles)(NodeInfo));
export default NodeInfo;
@@ -1,15 +1,14 @@
import {
createStyles,
makeStyles,
TableCell,
TableRow,
Theme,
withStyles,
WithStyles,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import RemoveIcon from "@material-ui/icons/Remove";
import classNames from "classnames";
import React from "react";
import React, { useState } from "react";
import {
NodeInfoResponse,
NodeInfoResponseWorker,
@@ -28,7 +27,7 @@ import { NodeSent, WorkerSent } from "./features/Sent";
import { NodeUptime, WorkerUptime } from "./features/Uptime";
import { NodeWorkers, WorkerWorkers } from "./features/Workers";
const styles = (theme: Theme) =>
const useNodeRowGroupStyles = makeStyles((theme: Theme) =>
createStyles({
cell: {
padding: theme.spacing(1),
@@ -49,12 +48,13 @@ const styles = (theme: Theme) =>
fontFamily: "SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace",
whiteSpace: "pre",
},
});
}),
);
type ArrayType<T> = T extends Array<infer U> ? U : never;
type Node = ArrayType<NodeInfoResponse["clients"]>;
type Props = {
type NodeRowGroupProps = {
node: Node;
clusterWorkers: Array<NodeInfoResponseWorker>;
raylet: RayletInfoResponse["nodes"][keyof RayletInfoResponse["nodes"]] | null;
@@ -71,118 +71,100 @@ type Props = {
initialExpanded: boolean;
};
type State = {
expanded: boolean;
const NodeRowGroup: React.FC<NodeRowGroupProps> = ({
node,
raylet,
clusterWorkers,
logCounts,
errorCounts,
setLogDialog,
setErrorDialog,
initialExpanded,
}) => {
const [expanded, setExpanded] = useState<boolean>(initialExpanded);
const toggleExpand = () => setExpanded(!expanded);
const classes = useNodeRowGroupStyles();
const features = [
{ NodeFeature: NodeHost, WorkerFeature: WorkerHost },
{
NodeFeature: NodeWorkers(clusterWorkers.length),
WorkerFeature: WorkerWorkers,
},
{ NodeFeature: NodeUptime, WorkerFeature: WorkerUptime },
{ NodeFeature: NodeCPU, WorkerFeature: WorkerCPU },
{ NodeFeature: NodeRAM, WorkerFeature: WorkerRAM },
{ NodeFeature: NodeGPU, WorkerFeature: WorkerGPU },
{ NodeFeature: NodeGRAM, WorkerFeature: WorkerGRAM },
{ NodeFeature: NodeDisk, WorkerFeature: WorkerDisk },
{ NodeFeature: NodeSent, WorkerFeature: WorkerSent },
{ NodeFeature: NodeReceived, WorkerFeature: WorkerReceived },
{
NodeFeature: makeNodeLogs(logCounts, setLogDialog),
WorkerFeature: makeWorkerLogs(logCounts, setLogDialog),
},
{
NodeFeature: makeNodeErrors(errorCounts, setErrorDialog),
WorkerFeature: makeWorkerErrors(errorCounts, setErrorDialog),
},
];
return (
<React.Fragment>
<TableRow hover>
<TableCell
className={classNames(classes.cell, classes.expandCollapseCell)}
onClick={toggleExpand}
>
{!expanded ? (
<AddIcon className={classes.expandCollapseIcon} />
) : (
<RemoveIcon className={classes.expandCollapseIcon} />
)}
</TableCell>
{features.map(({ NodeFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<NodeFeature node={node} />
</TableCell>
))}
</TableRow>
{expanded && (
<React.Fragment>
{raylet !== null && raylet.extraInfo !== undefined && (
<TableRow hover>
<TableCell className={classes.cell} />
<TableCell
className={classNames(classes.cell, classes.extraInfo)}
colSpan={features.length}
>
{raylet.extraInfo}
</TableCell>
</TableRow>
)}
{clusterWorkers.map((worker, index: number) => {
const rayletWorker =
raylet?.workersStats.find(
(rayletWorker) => worker.pid === rayletWorker.pid,
) || null;
return (
<TableRow hover key={index}>
<TableCell className={classes.cell} />
{features.map(({ WorkerFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<WorkerFeature
node={node}
worker={worker}
rayletWorker={rayletWorker}
/>
</TableCell>
))}
</TableRow>
);
})}
</React.Fragment>
)}
</React.Fragment>
);
};
class NodeRowGroup extends React.Component<
Props & WithStyles<typeof styles>,
State
> {
state: State = {
expanded: this.props.initialExpanded,
};
toggleExpand = () => {
this.setState((state) => ({
expanded: !state.expanded,
}));
};
render() {
const {
classes,
node,
raylet,
clusterWorkers,
logCounts,
errorCounts,
setLogDialog,
setErrorDialog,
} = this.props;
const { expanded } = this.state;
const features = [
{ NodeFeature: NodeHost, WorkerFeature: WorkerHost },
{
NodeFeature: NodeWorkers(clusterWorkers.length),
WorkerFeature: WorkerWorkers,
},
{ NodeFeature: NodeUptime, WorkerFeature: WorkerUptime },
{ NodeFeature: NodeCPU, WorkerFeature: WorkerCPU },
{ NodeFeature: NodeRAM, WorkerFeature: WorkerRAM },
{ NodeFeature: NodeGPU, WorkerFeature: WorkerGPU },
{ NodeFeature: NodeGRAM, WorkerFeature: WorkerGRAM },
{ NodeFeature: NodeDisk, WorkerFeature: WorkerDisk },
{ NodeFeature: NodeSent, WorkerFeature: WorkerSent },
{ NodeFeature: NodeReceived, WorkerFeature: WorkerReceived },
{
NodeFeature: makeNodeLogs(logCounts, setLogDialog),
WorkerFeature: makeWorkerLogs(logCounts, setLogDialog),
},
{
NodeFeature: makeNodeErrors(errorCounts, setErrorDialog),
WorkerFeature: makeWorkerErrors(errorCounts, setErrorDialog),
},
];
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 }, index) => (
<TableCell className={classes.cell} key={index}>
<NodeFeature node={node} />
</TableCell>
))}
</TableRow>
{expanded && (
<React.Fragment>
{raylet !== null && raylet.extraInfo !== undefined && (
<TableRow hover>
<TableCell className={classes.cell} />
<TableCell
className={classNames(classes.cell, classes.extraInfo)}
colSpan={features.length}
>
{raylet.extraInfo}
</TableCell>
</TableRow>
)}
{clusterWorkers.map((worker, index: number) => {
const rayletWorker =
raylet?.workersStats.find(
(rayletWorker) => worker.pid === rayletWorker.pid,
) || null;
return (
<TableRow hover key={index}>
<TableCell className={classes.cell} />
{features.map(({ WorkerFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<WorkerFeature
node={node}
worker={worker}
rayletWorker={rayletWorker}
/>
</TableCell>
))}
</TableRow>
);
})}
</React.Fragment>
)}
</React.Fragment>
);
}
}
export default withStyles(styles)(NodeRowGroup);
export default NodeRowGroup;
@@ -1,10 +1,9 @@
import {
createStyles,
makeStyles,
TableCell,
TableRow,
Theme,
WithStyles,
withStyles,
} from "@material-ui/core";
import LayersIcon from "@material-ui/icons/Layers";
import React from "react";
@@ -22,7 +21,7 @@ import { ClusterSent } from "./features/Sent";
import { ClusterUptime } from "./features/Uptime";
import { ClusterWorkers } from "./features/Workers";
const styles = (theme: Theme) =>
const useTotalRowStyles = makeStyles((theme: Theme) =>
createStyles({
cell: {
borderTopColor: theme.palette.divider,
@@ -39,9 +38,10 @@ const styles = (theme: Theme) =>
fontSize: "1.5em",
verticalAlign: "middle",
},
});
}),
);
type Props = {
type TotalRowProps = {
nodes: NodeInfoResponse["clients"];
clusterTotalWorkers: number;
logCounts: {
@@ -58,44 +58,40 @@ type Props = {
};
};
class TotalRow extends React.Component<Props & WithStyles<typeof styles>> {
render() {
const {
classes,
nodes,
clusterTotalWorkers,
logCounts,
errorCounts,
} = this.props;
const TotalRow: React.FC<TotalRowProps> = ({
nodes,
clusterTotalWorkers,
logCounts,
errorCounts,
}) => {
const classes = useTotalRowStyles();
const features = [
{ ClusterFeature: ClusterHost },
{ ClusterFeature: ClusterWorkers(clusterTotalWorkers) },
{ ClusterFeature: ClusterUptime },
{ ClusterFeature: ClusterCPU },
{ ClusterFeature: ClusterRAM },
{ ClusterFeature: ClusterGPU },
{ ClusterFeature: ClusterGRAM },
{ ClusterFeature: ClusterDisk },
{ ClusterFeature: ClusterSent },
{ ClusterFeature: ClusterReceived },
{ ClusterFeature: makeClusterLogs(logCounts) },
{ ClusterFeature: makeClusterErrors(errorCounts) },
];
const features = [
{ ClusterFeature: ClusterHost },
{ ClusterFeature: ClusterWorkers(clusterTotalWorkers) },
{ ClusterFeature: ClusterUptime },
{ ClusterFeature: ClusterCPU },
{ ClusterFeature: ClusterRAM },
{ ClusterFeature: ClusterGPU },
{ ClusterFeature: ClusterGRAM },
{ ClusterFeature: ClusterDisk },
{ ClusterFeature: ClusterSent },
{ ClusterFeature: ClusterReceived },
{ ClusterFeature: makeClusterLogs(logCounts) },
{ ClusterFeature: makeClusterErrors(errorCounts) },
];
return (
<TableRow hover>
<TableCell className={classes.cell}>
<LayersIcon className={classes.totalIcon} />
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>
{features.map(({ ClusterFeature }, index) => (
<TableCell className={classes.cell} key={index}>
<ClusterFeature nodes={nodes} />
</TableCell>
))}
</TableRow>
);
}
}
))}
</TableRow>
);
};
export default withStyles(styles)(TotalRow);
export default TotalRow;