[Dashboard] Add the new dashboard code and prompt users to try it (#11667)

This commit is contained in:
Dominic Ming
2021-01-29 15:22:26 +08:00
committed by GitHub
parent 42d501d747
commit 752da83bb7
52 changed files with 4650 additions and 33 deletions
@@ -0,0 +1,36 @@
import { makeStyles } from "@material-ui/core";
import React, { useEffect, useState } from "react";
import ActorTable from "../../components/ActorTable";
import TitleCard from "../../components/TitleCard";
import { getActors } from "../../service/actor";
import { Actor } from "../../type/actor";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
width: "100%",
},
}));
const Actors = () => {
const classes = useStyles();
const [actors, setActors] = useState<{ [actorId: string]: Actor }>({});
useEffect(() => {
getActors().then((res) => {
if (res?.data?.data?.actors) {
setActors(res.data.data.actors);
}
});
}, []);
return (
<div className={classes.root}>
<TitleCard title="ACTORS">
<ActorTable actors={actors} />
</TitleCard>
</div>
);
};
export default Actors;
@@ -0,0 +1,137 @@
import {
Button,
Grid,
makeStyles,
MenuItem,
Paper,
Select,
} from "@material-ui/core";
import React, { useCallback, useEffect, useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import LogVirtualView from "../../components/LogView/LogVirtualView";
import TitleCard from "../../components/TitleCard";
import { getJmap, getJstack, getJstat } from "../../service/util";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(4),
width: "100%",
},
table: {
marginTop: theme.spacing(4),
padding: theme.spacing(2),
},
pageMeta: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
search: {
margin: theme.spacing(1),
},
}));
const CMDResult = (
props: RouteComponentProps<{ cmd: string; ip: string; pid: string }>,
) => {
const classes = useStyles();
const {
match: { params },
} = props;
const { cmd, ip, pid } = params;
const [result, setResult] = useState<string>();
const [option, setOption] = useState("gcutil");
const executeJstat = useCallback(
() =>
getJstat(ip, pid, option)
.then((rsp) => {
if (rsp.data.result) {
setResult(rsp.data.data.output);
} else {
setResult(rsp.data.msg);
}
})
.catch((err) => setResult(err.toString())),
[ip, pid, option],
);
useEffect(() => {
switch (cmd) {
case "jstack":
getJstack(ip, pid)
.then((rsp) => {
if (rsp.data.result) {
setResult(rsp.data.data.output);
} else {
setResult(rsp.data.msg);
}
})
.catch((err) => setResult(err.toString()));
break;
case "jmap":
getJmap(ip, pid)
.then((rsp) => {
if (rsp.data.result) {
setResult(rsp.data.data.output);
} else {
setResult(rsp.data.msg);
}
})
.catch((err) => setResult(err.toString()));
break;
case "jstat":
executeJstat();
break;
default:
setResult(`Command ${cmd} is not supported.`);
break;
}
}, [cmd, executeJstat, ip, pid]);
return (
<div className={classes.root}>
<TitleCard title={cmd}>
{cmd === "jstat" && (
<Paper className={classes.pageMeta}>
<Grid container spacing={1}>
<Grid item>
<Select
value={option}
onChange={(e) => setOption(e.target.value as string)}
>
{[
"class",
"compiler",
"gc",
"gccapacity",
"gcmetacapacity",
"gcnew",
"gcnewcapacity",
"gcold",
"gcoldcapacity",
"gcutil",
"gccause",
"printcompilation",
].map((e) => (
<MenuItem value={e}>{e}</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<Button onClick={executeJstat}>Execute</Button>
</Grid>
</Grid>
</Paper>
)}
</TitleCard>
<TitleCard title={`IP: ${ip} / Pid: ${pid}`}>
<LogVirtualView
content={result || "loading"}
language="prolog"
height={800}
/>
</TitleCard>
</div>
);
};
export default CMDResult;
@@ -1,4 +1,5 @@
import {
Button,
createStyles,
makeStyles,
Tab,
@@ -8,6 +9,7 @@ import {
} from "@material-ui/core";
import React, { useCallback, useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { getActorGroups, getNodeInfo, getTuneAvailability } from "../../api";
import { StoreState } from "../../store";
import LastUpdated from "./LastUpdated";
@@ -59,6 +61,7 @@ const Dashboard: React.FC = () => {
const tuneAvailability = useSelector(tuneAvailabilitySelector);
const tab = useSelector(tabSelector);
const classes = useDashboardStyles();
const history = useHistory();
// Polling Function
const refreshInfo = useCallback(async () => {
@@ -103,6 +106,9 @@ const Dashboard: React.FC = () => {
return (
<div className={classes.root}>
<Typography variant="h5">Ray Dashboard</Typography>
<Button onClick={() => history.push("/summary")}>
Try New Dashboard
</Button>
<Tabs
className={classes.tabs}
indicatorColor="primary"
+32
View File
@@ -0,0 +1,32 @@
import { Typography } from "@material-ui/core";
import { HelpOutlineOutlined } from "@material-ui/icons";
import React from "react";
const Error404 = () => {
return (
<div
style={{
display: "flex",
position: "fixed",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
width: "100%",
height: "100%",
}}
>
<div style={{ height: 400 }}>
<Typography variant="h2">
<HelpOutlineOutlined fontSize="large" />
</Typography>
<Typography variant="h6">404 NOT FOUND</Typography>
<p>
We can't provide the page you wanted yet, better try with another path
next time.
</p>
</div>
</div>
);
};
export default Error404;
@@ -0,0 +1,21 @@
import React from "react";
import Logo from "../../logo.svg";
export default () => {
return (
<div style={{ height: "100vh", width: "100vw" }}>
<div
style={{
margin: "250px auto 0 auto",
textAlign: "center",
fontSize: 40,
fontWeight: 500,
}}
>
<img src={Logo} alt="Loading" width={100} />
<br />
Loading...
</div>
</div>
);
};
+110
View File
@@ -0,0 +1,110 @@
import {
makeStyles,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import React, { useEffect, useState } from "react";
import { version } from "../../../package.json";
import TitleCard from "../../components/TitleCard";
import { getRayConfig } from "../../service/cluster";
import { getNodeList } from "../../service/node";
import { RayConfig } from "../../type/config";
import { NodeDetail } from "../../type/node";
import { memoryConverter } from "../../util/converter";
const useStyle = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
},
label: {
fontWeight: "bold",
},
}));
const getVal = (key: string, value: any) => {
if (key === "containerMemory") {
return memoryConverter(value * 1024 * 1024);
}
return JSON.stringify(value);
};
const useIndex = () => {
const [rayConfig, setConfig] = useState<RayConfig>();
const [nodes, setNodes] = useState<NodeDetail[]>([]);
useEffect(() => {
getRayConfig().then((res) => {
if (res?.data?.data?.config) {
setConfig(res.data.data.config);
}
});
}, []);
useEffect(() => {
getNodeList().then((res) => {
if (res?.data?.data?.summary) {
setNodes(res.data.data.summary);
}
});
}, []);
return { rayConfig, nodes };
};
const Index = () => {
const { rayConfig } = useIndex();
const classes = useStyle();
return (
<div className={classes.root}>
<TitleCard title={rayConfig?.clusterName || "SUMMARY"}>
<p>Dashboard Frontend Version: {version}</p>
{rayConfig?.imageUrl && (
<p>
Image Url:{" "}
<a
href={rayConfig.imageUrl}
target="_blank"
rel="noopener noreferrer"
>
{rayConfig.imageUrl}
</a>
</p>
)}
{rayConfig?.sourceCodeLink && (
<p>
Source Code:{" "}
<a
href={rayConfig.sourceCodeLink}
target="_blank"
rel="noopener noreferrer"
>
{rayConfig.sourceCodeLink}
</a>
</p>
)}
</TitleCard>
{rayConfig && (
<TitleCard title="Config">
<TableContainer>
<TableHead>
<TableCell>Key</TableCell>
<TableCell>Value</TableCell>
</TableHead>
<TableBody>
{Object.entries(rayConfig).map(([key, value]) => (
<TableRow>
<TableCell className={classes.label}>{key}</TableCell>
<TableCell>{getVal(key, value)}</TableCell>
</TableRow>
))}
</TableBody>
</TableContainer>
</TitleCard>
)}
</div>
);
};
export default Index;
@@ -0,0 +1,246 @@
import {
Grid,
makeStyles,
Switch,
Tab,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tabs,
} from "@material-ui/core";
import React from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import ActorTable from "../../components/ActorTable";
import Loading from "../../components/Loading";
import { StatusChip } from "../../components/StatusChip";
import TitleCard from "../../components/TitleCard";
import RayletWorkerTable from "../../components/WorkerTable";
import { longTextCut } from "../../util/func";
import { useJobDetail } from "./hook/useJobDetail";
const useStyle = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
},
paper: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
label: {
fontWeight: "bold",
},
pageMeta: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
tab: {
marginBottom: theme.spacing(2),
},
dependenciesChip: {
margin: theme.spacing(0.5),
wordBreak: "break-all",
},
alert: {
color: theme.palette.error.main,
},
}));
const JobDetailPage = (props: RouteComponentProps<{ id: string }>) => {
const classes = useStyle();
const {
actorMap,
jobInfo,
job,
msg,
selectedTab,
handleChange,
handleSwitchChange,
params,
refreshing,
ipLogMap,
} = useJobDetail(props);
if (!job || !jobInfo) {
return (
<div className={classes.root}>
<Loading loading={msg.startsWith("Loading")} />
<TitleCard title={`JOB - ${params.id}`}>
<StatusChip type="job" status="LOADING" />
<br />
Auto Refresh:
<Switch
checked={refreshing}
onChange={handleSwitchChange}
name="refresh"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
<br />
Request Status: {msg} <br />
</TitleCard>
</div>
);
}
return (
<div className={classes.root}>
<TitleCard title={`JOB - ${params.id}`}>
<StatusChip type="job" status={jobInfo.isDead ? "DEAD" : "ALIVE"} />
<br />
Auto Refresh:
<Switch
checked={refreshing}
onChange={handleSwitchChange}
name="refresh"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
<br />
Request Status: {msg} <br />
</TitleCard>
<TitleCard title="Job Detail">
<Tabs
value={selectedTab}
onChange={handleChange}
className={classes.tab}
>
<Tab value="info" label="Info" />
<Tab value="dep" label="Dependencies" />
<Tab
value="worker"
label={`Worker(${job?.jobWorkers?.length || 0})`}
/>
<Tab
value="actor"
label={`Actor(${Object.entries(job?.jobActors || {}).length || 0})`}
/>
</Tabs>
{selectedTab === "info" && (
<Grid container spacing={2}>
<Grid item xs={4}>
<span className={classes.label}>Driver IP</span>:{" "}
{jobInfo.driverIpAddress}
</Grid>
{ipLogMap[jobInfo.driverIpAddress] && (
<Grid item xs={4}>
<span className={classes.label}>Driver Log</span>:{" "}
<Link
to={`/log/${encodeURIComponent(
ipLogMap[jobInfo.driverIpAddress],
)}?fileName=driver-${jobInfo.jobId}`}
target="_blank"
>
Log
</Link>
</Grid>
)}
<Grid item xs={4}>
<span className={classes.label}>Driver Pid</span>:{" "}
{jobInfo.driverPid}
</Grid>
{jobInfo.eventUrl && (
<Grid item xs={4}>
<span className={classes.label}>Event Link</span>:{" "}
<a
href={jobInfo.eventUrl}
target="_blank"
rel="noopener noreferrer"
>
Event Log
</a>
</Grid>
)}
{jobInfo.failErrorMessage && (
<Grid item xs={12}>
<span className={classes.label}>Fail Error</span>:{" "}
<span className={classes.alert}>
{jobInfo.failErrorMessage}
</span>
</Grid>
)}
</Grid>
)}
{jobInfo?.dependencies && selectedTab === "dep" && (
<div className={classes.paper}>
{jobInfo?.dependencies?.python && (
<TitleCard title="Python Dependencies">
<div
style={{
display: "flex",
justifyItems: "space-around",
flexWrap: "wrap",
}}
>
{jobInfo.dependencies.python.map((e) => (
<StatusChip
type="deps"
status={e.startsWith("http") ? longTextCut(e, 30) : e}
key={e}
/>
))}
</div>
</TitleCard>
)}
{jobInfo?.dependencies?.java && (
<TitleCard title="Java Dependencies">
<TableContainer>
<Table>
<TableHead>
<TableRow>
{["Name", "Version", "URL"].map((col) => (
<TableCell align="center" key={col}>
{col}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{jobInfo.dependencies.java.map(
({ name, version, url }) => (
<TableRow key={url}>
<TableCell align="center">{name}</TableCell>
<TableCell align="center">{version}</TableCell>
<TableCell align="center">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
>
{url}
</a>
</TableCell>
</TableRow>
),
)}
</TableBody>
</Table>
</TableContainer>
</TitleCard>
)}
</div>
)}
{selectedTab === "worker" && (
<div>
<TableContainer className={classes.paper}>
<RayletWorkerTable
workers={job.jobWorkers}
actorMap={actorMap || {}}
/>
</TableContainer>
</div>
)}
{selectedTab === "actor" && (
<div>
<TableContainer className={classes.paper}>
<ActorTable actors={actorMap || {}} workers={job.jobWorkers} />
</TableContainer>
</div>
)}
</TitleCard>
</div>
);
};
export default JobDetailPage;
@@ -0,0 +1,73 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { GlobalContext } from "../../../App";
import { getJobDetail } from "../../../service/job";
import { JobDetail } from "../../../type/job";
export const useJobDetail = (props: RouteComponentProps<{ id: string }>) => {
const {
match: { params },
} = props;
const [job, setJob] = useState<JobDetail>();
const [msg, setMsg] = useState("Loading the job detail");
const [refreshing, setRefresh] = useState(true);
const [selectedTab, setTab] = useState("info");
const { ipLogMap } = useContext(GlobalContext);
const tot = useRef<NodeJS.Timeout>();
const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => {
setTab(newValue);
};
const handleSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRefresh(event.target.checked);
};
const getJob = useCallback(async () => {
if (!refreshing) {
return;
}
const rsp = await getJobDetail(params.id);
if (rsp.data?.data?.detail) {
setJob(rsp.data.data.detail);
}
if (rsp.data?.msg) {
setMsg(rsp.data.msg || "");
}
if (rsp.data.result === false) {
setMsg("Job Query Error Please Check JobId");
setJob(undefined);
setRefresh(false);
}
tot.current = setTimeout(getJob, 4000);
}, [refreshing, params.id]);
useEffect(() => {
if (tot.current) {
clearTimeout(tot.current);
}
getJob();
return () => {
if (tot.current) {
clearTimeout(tot.current);
}
};
}, [getJob]);
const { jobInfo } = job || {};
const actorMap = job?.jobActors;
return {
actorMap,
jobInfo,
job,
msg,
selectedTab,
handleChange,
handleSwitchChange,
params,
refreshing,
ipLogMap,
};
};
@@ -0,0 +1,68 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getJobList } from "../../../service/job";
import { Job } from "../../../type/job";
export const useJobList = () => {
const [jobList, setList] = useState<Job[]>([]);
const [page, setPage] = useState({ pageSize: 10, pageNo: 1 });
const [msg, setMsg] = useState("Loading the job list...");
const [isRefreshing, setRefresh] = useState(true);
const [filter, setFilter] = useState<
{
key: "jobId" | "name" | "language" | "state" | "namespaceId";
val: string;
}[]
>([]);
const refreshRef = useRef(isRefreshing);
const tot = useRef<NodeJS.Timeout>();
const changeFilter = (
key: "jobId" | "name" | "language" | "state" | "namespaceId",
val: string,
) => {
const f = filter.find((e) => e.key === key);
if (f) {
f.val = val;
} else {
filter.push({ key, val });
}
setFilter([...filter]);
};
const onSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRefresh(event.target.checked);
};
refreshRef.current = isRefreshing;
const getJob = useCallback(async () => {
if (!refreshRef.current) {
return;
}
const rsp = await getJobList();
if (rsp?.data?.data?.summary) {
setList(rsp.data.data.summary.sort((a, b) => b.timestamp - a.timestamp));
setMsg(rsp.data.msg || "");
}
tot.current = setTimeout(getJob, 4000);
}, []);
useEffect(() => {
getJob();
return () => {
if (tot.current) {
clearTimeout(tot.current);
}
};
}, [getJob]);
return {
jobList: jobList.filter((node) =>
filter.every((f) => node[f.key] && node[f.key].includes(f.val)),
),
msg,
isRefreshing,
onSwitchChange,
changeFilter,
page,
originalJobs: jobList,
setPage: (key: string, val: number) => setPage({ ...page, [key]: val }),
};
};
+129
View File
@@ -0,0 +1,129 @@
import {
Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import Pagination from "@material-ui/lab/Pagination";
import dayjs from "dayjs";
import React from "react";
import { Link } from "react-router-dom";
import Loading from "../../components/Loading";
import { SearchInput, SearchSelect } from "../../components/SearchComponent";
import TitleCard from "../../components/TitleCard";
import { useJobList } from "./hook/useJobList";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
width: "100%",
},
}));
const columns = ["ID", "DriverIpAddress", "DriverPid", "IsDead", "Timestamp"];
const JobList = () => {
const classes = useStyles();
const {
msg,
isRefreshing,
onSwitchChange,
jobList,
changeFilter,
page,
setPage,
} = useJobList();
return (
<div className={classes.root}>
<Loading loading={msg.startsWith("Loading")} />
<TitleCard title="JOBS">
Auto Refresh:
<Switch
checked={isRefreshing}
onChange={onSwitchChange}
name="refresh"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
<br />
Request Status: {msg}
</TitleCard>
<TitleCard title="Job List">
<TableContainer>
<SearchInput
label="ID"
onChange={(value) => changeFilter("jobId", value)}
/>
<SearchSelect
label="Language"
onChange={(value) => changeFilter("language", value)}
options={["JAVA", "PYTHON"]}
/>
<SearchInput
label="Page Size"
onChange={(value) =>
setPage("pageSize", Math.min(Number(value), 500) || 10)
}
/>
<div>
<Pagination
count={Math.ceil(jobList.length / page.pageSize)}
page={page.pageNo}
onChange={(e, pageNo) => setPage("pageNo", pageNo)}
/>
</div>
<Table>
<TableHead>
<TableRow>
{columns.map((col) => (
<TableCell align="center" key={col}>
{col}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{jobList
.slice(
(page.pageNo - 1) * page.pageSize,
page.pageNo * page.pageSize,
)
.map(
({
jobId = "",
driverIpAddress,
isDead,
driverPid,
state,
timestamp,
namespaceId,
}) => (
<TableRow key={jobId}>
<TableCell align="center">
<Link to={`/job/${jobId}`}>{jobId}</Link>
</TableCell>
<TableCell align="center">{driverIpAddress}</TableCell>
<TableCell align="center">{driverPid}</TableCell>
<TableCell align="center">
{isDead ? "true" : "false"}
</TableCell>
<TableCell align="center">
{dayjs(timestamp * 1000).format("YYYY/MM/DD HH:mm:ss")}
</TableCell>
<TableCell align="center">{namespaceId}</TableCell>
</TableRow>
),
)}
</TableBody>
</Table>
</TableContainer>
</TitleCard>
</div>
);
};
export default JobList;
+167
View File
@@ -0,0 +1,167 @@
import { IconButton, Tooltip } from "@material-ui/core";
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import { NightsStay, VerticalAlignTop, WbSunny } from "@material-ui/icons";
import classnames from "classnames";
import React, { PropsWithChildren } from "react";
import { RouteComponentProps } from "react-router-dom";
import SpeedTools from "../../components/SpeedTools";
import Logo from "../../logo.svg";
const drawerWidth = 200;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& a": {
color: theme.palette.primary.main,
},
},
drawer: {
width: drawerWidth,
flexShrink: 0,
background: theme.palette.background.paper,
},
drawerPaper: {
width: drawerWidth,
border: "none",
background: theme.palette.background.paper,
boxShadow: theme.shadows[1],
},
title: {
padding: theme.spacing(2),
textAlign: "center",
lineHeight: "36px",
},
divider: {
background: "rgba(255, 255, 255, .12)",
},
menuItem: {
cursor: "pointer",
"&:hover": {
background: theme.palette.primary.main,
},
},
selected: {
background: `linear-gradient(45deg, ${theme.palette.primary.main} 30%, ${theme.palette.secondary.main} 90%)`,
},
child: {
flex: 1,
},
}));
const BasicLayout = (
props: PropsWithChildren<
{ setTheme: (theme: string) => void; theme: string } & RouteComponentProps
>,
) => {
const classes = useStyles();
const { location, history, children, setTheme, theme } = props;
return (
<div className={classes.root}>
<Drawer
variant="permanent"
anchor="left"
className={classes.drawer}
classes={{
paper: classes.drawerPaper,
}}
>
<Typography variant="h6" className={classes.title}>
<img width={48} src={Logo} alt="Ray" /> <br /> Ray Dashboard
</Typography>
<List>
<ListItem
button
className={classnames(
classes.menuItem,
location.pathname === "/summary" && classes.selected,
)}
onClick={() => history.push("/summary")}
>
<ListItemText>SUMMARY</ListItemText>
</ListItem>
<ListItem
button
className={classnames(
classes.menuItem,
location.pathname.includes("node") && classes.selected,
)}
onClick={() => history.push("/node")}
>
<ListItemText>NODES</ListItemText>
</ListItem>
<ListItem
button
className={classnames(
classes.menuItem,
location.pathname.includes("job") && classes.selected,
)}
onClick={() => history.push("/job")}
>
<ListItemText>JOBS</ListItemText>
</ListItem>
<ListItem
button
className={classnames(
classes.menuItem,
location.pathname.includes("actor") && classes.selected,
)}
onClick={() => history.push("/actors")}
>
<ListItemText>ACTORS</ListItemText>
</ListItem>
<ListItem
button
className={classnames(
classes.menuItem,
location.pathname.includes("log") && classes.selected,
)}
onClick={() => history.push("/log")}
>
<ListItemText>LOGS</ListItemText>
</ListItem>
<ListItem
button
className={classnames(classes.menuItem)}
onClick={() => history.push("/")}
>
<ListItemText>BACK TO LEGACY</ListItemText>
</ListItem>
<ListItem>
<IconButton
color="primary"
onClick={() => {
window.scrollTo(0, 0);
}}
>
<Tooltip title="Back To Top">
<VerticalAlignTop />
</Tooltip>
</IconButton>
<IconButton
color="primary"
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
>
<Tooltip title={`Theme - ${theme}`}>
{theme === "dark" ? <NightsStay /> : <WbSunny />}
</Tooltip>
</IconButton>
</ListItem>
<SpeedTools />
</List>
</Drawer>
<div className={classes.child}>{children}</div>
</div>
);
};
export default BasicLayout;
+306
View File
@@ -0,0 +1,306 @@
import {
Button,
InputAdornment,
LinearProgress,
List,
ListItem,
makeStyles,
Paper,
Switch,
TextField,
} from "@material-ui/core";
import { SearchOutlined } from "@material-ui/icons";
import React, { useEffect, useRef, useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import LogVirtualView from "../../components/LogView/LogVirtualView";
import { SearchInput } from "../../components/SearchComponent";
import TitleCard from "../../components/TitleCard";
import { getLogDetail } from "../../service/log";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
width: "100%",
},
table: {
marginTop: theme.spacing(4),
padding: theme.spacing(2),
},
pageMeta: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
search: {
margin: theme.spacing(1),
},
}));
type LogsProps = RouteComponentProps<{ host?: string; path?: string }> & {
theme?: "dark" | "light";
};
const useLogs = (props: LogsProps) => {
const {
match: { params },
location: { search: urlSearch },
theme,
} = props;
const { host, path } = params;
const searchMap = new URLSearchParams(urlSearch);
const urlFileName = searchMap.get("fileName");
const el = useRef<HTMLDivElement>(null);
const [origin, setOrigin] = useState<string>();
const [search, setSearch] = useState<{
keywords?: string;
lineNumber?: string;
fontSize?: number;
revert?: boolean;
}>();
const [fileName, setFileName] = useState(searchMap.get("fileName") || "");
const [log, setLogs] = useState<
undefined | string | { [key: string]: string }[]
>();
const [startTime, setStart] = useState<string>();
const [endTime, setEnd] = useState<string>();
useEffect(() => {
setFileName(urlFileName || "");
}, [urlFileName]);
useEffect(() => {
let url = "log_index";
setLogs("Loading...");
if (host) {
url = decodeURIComponent(host);
setOrigin(new URL(url).origin);
if (path) {
url += decodeURIComponent(path);
}
} else {
setOrigin(undefined);
}
getLogDetail(url)
.then((res) => {
if (res) {
setLogs(res);
} else {
setLogs("(null)");
}
})
.catch(() => {
setLogs("Failed to load");
});
}, [host, path]);
return {
log,
origin,
host,
path,
el,
search,
setSearch,
theme,
fileName,
setFileName,
startTime,
setStart,
endTime,
setEnd,
};
};
const Logs = (props: LogsProps) => {
const classes = useStyles();
const {
log,
origin,
path,
el,
search,
setSearch,
theme,
fileName,
setFileName,
startTime,
setStart,
endTime,
setEnd,
} = useLogs(props);
let href = "#/log/";
if (origin) {
if (path) {
const after = decodeURIComponent(path).split("/");
after.pop();
if (after.length > 1) {
href += encodeURIComponent(origin);
href += "/";
href += encodeURIComponent(after.join("/"));
}
}
}
return (
<div className={classes.root} ref={el}>
<TitleCard title="Logs Viewer">
<Paper>
{!origin && <p>Please choose an url to get log path</p>}
{origin && (
<p>
Now Path: {origin}
{decodeURIComponent(path || "")}
</p>
)}
{origin && (
<div>
<Button
variant="contained"
href={href}
className={classes.search}
>
Back To ../
</Button>
{typeof log === "object" && (
<SearchInput
defaultValue={fileName}
label="File Name"
onChange={(val) => {
setFileName(val);
}}
/>
)}
</div>
)}
</Paper>
<Paper>
{typeof log === "object" && (
<List>
{log
.filter((e) => !fileName || e?.name?.includes(fileName))
.map((e: { [key: string]: string }) => (
<ListItem key={e.name}>
<a
href={`#/log/${
origin ? `${encodeURIComponent(origin)}/` : ""
}${encodeURIComponent(e.href)}`}
>
{e.name}
</a>
</ListItem>
))}
</List>
)}
{typeof log === "string" && log !== "Loading..." && (
<div>
<div>
<TextField
className={classes.search}
label="Keyword"
InputProps={{
onChange: ({ target: { value } }) => {
setSearch({ ...search, keywords: value });
},
type: "",
endAdornment: (
<InputAdornment position="end">
<SearchOutlined />
</InputAdornment>
),
}}
/>
<TextField
className={classes.search}
label="Line Number"
InputProps={{
onChange: ({ target: { value } }) => {
setSearch({ ...search, lineNumber: value });
},
type: "",
endAdornment: (
<InputAdornment position="end">
<SearchOutlined />
</InputAdornment>
),
}}
/>
<TextField
className={classes.search}
label="Font Size"
InputProps={{
onChange: ({ target: { value } }) => {
setSearch({ ...search, fontSize: Number(value) });
},
type: "",
}}
/>
<TextField
id="datetime-local"
label="Start Time"
type="datetime-local"
value={startTime}
className={classes.search}
onChange={(val) => {
setStart(val.target.value);
}}
InputLabelProps={{
shrink: true,
}}
/>
<TextField
label="End Time"
type="datetime-local"
value={endTime}
className={classes.search}
onChange={(val) => {
setEnd(val.target.value);
}}
InputLabelProps={{
shrink: true,
}}
/>
<div className={classes.search}>
Reverse:{" "}
<Switch
checked={search?.revert}
onChange={(e, v) => setSearch({ ...search, revert: v })}
/>
<Button
className={classes.search}
variant="contained"
onClick={() => {
setStart("");
setEnd("");
}}
>
Reset Time
</Button>
</div>
</div>
<LogVirtualView
height={600}
theme={theme}
revert={search?.revert}
keywords={search?.keywords}
focusLine={Number(search?.lineNumber) || undefined}
fontSize={search?.fontSize || 12}
content={log}
language="prolog"
startTime={startTime}
endTime={endTime}
/>
</div>
)}
{log === "Loading..." && (
<div>
<br />
<LinearProgress />
</div>
)}
</Paper>
</TitleCard>
</div>
);
};
export default Logs;
@@ -0,0 +1,287 @@
import {
Grid,
makeStyles,
Switch,
Tab,
TableContainer,
Tabs,
} from "@material-ui/core";
import dayjs from "dayjs";
import React from "react";
import { Link, RouteComponentProps } from "react-router-dom";
import ActorTable from "../../components/ActorTable";
import Loading from "../../components/Loading";
import PercentageBar from "../../components/PercentageBar";
import { StatusChip } from "../../components/StatusChip";
import TitleCard from "../../components/TitleCard";
import RayletWorkerTable from "../../components/WorkerTable";
import { ViewMeasures } from "../../type/raylet";
import { memoryConverter } from "../../util/converter";
import { useNodeDetail } from "./hook/useNodeDetail";
const useStyle = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
},
paper: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
label: {
fontWeight: "bold",
},
tab: {
marginBottom: theme.spacing(2),
},
}));
const showMeasureKeys = [
"local_total_resource",
"local_available_resource",
"actor_stats",
"task_dependency_manager_stats",
"reconstruction_policy_stats",
"scheduling_queue_stats",
"object_manager_stats",
];
const ViewDataDisplayer = ({ view }: { view?: ViewMeasures }) => {
if (!view) {
return null;
}
const { tags = "", ...otherProps } = view;
return (
<Grid item xs={6}>
<span>{tags.split(",").pop()?.split(":").slice(1).join(":")}</span>=
{Object.keys(otherProps).length > 0 ? (
JSON.stringify(Object.values(otherProps).pop())
) : (
<span style={{ color: "gray" }}>null</span>
)}
</Grid>
);
};
const NodeDetailPage = (props: RouteComponentProps<{ id: string }>) => {
const classes = useStyle();
const {
params,
selectedTab,
nodeDetail,
msg,
isRefreshing,
onRefreshChange,
raylet,
handleChange,
} = useNodeDetail(props);
return (
<div className={classes.root}>
<Loading loading={msg.startsWith("Loading")} />
<TitleCard title={`NODE - ${params.id}`}>
<StatusChip
type="node"
status={nodeDetail?.raylet?.state || "LOADING"}
/>
<br />
Auto Refresh:
<Switch
checked={isRefreshing}
onChange={onRefreshChange}
name="refresh"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
<br />
Request Status: {msg}
</TitleCard>
<TitleCard title="Node Detail">
<Tabs
value={selectedTab}
onChange={handleChange}
className={classes.tab}
>
<Tab value="info" label="Info" />
<Tab value="raylet" label="Raylet" />
<Tab
value="worker"
label={`Worker (${nodeDetail?.workers.length || 0})`}
/>
<Tab
value="actor"
label={`Actor (${
Object.values(nodeDetail?.actors || {}).length || 0
})`}
/>
</Tabs>
{nodeDetail && selectedTab === "info" && (
<div className={classes.paper}>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Hostname</div>{" "}
{nodeDetail.hostname}
</Grid>
<Grid item xs>
<div className={classes.label}>IP</div> {nodeDetail.ip}
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>CPU (Logic/Physic)</div>{" "}
{nodeDetail.cpus[0]}/ {nodeDetail.cpus[1]}
</Grid>
<Grid item xs>
<div className={classes.label}>Load (1/5/15min)</div>{" "}
{nodeDetail?.loadAvg[0] &&
nodeDetail.loadAvg[0]
.map((e) => Number(e).toFixed(2))
.join("/")}
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Load per CPU (1/5/15min)</div>{" "}
{nodeDetail?.loadAvg[1] &&
nodeDetail.loadAvg[1]
.map((e) => Number(e).toFixed(2))
.join("/")}
</Grid>
<Grid item xs>
<div className={classes.label}>Boot Time</div>{" "}
{dayjs(nodeDetail.bootTime * 1000).format(
"YYYY/MM/DD HH:mm:ss",
)}
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Sent Tps</div>{" "}
{memoryConverter(nodeDetail?.net[0])}/s
</Grid>
<Grid item xs>
<div className={classes.label}>Recieved Tps</div>{" "}
{memoryConverter(nodeDetail?.net[1])}/s
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Memory</div>{" "}
{nodeDetail?.mem && (
<PercentageBar
num={Number(nodeDetail?.mem[0] - nodeDetail?.mem[1])}
total={nodeDetail?.mem[0]}
>
{memoryConverter(nodeDetail?.mem[0] - nodeDetail?.mem[1])}/
{memoryConverter(nodeDetail?.mem[0])}({nodeDetail?.mem[2]}%)
</PercentageBar>
)}
</Grid>
<Grid item xs>
<div className={classes.label}>CPU</div>{" "}
<PercentageBar num={Number(nodeDetail.cpu)} total={100}>
{nodeDetail.cpu}%
</PercentageBar>
</Grid>
</Grid>
<Grid container spacing={2}>
{nodeDetail?.disk &&
Object.entries(nodeDetail?.disk).map(([path, obj]) => (
<Grid item xs={6} key={path}>
<div className={classes.label}>Disk ({path})</div>{" "}
{obj && (
<PercentageBar num={Number(obj.used)} total={obj.total}>
{memoryConverter(obj.used)}/{memoryConverter(obj.total)}
({obj.percent}%, {memoryConverter(obj.free)} free)
</PercentageBar>
)}
</Grid>
))}
</Grid>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Logs</div>{" "}
<Link to={`/log/${encodeURIComponent(nodeDetail.logUrl)}`}>
log
</Link>
</Grid>
</Grid>
</div>
)}
{raylet && Object.keys(raylet).length > 0 && selectedTab === "raylet" && (
<React.Fragment>
<div className={classes.paper}>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Command</div>
<br />
<div style={{ height: 200, overflow: "auto" }}>
{nodeDetail?.cmdline.join(" ")}
</div>
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid item xs>
<div className={classes.label}>Pid</div> {raylet?.pid}
</Grid>
<Grid item xs>
<div className={classes.label}>Workers Num</div>{" "}
{raylet?.numWorkers}
</Grid>
<Grid item xs>
<div className={classes.label}>Node Manager Port</div>{" "}
{raylet?.nodeManagerPort}
</Grid>
</Grid>
{showMeasureKeys
.map((e) => raylet.viewData.find((view) => view.viewName === e))
.map((e) =>
e ? (
<React.Fragment key={e.viewName}>
<p className={classes.label}>
{e.viewName
.split("_")
.map((e) => e[0].toUpperCase() + e.slice(1))
.join(" ")}
</p>
<Grid
container
spacing={2}
style={{ maxHeight: 177, overflow: "auto" }}
>
{e.measures.map((e) => (
<ViewDataDisplayer key={e.tags} view={e} />
))}
</Grid>
</React.Fragment>
) : null,
)}
</div>
</React.Fragment>
)}
{nodeDetail?.workers && selectedTab === "worker" && (
<React.Fragment>
<TableContainer className={classes.paper}>
<RayletWorkerTable
workers={nodeDetail?.workers}
actorMap={nodeDetail?.actors}
/>
</TableContainer>
</React.Fragment>
)}
{nodeDetail?.actors && selectedTab === "actor" && (
<React.Fragment>
<TableContainer className={classes.paper}>
<ActorTable
actors={nodeDetail.actors}
workers={nodeDetail?.workers}
/>
</TableContainer>
</React.Fragment>
)}
</TitleCard>
</div>
);
};
export default NodeDetailPage;
@@ -0,0 +1,66 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { GlobalContext } from "../../../App";
import { getNodeDetail } from "../../../service/node";
import { NodeDetailExtend } from "../../../type/node";
export const useNodeDetail = (props: RouteComponentProps<{ id: string }>) => {
const {
match: { params },
} = props;
const [selectedTab, setTab] = useState("info");
const [nodeDetail, setNode] = useState<NodeDetailExtend | undefined>();
const [msg, setMsg] = useState("Loading the node infos...");
const { namespaceMap } = useContext(GlobalContext);
const [isRefreshing, setRefresh] = useState(true);
const tot = useRef<NodeJS.Timeout>();
const onRefreshChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRefresh(event.target.checked);
};
const getDetail = useCallback(async () => {
if (!isRefreshing) {
return;
}
const { data } = await getNodeDetail(params.id);
const { data: rspData, msg, result } = data;
if (rspData?.detail) {
setNode(rspData.detail);
}
if (msg) {
setMsg(msg);
}
if (result === false) {
setMsg("Node Query Error Please Check Node Name");
setRefresh(false);
}
tot.current = setTimeout(getDetail, 4000);
}, [isRefreshing, params.id]);
const raylet = nodeDetail?.raylet;
const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => {
setTab(newValue);
};
useEffect(() => {
getDetail();
return () => {
if (tot.current) {
clearTimeout(tot.current);
}
};
}, [getDetail]);
return {
params,
selectedTab,
nodeDetail,
msg,
isRefreshing,
onRefreshChange,
raylet,
handleChange,
namespaceMap,
};
};
@@ -0,0 +1,74 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getNodeList } from "../../../service/node";
import { NodeDetail } from "../../../type/node";
import { useSorter } from "../../../util/hook";
export const useNodeList = () => {
const [nodeList, setList] = useState<NodeDetail[]>([]);
const [msg, setMsg] = useState("Loading the nodes infos...");
const [isRefreshing, setRefresh] = useState(true);
const [mode, setMode] = useState("table");
const [filter, setFilter] = useState<
{ key: "hostname" | "ip" | "state"; val: string }[]
>([]);
const [page, setPage] = useState({ pageSize: 10, pageNo: 1 });
const { sorterFunc, setOrderDesc, setSortKey, sorterKey } = useSorter("cpu");
const tot = useRef<NodeJS.Timeout>();
const changeFilter = (key: "hostname" | "ip" | "state", val: string) => {
const f = filter.find((e) => e.key === key);
if (f) {
f.val = val;
} else {
filter.push({ key, val });
}
setFilter([...filter]);
};
const onSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRefresh(event.target.checked);
};
const getList = useCallback(async () => {
if (!isRefreshing) {
return;
}
const { data } = await getNodeList();
const { data: rspData, msg } = data;
setList(rspData.summary || []);
if (msg) {
setMsg(msg);
} else {
setMsg("");
}
tot.current = setTimeout(getList, 4000);
}, [isRefreshing]);
useEffect(() => {
getList();
return () => {
if (tot.current) {
clearTimeout(tot.current);
}
};
}, [getList]);
return {
nodeList: nodeList
.map((e) => ({ ...e, state: e.raylet.state }))
.sort((a, b) => (a.raylet.nodeId > b.raylet.nodeId ? 1 : -1))
.sort(sorterFunc)
.filter((node) =>
filter.every((f) => node[f.key] && node[f.key].includes(f.val)),
),
msg,
isRefreshing,
onSwitchChange,
changeFilter,
page,
originalNodes: nodeList,
setPage: (key: string, val: number) => setPage({ ...page, [key]: val }),
sorterKey,
setSortKey,
setOrderDesc,
mode,
setMode,
};
};
+392
View File
@@ -0,0 +1,392 @@
import {
Button,
ButtonGroup,
Grid,
Paper,
Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import Pagination from "@material-ui/lab/Pagination";
import dayjs from "dayjs";
import React from "react";
import { Link } from "react-router-dom";
import Loading from "../../components/Loading";
import PercentageBar from "../../components/PercentageBar";
import { SearchInput, SearchSelect } from "../../components/SearchComponent";
import StateCounter from "../../components/StatesCounter";
import { StatusChip } from "../../components/StatusChip";
import TitleCard from "../../components/TitleCard";
import { NodeDetail } from "../../type/node";
import { memoryConverter } from "../../util/converter";
import { useNodeList } from "./hook/useNodeList";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(2),
width: "100%",
position: "relative",
},
}));
const columns = [
"State",
"ID",
"Host",
"IP",
"CPU Usage",
"Memory",
"Disk(root)",
"Sent",
"Received",
"BRPC Port",
"Time Info",
"Log",
];
export const brpcLinkChanger = (href: string) => {
const { location } = window;
const { pathname } = location;
const pathArr = pathname.split("/");
if (pathArr.some((e) => e.split(".").length > 1)) {
const index = pathArr.findIndex((e) => e.includes("."));
const resultArr = pathArr.slice(0, index);
resultArr.push(href);
return `${location.protocol}//${location.host}${resultArr.join("/")}`;
}
return `http://${href}`;
};
export const NodeCard = (props: { node: NodeDetail }) => {
const { node } = props;
if (!node) {
return null;
}
const { raylet, hostname, ip, cpu, mem, net, disk, logUrl } = node;
const { nodeId, state, brpcPort } = raylet;
return (
<Paper variant="outlined" style={{ padding: "12px 12px", margin: 12 }}>
<p style={{ fontWeight: "bold", fontSize: 12, textDecoration: "none" }}>
<Link to={`node/${nodeId}`}>{nodeId}</Link>{" "}
</p>
<p>
<Grid container spacing={1}>
<Grid item>
<StatusChip type="node" status={state} />
</Grid>
<Grid item>
{hostname}({ip})
</Grid>
{net && net[0] >= 0 && (
<Grid item>
<span style={{ fontWeight: "bold" }}>Sent</span>{" "}
{memoryConverter(net[0])}/s{" "}
<span style={{ fontWeight: "bold" }}>Received</span>{" "}
{memoryConverter(net[1])}/s
</Grid>
)}
</Grid>
</p>
<Grid container spacing={1} alignItems="baseline">
{cpu >= 0 && (
<Grid item xs>
CPU
<PercentageBar num={Number(cpu)} total={100}>
{cpu}%
</PercentageBar>
</Grid>
)}
{mem && (
<Grid item xs>
Memory
<PercentageBar num={Number(mem[0] - mem[1])} total={mem[0]}>
{memoryConverter(mem[0] - mem[1])}/{memoryConverter(mem[0])}(
{mem[2]}%)
</PercentageBar>
</Grid>
)}
{disk && disk["/"] && (
<Grid item xs>
Disk('/')
<PercentageBar num={Number(disk["/"].used)} total={disk["/"].total}>
{memoryConverter(disk["/"].used)}/
{memoryConverter(disk["/"].total)}({disk["/"].percent}%)
</PercentageBar>
</Grid>
)}
</Grid>
<Grid container justify="flex-end" spacing={1} style={{ margin: 8 }}>
<Grid>
<Button
target="_blank"
rel="noopener noreferrer"
href={brpcLinkChanger(`${ip}:${raylet.brpcPort}`)}
>
BRPC {brpcPort}
</Button>
</Grid>
<Grid>
<Button>
<Link to={`/log/${encodeURIComponent(logUrl)}`}>log</Link>
</Button>
</Grid>
</Grid>
</Paper>
);
};
const Nodes = () => {
const classes = useStyles();
const {
msg,
isRefreshing,
onSwitchChange,
nodeList,
changeFilter,
page,
setPage,
setSortKey,
setOrderDesc,
mode,
setMode,
} = useNodeList();
return (
<div className={classes.root}>
<Loading loading={msg.startsWith("Loading")} />
<TitleCard title="NODES">
Auto Refresh:
<Switch
checked={isRefreshing}
onChange={onSwitchChange}
name="refresh"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
<br />
Request Status: {msg}
</TitleCard>
<TitleCard title="Statistics">
<StateCounter type="node" list={nodeList} />
</TitleCard>
<TitleCard title="Node List">
<Grid container alignItems="center">
<Grid item>
<SearchInput
label="Host"
onChange={(value) => changeFilter("hostname", value.trim())}
/>
</Grid>
<Grid item>
<SearchInput
label="IP"
onChange={(value) => changeFilter("ip", value.trim())}
/>
</Grid>
<Grid item>
<SearchSelect
label="State"
onChange={(value) => changeFilter("state", value.trim())}
options={["ALIVE", "DEAD"]}
/>
</Grid>
<Grid item>
<SearchInput
label="Page Size"
onChange={(value) =>
setPage("pageSize", Math.min(Number(value), 500) || 10)
}
/>
</Grid>
<Grid item>
<SearchSelect
label="Sort By"
options={[
["state", "State"],
["mem[2]", "Used Memory"],
["mem[0]", "Total Memory"],
["cpu", "CPU"],
["net[0]", "Sent"],
["net[1]", "Received"],
["disk./.used", "Used Disk"],
]}
onChange={(val) => setSortKey(val)}
/>
</Grid>
<Grid item>
<span style={{ margin: 8, marginTop: 0 }}>
Reverse:
<Switch onChange={(_, checked) => setOrderDesc(checked)} />
</span>
</Grid>
<Grid item>
<ButtonGroup size="small">
<Button
onClick={() => setMode("table")}
color={mode === "table" ? "primary" : "default"}
>
Table
</Button>
<Button
onClick={() => setMode("card")}
color={mode === "card" ? "primary" : "default"}
>
Card
</Button>
</ButtonGroup>
</Grid>
</Grid>
<div>
<Pagination
count={Math.ceil(nodeList.length / page.pageSize)}
page={page.pageNo}
onChange={(e, pageNo) => setPage("pageNo", pageNo)}
/>
</div>
{mode === "table" && (
<TableContainer>
<Table>
<TableHead>
<TableRow>
{columns.map((col) => (
<TableCell align="center" key={col}>
{col}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{nodeList
.slice(
(page.pageNo - 1) * page.pageSize,
page.pageNo * page.pageSize,
)
.map(
(
{
hostname = "",
ip = "",
cpu = 0,
mem = [],
disk,
net = [0, 0],
raylet,
logUrl,
}: NodeDetail,
i,
) => (
<TableRow key={hostname + i}>
<TableCell>
<StatusChip type="node" status={raylet.state} />
</TableCell>
<TableCell align="center">
<Tooltip title={raylet.nodeId} arrow interactive>
<Link to={`/node/${raylet.nodeId}`}>
{raylet.nodeId.slice(0, 5)}
</Link>
</Tooltip>
</TableCell>
<TableCell align="center">{hostname}</TableCell>
<TableCell align="center">{ip}</TableCell>
<TableCell>
<PercentageBar num={Number(cpu)} total={100}>
{cpu}%
</PercentageBar>
</TableCell>
<TableCell>
<PercentageBar
num={Number(mem[0] - mem[1])}
total={mem[0]}
>
{memoryConverter(mem[0] - mem[1])}/
{memoryConverter(mem[0])}({mem[2]}%)
</PercentageBar>
</TableCell>
<TableCell>
{disk && disk["/"] && (
<PercentageBar
num={Number(disk["/"].used)}
total={disk["/"].total}
>
{memoryConverter(disk["/"].used)}/
{memoryConverter(disk["/"].total)}(
{disk["/"].percent}%)
</PercentageBar>
)}
</TableCell>
<TableCell align="center">
{memoryConverter(net[0])}/s
</TableCell>
<TableCell align="center">
{memoryConverter(net[1])}/s
</TableCell>
<TableCell align="center">
{raylet.brpcPort && (
<a
target="_blank"
rel="noopener noreferrer"
href={brpcLinkChanger(`${ip}:${raylet.brpcPort}`)}
>
{raylet.brpcPort}
</a>
)}
</TableCell>
<TableCell align="center">
{!!raylet.startTime && (
<p>
Start Time:{" "}
{dayjs(raylet.startTime * 1000).format(
"YYYY/MM/DD HH:mm:ss",
)}
</p>
)}
{!!raylet.terminateTime && (
<p>
End Time:{" "}
{dayjs(raylet.terminateTime * 1000).format(
"YYYY/MM/DD HH:mm:ss",
)}
</p>
)}
</TableCell>
<TableCell>
<Link to={`/log/${encodeURIComponent(logUrl)}`}>
Log
</Link>
</TableCell>
</TableRow>
),
)}
</TableBody>
</Table>
</TableContainer>
)}
{mode === "card" && (
<Grid container>
{nodeList
.slice(
(page.pageNo - 1) * page.pageSize,
page.pageNo * page.pageSize,
)
.map((e) => (
<Grid item xs={6}>
<NodeCard node={e} />
</Grid>
))}
</Grid>
)}
</TitleCard>
</div>
);
};
export default Nodes;