feat: Show task_executor heartbeat #3409 (#3461)

### What problem does this PR solve?

feat: Show task_executor heartbeat #3409
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-11-18 17:23:49 +08:00 committed by GitHub
parent 4b3eeaa6ef
commit 3824c1fec0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 118 additions and 84 deletions

View File

@ -22,16 +22,23 @@ export interface IUserInfo {
export type TaskExecutorElapsed = Record<string, number[]>;
export interface TaskExecutorHeartbeatItem {
boot_at: string;
current: null;
done: number;
failed: number;
lag: number;
name: string;
now: string;
pending: number;
}
export interface ISystemStatus {
es: Es;
storage: Storage;
database: Database;
redis: Redis;
task_executor: {
error?: string;
status: string;
elapsed?: TaskExecutorElapsed;
};
task_executor_heartbeat: Record<string, TaskExecutorHeartbeatItem[]>;
}
interface Redis {

View File

@ -2,7 +2,7 @@ import SvgIcon from '@/components/svg-icon';
import { useFetchSystemStatus } from '@/hooks/user-setting-hooks';
import {
ISystemStatus,
TaskExecutorElapsed,
TaskExecutorHeartbeatItem,
} from '@/interfaces/database/user-setting';
import { Badge, Card, Flex, Spin, Typography } from 'antd';
import classNames from 'classnames';
@ -11,6 +11,7 @@ import upperFirst from 'lodash/upperFirst';
import { useEffect } from 'react';
import { toFixed } from '@/utils/common-util';
import { isObject } from 'lodash';
import styles from './index.less';
import TaskBarChat from './task-bar-chat';
@ -27,7 +28,7 @@ const TitleMap = {
storage: 'Object Storage',
redis: 'Redis',
database: 'Database',
task_executor: 'Task Executor',
task_executor_heartbeats: 'Task Executor',
};
const IconMap = {
@ -60,10 +61,13 @@ const SystemInfo = () => {
type="inner"
title={
<Flex align="center" gap={10}>
{key === 'task_executor' ? (
{key === 'task_executor_heartbeats' ? (
<img src="/logo.svg" alt="" width={26} />
) : (
<SvgIcon name={IconMap[key as keyof typeof IconMap]} width={26}></SvgIcon>
<SvgIcon
name={IconMap[key as keyof typeof IconMap]}
width={26}
></SvgIcon>
)}
<span className={styles.title}>
{TitleMap[key as keyof typeof TitleMap]}
@ -76,13 +80,15 @@ const SystemInfo = () => {
}
key={key}
>
{key === 'task_executor' ? (
info?.elapsed ? (
{key === 'task_executor_heartbeats' ? (
isObject(info) ? (
<TaskBarChat
data={info.elapsed as TaskExecutorElapsed}
data={info as Record<string, TaskExecutorHeartbeatItem[]>}
></TaskBarChat>
) : (
<Text className={styles.error}>{info.error}</Text>
<Text className={styles.error}>
{typeof info.error === 'string' ? info.error : ''}
</Text>
)
) : (
Object.keys(info)

View File

@ -1,57 +1,47 @@
import { TaskExecutorElapsed } from '@/interfaces/database/user-setting';
import { TaskExecutorHeartbeatItem } from '@/interfaces/database/user-setting';
import { Divider, Flex } from 'antd';
import { max } from 'lodash';
import {
Bar,
BarChart,
CartesianGrid,
Legend,
Rectangle,
ResponsiveContainer,
Tooltip,
XAxis,
} from 'recharts';
import { formatDate, formatTime } from '@/utils/date';
import dayjs from 'dayjs';
import { get } from 'lodash';
import styles from './index.less';
interface IProps {
data: TaskExecutorElapsed;
data: Record<string, TaskExecutorHeartbeatItem[]>;
}
const getColor = (value: number) => {
if (value > 120) {
return 'red';
} else if (value <= 120 && value > 50) {
return '#faad14';
}
return '#52c41a';
};
const getMaxLength = (data: TaskExecutorElapsed) => {
const lengths = Object.keys(data).reduce<number[]>((pre, cur) => {
pre.push(data[cur].length);
return pre;
}, []);
return max(lengths) ?? 0;
};
const fillEmptyElementByMaxLength = (list: any[], maxLength: number) => {
if (list.length === maxLength) {
return list;
}
return list.concat(
new Array(maxLength - list.length).fill({
value: 0,
actualValue: 0,
fill: getColor(0),
}),
);
};
const CustomTooltip = ({ active, payload }: any) => {
const CustomTooltip = ({ active, payload, ...restProps }: any) => {
if (active && payload && payload.length) {
const taskExecutorHeartbeatItem: TaskExecutorHeartbeatItem = get(
payload,
'0.payload',
{},
);
return (
<div className="custom-tooltip">
<p
className={styles.taskBarTooltip}
>{`${payload[0].payload.actualValue}`}</p>
<div className="bg-slate-50 p-2 rounded-md border border-indigo-100">
<div className="font-semibold text-lg">
{formatDate(restProps.label)}
</div>
{Object.entries(taskExecutorHeartbeatItem).map(([key, val], index) => {
return (
<div key={index} className="flex gap-1">
<span className="font-semibold">{`${key}: `}</span>
<span>
{key === 'now' || key === 'boot_at' ? formatDate(val) : val}
</span>
</div>
);
})}
</div>
);
}
@ -60,32 +50,56 @@ const CustomTooltip = ({ active, payload }: any) => {
};
const TaskBarChat = ({ data }: IProps) => {
const maxLength = getMaxLength(data);
return (
<Flex gap="middle" vertical>
{Object.keys(data).map((key) => {
const list = data[key].map((x) => ({
value: x > 120 ? 120 : x,
actualValue: x,
fill: getColor(x),
}));
const nextList = fillEmptyElementByMaxLength(list, maxLength);
return (
<Flex key={key} className={styles.taskBar} vertical>
<b className={styles.taskBarTitle}>ID: {key}</b>
<ResponsiveContainer>
<BarChart data={nextList} barSize={20}>
<CartesianGrid strokeDasharray="3 3" />
<Tooltip content={<CustomTooltip></CustomTooltip>} />
<Bar dataKey="value" />
</BarChart>
</ResponsiveContainer>
<Divider></Divider>
</Flex>
);
})}
</Flex>
);
return Object.entries(data).map(([key, val]) => {
const data = val.map((x) => ({
...x,
now: dayjs(x.now).valueOf(),
failed: 5,
}));
const firstItem = data[0];
const lastItem = data[data.length - 1];
const domain = [firstItem.now, lastItem.now];
return (
<Flex key={key} className={styles.taskBar} vertical>
<div className="flex gap-8">
<b className={styles.taskBarTitle}>ID: {key}</b>
<b className={styles.taskBarTitle}>Lag: {lastItem.lag}</b>
<b className={styles.taskBarTitle}>Pending: {lastItem.pending}</b>
</div>
<ResponsiveContainer>
<BarChart data={data} barSize={20}>
<XAxis
dataKey="now"
type="number"
scale={'time'}
domain={domain}
tickFormatter={(x) => formatTime(x)}
allowDataOverflow
angle={60}
padding={{ left: 20, right: 20 }}
tickMargin={20}
/>
<CartesianGrid strokeDasharray="3 3" />
<Tooltip content={<CustomTooltip></CustomTooltip>} />
<Legend wrapperStyle={{ bottom: -22 }} />
<Bar
dataKey="done"
fill="#8884d8"
activeBar={<Rectangle fill="pink" stroke="blue" />}
/>
<Bar
dataKey="failed"
fill="#82ca9d"
activeBar={<Rectangle fill="gold" stroke="purple" />}
/>
</BarChart>
</ResponsiveContainer>
<Divider></Divider>
</Flex>
);
});
};
export default TaskBarChat;

View File

@ -1,5 +1,19 @@
import dayjs from 'dayjs';
export function formatDate(date: any) {
if (!date) {
return '';
}
return dayjs(date).format('DD/MM/YYYY HH:mm:ss');
}
export function formatTime(date: any) {
if (!date) {
return '';
}
return dayjs(date).format('HH:mm:ss');
}
export function today() {
return formatDate(dayjs());
}
@ -11,10 +25,3 @@ export function lastDay() {
export function lastWeek() {
return formatDate(dayjs().subtract(1, 'weeks'));
}
export function formatDate(date: any) {
if (!date) {
return '';
}
return dayjs(date).format('DD/MM/YYYY HH:mm:ss');
}