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 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 { export interface ISystemStatus {
es: Es; es: Es;
storage: Storage; storage: Storage;
database: Database; database: Database;
redis: Redis; redis: Redis;
task_executor: { task_executor_heartbeat: Record<string, TaskExecutorHeartbeatItem[]>;
error?: string;
status: string;
elapsed?: TaskExecutorElapsed;
};
} }
interface Redis { interface Redis {

View File

@ -2,7 +2,7 @@ import SvgIcon from '@/components/svg-icon';
import { useFetchSystemStatus } from '@/hooks/user-setting-hooks'; import { useFetchSystemStatus } from '@/hooks/user-setting-hooks';
import { import {
ISystemStatus, ISystemStatus,
TaskExecutorElapsed, TaskExecutorHeartbeatItem,
} from '@/interfaces/database/user-setting'; } from '@/interfaces/database/user-setting';
import { Badge, Card, Flex, Spin, Typography } from 'antd'; import { Badge, Card, Flex, Spin, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
@ -11,6 +11,7 @@ import upperFirst from 'lodash/upperFirst';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { toFixed } from '@/utils/common-util'; import { toFixed } from '@/utils/common-util';
import { isObject } from 'lodash';
import styles from './index.less'; import styles from './index.less';
import TaskBarChat from './task-bar-chat'; import TaskBarChat from './task-bar-chat';
@ -27,7 +28,7 @@ const TitleMap = {
storage: 'Object Storage', storage: 'Object Storage',
redis: 'Redis', redis: 'Redis',
database: 'Database', database: 'Database',
task_executor: 'Task Executor', task_executor_heartbeats: 'Task Executor',
}; };
const IconMap = { const IconMap = {
@ -60,10 +61,13 @@ const SystemInfo = () => {
type="inner" type="inner"
title={ title={
<Flex align="center" gap={10}> <Flex align="center" gap={10}>
{key === 'task_executor' ? ( {key === 'task_executor_heartbeats' ? (
<img src="/logo.svg" alt="" width={26} /> <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}> <span className={styles.title}>
{TitleMap[key as keyof typeof TitleMap]} {TitleMap[key as keyof typeof TitleMap]}
@ -76,13 +80,15 @@ const SystemInfo = () => {
} }
key={key} key={key}
> >
{key === 'task_executor' ? ( {key === 'task_executor_heartbeats' ? (
info?.elapsed ? ( isObject(info) ? (
<TaskBarChat <TaskBarChat
data={info.elapsed as TaskExecutorElapsed} data={info as Record<string, TaskExecutorHeartbeatItem[]>}
></TaskBarChat> ></TaskBarChat>
) : ( ) : (
<Text className={styles.error}>{info.error}</Text> <Text className={styles.error}>
{typeof info.error === 'string' ? info.error : ''}
</Text>
) )
) : ( ) : (
Object.keys(info) 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 { Divider, Flex } from 'antd';
import { max } from 'lodash';
import { import {
Bar, Bar,
BarChart, BarChart,
CartesianGrid, CartesianGrid,
Legend,
Rectangle,
ResponsiveContainer, ResponsiveContainer,
Tooltip, Tooltip,
XAxis,
} from 'recharts'; } from 'recharts';
import { formatDate, formatTime } from '@/utils/date';
import dayjs from 'dayjs';
import { get } from 'lodash';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
data: TaskExecutorElapsed; data: Record<string, TaskExecutorHeartbeatItem[]>;
} }
const getColor = (value: number) => { const CustomTooltip = ({ active, payload, ...restProps }: any) => {
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) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const taskExecutorHeartbeatItem: TaskExecutorHeartbeatItem = get(
payload,
'0.payload',
{},
);
return ( return (
<div className="custom-tooltip"> <div className="bg-slate-50 p-2 rounded-md border border-indigo-100">
<p <div className="font-semibold text-lg">
className={styles.taskBarTooltip} {formatDate(restProps.label)}
>{`${payload[0].payload.actualValue}`}</p> </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> </div>
); );
} }
@ -60,32 +50,56 @@ const CustomTooltip = ({ active, payload }: any) => {
}; };
const TaskBarChat = ({ data }: IProps) => { const TaskBarChat = ({ data }: IProps) => {
const maxLength = getMaxLength(data); return Object.entries(data).map(([key, val]) => {
return ( const data = val.map((x) => ({
<Flex gap="middle" vertical> ...x,
{Object.keys(data).map((key) => { now: dayjs(x.now).valueOf(),
const list = data[key].map((x) => ({ failed: 5,
value: x > 120 ? 120 : x, }));
actualValue: x, const firstItem = data[0];
fill: getColor(x), const lastItem = data[data.length - 1];
}));
const nextList = fillEmptyElementByMaxLength(list, maxLength); const domain = [firstItem.now, lastItem.now];
return (
<Flex key={key} className={styles.taskBar} vertical> return (
<b className={styles.taskBarTitle}>ID: {key}</b> <Flex key={key} className={styles.taskBar} vertical>
<ResponsiveContainer> <div className="flex gap-8">
<BarChart data={nextList} barSize={20}> <b className={styles.taskBarTitle}>ID: {key}</b>
<CartesianGrid strokeDasharray="3 3" /> <b className={styles.taskBarTitle}>Lag: {lastItem.lag}</b>
<Tooltip content={<CustomTooltip></CustomTooltip>} /> <b className={styles.taskBarTitle}>Pending: {lastItem.pending}</b>
<Bar dataKey="value" /> </div>
</BarChart> <ResponsiveContainer>
</ResponsiveContainer> <BarChart data={data} barSize={20}>
<Divider></Divider> <XAxis
</Flex> dataKey="now"
); type="number"
})} scale={'time'}
</Flex> 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; export default TaskBarChat;

View File

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