mirror of
https://github.com/wassname/ray.git
synced 2026-08-16 11:27:09 +08:00
[Dashboard] Add the new dashboard code and prompt users to try it (#11667)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 }),
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user