mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-14 04:55:54 +08:00
### 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:
parent
4b3eeaa6ef
commit
3824c1fec0
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
|
||||||
fill: getColor(x),
|
|
||||||
}));
|
}));
|
||||||
const nextList = fillEmptyElementByMaxLength(list, maxLength);
|
const firstItem = data[0];
|
||||||
|
const lastItem = data[data.length - 1];
|
||||||
|
|
||||||
|
const domain = [firstItem.now, lastItem.now];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex key={key} className={styles.taskBar} vertical>
|
<Flex key={key} className={styles.taskBar} vertical>
|
||||||
|
<div className="flex gap-8">
|
||||||
<b className={styles.taskBarTitle}>ID: {key}</b>
|
<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>
|
<ResponsiveContainer>
|
||||||
<BarChart data={nextList} barSize={20}>
|
<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" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<Tooltip content={<CustomTooltip></CustomTooltip>} />
|
<Tooltip content={<CustomTooltip></CustomTooltip>} />
|
||||||
<Bar dataKey="value" />
|
<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>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
})}
|
});
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskBarChat;
|
export default TaskBarChat;
|
||||||
|
@ -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');
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user