mirror of
https://github.com/wassname/ray.git
synced 2026-07-26 13:37:24 +08:00
[Dashboard] Add the new dashboard code and prompt users to try it (#11667)
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
InputAdornment,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
} from "@material-ui/core";
|
||||
import { orange } from "@material-ui/core/colors";
|
||||
import { SearchOutlined } from "@material-ui/icons";
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete";
|
||||
import Pagination from "@material-ui/lab/Pagination";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { GlobalContext } from "../App";
|
||||
import { Actor } from "../type/actor";
|
||||
import { Worker } from "../type/worker";
|
||||
import { longTextCut } from "../util/func";
|
||||
import { useFilter } from "../util/hook";
|
||||
import StateCounter from "./StatesCounter";
|
||||
import { StatusChip } from "./StatusChip";
|
||||
import RayletWorkerTable, { ExpandableTableRow } from "./WorkerTable";
|
||||
|
||||
const ActorTable = ({
|
||||
actors = {},
|
||||
workers = [],
|
||||
}: {
|
||||
actors: { [actorId: string]: Actor };
|
||||
workers?: Worker[];
|
||||
}) => {
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
const { changeFilter, filterFunc } = useFilter();
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const { ipLogMap } = useContext(GlobalContext);
|
||||
const actorList = Object.values(actors || {})
|
||||
.map((e) => ({
|
||||
...e,
|
||||
functionDesc: Object.values(
|
||||
e.taskSpec?.functionDescriptor?.javaFunctionDescriptor ||
|
||||
e.taskSpec?.functionDescriptor?.pythonFunctionDescriptor ||
|
||||
{},
|
||||
).join(" "),
|
||||
}))
|
||||
.filter(filterFunc);
|
||||
const list = actorList.slice((pageNo - 1) * pageSize, pageNo * pageSize);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center" }}>
|
||||
<Autocomplete
|
||||
style={{ margin: 8, width: 120 }}
|
||||
options={Array.from(
|
||||
new Set(Object.values(actors).map((e) => e.state)),
|
||||
)}
|
||||
onInputChange={(_: any, value: string) => {
|
||||
changeFilter("state", value.trim());
|
||||
}}
|
||||
renderInput={(params: TextFieldProps) => (
|
||||
<TextField {...params} label="State" />
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
style={{ margin: 8, width: 150 }}
|
||||
options={Array.from(
|
||||
new Set(Object.values(actors).map((e) => e.address?.ipAddress)),
|
||||
)}
|
||||
onInputChange={(_: any, value: string) => {
|
||||
changeFilter("address.ipAddress", value.trim());
|
||||
}}
|
||||
renderInput={(params: TextFieldProps) => (
|
||||
<TextField {...params} label="IP" />
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
style={{ margin: 8, width: 120 }}
|
||||
label="PID"
|
||||
size="small"
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
changeFilter("pid", value.trim());
|
||||
},
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchOutlined />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{ margin: 8, width: 200 }}
|
||||
label="Task Func Desc"
|
||||
size="small"
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
changeFilter("functionDesc", value.trim());
|
||||
},
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchOutlined />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{ margin: 8, width: 120 }}
|
||||
label="Name"
|
||||
size="small"
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
changeFilter("name", value.trim());
|
||||
},
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchOutlined />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{ margin: 8, width: 120 }}
|
||||
label="Actor ID"
|
||||
size="small"
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
changeFilter("actorId", value.trim());
|
||||
},
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchOutlined />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{ margin: 8, width: 120 }}
|
||||
label="Page Size"
|
||||
size="small"
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
setPageSize(Math.min(Number(value), 500) || 10);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div>
|
||||
<Pagination
|
||||
page={pageNo}
|
||||
onChange={(e, num) => setPageNo(num)}
|
||||
count={Math.ceil(actorList.length / pageSize)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<StateCounter type="actor" list={actorList} />
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{[
|
||||
"",
|
||||
"ID(Num Restarts)",
|
||||
"Name",
|
||||
"Task Func Desc",
|
||||
"Job Id",
|
||||
"Pid",
|
||||
"IP",
|
||||
"Port",
|
||||
"State",
|
||||
"Log",
|
||||
].map((col) => (
|
||||
<TableCell align="center" key={col}>
|
||||
{col}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{list.map(
|
||||
({
|
||||
actorId,
|
||||
functionDesc,
|
||||
jobId,
|
||||
pid,
|
||||
address,
|
||||
state,
|
||||
name,
|
||||
numRestarts,
|
||||
}) => (
|
||||
<ExpandableTableRow
|
||||
length={
|
||||
workers.filter(
|
||||
(e) =>
|
||||
e.pid === pid &&
|
||||
address.ipAddress === e.coreWorkerStats[0].ipAddress,
|
||||
).length
|
||||
}
|
||||
expandComponent={
|
||||
<RayletWorkerTable
|
||||
actorMap={{}}
|
||||
workers={workers.filter(
|
||||
(e) =>
|
||||
e.pid === pid &&
|
||||
address.ipAddress === e.coreWorkerStats[0].ipAddress,
|
||||
)}
|
||||
mini
|
||||
/>
|
||||
}
|
||||
key={actorId}
|
||||
>
|
||||
<TableCell
|
||||
align="center"
|
||||
style={{
|
||||
color: Number(numRestarts) > 0 ? orange[500] : "inherit",
|
||||
}}
|
||||
>
|
||||
{actorId}({numRestarts})
|
||||
</TableCell>
|
||||
<TableCell align="center">{name}</TableCell>
|
||||
<TableCell align="center">
|
||||
{longTextCut(functionDesc, 60)}
|
||||
</TableCell>
|
||||
<TableCell align="center">{jobId}</TableCell>
|
||||
<TableCell align="center">{pid}</TableCell>
|
||||
<TableCell align="center">{address?.ipAddress}</TableCell>
|
||||
<TableCell align="center">{address?.port}</TableCell>
|
||||
<TableCell align="center">
|
||||
<StatusChip type="actor" status={state} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{ipLogMap[address?.ipAddress] && (
|
||||
<Link
|
||||
target="_blank"
|
||||
to={`/log/${encodeURIComponent(
|
||||
ipLogMap[address?.ipAddress],
|
||||
)}?fileName=${jobId}-${pid}`}
|
||||
>
|
||||
Log
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
</ExpandableTableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActorTable;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Backdrop, CircularProgress } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
const Loading = ({ loading }: { loading: boolean }) => (
|
||||
<Backdrop open={loading} style={{ zIndex: 100 }}>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,221 @@
|
||||
import dayjs from "dayjs";
|
||||
import low from "lowlight";
|
||||
import React, {
|
||||
CSSProperties,
|
||||
MutableRefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FixedSizeList as List } from "react-window";
|
||||
import "./darcula.css";
|
||||
import "./github.css";
|
||||
import "./index.css";
|
||||
import { getDefaultTheme } from "../../App";
|
||||
|
||||
const uniqueKeySelector = () => Math.random().toString(16).slice(-8);
|
||||
|
||||
const timeReg = /(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)\s+([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]/;
|
||||
|
||||
const value2react = (
|
||||
{ type, tagName, properties, children, value = "" }: any,
|
||||
key: string,
|
||||
keywords: string = "",
|
||||
) => {
|
||||
switch (type) {
|
||||
case "element":
|
||||
return React.createElement(
|
||||
tagName,
|
||||
{
|
||||
className: properties.className[0],
|
||||
key: `${key}line${uniqueKeySelector()}`,
|
||||
},
|
||||
children.map((e: any, i: number) =>
|
||||
value2react(e, `${key}-${i}`, keywords),
|
||||
),
|
||||
);
|
||||
case "text":
|
||||
if (keywords && value.includes(keywords)) {
|
||||
const afterChildren = [];
|
||||
const vals = value.split(keywords);
|
||||
let tmp = vals.shift();
|
||||
if (!tmp) {
|
||||
return React.createElement(
|
||||
"span",
|
||||
{ className: "find-kws" },
|
||||
keywords,
|
||||
);
|
||||
}
|
||||
while (typeof tmp === "string") {
|
||||
if (tmp !== "") {
|
||||
afterChildren.push(tmp);
|
||||
} else {
|
||||
afterChildren.push(
|
||||
React.createElement("span", { className: "find-kws" }, keywords),
|
||||
);
|
||||
}
|
||||
|
||||
tmp = vals.shift();
|
||||
if (tmp) {
|
||||
afterChildren.push(
|
||||
React.createElement("span", { className: "find-kws" }, keywords),
|
||||
);
|
||||
}
|
||||
}
|
||||
return afterChildren;
|
||||
}
|
||||
return value;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export type LogVirtualViewProps = {
|
||||
content: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
theme?: "light" | "dark";
|
||||
language?: string;
|
||||
focusLine?: number;
|
||||
keywords?: string;
|
||||
style?: { [key: string]: string | number };
|
||||
listRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
onScrollBottom?: (event: Event) => void;
|
||||
revert?: boolean;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
const LogVirtualView: React.FC<LogVirtualViewProps> = ({
|
||||
content,
|
||||
width = "100%",
|
||||
height,
|
||||
fontSize = 12,
|
||||
theme = getDefaultTheme(),
|
||||
keywords = "",
|
||||
language = "dos",
|
||||
focusLine = 1,
|
||||
style = {},
|
||||
listRef,
|
||||
onScrollBottom,
|
||||
revert = false,
|
||||
startTime,
|
||||
endTime,
|
||||
}) => {
|
||||
const [logs, setLogs] = useState<{ i: number; origin: string }[]>([]);
|
||||
const total = logs.length;
|
||||
const timmer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const el = useRef<List>(null);
|
||||
const outter = useRef<HTMLDivElement>(null);
|
||||
if (listRef) {
|
||||
listRef.current = outter.current;
|
||||
}
|
||||
const itemRenderer = ({
|
||||
index,
|
||||
style: s,
|
||||
}: {
|
||||
index: number;
|
||||
style: CSSProperties;
|
||||
}) => {
|
||||
const { i, origin } = logs[revert ? logs.length - 1 - index : index];
|
||||
return (
|
||||
<div
|
||||
key={`${index}list`}
|
||||
style={{ ...s, overflowX: "visible", whiteSpace: "pre" }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
marginRight: 4,
|
||||
width: `${logs.length}`.length * 6 + 4,
|
||||
color: "#999",
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
{low
|
||||
.highlight(language, origin)
|
||||
.value.map((v) => value2react(v, index.toString(), keywords))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const originContent = content.split("\n");
|
||||
if (timmer.current) {
|
||||
clearTimeout(timmer.current);
|
||||
}
|
||||
timmer.current = setTimeout(() => {
|
||||
setLogs(
|
||||
originContent
|
||||
.map((e, i) => ({
|
||||
i,
|
||||
origin: e,
|
||||
time: (e?.match(timeReg) || [""])[0],
|
||||
}))
|
||||
.filter((e) => {
|
||||
let bool = e.origin.includes(keywords);
|
||||
if (
|
||||
e.time &&
|
||||
startTime &&
|
||||
!dayjs(e.time).isAfter(dayjs(startTime))
|
||||
) {
|
||||
bool = false;
|
||||
}
|
||||
if (e.time && endTime && !dayjs(e.time).isBefore(dayjs(endTime))) {
|
||||
bool = false;
|
||||
}
|
||||
return bool;
|
||||
})
|
||||
.map((e) => ({
|
||||
...e,
|
||||
})),
|
||||
);
|
||||
}, 500);
|
||||
}, [content, keywords, language, startTime, endTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (el.current) {
|
||||
el.current?.scrollTo((focusLine - 1) * (fontSize + 6));
|
||||
}
|
||||
}, [focusLine, fontSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (outter.current) {
|
||||
const scrollFunc = (event: any) => {
|
||||
const { target } = event;
|
||||
if (
|
||||
target &&
|
||||
target.scrollTop + target.clientHeight === target.scrollHeight
|
||||
) {
|
||||
if (onScrollBottom) {
|
||||
onScrollBottom(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
outter.current.addEventListener("scroll", scrollFunc);
|
||||
return () => outter?.current?.removeEventListener("scroll", scrollFunc);
|
||||
}
|
||||
}, [onScrollBottom]);
|
||||
|
||||
return (
|
||||
<List
|
||||
height={height || (content.split("\n").length + 1) * 18}
|
||||
width={width}
|
||||
ref={el}
|
||||
outerRef={outter}
|
||||
className={`hljs-${theme}`}
|
||||
style={{
|
||||
fontSize,
|
||||
...style,
|
||||
}}
|
||||
itemSize={fontSize + 6}
|
||||
itemCount={total}
|
||||
>
|
||||
{itemRenderer}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogVirtualView;
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Dracula Theme v1.2.0
|
||||
https://github.com/zenorocha/dracula-theme
|
||||
Copyright 2015, All rights reserved
|
||||
Code licensed under the MIT license
|
||||
http://zenorocha.mit-license.org
|
||||
@author Éverton Ribeiro <nuxlli@gmail.com>
|
||||
@author Zeno Rocha <hi@zenorocha.com>
|
||||
*/
|
||||
.hljs-dark {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
.hljs-dark .hljs-number,
|
||||
.hljs-dark .hljs-keyword,
|
||||
.hljs-dark .hljs-selector-tag,
|
||||
.hljs-dark .hljs-literal,
|
||||
.hljs-dark .hljs-section,
|
||||
.hljs-dark .hljs-link {
|
||||
color: #8be9fd;
|
||||
}
|
||||
.hljs-dark .hljs-function .hljs-keyword {
|
||||
color: #ff79c6;
|
||||
}
|
||||
.hljs-dark .hljs-string,
|
||||
.hljs-dark .hljs-title,
|
||||
.hljs-dark .hljs-name,
|
||||
.hljs-dark .hljs-type,
|
||||
.hljs-dark .hljs-attribute,
|
||||
.hljs-dark .hljs-symbol,
|
||||
.hljs-dark .hljs-bullet,
|
||||
.hljs-dark .hljs-addition,
|
||||
.hljs-dark .hljs-variable,
|
||||
.hljs-dark .hljs-template-tag,
|
||||
.hljs-dark .hljs-template-variable {
|
||||
color: #f1fa8c;
|
||||
}
|
||||
.hljs-dark .hljs-comment,
|
||||
.hljs-dark .hljs-quote,
|
||||
.hljs-dark .hljs-deletion,
|
||||
.hljs-dark .hljs-meta {
|
||||
color: #6272a4;
|
||||
}
|
||||
.hljs-dark .hljs-keyword,
|
||||
.hljs-dark .hljs-selector-tag,
|
||||
.hljs-dark .hljs-literal,
|
||||
.hljs-dark .hljs-title,
|
||||
.hljs-dark .hljs-section,
|
||||
.hljs-dark .hljs-doctag,
|
||||
.hljs-dark .hljs-type,
|
||||
.hljs-dark .hljs-name,
|
||||
.hljs-dark .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-dark .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
*/
|
||||
|
||||
.hljs-light {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-comment,
|
||||
.hljs-light .hljs-quote {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-keyword,
|
||||
.hljs-light .hljs-selector-tag,
|
||||
.hljs-light .hljs-subst {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-number,
|
||||
.hljs-light .hljs-literal,
|
||||
.hljs-light .hljs-variable,
|
||||
.hljs-light .hljs-template-variable,
|
||||
.hljs-light .hljs-tag .hljs-attr {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-string,
|
||||
.hljs-light .hljs-doctag {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-title,
|
||||
.hljs-light .hljs-section,
|
||||
.hljs-light .hljs-selector-id {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-type,
|
||||
.hljs-light .hljs-class .hljs-title {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-tag,
|
||||
.hljs-light .hljs-name,
|
||||
.hljs-light .hljs-attribute {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-regexp,
|
||||
.hljs-light .hljs-link {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-symbol,
|
||||
.hljs-light .hljs-bullet {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-built_in,
|
||||
.hljs-light .hljs-builtin-name {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-meta {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-light .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
span.find-kws {
|
||||
background-color: #ffd800;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { makeStyles } from "@material-ui/core";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
const useStyle = makeStyles((theme) => ({
|
||||
container: {
|
||||
background: "linear-gradient(45deg, #21CBF3ee 30%, #2196F3ee 90%)",
|
||||
border: `1px solid #ffffffbb`,
|
||||
padding: "0 12px",
|
||||
height: 18,
|
||||
lineHeight: "18px",
|
||||
position: "relative",
|
||||
boxSizing: "content-box",
|
||||
borderRadius: 4,
|
||||
},
|
||||
displayBar: {
|
||||
background: theme.palette.background.paper,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
height: 18,
|
||||
transition: "0.5s width",
|
||||
borderRadius: 2,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
border: "2px solid transparent",
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
zIndex: 2,
|
||||
position: "relative",
|
||||
color: theme.palette.text.primary,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
},
|
||||
}));
|
||||
|
||||
const PercentageBar = (
|
||||
props: PropsWithChildren<{ num: number; total: number }>,
|
||||
) => {
|
||||
const { num, total } = props;
|
||||
const classes = useStyle();
|
||||
const per = Math.round((num / total) * 100);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<div
|
||||
className={classes.displayBar}
|
||||
style={{
|
||||
width: `${Math.min(Math.max(0, 100 - per), 100)}%`,
|
||||
}}
|
||||
/>
|
||||
<div className={classes.text}>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PercentageBar;
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
InputAdornment,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
import { SearchOutlined } from "@material-ui/icons";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
search: {
|
||||
margin: theme.spacing(1),
|
||||
marginTop: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export const SearchInput = ({
|
||||
label,
|
||||
onChange,
|
||||
defaultValue,
|
||||
}: {
|
||||
label: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
className={classes.search}
|
||||
size="small"
|
||||
label={label}
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
},
|
||||
defaultValue,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchOutlined />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchSelect = ({
|
||||
label,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
label: string;
|
||||
onChange?: (value: string) => void;
|
||||
options: (string | [string, string])[];
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<TextField
|
||||
className={classes.search}
|
||||
size="small"
|
||||
label={label}
|
||||
select
|
||||
SelectProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
if (onChange) {
|
||||
onChange(value as string);
|
||||
}
|
||||
},
|
||||
style: {
|
||||
width: 100,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">All</MenuItem>
|
||||
{options.map((e) =>
|
||||
typeof e === "string" ? (
|
||||
<MenuItem value={e}>{e}</MenuItem>
|
||||
) : (
|
||||
<MenuItem value={e[0]}>{e[1]}</MenuItem>
|
||||
),
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
Grow,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
import { red } from "@material-ui/core/colors";
|
||||
import { Build, Close } from "@material-ui/icons";
|
||||
import React, { useState } from "react";
|
||||
import { StatusChip } from "./StatusChip";
|
||||
|
||||
const chunkArray = (myArray: string[], chunk_size: number) => {
|
||||
const results = [];
|
||||
|
||||
while (myArray.length) {
|
||||
results.push(myArray.splice(0, chunk_size));
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const revertBit = (str: string) => {
|
||||
return chunkArray(str.split(""), 2)
|
||||
.reverse()
|
||||
.map((e) => e.join(""))
|
||||
.join("");
|
||||
};
|
||||
|
||||
const detectFlag = (str: string, offset: number) => {
|
||||
const flag = parseInt(str, 16);
|
||||
const mask = 1 << offset;
|
||||
|
||||
return Number(!!(flag & mask));
|
||||
};
|
||||
|
||||
const useStyle = makeStyles((theme) => ({
|
||||
toolContainer: {
|
||||
background: theme.palette.primary.main,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 48,
|
||||
position: "fixed",
|
||||
bottom: 100,
|
||||
left: 50,
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
icon: {
|
||||
position: "absolute",
|
||||
left: 12,
|
||||
cursor: "pointer",
|
||||
top: 12,
|
||||
},
|
||||
popover: {
|
||||
position: "absolute",
|
||||
left: 50,
|
||||
bottom: 48,
|
||||
width: 500,
|
||||
height: 300,
|
||||
padding: 6,
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.text.disabled,
|
||||
},
|
||||
close: {
|
||||
float: "right",
|
||||
color: theme.palette.error.main,
|
||||
cursor: "pointer",
|
||||
},
|
||||
}));
|
||||
|
||||
const ObjectIdReader = () => {
|
||||
const [id, setId] = useState("");
|
||||
const tagList = [
|
||||
["Create From Task", 15, 1],
|
||||
["Put Object", 14, 0],
|
||||
["Return Object", 14, 1],
|
||||
] as [string, number, number][];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 8 }}>
|
||||
<TextField
|
||||
style={{ width: "100%" }}
|
||||
id="standard-basic"
|
||||
label="Object Id"
|
||||
InputProps={{
|
||||
onChange: ({ target: { value } }) => {
|
||||
setId(value);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
{id.length === 40 ? (
|
||||
<div style={{ padding: 8 }}>
|
||||
Job ID: {id.slice(24, 28)} <br />
|
||||
Actor ID: {id.slice(16, 28)} <br />
|
||||
Task ID: {id.slice(0, 28)} <br />
|
||||
Index: {parseInt(revertBit(id.slice(32)), 16)} <br />
|
||||
Flag: {revertBit(id.slice(28, 32))}
|
||||
<br />
|
||||
<br />
|
||||
{tagList
|
||||
.filter(
|
||||
([a, b, c]) => detectFlag(revertBit(id.slice(28, 32)), b) === c,
|
||||
)
|
||||
.map(([name]) => (
|
||||
<StatusChip key={name} type="tag" status={name} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: red[500] }}>
|
||||
Object ID should be 40 letters long
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Tools = () => {
|
||||
const [sel, setSel] = useState("oid_converter");
|
||||
const toolMap = {
|
||||
oid_converter: <ObjectIdReader />,
|
||||
} as { [key: string]: JSX.Element };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs value={sel} onChange={(e, val) => setSel(val)}>
|
||||
<Tab
|
||||
value="oid_converter"
|
||||
label={<span style={{ fontSize: 12 }}>Object ID Reader</span>}
|
||||
/>
|
||||
</Tabs>
|
||||
{toolMap[sel]}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SpeedTools = () => {
|
||||
const [show, setShow] = useState(false);
|
||||
const classes = useStyle();
|
||||
|
||||
return (
|
||||
<Paper className={classes.toolContainer}>
|
||||
<Build className={classes.icon} onClick={() => setShow(!show)} />
|
||||
<Grow in={show} style={{ transformOrigin: "300 500 0" }}>
|
||||
<Paper className={classes.popover}>
|
||||
<Close className={classes.close} onClick={() => setShow(false)} />
|
||||
<Tools />
|
||||
</Paper>
|
||||
</Grow>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeedTools;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Grid } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { StatusChip } from "./StatusChip";
|
||||
|
||||
const StateCounter = ({
|
||||
type,
|
||||
list,
|
||||
}: {
|
||||
type: string;
|
||||
list: { state: string }[];
|
||||
}) => {
|
||||
const stateMap = {} as { [state: string]: number };
|
||||
list.forEach(({ state }) => {
|
||||
stateMap[state] = stateMap[state] + 1 || 1;
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item>
|
||||
<StatusChip status="TOTAL" type={type} suffix={`x ${list.length}`} />
|
||||
</Grid>
|
||||
{Object.entries(stateMap).map(([s, num]) => (
|
||||
<Grid item>
|
||||
<StatusChip status={s} type={type} suffix={` x ${num}`} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default StateCounter;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Color } from "@material-ui/core";
|
||||
import {
|
||||
blue,
|
||||
blueGrey,
|
||||
cyan,
|
||||
green,
|
||||
grey,
|
||||
lightBlue,
|
||||
red,
|
||||
} from "@material-ui/core/colors";
|
||||
import { CSSProperties } from "@material-ui/core/styles/withStyles";
|
||||
import React, { ReactNode } from "react";
|
||||
import { ActorEnum } from "../type/actor";
|
||||
|
||||
const colorMap = {
|
||||
node: {
|
||||
ALIVE: green,
|
||||
DEAD: red,
|
||||
},
|
||||
actor: {
|
||||
[ActorEnum.ALIVE]: green,
|
||||
[ActorEnum.DEAD]: red,
|
||||
[ActorEnum.PENDING]: blue,
|
||||
[ActorEnum.RECONSTRUCTING]: lightBlue,
|
||||
},
|
||||
job: {
|
||||
INIT: grey,
|
||||
SUBMITTED: blue,
|
||||
DISPATCHED: lightBlue,
|
||||
RUNNING: green,
|
||||
COMPLETED: cyan,
|
||||
FINISHED: cyan,
|
||||
FAILED: red,
|
||||
},
|
||||
} as {
|
||||
[key: string]: {
|
||||
[key: string]: Color;
|
||||
};
|
||||
};
|
||||
|
||||
const typeMap = {
|
||||
deps: blue,
|
||||
INFO: cyan,
|
||||
ERROR: red,
|
||||
} as {
|
||||
[key: string]: Color;
|
||||
};
|
||||
|
||||
export const StatusChip = ({
|
||||
type,
|
||||
status,
|
||||
suffix,
|
||||
}: {
|
||||
type: string;
|
||||
status: string | ActorEnum | ReactNode;
|
||||
suffix?: string;
|
||||
}) => {
|
||||
const style = {
|
||||
padding: "2px 8px",
|
||||
border: "solid 1px",
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
margin: 2,
|
||||
} as CSSProperties;
|
||||
|
||||
let color = blueGrey as Color;
|
||||
|
||||
if (typeMap[type]) {
|
||||
color = typeMap[type];
|
||||
} else if (
|
||||
typeof status === "string" &&
|
||||
colorMap[type] &&
|
||||
colorMap[type][status]
|
||||
) {
|
||||
color = colorMap[type][status];
|
||||
}
|
||||
|
||||
style.color = color[500];
|
||||
style.borderColor = color[500];
|
||||
if (color !== blueGrey) {
|
||||
style.backgroundColor = `${color[500]}20`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={style}>
|
||||
{status}
|
||||
{suffix}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { makeStyles, Paper } from "@material-ui/core";
|
||||
import React, { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: theme.spacing(1.5),
|
||||
margin: [theme.spacing(2), theme.spacing(1)].map((e) => `${e}px`).join(" "),
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.typography.fontSize + 2,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
body: {
|
||||
padding: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
const TitleCard = ({
|
||||
title,
|
||||
children,
|
||||
}: PropsWithChildren<{ title: ReactNode | string }>) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.card}>
|
||||
<div className={classes.title}>{title}</div>
|
||||
<div className={classes.body}>{children}</div>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleCard;
|
||||
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
IconButton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from "@material-ui/icons";
|
||||
import dayjs from "dayjs";
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { GlobalContext } from "../App";
|
||||
import { Actor } from "../type/actor";
|
||||
import { CoreWorkerStats, Worker } from "../type/worker";
|
||||
import { memoryConverter } from "../util/converter";
|
||||
import { longTextCut } from "../util/func";
|
||||
|
||||
import { useFilter } from "../util/hook";
|
||||
import ActorTable from "./ActorTable";
|
||||
import PercentageBar from "./PercentageBar";
|
||||
import { SearchInput } from "./SearchComponent";
|
||||
|
||||
export const ExpandableTableRow = ({
|
||||
children,
|
||||
expandComponent,
|
||||
length,
|
||||
stateKey = "",
|
||||
...otherProps
|
||||
}: PropsWithChildren<{
|
||||
expandComponent: ReactNode;
|
||||
length: number;
|
||||
stateKey?: string;
|
||||
}>) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (stateKey.startsWith("ON")) {
|
||||
setIsExpanded(true);
|
||||
} else if (stateKey.startsWith("OFF")) {
|
||||
setIsExpanded(false);
|
||||
}
|
||||
}, [stateKey]);
|
||||
|
||||
if (length < 1) {
|
||||
return (
|
||||
<TableRow {...otherProps}>
|
||||
<TableCell padding="checkbox" />
|
||||
{children}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow {...otherProps}>
|
||||
<TableCell padding="checkbox">
|
||||
<IconButton
|
||||
style={{ color: "inherit" }}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{length}
|
||||
{isExpanded ? <KeyboardArrowDown /> : <KeyboardArrowRight />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{children}
|
||||
</TableRow>
|
||||
{isExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={24}>{expandComponent}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkerDetailTable = ({
|
||||
actorMap,
|
||||
coreWorkerStats,
|
||||
}: {
|
||||
actorMap: { [actorId: string]: Actor };
|
||||
coreWorkerStats: CoreWorkerStats[];
|
||||
}) => {
|
||||
const actors = {} as { [actorId: string]: Actor };
|
||||
(coreWorkerStats || [])
|
||||
.filter((e) => actorMap[e.actorId])
|
||||
.forEach((e) => (actors[e.actorId] = actorMap[e.actorId]));
|
||||
|
||||
if (!Object.values(actors).length) {
|
||||
return <p>The Worker Haven't Had Related Actor Yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<ActorTable actors={actors} />
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const RayletWorkerTable = ({
|
||||
workers = [],
|
||||
actorMap,
|
||||
mini,
|
||||
}: {
|
||||
workers: Worker[];
|
||||
actorMap: { [actorId: string]: Actor };
|
||||
mini?: boolean;
|
||||
}) => {
|
||||
const { changeFilter, filterFunc } = useFilter();
|
||||
const [key, setKey] = useState("");
|
||||
const { nodeMap, ipLogMap } = useContext(GlobalContext);
|
||||
const open = () => setKey(`ON${Math.random()}`);
|
||||
const close = () => setKey(`OFF${Math.random()}`);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!mini && (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<SearchInput
|
||||
label="Pid"
|
||||
onChange={(value) => changeFilter("pid", value)}
|
||||
/>
|
||||
<Button onClick={open}>Expand All</Button>
|
||||
<Button onClick={close}>Collapse All</Button>
|
||||
</div>
|
||||
)}{" "}
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{[
|
||||
"",
|
||||
"Pid",
|
||||
"CPU",
|
||||
"CPU Times",
|
||||
"Memory",
|
||||
"CMD Line",
|
||||
"Create Time",
|
||||
"Log",
|
||||
"Ops",
|
||||
"IP/Hostname",
|
||||
].map((col) => (
|
||||
<TableCell align="center" key={col}>
|
||||
{col}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{workers
|
||||
.filter(filterFunc)
|
||||
.sort((aWorker, bWorker) => {
|
||||
const a =
|
||||
(aWorker.coreWorkerStats || []).filter(
|
||||
(e) => actorMap[e.actorId],
|
||||
).length || 0;
|
||||
const b =
|
||||
(bWorker.coreWorkerStats || []).filter(
|
||||
(e) => actorMap[e.actorId],
|
||||
).length || 0;
|
||||
return b - a;
|
||||
})
|
||||
.map(
|
||||
({
|
||||
pid,
|
||||
cpuPercent,
|
||||
cpuTimes,
|
||||
memoryInfo,
|
||||
cmdline,
|
||||
createTime,
|
||||
coreWorkerStats = [],
|
||||
language,
|
||||
ip,
|
||||
hostname,
|
||||
}) => (
|
||||
<ExpandableTableRow
|
||||
expandComponent={
|
||||
<WorkerDetailTable
|
||||
actorMap={actorMap}
|
||||
coreWorkerStats={coreWorkerStats}
|
||||
/>
|
||||
}
|
||||
length={
|
||||
(coreWorkerStats || []).filter((e) => actorMap[e.actorId])
|
||||
.length
|
||||
}
|
||||
key={pid}
|
||||
stateKey={key}
|
||||
>
|
||||
<TableCell align="center">{pid}</TableCell>
|
||||
<TableCell align="center">
|
||||
<PercentageBar num={Number(cpuPercent)} total={100}>
|
||||
{cpuPercent}%
|
||||
</PercentageBar>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div style={{ maxHeight: 55, overflow: "auto" }}>
|
||||
{Object.entries(cpuTimes || {}).map(([key, val]) => (
|
||||
<div style={{ margin: 4 }}>
|
||||
{key}:{val}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div style={{ maxHeight: 55, overflow: "auto" }}>
|
||||
{Object.entries(memoryInfo || {}).map(([key, val]) => (
|
||||
<div style={{ margin: 4 }}>
|
||||
{key}:{memoryConverter(val)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" style={{ lineBreak: "anywhere" }}>
|
||||
{cmdline && longTextCut(cmdline.filter((e) => e).join(" "))}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{dayjs(createTime * 1000).format("YYYY/MM/DD HH:mm:ss")}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Grid container spacing={2}>
|
||||
{ipLogMap[ip] && (
|
||||
<Grid item>
|
||||
<Link
|
||||
target="_blank"
|
||||
to={`/log/${encodeURIComponent(
|
||||
ipLogMap[ip],
|
||||
)}?fileName=${
|
||||
coreWorkerStats[0].jobId || ""
|
||||
}-${pid}`}
|
||||
>
|
||||
Log
|
||||
</Link>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{language === "JAVA" && (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`#/cmd/jstack/${coreWorkerStats[0]?.ipAddress}/${pid}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
jstack
|
||||
</Button>{" "}
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`#/cmd/jmap/${coreWorkerStats[0]?.ipAddress}/${pid}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
jmap
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`#/cmd/jstat/${coreWorkerStats[0]?.ipAddress}/${pid}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
jstat
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{ip}
|
||||
<br />
|
||||
{nodeMap[hostname] ? (
|
||||
<Link target="_blank" to={`/node/${nodeMap[hostname]}`}>
|
||||
{hostname}
|
||||
</Link>
|
||||
) : (
|
||||
hostname
|
||||
)}
|
||||
</TableCell>
|
||||
</ExpandableTableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default RayletWorkerTable;
|
||||
Reference in New Issue
Block a user