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

This commit is contained in:
Dominic Ming
2021-01-29 15:22:26 +08:00
committed by GitHub
parent 42d501d747
commit 752da83bb7
52 changed files with 4650 additions and 33 deletions
@@ -0,0 +1,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;